Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Multi-server composition
TL;DR: A host connected to several servers runs one client per server, each an isolated scope for capabilities, state, and credentials. The host merges what those servers expose into a single coherent surface for the model and the user: it aggregates each server’s capabilities, namespaces them so two servers’ tools never collide, and routes every call back to the server that owns it. Servers never compose with each other directly; every cross-server step is mediated by the host. On Vercel this is concrete: each server is its own project at its own URL, reached over the Streamable HTTP transport, and there is no private path between projects for servers to talk behind the host’s back. Under MCP 2026-07-28 the wire cooperates with this picture: the protocol session and its Mcp-Session-Id header are gone (SEP-2567), every request is self-contained, and the only thing that groups calls to one server is the host’s own bookkeeping.
Plain-language explanation
A server only knows about itself. It answers tools/list with its tools and resources/list with its resources, and it has no idea the host has five other servers connected. Composition, turning “six deployments, each with a few tools” into “one tool menu the model can pick from”, happens entirely on the host side.
The host does four things no server can do for it:
- Connect to each server with its own client. There is no initialization handshake under 2026-07-28: every request carries the protocol version and client capabilities in
_meta(io.modelcontextprotocol/protocolVersion,io.modelcontextprotocol/clientCapabilities), and the client may call the mandatoryserver/discoverRPC up front to learn each server’s supported versions, capabilities, and identity (SEP-2575). - Aggregate the capabilities each server advertises during discovery into one merged view.
- Namespace the merged entries so a
readtool on a files server and areadtool on a mail server stay distinct. - Route each invocation back to the one client that owns the named tool, applying consent before anything destructive runs, and routing any Multi Round-Trip Request retry back to the same server that asked for it.
Architecture
Diagram source (Mermaid)
flowchart TB
user((User)) --- host
subgraph host["Host: aggregate, namespace, route, gate consent"]
agg["Merged tool list: files.read, mail.send"]
ca[Client A]
cb[Client B]
end
agg -. "route files.read" .-> ca
agg -. "route mail.send" .-> cb
ca -- "Streamable HTTP, self-contained requests" --> sa["Vercel project: files server"]
cb -- "Streamable HTTP, self-contained requests" --> sb["Vercel project: mail server"]
sa x-.-x sbThe crossed dashed line between the two servers is the boundary that must never be crossed directly: server A cannot call server B, read its state, or see its results. If the output of files.read is to feed mail.send, the host carries it across, deliberately, never the servers themselves.
Protocol detail
- Capabilities are per server, and they are discovered, not negotiated. 2026-07-28 removed the
initialize/notifications/initializedhandshake (SEP-2575). Each client learns a server’s supported protocol versions, capabilities, and identity from the mandatoryserver/discoverRPC, or opportunistically from theio.modelcontextprotocol/serverInfoa server SHOULD return in each result’s_meta; the client’s own capabilities travel to the server in every request’s_meta. One server advertisingtoolssays nothing about another. Track capabilities per server, never globally, and expectUnsupportedProtocolVersionErrorwhen a server does not speak the version a request declares. - Discovery is per server. The host calls
tools/list(andresources/list,prompts/list) on each client and merges the results. Every merged entry must carry the server it came from, or later routing is guesswork. - List results now say how long they are good for. Results of
tools/list,prompts/list,resources/list,resources/read, andresources/templates/listcarry requiredttlMsandcacheScopefields (SEP-2549). The host may cache each server’s slice of the merged view for at mostttlMs, then re-run discovery; acacheScopeof"private"means the cached entries must not be shared across principals, which matters the moment the host serves more than one user. - Names are server-local, so the host must namespace. Tool names are unique only within a server; across servers they collide freely. The host derives a composed identifier, for example prefixing with a stable server id (
files.read,mail.send). The original unprefixed name is what goes over the wire in the eventualtools/call; the prefix is a host-side routing key, not part of the call. The 2026-07-28 spec’s tool-naming guidance (letters, digits, underscore, hyphen, dot; names SHOULD be unique within a server) governs the server-local name; the host’s prefix is a second, complementary layer on top. - Change notifications are opt-in via
subscriptions/listen. The HTTP GET stream andresources/subscribe/resources/unsubscribeare replaced by one long-lived POST-response stream per server carrying the change notifications the client opted into (toolsListChanged,promptsListChanged,resourcesListChanged,resourceSubscriptions), tagged withio.modelcontextprotocol/subscriptionId(SEP-2575). On a change, re-run discovery for that server and refresh the merged view; without a subscription,ttlMsexpiry is the refresh signal. - Every result carries
resultType, and routing must survive a retry. Results are"complete"or"input_required"(SEP-2322). Under Multi Round-Trip Requests (MRTR), the pattern that replaces server-initiated requests, a server that needs more input returns anInputRequiredResultwhoseinputRequestscarry what it needs; the client retries the original request withinputResponses, and the server correlates the retry viarequestState. In a composed surface the retry must go back to the owning server carrying therequestStatethat server issued, which is one more reason every merged entry keeps its origin. Treat results from earlier-protocol servers that omitresultTypeas"complete". - Filter per principal. When the host serves multiple users, filter the merged list to what the current authenticated principal may use; see Where the principal comes from. The merged surface is host state, so per-user filtering is a host decision.
Composition when every server is a deployment
The protocol mechanics above are platform-agnostic. Vercel adds four operational facts the host must design for:
- One origin, one credential per server. Each composed server is a distinct URL with, if protected, its own access token. RFC 8707 audience binding means a token minted for server A MUST be rejected by server B, so the host keeps a strict per-server credential map and never reuses a bearer token across origins; see credential brokering.
- There is no session to lose anymore. 2026-07-28 removed the protocol session outright (SEP-2567): no
Mcp-Session-Id, no server-side connection state for a redeploy or an idle-instance recycle to invalidate. Cross-call state is explicit server-minted handles passed as ordinary tool arguments, and a handle from server A is host-held data that only the host decides to carry anywhere. What can still break mid-flight is a response stream: SSE resumability is gone (noLast-Event-ID, no redelivery), so a broken stream loses the in-flight request and the client MUST re-issue it as a new request with a new request ID (SEP-2575). And a redeploy can still change the tool list, so honorttlMsand re-discover rather than trusting a stale merged view. Serverless sessions covers how the protocol and the platform converged on this model. - The registry is host state. Which servers to compose, at which URLs, with which credentials, is configuration the host owns and should treat as an inventory (URL, version, owner, credential); a quietly added registry entry is a user compromise waiting to happen.
- Preview URLs are not production. Composing against a preview deployment requires Deployment Protection plus its bypass secret, held per server in the host’s credential store, and preview surfaces should never be merged into a production user’s tool list.
Why servers do not compose directly
Letting one server call another would collapse the trust boundary the host exists to hold. Each client’s scope is isolated by design: a server must not see the conversation, the model’s full context, or another server’s handles and results. Host-mediated composition preserves that isolation; a compromised or prompt-injected server can only return data to the host, which decides whether any of it reaches another server. Direct server-to-server calls would route around every consent prompt and every isolation guarantee. On Vercel the platform topology backs the discipline: separate projects share no private network, so the mediated path through the host is the only path there is. The orchestrator pattern is this discipline applied to agent systems.
Consent in a composed surface
Consent is per server and per tool, and only the host can enforce it: a server can declare a tool destructive via tool annotations, but only the host can gate it. In a composed surface that means:
- Approval granted for
files.readimplies nothing aboutmail.send; never infer consent across servers from a prior grant. - The gate is fail-closed: an indeterminate or missing consent decision denies the call.
- The user must be able to see which server a tool belongs to before approving it, which is one more reason the namespaced identifier matters.
- An MRTR retry is a dispatch like any other: the
inputResponsesit carries go through the same per-server gate before they leave the host. Consent UX covers the prompt itself and the retry path.
Common pitfalls
- Flattening names and losing the origin. Merging tools into one list without recording each entry’s server makes routing impossible and invites collisions. Keep the server id on every entry.
- Caching the merged list past its terms. The spec now states the cache contract: keeping a server’s list beyond its
ttlMs, sharing a"private"cacheScoperesult across principals, or ignoring subscribed change notifications leaves the host calling tools that no longer exist behind that URL. - Treating capabilities as global. Assuming every server supports a capability because one does earns you method-not-found failures; discovery is per server.
- Sharing one token across servers. Audience binding will reject it at best; at worst a lax server accepts a token that was never meant for it. One credential per server, always.
- Replaying
requestStateagainst the wrong server. An MRTR retry routed to any server but the one that issued therequestStatefails at best; at worst it hands one server another server’s request state across the boundary the composition exists to keep. - Inferring consent across servers. A shared “always allow” across the whole composed surface defeats the boundary the composition exists to keep.
Example implementation
examples/orchestrator-host(in the repository) - the runnable host-side example. It composes two of the repository’s servers (examples/minimal-server(in the repository) andexamples/secure-tools-server(in the repository)), running one client per server; in its tests both servers are wired through in-memory transport pairs so the whole flow is deterministic and offline, while the composition logic is exactly what a host runs against deployed URLs. 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.
When you run its tests, watch the composition steps in order: two clients independently connected; a merged list in which the bare tool names are asserted absent and only the <server>.<tool> forms appear; a namespaced call routed back to the owning client; the fail-closed consent gate blocking the destructive tool before dispatch; and a server isError result surfaced as a typed host error, never as a success.
Related
- orchestrator - the agent-systems pattern built on host-mediated multi-server composition, including its Vercel mapping.
- trust-boundaries - why each client’s scope is isolated and composition is host-mediated.
- facade - the server-side alternative: one deployment fronting many backends, when per-backend isolation is not required.
- Serverless sessions - how 2026-07-28’s stateless model and the platform’s function-invocation model converged.
- Where the principal comes from - per-principal filtering of the merged surface.
- MCP internals overview - the host, client, and server roles and the request lifecycle.
Bibliography
- Model Context Protocol Specification, Architecture, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/architecture
- Model Context Protocol Specification, Versioning (
server/discover,UnsupportedProtocolVersionError), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning - Model Context Protocol Specification, Base Protocol (protocol version, capabilities, and client/server info in
_meta), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/index - Model Context Protocol Specification, Streamable HTTP transport (stateless requests,
subscriptions/listen, broken-stream re-issue), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http - Model Context Protocol Specification, Multi Round-Trip Requests (
resultType,inputRequests,requestState), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr - Model Context Protocol Specification, Tools (tool-naming guidance,
ttlMsandcacheScopeon list results), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/server/tools - Model Context Protocol Specification, Authorization (RFC 8707 audience binding), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- Model Context Protocol Specification, Changelog (SEP-2567 sessions removed, SEP-2575 stateless initialization, SEP-2549 cacheable results, SEP-2322 MRTR), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Vercel Documentation, Deploy MCP servers to Vercel - https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel