How to Build a Custom MCP Server for Your Own API

October 20, 2026

A working MCP server for your own API is roughly 80 lines of TypeScript using the official SDK, a handful of tool definitions, and a stdio transport. You export a main() that registers tools with JSON Schema inputs, and the client discovers them on connect. No hosted infrastructure required.

The hard part isn’t the code. It’s deciding how granular your tools should be.

What does a minimal MCP server contain?

Three things: a server instance with a name and version, one or more registered tools, and a transport to talk to the client. The @modelcontextprotocol/sdk package gives you all three. Python has an equivalent with mcp on PyPI and the FastMCP helper.

Nothing here requires Anthropic’s blessing or a manifest upload. The protocol is open and the client just spawns your process.

Step 1: Scaffold and install

mkdir my-mcp && cd my-mcp
npm init -y
npm i @modelcontextprotocol/sdk zod

Add "type": "module" to package.json and set a bin entry if you want to run it after npm link. You’ll be writing ESM whether you like it or not — the SDK ships ESM-only now, so CommonJS require will fail.

Step 2: Define tools by user intent, not by endpoint

The most common mistake is one tool per REST endpoint. Give a model 40 endpoints and it picks wrong constantly. Give it four tools that match how a person would ask, and accuracy climbs sharply.

Compare these two designs for an invoicing API:

Design Tools Typical result
Endpoint mirroring list_invoices, get_invoice, list_customers, get_customer, create_invoice_line, finalize_invoice (and 30 more) 4-6 round trips, wrong tool chosen
Task oriented find_invoices(query, customer, date_range), get_invoice_detail(id), draft_invoice(customer, items) 1-2 round trips

Task-oriented tools need server-side logic. That’s fine — it’s your server.

Step 3: Write schemas the model can read

Every tool needs a name, a description that reads like an instruction, and an inputSchema in JSON Schema. Descriptions carry more weight than you’d expect: the model selects tools by matching the user’s words against them.

Write descriptions that include the trigger conditions. “Search invoices. Use when the user asks about billing history, overdue amounts, or a specific invoice number” beats “Search invoices” by a wide margin in my testing. Avoid clever names — find_invoices is better than invQ.

Use zod for validation. Mark required fields as required; models do respect required arrays, but only if you also reject bad input rather than coercing it.

Step 4: Handle errors as tool results, not exceptions

This is the item that separates a toy server from a usable one. If your handler throws, the client sees a transport error and the conversation stalls. Instead, return a result with isError: true and a message the model can act on.

Say the API returns 429. Return something like “Rate limited, retry after 30 seconds” rather than a stack trace. The model will wait or tell the user. A raw exception gets you a broken turn.

Also: never return a whole JSON blob if a short summary works. Token cost is real, and dumping 400 invoice rows fills the context window in one call. Paginate with a default page size of 20.

Step 5: Wire up stdio, then add to the client

const transport = new StdioServerTransport();
await server.connect(transport);

Stdio means the client launches your process and speaks over stdin/stdout. Two rules follow: never write logs to stdout (it corrupts the protocol — use stderr), and keep startup under a few seconds, because clients time out fast.

For local development, test in the MCP Inspector before wiring it into Claude Desktop. Build the config entry with command: "node" and args: ["/absolute/path/to/build/index.js"].

Should you use stdio or HTTP transport?

Stdio for anything local: it’s simpler, needs no auth, and dies when the client closes. HTTP with the streamable transport for anything shared across machines or served to a team. Multi-user HTTP needs real auth — an unauthenticated MCP endpoint exposed to the internet is an open door to whatever API keys you hold.

When is a custom server overkill?

If your need is one or two read-only calls, a script plus a paste beats a protocol. If you’re the only user and you never change editors, a shell alias works. Build a server when you want the same capability available in several MCP clients, or when teammates should reuse it without copying configs.

Quick Summary

  • The official SDK scaffolds an MCP server in under 100 lines with stdio transport.
  • Design 3-6 tools around user tasks, not one per API endpoint.
  • Tool descriptions are prompts: include when to use the tool.
  • Return errors as isError results with actionable text, never as thrown exceptions.
  • Log to stderr only; anything on stdout breaks the protocol.
Tags: