Writing an MCP server is now a one-afternoon task. Every serious SDK ships a quickstart, the official registry has grown from about 2,000 servers in late 2025 to almost 19,000 by July 2026, and every platform you care about — OpenAI, Google, Microsoft, AWS — speaks the protocol. That is precisely the problem. The quickstart server and the production server look identical in a demo, and the difference only shows up later, as a security incident, a scaling wall, or a spec migration you did not plan for.
We build MCP servers for clients, and the gap we see is consistent: teams get tools working in a day, then spend months on everything the quickstart never mentioned. This post is about that everything — authorization, statelessness, multi-tenancy, and the attack patterns that have already produced real incidents at real companies — updated for the spec revision that landed on 28 July 2026 and quietly redefined what a well-built server looks like.
The protocol has stopped being Anthropic's
First, the governance fact that changes the risk calculus for betting on MCP at all. In December 2025, Anthropic donated the protocol to the Agentic AI Foundation under the Linux Foundation, co-founded with OpenAI and Block, with Google, Microsoft, AWS and Cloudflare as supporters. MCP is now vendor-neutral in the way Kubernetes is vendor-neutral: no single company can move it, and every major model vendor supports it. The SDKs crossed roughly 97 million monthly downloads around the same announcement.
For a CTO, that settles the "is this protocol going to survive" question. What it does not settle is which revision of it you should build against — because the answer just changed.
| Spec revision | What it added or removed |
|---|---|
| 2024-11-05 | Initial release: JSON-RPC, stdio and HTTP+SSE transports |
| 2025-03-26 | OAuth 2.1 authorization, Streamable HTTP replaces SSE, tool annotations |
| 2025-06-18 | Structured tool output, elicitation, resource binding (RFC 8707) required |
| 2025-11-25 | Async tasks, extensions framework, Client ID Metadata Documents |
| 2026-07-28 | Sessions removed, initialize handshake removed, MRTR replaces server push, routing headers required |
The 2026-07-28 MCP spec revision: the protocol went stateless
The July revision is the largest architectural change in the protocol's history, and it is a direct response to how painful the old model was to operate. Protocol-level sessions are gone — the Mcp-Session-Id header no longer exists. The initialize handshake is gone; every request now self-describes its protocol version and capabilities. Server-initiated SSE push for sampling and elicitation is replaced by Multi-Round-Trip Requests, where the server returns an input-required result and the client retries with answers. And every Streamable HTTP request must now carry Mcp-Method and Mcp-Name headers, so gateways and load balancers can route without inspecting the body.
Read that list again from an operator's perspective: every item removes a reason MCP servers needed sticky sessions, shared session stores, or long-lived connections. A compliant 2026-07-28 server is a plain stateless HTTP service. It scales behind a round-robin load balancer like any other API, tool listings are client-cacheable with the new ttlMs field, and there is nothing to lose when a pod restarts.
The catch is that almost nothing deployed today works this way. Most production servers implement the 2025-06-18 or 2025-11-25 semantics — stateful sessions, SSE streams, the old handshake — all of which are now formally deprecated with a twelve-month window. If you are building a new server this quarter, build it stateless against the new revision and let clients with older expectations degrade gracefully. If you already operate MCP servers, you have a migration to schedule, and "the spec changed underneath us" is much cheaper to handle now than after your clients upgrade.
If your server needs state across calls — a query cursor, a draft object, a running job — mint your own opaque handle and pass it as an ordinary tool argument. That is not a workaround; it is the intended design, and it is how the new async task workflow operates too — the same externalise-your-state discipline we covered for LangGraph checkpointers in production.
MCP server authentication: the part everyone gets wrong first
The spec's authorization model is opinionated, and the opinions exist because the naive designs all fail the same way. Three rules cover most of it.
Your MCP server is an OAuth resource server, not an authorization server. It validates tokens; it does not issue them. Identity lives with your existing IdP. Concretely, the server must publish Protected Resource Metadata (RFC 9728): an unauthenticated request gets a 401 with a WWW-Authenticate header pointing at a well-known document that names which authorization servers can issue tokens for this resource.
01GET /mcp HTTP/1.102Host: mcp.example.com0304HTTP/1.1 401 Unauthorized05WWW-Authenticate: Bearer resource_metadata=06 "https://mcp.example.com/.well-known/oauth-protected-resource"0708{09 "resource": "https://mcp.example.com",10 "authorization_servers": ["https://auth.example.com"],11 "scopes_supported": ["tickets:read", "tickets:write"]12}Tokens must be bound to your server. Clients are required to send the RFC 8707 resource parameter when requesting tokens, so a token minted for your server cannot be replayed against someone else's. Validate the audience on every request. This is your defense against an entire class of token-confusion attacks, and it costs a few lines.
Never pass the client's token downstream. The spec calls token passthrough a MUST NOT, and it is the root of the confused-deputy problem: your server, holding a powerful inbound token, gets tricked into spending that authority on an attacker's behalf. If your server calls Jira or your own APIs, it authenticates as itself — its own OAuth client, its own narrowly-scoped token, ideally exchanged per-user. The blast radius of a compromised MCP server should be the server's own permissions, never the union of every caller's.
One more 2026 note: dynamic client registration is on its way out, replaced by Client ID Metadata Documents — a client identifies itself by a URL it controls, and you decide which URLs you trust. If your enterprise rollout stalled because the IdP team refused to run an open registration endpoint, that objection is now obsolete.

Tool poisoning is not hypothetical
Tool poisoning is an attack where instructions hidden in a tool's description or its returned data are treated by the model as trusted commands — steering an agent into leaking data or misusing its permissions. The security story that matters for MCP is not classic API security — you presumably have that. It is that your tool descriptions are prompt, and your tool results are prompt, and models treat both as trusted instructions. OWASP now maintains a dedicated MCP Top 10, and tool poisoning sits at MCP03.
The incidents are real and recent. In May 2025, researchers showed that a malicious public GitHub issue could steer an agent connected to the GitHub MCP server into reading the user's private repos and leaking the contents through a public pull request — the agent held one token spanning both. In June 2025, Asana's MCP server had a tenant-isolation flaw that let one organization's users receive another customer's project data; roughly a thousand customers were potentially exposed over a twelve-day window. In July 2025, researchers demonstrated exfiltration of private database tables through Supabase's MCP server, driven entirely by instructions embedded in a support ticket the agent later read. And CVE-2025-54136 put a CVE number on the "rug pull": a tool definition approved by a user is silently rewritten server-side afterwards, and the client keeps trusting it.
None of these were protocol bugs. They were servers wired into what Simon Willison calls the lethal trifecta: access to private data, exposure to untrusted content, and a path to exfiltrate. Your job as a server author is to make sure your server cannot complete that triangle.

How do you defend an MCP server against tool poisoning?
What that means in practice:
- Treat tool results as untrusted input to the model. Anything your server returns that originated from users — ticket bodies, file contents, search results — can carry injected instructions. Wrap it, label it as data, and never design flows where acting on embedded instructions would be catastrophic.
- Scope one server to one privilege level. The GitHub incident happened because one token spanned public and private. If some tools touch sensitive data and others touch the public internet, that is two servers, not one server with a warning in the README.
- Pin and hash tool definitions. Clients should re-approve when a description changes; as a server author, version your tool schemas and publish a changelog, because "silently improved the prompt in the description" is indistinguishable from a rug pull.
- Enforce tenancy in the tool handler, not the gateway alone. Asana's flaw was one missing boundary check deep in the stack. Every handler should re-derive the tenant from the validated token and refuse cross-tenant object IDs, even when the caller "should" never send them.
- Rate-limit as if the caller is a loop, because it is. An agent that misreads a result can happily issue a thousand calls a minute against your paid downstream API. Token-bucket per client, small burst, low sustained rate.
The MCP server production checklist
Beyond auth and poisoning, the operational bar for "production" in 2026 looks like this:
- Stateless handlers, state in handles. No Mcp-Session-Id, no in-memory conversation state, safe to kill any pod at any time.
- Structured output with schemas. Use structuredContent and output schemas so callers get machine-checkable results instead of prose to re-parse.
- Honest tool annotations. Mark tools read-only, destructive, or idempotent accurately — clients increasingly gate approval flows on these hints, and lying in annotations is how you end up auto-approved for a destructive action.
- OpenTelemetry from day one. The spec now formalizes trace-context propagation in _meta. You want one trace that crosses agent, gateway, MCP server and downstream API, and you want per-tool, per-client invocation metrics the day something goes wrong. That trace is also the raw material for a SOC 2-ready agent audit trail.
- Sandbox execution-class tools. Anything that shells out, evaluates code, or fetches arbitrary URLs runs in a container or WASM sandbox with an egress allowlist, treated exactly like running untrusted code.
- Version your tools like an API. Deterministic tools/list ordering, semantic versions in descriptions, deprecation windows. Your tool schema is a public contract the moment one external client depends on it.
A gateway helps with the cross-cutting parts — central token validation, rate limits, routing on the new Mcp-Method headers — and the market has matured into a real product category. But a gateway does not fix a server that passes tokens through or trusts its own tool results. The boundary that matters is in your handler code.
FAQ
We already expose a REST API. Why not just let the agent call that?
Function-calling against your REST API works for one model in one codebase you control. MCP buys you the same tools working in Claude, ChatGPT, Copilot, Gemini and whatever your customers run, with discovery, schemas, annotations and auth standardized. If your tools will only ever be called by your own backend, you may not need MCP. The moment a customer asks to plug your product into their agent, you do.
stdio or Streamable HTTP?
stdio is for local, single-user tools — a developer's machine, a desktop app. Anything multi-user, remote, or customer-facing is Streamable HTTP behind TLS with OAuth. The 2026-07-28 revision makes remote servers dramatically easier to operate, and old-style HTTP+SSE is deprecated and scheduled for removal, so do not start anything new on it.
Which SDK should we build on?
TypeScript and Python are the tier-one implementations and track the spec fastest; both shipped betas for the 2026-07-28 revision within days. C# hit 2.0 with the new revision and is the natural choice in Microsoft shops. Go is production-ready in practice. Whatever you pick, pin the protocol revision you target and test against more than one client.
How do we connect it to our IdP?
Your server publishes protected resource metadata naming your IdP as the authorization server; clients discover it from the 401, obtain a token with your server as the resource, and you validate issuer, audience and scopes on each request. No custom login flows, no password handling in the server. With Client ID Metadata Documents you also skip open dynamic registration — you allowlist client URLs instead.
Is it safe to expose an MCP server to customers at all?
Yes, with the same seriousness you expose any authenticated API — plus the poisoning defenses above, because this API's consumer is a language model that believes what it reads. The companies that had incidents in 2025 were not careless startups; they were teams that shipped the quickstart architecture into a threat model it was never designed for. If any of your customers are in the EU, an agent-facing surface also pulls you into EU AI Act obligations.
Building or hardening an MCP server is exactly the kind of engagement we do — our MCP server development practice covers architecture, auth wiring against your IdP, the security review, and the migration to the stateless revision. If you want a second pair of eyes before you put an agent-facing surface in front of customers, talk to us.

