Building an MCP server: exposing your thing to the model
Anthropic API
This page covers tools outside your selection. You can still read it. Find matching guides
A server is a small adapter between the model and something you own. The hard parts are not the protocol; they are the trust boundary you just opened.
Two other pages on this site look at MCP from the outside: whether adding one is worth it as a user, and how to vet one you did not write. This page is the inside — you are building the server — and its argument is that the protocol is the easy half and the trust boundary is the whole job.
What a server actually offers
The specification narrows the design space to three primitives: "MCP servers can provide three main types of capabilities", and naming them well is most of the design.
- Resources: "File-like data that can be read by clients": the read side, pulled into context when relevant.
- Tools: "Functions that can be called by the LLM (with user approval)": the act side, where side effects live.
- Prompts: "Pre-written templates that help users accomplish specific tasks": reusable openings the user invokes.
Most first servers are, like the docs' own example, "a simple MCP weather server" exposing a couple of tools. The useful question before writing any of it: is this a resource (the model should be able to read X) or a tool (the model should be able to do X)? The distinction is the security model, because reading and doing have different blast radii.
Tool descriptions are the actual interface
The model does not read your source; it reads your tool descriptions and decides from those. So the same care that tool descriptions demand in any agent applies double here: an ambiguous description is not a documentation problem, it is a wrong-tool-called problem, at a boundary you exposed on purpose. The tool-use footguns of non-idempotent actions, silent failure and over-broad scope are yours to prevent now, because your server is where they originate.
The part the tutorial mentions in passing
The build guide has a quiet warning that is the most important sentence in it: "When implementing MCP servers, be careful about how you handle logging". For stdio-transport servers, stray writes to standard output corrupt the protocol stream (a real and common first bug), but the deeper point generalises. Your server is a new component that:
Runs with whatever authority you give it. A server that reaches a database reaches it with the server's credentials, on behalf of whatever asked. This is the confused-deputy seam: validate that the request deserves the authority before spending it, and scope the server's own credentials to the minimum the tools need.
Sees whatever the model sends it. Tool arguments are model-generated and can carry content that arrived by injection. Treat every argument as untrusted input: validate, parameterise queries, never shell-interpolate. A tool that takes a path must not accept ../ into somewhere it should not reach.
Becomes a dependency the moment anyone else installs it. The second a colleague or a marketplace ships your server, you are the supply chain the vetting page warns others about. Version it, document what it touches, and say plainly what authority it needs and why.
Transport, briefly
Local servers speak over stdio; remote servers speak over HTTP. The choice is a deployment question with a security tail: a stdio server runs on the user's machine with the user's reach, while an HTTP server is a networked service with everything that implies: authentication, transport security, and an attack surface that is no longer just yours. Start local; go remote only when a shared service genuinely needs it, and read the protocol's own security-best-practices page before you do.
What goes wrong
A tool that should have been a resource. Exposing a mutating function where the model only needed to read invites actions where none were wanted. Read and write are different grants.
Descriptions written for humans who see the code. The model does not. Vague names and missing constraints produce wrong calls that look like model error and are interface error.
Trusting the arguments. Model-supplied input flowing unvalidated into a query, a path, or a shell: the injection endpoint you built yourself.
stdout as a debug channel. On stdio transport, a print in the wrong place breaks the protocol, and the failure looks like anything but its cause.
Shipping without a scope statement. A server whose credentials and reach are undocumented cannot be vetted by anyone downstream, which means it will be either blindly trusted or blocked — both your fault.
How to check it worked
Two tests, not one. Functionally: connect the server to a host and confirm the model calls the right tool for a plain-language request — the docs' own acceptance check. Then the adversarial test the tutorial does not run: feed a tool a hostile argument (a traversal path, an injection string, a malformed payload) and confirm it is rejected, not executed. A server that passes only the first test is a feature; one that passes both is safe to let other people install.
Sources
- Build an MCP server — Model Context Protocol documentation Tier 1 2026-09-04
Something wrong with this page?
Say what you expected and what you got. That is usually the shortest route to a correction, and it goes on the public issue tracker so the fix is visible.