Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
MCP internals overview
Plain-language explanation
TL;DR: MCP connects an AI application (the host) to outside systems (servers) through a small middleman (the client). They speak a structured message format called JSON-RPC. As of the 2026-07-28 revision, MCP is a stateless protocol: there is no handshake and no session. Every request carries its own protocol version and capability declarations, and every result says what kind of result it is. On Vercel, the server side is a Vercel Function behind an HTTP route, not a long-running program, and the protocol now matches that shape natively instead of merely tolerating it.
SDK status. Wire status: the pinned stack (
mcp-handler2.1.1 on@modelcontextprotocol/server2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDKClientdefaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.
Think of MCP as a standard way for an AI assistant to talk to tools, the way a web browser talks to web servers. The user’s application (a chat app, an IDE, a desktop assistant) is the host. It owns the screen, the user’s trust, and the model. The host opens connections to backend programs called servers; each server knows how to do one job: read a calendar, query a database, run a build. Between them sits a client, one client per server, all living inside the host. The host runs as many clients as it has servers connected.
Earlier revisions began with a handshake: each side said hello, listed its features, and waited for acknowledgment before anything else could happen. The 2026-07-28 revision deletes that opening ceremony. A client simply sends its first request, and the request itself carries everything the server needs to answer it: which protocol version the client speaks, which optional features the client supports, and (optionally) who the client is. If the server cannot speak that version, it says so in a structured error listing the versions it can speak, and the client retries with one of those. A server also publishes a standing self-description via a mandatory server/discover method, which a client may call up front, or never.
Here is why this matters on Vercel. When your server is deployed as a function, there is no resident process to hold a conversation. Each message arrives as an HTTP request, Vercel runs your function to answer it, and nothing is remembered in between. Under the old model, both sides pretended a stateful session existed on top of that; Serverless sessions documents the workarounds it took. Under 2026-07-28 the pretense is gone: the protocol itself says every request must stand alone, and anything that has to span requests travels as an explicit handle in ordinary arguments.
Formal protocol perspective
MCP is layered on top of JSON-RPC 2.0. Every message is a JSON object with one of three shapes: a request (has an id and a method), a response (has an id and a result or error), or a notification (has a method but no id, so no response is expected). The MCP specification adds method names, metadata rules, and capability semantics on top of that base.
The load-bearing structure is the _meta field on every request. Two keys are required on every request: io.modelcontextprotocol/protocolVersion (the revision this request speaks) and io.modelcontextprotocol/clientCapabilities (the client capabilities relevant to this request). Clients SHOULD also send io.modelcontextprotocol/clientInfo, and servers SHOULD return io.modelcontextprotocol/serverInfo in every result’s _meta. A request missing a required _meta field is malformed and MUST be rejected with JSON-RPC -32602 (Invalid params). If the server does not implement the requested protocol version, it MUST answer with UnsupportedProtocolVersionError (-32022) listing its supported versions, and the client SHOULD retry with a mutually supported one. If a request needs a client capability the client did not declare on that request, the server MUST return MissingRequiredClientCapabilityError (-32021); a server MUST NOT rely on capabilities the client has not declared. Both ClientCapabilities and ServerCapabilities also carry an extensions map for negotiating optional extensions such as tasks.
Servers MUST implement server/discover, an RPC that returns the server’s supported protocol versions, capabilities, identity, and optional instructions. Calling it is optional for clients: it is a convenient single-request way to present a server’s identity and features, and on stdio it doubles as the backward-compatibility probe for detecting legacy servers. Its result is cacheable, like all discovery-shaped results.
The spec makes statelessness explicit: servers MUST NOT rely on prior requests over the same connection to establish context, and state that spans requests (long-running work, application-level handles) MUST be referenced by an explicit identifier the client passes on each request. There is no session, no Mcp-Session-Id header, and no initialization phase. Discovery in practice is just the list calls: tools/list, resources/list, and prompts/list, then tools/call, resources/read, or prompts/get as the user or model directs. Two more rules ride on results: every result MUST carry a resultType ("complete" for final results, "input_required" when the server needs more input under the MRTR pattern, where the server embeds its sampling, elicitation, or roots requests in the result and the client retries the original request with the answers), and results from the list and read methods MUST carry ttlMs and cacheScope caching hints. Server-initiated requests no longer exist as wire messages; see Capability primitives for the full MRTR treatment.
MCP specifies two standard transports, and on Vercel the choice is made for you. The Streamable HTTP transport carries each client message as its own HTTP POST to a single MCP endpoint; the server answers with either a single JSON body or a Server-Sent Events stream scoped to that request. Three headers are required on every POST: MCP-Protocol-Version (which MUST match the _meta version in the body), Mcp-Method (mirroring method), and, on tools/call, resources/read, and prompts/get, Mcp-Name (mirroring the tool name or URI). These mirrored headers exist so edge infrastructure can route, rate-limit, and filter without parsing bodies; servers MUST reject header/body mismatches with 400 and HeaderMismatch (-32020). Long-lived change notifications arrive on the response stream of a subscriptions/listen request rather than a GET stream, and broken streams are not resumable: the client re-issues the request with a new id. The stdio transport carries the same JSON-RPC payloads over a child process’s standard input and output; it exists on your laptop, not on Vercel. Only the framing changes between transports; the JSON-RPC semantics are identical. See Transports for the full rules.
On Vercel specifically, the deployable shape is a Next.js route handler created by mcp-handler, exported at a single route such as /api/mcp. Every JSON-RPC message is a fresh function invocation. Fluid compute may route several invocations to a warm instance, but that reuse is a performance optimization, never a correctness guarantee, and under 2026-07-28 the protocol finally agrees: correctness may not depend on landing warm.
Request / lifecycle flow
A first contact and tool invocation, as a deployed Vercel server sees it. Every solid arrow into the server is a separate HTTP POST, and each one may be a separate function invocation. Note what is absent: no handshake, no session header, no ordering gate:
Diagram source (Mermaid)
sequenceDiagram
participant Host
participant Client
participant Server as Vercel Function
Host->>Client: connect to server
Client->>Server: POST server/discover (_meta, optional call)
Server-->>Client: DiscoverResult (versions, capabilities, identity)
Client->>Server: POST tools/list (_meta on the request)
Server-->>Client: result (resultType complete, ttlMs, cacheScope)
Host->>Client: user or model selects a tool
Client->>Server: POST tools/call (name, args, _meta)
Server-->>Client: result (resultType complete)
Client-->>Host: surface resultDashed arrows are responses and results surfaced back to the host. Every client POST carries the MCP-Protocol-Version and Mcp-Method headers, and tools/call also carries Mcp-Name.
Version agreement is per request, not per connection. The negotiation, when it happens at all, is one structured error and one retry:
Diagram source (Mermaid)
sequenceDiagram
participant C as Client
participant S as Server
C->>S: any request (_meta protocolVersion)
alt server supports the requested version
S-->>C: result (resultType complete)
else version unsupported
S-->>C: error -32022 listing supported versions
C->>S: same request retried at a mutually supported version
endThere is no shutdown, because there is nothing to shut down: when the client is done, it stops sending requests. On Streamable HTTP, closing an in-flight request’s response stream is itself the cancellation signal for that request.
Key messages / state transitions
server/discover- client to server, request. No params beyond_meta. Servers MUST implement it; clients MAY call it. ReturnssupportedVersions,capabilities, optionalinstructions, and the server’s identity in result_meta; the result is cacheable viattlMs/cacheScope.tools/list- client to server, request. Returns the server’s currently exposed tools with requiredttlMsandcacheScopehints; servers SHOULD return tools in deterministic order. Re-issued after anotifications/tools/list_changed(delivered only on asubscriptions/listenstream the client opened).tools/call- client to server, request. Fields:name,arguments. Returns acontentarray and anisErrorflag, or an interimresultType: "input_required"result when the server needs client input first. See Capability primitives for the full result shape.subscriptions/listen- client to server, request. Opens one long-lived response stream carrying only the change-notification types the client opted into; the server acknowledges first and tags every notification withio.modelcontextprotocol/subscriptionId.notifications/cancelled- client to server, notification, stdio only. On Streamable HTTP, closing the request’s SSE response stream MUST be treated by the server as cancellation of that request.- Errors that replace lifecycle machinery -
UnsupportedProtocolVersionError(-32022) with the server’s supported version list,MissingRequiredClientCapabilityError(-32021) naming the missing capabilities, andHeaderMismatch(-32020) when required HTTP headers are absent or disagree with the body. - Removed in 2026-07-28 -
initialize,notifications/initialized,ping,logging/setLevel,notifications/roots/list_changed, theMcp-Session-Idheader, the HTTP GET listening stream, and SSE resumability. A modern-only server answers GET or DELETE on the MCP endpoint with405and ignores anyMcp-Session-Ida legacy client sends.
Common misconceptions
- Misconception: MCP is HTTP. Reality: MCP is a JSON-RPC application protocol that runs over a chosen transport. Streamable HTTP is the transport that matters on Vercel, but stdio carries identical JSON-RPC semantics for local development. See the spec’s Transports section.
- Misconception: A client must call
server/discoverbefore anything else. Reality: It MAY. A client is free to send any RPC cold and handleUnsupportedProtocolVersionErrorif the version does not line up.server/discoveris mandatory to implement, optional to call. - Misconception: Capabilities are negotiated once per connection. Reality: There is no connection-scoped negotiation anymore. The client declares relevant capabilities in
_metaon every request, and the server MUST NOT rely on anything not declared on the request it is processing. A warm function instance that remembers the last request’s capabilities is caching, not negotiating. - Misconception: Something must track the session for the protocol to work. Reality: The 2026-07-28 revision removed protocol-level sessions entirely. Cross-request state is carried by explicit server-minted handles passed as ordinary arguments, which is exactly what serverless sessions always had to do anyway.
- Misconception: One client can talk to many servers. Reality: One client maps to exactly one server. A host that connects to many servers runs many clients in parallel. The orchestrator pattern builds on exactly this.
- Misconception: Discovery happens once. Reality: Lists are point-in-time snapshots with explicit freshness: every list result carries
ttlMsandcacheScope, and a client that wants push-based invalidation opts intonotifications/tools/list_changed(and the resource and prompt equivalents) viasubscriptions/listen.
Debugging notes
- Symptom: every request gets HTTP 400 with a
-32602error mentioning_meta. Likely cause: the client is not sending the requiredio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesfields, which usually means a legacy (2025-11-25 or earlier) client talking to a modern-only server. Where to look: the request body’sparams._metain the client’s HTTP layer. - Symptom: HTTP 400 with error code
-32020. Likely cause: a missing or mismatchedMCP-Protocol-Version,Mcp-Method, orMcp-Nameheader; proxies and header-rewriting middleware are the usual culprits. Where to look: compare the headers and the JSON body of the rejected request; they must agree. - Symptom: HTTP 400 with error code
-32022. Likely cause: version mismatch. This is normal negotiation, not an outage: readerror.data.supportedand retry at a version both sides speak. Where to look: the error body, then your client’s version-retry logic. - Symptom:
tools/callfails with-32601(HTTP 404). Likely cause: the server does not implement the method, or the endpoint is not a modern MCP endpoint at all. Where to look: the JSON-RPC error body (a modern server returns one; a bare 404 with no modern error body suggests a legacy server or a wrong URL), and thecapabilitiesinserver/discover. - Symptom: long tool calls die at a suspiciously round wall-clock time. Likely cause: the function hit its
maxDurationceiling (300s on Hobby; 800s on Pro and Enterprise) and Vercel ended the invocation mid-stream. The stream is not resumable; the client must re-issue the request. Where to look: function duration in the Vercel runtime logs, themaxDurationsetting invercel.json, and the async-jobs pattern when the work genuinely needs longer. - Useful observability hooks: log every JSON-RPC frame with
id,method, direction, and elapsed time. With sessions gone, correlate across requests by the OpenTelemetrytraceparentkey in_meta(a reserved key as of 2026-07-28) plus the authenticated principal. On Vercel, structuredconsole.logoutput lands in runtime logs and can be forwarded via log drains; see Observability.
Security implications
The host-server boundary is the most important trust boundary MCP creates. A server should be treated as untrusted external code: it returns text the model will read, structured data the host might render, and under MRTR it can embed requests for model inference or user input inside its results. The host is responsible for showing the user what the server is and what it can do, and for gating sensitive actions with consent. Note that clientInfo and serverInfo are self-reported and unverified; the spec says implementations SHOULD NOT use them for security decisions.
On Vercel, every MCP server is a remote server. There is no cozy local-subprocess trust model to fall back on: your endpoint is reachable from the public internet the moment it deploys, so authentication is mandatory in practice even though the spec words it as a SHOULD. Wire it up per Authorization, and treat the transport rules as load-bearing: the server MUST validate the Origin header and answer 403 when it is present and invalid (this is the DNS-rebinding defense), and a preview deployment is a publicly reachable URL unless Deployment Protection is enabled. An unprotected preview of your MCP server is a second, forgotten production endpoint.
The removal of sessions removes a whole attack class and relocates another. There is no protocol session to hijack anymore, but the things that replace it inherit its duties: server-minted handles passed as tool arguments are capabilities and must be scoped, expiring, and bound to the verified principal from the auth token (see the security checklist), and the opaque requestState blob that rides MRTR retries is attacker-controlled input the server MUST integrity-protect if it influences authorization or business logic. Multi-tenant deployments key every piece of per-user state to the verified principal, never to anything the client can mint or replay; Identity and principals explains why. The required Mcp-Method and Mcp-Name headers are a defensive gift on Vercel: WAF rules and per-tool rate limits can act at the edge without body inspection, as long as the server enforces the header/body match the spec mandates.
Runnable example
The smallest end-to-end demonstration in this repository:
- Example:
examples/minimal-server(in the repository)
Run npm run dev and point MCP Inspector at http://localhost:3000/api/mcp over Streamable HTTP. Be aware of what you will capture: the server serves both eras, so the exchange depends on the client. An Inspector build (or SDK Client) that still opens with the legacy initialize handshake at protocol version 2025-11-25 shows the legacy shape; a curl with the 2026-07-28 headers shows the normative exchange. The annotated message trace presents the 2026-07-28 frames one by one, with the curl that produces them, and summarizes the legacy shape for comparison.
Related
- Transports - the full Streamable HTTP framing, header, and streaming rules this page only sketches.
- Serverless sessions - how the 2026-07-28 statelessness model maps onto function invocations, and where cross-request state now lives.
- Annotated message trace - the exchange above, frame by frame.
- Capability primitives - tools, resources, prompts, and the MRTR pattern that replaced server-initiated requests.
- The 2026-07-28 stateless revision - the full change set from 2025-11-25, with SEP references.
- Security overview - the authorization and identity rules that ride on this protocol.
Bibliography
- Model Context Protocol Specification, Versioning and Compatibility, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning
- Model Context Protocol Specification, Overview (base protocol,
_meta, error codes), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/index - Model Context Protocol Specification, Transports, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/transports
- Model Context Protocol Specification, Streamable HTTP, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
- Model Context Protocol Specification, Discovery (
server/discover), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/server/discover - Model Context Protocol Specification, Multi Round-Trip Requests, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr
- Model Context Protocol Specification, Architecture, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/architecture
- Model Context Protocol Specification, Key Changes, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Model Context Protocol, Security Best Practices - https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- Vercel Documentation, Deploy MCP servers to Vercel - https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel
- Vercel Documentation, Fluid compute - https://vercel.com/docs/fluid-compute
- Vercel Documentation, Vercel Functions - https://vercel.com/docs/functions
- vercel/mcp-handler, project repository - https://github.com/vercel/mcp-handler
- JSON-RPC 2.0 Specification - https://www.jsonrpc.org/specification
- Model Context Protocol, official site - https://modelcontextprotocol.io