# Multi-server composition

Canonical URL: https://vercel-mcp-reference.vercel.app/client-side/multi-server-composition/
Markdown: https://vercel-mcp-reference.vercel.app/client-side/multi-server-composition.md
Audience: engineer, architect, security. MCP spec version: 2026-07-28. Last reviewed: 2026-08-26. Status: stable.

**TL;DR:** A host connected to several servers runs **one [client](https://vercel-mcp-reference.vercel.app/glossary/#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](https://vercel-mcp-reference.vercel.app/glossary/#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](https://vercel-mcp-reference.vercel.app/glossary/#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:

1. **Connect** to each server with its own client. There is no [initialization](https://vercel-mcp-reference.vercel.app/glossary/#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 mandatory `server/discover` RPC up front to learn each server's supported versions, capabilities, and identity (SEP-2575).
2. **Aggregate** the capabilities each server advertises during [discovery](https://vercel-mcp-reference.vercel.app/glossary/#discovery) into one merged view.
3. **Namespace** the merged entries so a `read` tool on a *files* server and a `read` tool on a *mail* server stay distinct.
4. **Route** each invocation back to the one client that owns the named tool, applying [consent](https://vercel-mcp-reference.vercel.app/glossary/#consent) before anything destructive runs, and routing any Multi Round-Trip Request retry back to the same server that asked for it.

## Architecture

```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 sb
```

The 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/initialized` handshake (SEP-2575). Each client learns a server's supported protocol versions, capabilities, and identity from the mandatory `server/discover` RPC, or opportunistically from the `io.modelcontextprotocol/serverInfo` a server SHOULD return in each result's `_meta`; the client's own capabilities travel to the server in every request's `_meta`. One server advertising `tools` says nothing about another. Track capabilities per server, never globally, and expect `UnsupportedProtocolVersionError` when a server does not speak the version a request declares.
- **Discovery is per server.** The host calls `tools/list` (and `resources/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`, and `resources/templates/list` carry required `ttlMs` and `cacheScope` fields (SEP-2549). The host may cache each server's slice of the merged view for at most `ttlMs`, then re-run discovery; a `cacheScope` of `"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 eventual `tools/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 and `resources/subscribe`/`resources/unsubscribe` are replaced by one long-lived POST-response stream per server carrying the change notifications the client opted into (`toolsListChanged`, `promptsListChanged`, `resourcesListChanged`, `resourceSubscriptions`), tagged with `io.modelcontextprotocol/subscriptionId` (SEP-2575). On a change, re-run discovery for *that* server and refresh the merged view; without a subscription, `ttlMs` expiry 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 an `InputRequiredResult` whose `inputRequests` carry what it needs; the client retries the original request with `inputResponses`, and the server correlates the retry via `requestState`. In a composed surface the retry must go back to the owning server carrying the `requestState` that server issued, which is one more reason every merged entry keeps its origin. Treat results from earlier-protocol servers that omit `resultType` as `"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](https://vercel-mcp-reference.vercel.app/security/identity-and-principals/). 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](https://vercel-mcp-reference.vercel.app/client-side/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 (no `Last-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 honor `ttlMs` and re-discover rather than trusting a stale merged view. [Serverless sessions](https://vercel-mcp-reference.vercel.app/internals/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](https://vercel-mcp-reference.vercel.app/glossary/#preview-deployment) requires [Deployment Protection](https://vercel-mcp-reference.vercel.app/glossary/#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](https://vercel-mcp-reference.vercel.app/glossary/#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](https://vercel-mcp-reference.vercel.app/patterns/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](https://vercel-mcp-reference.vercel.app/glossary/#tool-annotation), but only the host can *gate* it. In a composed surface that means:

- Approval granted for `files.read` implies nothing about `mail.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 `inputResponses` it carries go through the same per-server gate before they leave the host. [Consent UX](https://vercel-mcp-reference.vercel.app/client-side/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"` `cacheScope` result 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 `requestState` against the wrong server.** An MRTR retry routed to any server but the one that issued the `requestState` fails 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) and `examples/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-handler` 2.1.1 on `@modelcontextprotocol/server` 2.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 SDK `Client` defaults 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](https://vercel-mcp-reference.vercel.app/patterns/orchestrator/) - the agent-systems pattern built on host-mediated multi-server composition, including its Vercel mapping.
- [trust-boundaries](https://vercel-mcp-reference.vercel.app/patterns/trust-boundaries/) - why each client's scope is isolated and composition is host-mediated.
- [facade](https://vercel-mcp-reference.vercel.app/patterns/facade/) - the *server*-side alternative: one deployment fronting many backends, when per-backend isolation is not required.
- [Serverless sessions](https://vercel-mcp-reference.vercel.app/internals/serverless-sessions/) - how 2026-07-28's stateless model and the platform's function-invocation model converged.
- [Where the principal comes from](https://vercel-mcp-reference.vercel.app/security/identity-and-principals/) - per-principal filtering of the merged surface.
- [MCP internals overview](https://vercel-mcp-reference.vercel.app/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, `ttlMs` and `cacheScope` on 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>
