Skip to content
Agent Search Engine

Guide · MCP server

How to build an MCP server: the protocol is trivial, tool design is the craft

An MCP server is a few functions and a transport — a spec you can skim in an afternoon. What separates a server an LLM reaches for from one it ignores is everything the spec doesn’t cover: tool design. This guide walks it the way a senior engineer would — wrap, build, or aggregate; stdio or HTTP; then the layers, all downstream of designing tools a non-deterministic caller can actually use.

Independent · sponsor-blind · updated 2026-07-21 · how we choose

The protocol is the easy part. MCP is a small JSON-RPC spec, and a modern SDK will stand up a working server in an afternoon — a few functions, a transport, done. The part that decides whether your server is any good never appears in the spec: designing tools a non-deterministic caller can use well. A model picks a tool from its description, fills the arguments from whatever it inferred, and reads back whatever you return. Get the granularity, the descriptions, the schemas, and the output shape wrong and no amount of clean implementation saves you. Get them right and even a plain implementation feels great to use.

An MCP server exposes three kinds of things to any MCP client — Claude Desktop, Cursor, ChatGPT, an agent you wrote — over one connection: tools the model can invoke, resources it can read, and prompts a user can trigger. That is the whole surface. Everything below is about the distance between “it works” and “an LLM reaches for it at the right moment and gets exactly what it needed.”

The core decision: wrap an existing API, build fresh, or aggregate

There are three ways to start, and you should pick before you write code.

Wrap an existing API. If the capability already lives behind a REST service, you may not need to hand-write tools at all. fastapi-mcp turns FastAPI endpoints into MCP tools in place, auth included; mcpo runs the mapping the other direction, proxying an MCP server out to a plain OpenAPI/REST surface for clients that don’t speak MCP. Choose this when you already have a working API and a schema — you’re adapting an interface, not inventing one. The catch: an endpoint designed for a human-driven frontend rarely makes a good tool as-is, which is the whole problem the next section is about.

Build fresh with an SDK. When the logic doesn’t exist yet, or your endpoints map badly to what a model needs, start from a server SDK. In Python the ergonomic path is fastmcp; arcade-mcp pairs a server framework with a tool-development library, and concierge bills itself as a universal SDK for the job. For fullstack TypeScript there’s mcp-use. On .NET, the official C# SDK is maintained with Microsoft. Choose this when the tools are the product and you want full control over their shape. The official Python and TypeScript SDKs from modelcontextprotocol.io are the reference implementations underneath much of this.

Aggregate, or don’t build at all. If the servers you need already exist, the job may be plumbing rather than authoring — run and route existing servers instead of writing another one. Reach for this when the value is in composition and governance, not new capability.

Orthogonal to all three: transport. A stdio server runs as a local subprocess beside a single client on the user’s machine — no network, no auth, trivial to distribute, but one user at a time. A streamable-HTTP server is hosted, reachable by many clients at once, and therefore owns authentication, rate limits, and an operations story. Decide by where the server has to run and who calls it: stdio for a personal or desktop tool, HTTP for anything multi-tenant or hosted.

The layers

Under that decision sit five layers — roughly the order you’ll get them wrong in.

1 · The SDK & transport

Pick the language your tools’ logic already lives in. An MCP server is a thin shell around code you’d have to write anyway, so put it where your domain logic, auth, and libraries already are: fastmcp, arcade-mcp, or concierge in Python; mcp-use in TypeScript; the C# SDK on .NET; fastapi-mcp if you’re bolting MCP onto an existing FastAPI service. Then set transport by deployment, not taste — stdio for local and single-user, HTTP for hosted and multi-tenant.

Watch outshipping stdio when you needed a hosted multi-user server (or the reverse) is a re-architecture, not a config flag: auth, state, and concurrency assumptions differ all the way down.

2 · Tool design — the craft

This is where servers are won or lost, and it’s the part the SDK can’t do for you. Four decisions carry most of the weight.Granularity. A model has to choose the right tool and fill its arguments from a description alone, so tools should map to intents, not to your internal API. One god-tool with twenty modes forces the model to learn a mini-DSL of flags and get it right blind; forty micro-tools bury the one that matters and burn context on all of them. Aim for a handful of tools that each correspond to something a user would actually ask for — a tool that searches issues, not a generic query with a mode flag; a tool that cuts a release, not eight primitives the model has to sequence itself.Descriptions are prompts. The description is not documentation the model reads if it gets curious — it is the entire interface the model selects on. It should say what the tool does, when to use it and when not to, what the arguments mean, and what comes back. Write it the way you’d brief a sharp colleague who can’t see your code and won’t ask a follow-up. A precise description routes the call; a vague one (“manages data”) gets the tool ignored or fired at the wrong moment.Input schemas do the validation the model won’t. Constrain hard: enums instead of free strings, required fields marked required, formats and ranges declared, sane defaults supplied. Every constraint you encode is an error the model can’t make and a round-trip you don’t spend correcting it. A tight schema also documents itself — the model reads the shape and infers correct usage.Shape output for a model, not a person. Return what the next step needs and little else. Trim the forty-field payload to the five fields that matter, resolve opaque IDs to names the model can act on, and keep it small — every token you return is a token the model pays to read and can drown in. If a result is paginated, say so in the result itself (“15 of 210 — call again with the next cursor”) rather than dumping everything.

Watch out — the description is the whole interface: a vague or wrong one means the model calls the tool at the wrong time or not at all, and no cleverness in the implementation fixes a tool the model never correctly reaches for.

3 · Resources & prompts (the other primitives)

MCP is not only tools. Resources expose readable data addressed by a URI — a file, a schema, a document, the current state of something — that the client can pull into context; the host application decides when to include them, so they are read-context, not actions. Prompts are reusable templates a user invokes, usually surfaced as slash-commands in the client. Decide by who is in control and whether anything happens: if the model should read a thing, make it a resource; if the model should do a thing, make it a tool; if a person will trigger a canned workflow, make it a prompt.

Watch outcramming read-only data behind a tool call spends a round-trip and pays tokens for a function invocation when a resource the client can read directly would have been cheaper — expose reference data as a resource.

4 · Errors, reliability & context economy

Two quiet failure modes live here. First, errors: a tool that fails should return something the model can act on, not a stack trace. Say what went wrong and what to try next — “no user with that email; search by name first” beats a 500 and a traceback the model can only parrot. Make writes idempotent where you can, because a model will retry, and set timeouts so a hung call fails cleanly instead of stalling the whole turn. Second, context economy: every tool’s name, description, and schema rides in the model’s context on every turn, whether or not it’s called. Fifty tools is fifty tools’ worth of tokens and fifty chances to pick wrong, on every single call.

Watch outfifty tools on one server taxes every call and makes selection worse, not better — split the surface across scoped servers, or gate tools by context, rather than piling them onto one.

5 · Auth, security & distribution

The moment a server leaves the user’s machine it needs auth — the transport is now a network endpoint, and modern MCP authorization is OAuth-based; fastapi-mcp carries auth through if you’re wrapping an existing service. Then look hard at the attack surface: a tool that acts can be prompt-injected into acting, because the data it reads may carry instructions aimed at the model. Treat every write-capable tool as an injection target. Distribution is the last mile: list the server on the community registry so clients can find it; use mcpo to reach OpenAPI/REST clients or mcp-bridge to put an OpenAI-compatible endpoint in front of your MCP tools; and for fleets of servers, reach for management and aggregation — toolhive and metamcp to run and route them, mcp-router to manage them, klavis and archestra for integration and governance at platform scale.

Watch outan unauthenticated remote server with write-capable tools is an open door — anyone who finds the URL runs your tools, and every tool is a potential injection target.

Learn from real servers

The fastest way to learn tool design is to read servers that got it right — open their tool list and study the descriptions and schemas, not the plumbing. github-mcp-server is the official, broad reference: a wide surface across repos, issues, and PRs, and a working answer to the tool-count question at scale. context7 is the docs-as-context example, shaping up-to-date library documentation into something an agent reads. serena is a semantic code toolkit: retrieval and editing tools built for an agent rather than a human IDE. For action-heavy, single-domain servers, read chrome-devtools-mcp and firecrawl-mcp-server and exa-mcp-server; for tightly scoped ones, excel-mcp-server, mcp-server-chart, and xcodebuildmcp each do one job well. AWS MCP Servers shows the other axis entirely: a vendor shipping many focused servers instead of one that tries to do everything. Read four of these before you design your own.

Which should you pick?

  • I already have a REST API → wrap it: fastapi-mcp if it’s FastAPI, mcpo to expose an existing MCP server to OpenAPI/REST clients. Then revisit the tool shapes — thin the payloads, tighten the descriptions. Raw endpoints are a starting point, not finished tools.
  • I’m building fresh in Python fastmcp for the ergonomic path; arcade-mcp if you want a tool-development library alongside the server; concierge as a universal-SDK alternative. Spend your time on tool design, not transport.
  • Enterprise — many servers to run and govern toolhive, metamcp, and mcp-router to run, route, and manage a fleet; klavis or archestra when you need integration and guardrails at platform scale.
  • It has to be callable from a non-MCP client mcpo to present the server as OpenAPI/REST, or mcp-bridge to put an OpenAI-compatible endpoint in front of your MCP tools.

The mistakes that sink MCP servers

  • Designing tools around your API, not the model’s intent. A model picks a tool and fills its arguments from a description alone. Map tools to what a user would actually ask for — “cut a release,” not eight primitives it has to sequence, and not one god-tool with twenty mode flags.
  • Treating the description as documentation. It isn’t reference material the model reads if curious — it’s the entire interface the model selects on. “Manages data” gets the tool ignored or fired at the wrong moment; no cleverness in the implementation fixes that.
  • Returning payloads shaped for humans. A forty-field JSON blob or an HTML page is worse than a terse structured summary — every token you return is a token the model pays to read and can drown in. Return the few fields the next step needs, and resolve opaque IDs to names.
  • Piling fifty tools onto one server. Every tool’s name, description, and schema rides in the model’s context on every turn, called or not — fifty tools is fifty chances to pick wrong, every call. Split the surface across scoped servers or gate tools by context.
  • Shipping a remote server without auth. A hosted server is a public network endpoint; an unauthenticated one with write-capable tools is an open door. And because tool inputs can carry instructions aimed at the model, treat every write tool as a prompt-injection target.
  • Reaching for MCP when a normal API would do. MCP earns its keep when the caller is an AI client you don’t control. If everything on the other end is your own code or a human frontend, you control both ends and don’t need the model-facing socket.

Frequently asked

stdio or HTTP — which transport should I pick?
Deployment decides, not preference. Use stdio if the server runs locally beside one client (a desktop app or a personal dev tool): no network, no auth, dead-simple to distribute. Use streamable HTTP if it’s hosted and serves multiple users — now you own authentication, rate limits, and uptime. The mistake is treating it as a late toggle: the two imply different assumptions about state, concurrency, and auth, so choose up front.
How many tools is too many?
Fewer than instinct says. Every tool’s schema rides in context on every turn, so each one costs tokens and dilutes selection whether or not it’s used. A focused server with a handful of well-named tools outperforms one exposing fifty. If you’re past a dozen or two, take it as a signal to split into scoped servers or gate tools by context. Ship the tools a model actually reaches for and drop the rest.
Do I need resources and prompts, or just tools?
Often just tools — but not always. If the model should read reference data (a schema, a doc, current state), a resource is cheaper than a tool call and doesn’t spend a round-trip. If a person will trigger a canned workflow, a prompt — a client slash-command — fits better than a tool. Rule of thumb: model reads, use a resource; model acts, use a tool; human triggers, use a prompt.
How do I secure a remote MCP server?
Authenticate it — a hosted server is a public network endpoint, and MCP’s authorization is OAuth-based; if you’re wrapping an existing API, fastapi-mcp can carry its auth through. Then treat every write-capable tool as an injection target: the content a tool ingests can contain instructions aimed at the model, so scope credentials narrowly, confirm destructive actions, and never rely on the model to refuse a poisoned instruction. An unauthenticated server with write tools is an open door.
How do I test and debug one?
Exercise the server directly before wiring it to an agent. The MCP Inspector — the official debug UI — connects to your server and lets you list and call tools, read resources, and see exactly what comes back, which is where you catch a misleading description or a bloated payload. Then test through a real client (Claude Desktop, Cursor) on the tasks you care about, and read the tool calls the model makes — the model’s mistakes are the truest bug report you’ll get on your descriptions and schemas.
MCP server or just a normal API — why bother?
Build an MCP server when the caller is an AI client you don’t control and want to give plug-in access — Claude, Cursor, ChatGPT, agents — without each one writing a custom integration; MCP is the standard socket they already speak. Build a normal API when the caller is your own code or a human frontend. It isn’t either/or — fastapi-mcp and mcpo exist precisely so one service can present a REST face to your app and an MCP face to agents. If nothing on the other end is a model, you probably don’t need MCP.

More stack guides