Model Context Protocol for a Small Server — What the Specification Standardizes, and What It Leaves to You
There is a piece of jargon that shows up in almost every conversation about connecting an AI assistant to real software: MCP. The Model Context Protocol is described as the standard way to give models access to tools and data, and it is genuinely useful. But "it is a standard" hides the question a small server actually has to answer: standardized enough for two programs written by strangers to interoperate? Or standardized enough that I can stop thinking about security? For a protocol versioned less than two months before this article was written, the answer is worth unpacking carefully.
I want to keep three things apart here: what the specification actually says, what follows from it for a server I run myself, and what I could not verify. Everything version-specific below is pinned to the 2026-07-28 revision, the newest one available today.
What the protocol actually standardizes
The specification describes MCP as an open protocol for connecting LLM applications to external data sources and tools, carried over JSON-RPC 2.0 messages between three roles: hosts, which are the AI applications that initiate connections; clients, the connectors inside a host; and servers, the services that provide context and capabilities. The spec is explicit that it was inspired by the Language Server Protocol, which solved a similar "every editor integrates every language separately" problem for programming languages.
The architecture page adds the part that matters operationally: a host runs many clients, and each client talks to exactly one server. A single AI application can therefore reach a file server, a database, and a remote API without any of them knowing about the others. One of the four design principles listed there is a boundary rather than a feature: servers "should not be able to read the whole conversation, nor 'see into' other servers".
What a server can offer is small and concrete: resources (context and data), prompts (templated messages), and tools (functions the model can execute). What a client offers back is narrower in this revision — elicitation, which is the server asking the user for more information.
A tool is a name, a description, and a schema
The tools specification is refreshingly boring, which is a compliment. A tool has a unique name, a human-readable description, and an inputSchema that is a JSON Schema object, defaulting to JSON Schema 2020-12 when no $schema is declared. Here is the tool definition from the specification's own example:
{
"name": "get_weather",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
}
}
Calling it is a tools/call request, and the result comes back with content, optionally structuredContent matching an outputSchema, and an isError flag. That flag carries more weight than it looks. The spec splits failures in two: protocol errors (unknown tool, malformed request) come back as JSON-RPC errors, while tool execution errors — a date in the wrong format, a value out of range, a failed API call — come back as a normal result with isError: true. Clients are told to hand those to the model so it can correct itself, and to be more careful about handing over protocol errors, which the model is unlikely to be able to fix.
Two details are worth more than they first appear. First, tools are described as model-controlled: the model decides to call them, and the protocol does not mandate how a user sees that happen. The spec's recommendation is that there "SHOULD always be a human in the loop with the ability to deny tool invocations". Second, a tool definition may carry annotations about its behaviour, and both the tools page and the top-level security section warn that those annotations must be treated as untrusted unless they came from a server you already trust. A tool description is content from a stranger, not a guarantee from your own code.
Two transports, two very different risk profiles
The same tool list travels very differently depending on how the server is reached, and the specification treats these as different security situations.
stdio is the local case. The client launches the server as a subprocess and speaks newline-delimited JSON-RPC over its standard streams. Messages must not contain embedded newlines, stderr is the place for logs, and the server "MUST NOT write anything to its stdout that is not a valid MCP message" — a rule that exists because a stray debug print() in a script will corrupt the stream a client is parsing. Servers should exit when their input closes, and clients are told to restart the process if it dies unexpectedly, which is now cheap because there is no session state to lose.
Streamable HTTP is the remote case. The server exposes a single endpoint that accepts POST; every request is its own POST, answered either with a JSON object or with a Server-Sent Events stream scoped to that request. The security section of the transport specification is unusually direct, and it is worth reading as a checklist rather than as theory:
- Servers MUST validate the
Originheader on incoming connections and answer403 Forbiddenwhen it is present and invalid, because without this an attacker can use DNS rebinding to reach a local MCP server from a web page. - When running locally, servers SHOULD bind only to
127.0.0.1rather than0.0.0.0. - Servers SHOULD implement proper authentication for all connections.
- Behind a reverse proxy, servers SHOULD send
X-Accel-Buffering: noso nginx stops buffering the event stream.
Requests also carry required metadata. Every POST must include an MCP-Protocol-Version header, an Mcp-Method header, and an Mcp-Name header for tools/call, resources/read, and prompts/get. If a header disagrees with the value in the body, the server must reject the request with 400 Bad Request and a HeaderMismatch error, which exists to stop a load balancer and a backend making decisions from two different sources of truth. Written as a shell command rather than as a client, a tool listing looks like this:
curl -i -X POST https://mcp.example.lan/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'
That request is assembled from the specification's own requirements, not copied from a live session — I have not stood up an MCP server to watch this succeed. The `_meta` block is not decoration: io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities are required on every request, and a request missing either is malformed, to be rejected with JSON-RPC -32602 and, over HTTP, 400 Bad Request.
The newest revision removed the handshake
Here is the part that makes reading the specification rather than a blog post worthwhile. In revision 2026-07-28, MCP became stateless at the protocol level. The initialize / notifications/initialized handshake that defined earlier sessions is gone, as are protocol sessions and the Mcp-Session-Id header; every request carries its own version and capabilities instead. Servers that need state across calls are expected to mint explicit handles and accept them back as ordinary tool arguments — a shopping cart id is just a string, and the model is responsible for carrying it forward.
Version negotiation moved with it. There is no negotiation handshake: a client puts its preferred version in the request, and a server that does not implement it replies with UnsupportedProtocolVersionError, carrying JSON-RPC code -32022 and the list of versions it does support. Servers must implement a server/discover method that advertises supported versions, capabilities, and identity; clients may call it first, and the versioning page uses it as the probe for telling a modern server from a legacy one.
For anyone running software that has to interoperate, the changelog is the honest summary of how unsettled this still is. Streamable HTTP lost its GET endpoint, its session header, and its resumable event streams; subscriptions were rebuilt around a single subscriptions/listen request; experimental tasks moved out of the core protocol into an opt-in extension; and Roots, Sampling, and Logging — the features most tutorials from 2024 and 2025 use — are now deprecated, alongside the old HTTP+SSE transport. The project has also adopted a deprecation policy with a minimum twelve-month window, which at least means a working feature is not supposed to vanish overnight.
What the protocol refuses to do for you
The most quotable sentence in the whole specification is also the most important one for an operator. After listing principles around user consent, data privacy, tool safety, and sampling controls, the spec says plainly that "MCP itself cannot enforce these security principles at the protocol level" and lists what implementers should do instead: build consent and authorization flows, document the security implications, implement access controls, follow security best practices, and consider privacy in feature design.
In practice this means the safety boundary is your server and your host application, not the wire format. The tools page puts the server's obligations in a list of MUSTs: validate all tool inputs, implement proper access controls, rate limit tool invocations, and sanitize tool outputs. Everything about whether a human approved the call is an interface decision the protocol leaves open — which is exactly why it recommends human-in-the-loop behaviour rather than requiring it.
If the server is not on your laptop
Authorization is the part where a small server is most likely to cut a corner, so it is worth stating what the specification actually requires. Authorization is optional for MCP implementations. Implementations using an HTTP transport should follow the authorization specification; implementations using stdio should not follow it and should instead retrieve credentials from the environment. That distinction is practical: a subprocess your desktop app launched has a different trust relationship with you than a URL reachable from the internet.
For HTTP servers, the specification builds on existing OAuth work rather than inventing a scheme. It requires servers to implement OAuth 2.0 Protected Resource Metadata (RFC 9728, April 2025) and requires clients to use Resource Indicators (RFC 8707, February 2020), which is the mechanism that binds an access token to the resource it was actually issued for. The security-considerations page adds that a server "MUST NOT pass through the token it received from the MCP client" when calling an upstream API, and the companion Security Best Practices document explains why: a server that forwards tokens it has not validated for itself becomes a confused deputy, a middleman whose requests look like they came from someone else.
There is one more risk that only exists because the protocol became stateless. With no protocol session, any cross-call state is a handle your server minted — and the security document treats possession of that handle as possession of somebody's state unless you do something about it. Its advice is to generate handles with a secure random generator, expire them, and bind them server-side to the authenticated user rather than trusting the string that came back in a tool result.
A checklist I would actually use
This part is my own interpretation, not specification text, and it is specific to a small self-hosted setup:
- Start with stdio. A local subprocess avoids the entire HTTP attack surface, and the specification's own guidance for stdio credentials is simply the environment.
- Before binding to anything other than localhost, have the
Originvalidation and authentication written, not planned. Both are MUST/SHOULD items in the spec, and both are cheap while you are still writing the server. - Read the version your client actually sends, not the version your client advertises in a README. The revision this article is based on changed the handshake model entirely, and a tool list that "disappears" is often just a version mismatch.
- Treat tool descriptions and annotations from a server you did not write as untrusted input, in the same category as a web page you would not paste into a prompt.
- If you mint handles for state, bind them to the caller. If you proxy to another API, exchange the token rather than forwarding it.
What I could not verify
I have not run any of this against a live MCP server, so I cannot tell you how gracefully current clients handle a stateless revision that was published two months ago, and I deliberately make no claim about which applications support which revision — the specification itself only guarantees what a conforming implementation does. I also did not audit the extension ecosystem, the registry, or the deprecated-features list, and I did not evaluate any server implementation's security. Where this article describes the protocol, it describes the document; where it describes risk, it is my reading of what the document asks implementers to take responsibility for.
The honest summary is narrow and, I think, useful. MCP standardizes the conversation between an AI application and your tools: a wire format whose specification dates from 2010, a tool list described by JSON Schema, and a versioned way to say "I do not speak your dialect". It does not standardize whether anyone should have called your tool, and it is explicit about that. For a small server, the protocol is the easy half. The hard half is exactly the half the specification refuses to do for you.
References
- Model Context Protocol project, Specification, revision 2026-07-28. Accessed 29 September 2026.
- Model Context Protocol project, Architecture. Accessed 29 September 2026.
- Model Context Protocol project, Versioning and Compatibility. Accessed 29 September 2026.
- Model Context Protocol project, Key Changes (changelog for revision 2026-07-28). Accessed 29 September 2026.
- Model Context Protocol project, stdio transport. Accessed 29 September 2026.
- Model Context Protocol project, Streamable HTTP transport. Accessed 29 September 2026.
- Model Context Protocol project, Tools. Accessed 29 September 2026.
- Model Context Protocol project, Authorization. Accessed 29 September 2026.
- Model Context Protocol project, Authorization Security Considerations. Accessed 29 September 2026.
- Model Context Protocol project, Security Best Practices. Accessed 29 September 2026.
- JSON-RPC Working Group, JSON-RPC 2.0 Specification (origin 26 March 2010, updated 4 January 2013). Accessed 29 September 2026.
- B. Campbell, P. Bradley, Y. Tschofenig, RFC 8707: Resource Indicators for OAuth 2.0, IETF, February 2020. Accessed 29 September 2026.
- M. Jones, P. Hunt, A. Parecki, RFC 9728: OAuth 2.0 Protected Resource Metadata, IETF, April 2025. Accessed 29 September 2026.
