# Facade

Canonical URL: https://vercel-mcp-reference.vercel.app/patterns/facade/
Markdown: https://vercel-mcp-reference.vercel.app/patterns/facade.md
Audience: engineer, architect, security, non-technical. MCP spec version: 2026-07-28. Last reviewed: 2026-08-26. Status: stable.

## Summary

A [facade](https://vercel-mcp-reference.vercel.app/glossary/#facade) (sometimes called a gateway) is a single MCP [server](https://vercel-mcp-reference.vercel.app/glossary/#server) that fronts many backend systems and exposes them through one unified set of [tools](https://vercel-mcp-reference.vercel.app/glossary/#tool) and [resources](https://vercel-mcp-reference.vercel.app/glossary/#resource). It collapses many integrations into one client-facing surface and centralizes policy enforcement. On Vercel the facade is one project and one [Vercel Function](https://vercel-mcp-reference.vercel.app/glossary/#vercel-function) route, with rewrites and the Firewall forming its front door at the edge.

## Problem addressed

When a host needs to reach a dozen backends, the per-backend [adapter](https://vercel-mcp-reference.vercel.app/patterns/adapter/) approach produces a dozen servers. Each one is small and isolated, but the [host](https://vercel-mcp-reference.vercel.app/glossary/#host) now manages a dozen endpoints, a dozen [discovery](https://vercel-mcp-reference.vercel.app/glossary/#discovery) rounds, a dozen credential rotations, and a dozen deploy cadences. Discovery latency grows linearly. Cross-cutting policy (rate limits, audit logging, naming conventions) has to be re-implemented in each server, and inevitably drifts.

The facade collapses this fanout into one server. The cost is loss of per-backend isolation; the benefit is one place to enforce policy and one connection to manage.

## When to use

- A single team owns many related backends and wants to expose them under one coherent vocabulary.
- Cross-cutting policy (auth, rate limiting, audit, redaction, naming) must be enforced uniformly and centrally.
- The number of backends is large enough that the operational cost of one project per backend outweighs the isolation benefit.
- All fronted backends sit at a similar trust level; none carries a credential so sensitive that it must not share a process.
- Client-side simplicity matters: the host should not have to discover, version, and approve many servers separately.

## When not to use

- Backends have different trust levels, blast radii, or credential sensitivity. Give each its own project (an [adapter](https://vercel-mcp-reference.vercel.app/patterns/adapter/), or the [sidecar](https://vercel-mcp-reference.vercel.app/patterns/sidecar/) shape) instead.
- Backends are owned by different teams that ship on different cadences; a shared facade becomes a coordination bottleneck.
- A backend can fail in ways that degrade unrelated backends sharing the same process (memory exhaustion, blocking I/O, a crashing native dependency).
- You want per-backend revocation: with a facade, revoking one backend means redeploying the whole surface.

## Architecture / flow diagram

```mermaid
flowchart LR
    Host[Host] --> Client[MCP Client]
    Client -->|Streamable HTTP| Edge[Firewall and rewrites]
    Edge --> Fn[Facade Function]
    Fn --> A[Backend A]
    Fn --> B[Backend B]
    Fn --> C[Backend C]
```

## Protocol implications

- One server covers all backends: a single `server/discover` identity and one round of [discovery](https://vercel-mcp-reference.vercel.app/glossary/#discovery) span the whole surface. MCP 2026-07-28 removed protocol sessions and the `initialize` handshake (SEP-2567, SEP-2575), so there is no per-connection state to multiply either; every request carries the protocol version and client capabilities in `_meta`.
- 2026-07-28 makes list results cacheable by contract: `tools/list`, `prompts/list`, `resources/list`, `resources/read`, and `resources/templates/list` results carry required `ttlMs` and `cacheScope` fields (SEP-2549). The facade's front door can now legitimately cache discovery for its whole aggregated surface, but scope honestly: a facade that filters listings per principal must mark them `"private"` so no shared cache serves one user's tool set to another; only a truly principal-independent listing may claim `"public"`.
- Return `tools/list` in deterministic order, which 2026-07-28 recommends for client caching and LLM prompt-cache hit rates. For a facade that means a stable sort across the aggregated registry, not per-backend registration accident, so the order survives backends being added or split out.
- Tool names should be namespaced by backend so model-generated calls are unambiguous and `tools/list` stays navigable. Tool names must be plain identifiers, so use underscores (`github_create_issue`, `jira_create_issue`) rather than dots. MCP's official [tool-naming guidance](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) (SEP-986) applies; follow it so the aggregated surface stays consistent and collision-free.
- Resource URIs should carry a scheme or prefix that identifies the backend (`github://...`, `blob://...`) to avoid collisions and keep audit logs unambiguous.
- If a backend's tools change at runtime, the `toolsListChanged` notification now travels on the `subscriptions/listen` stream that clients opt into (2026-07-28 replaced the HTTP GET notification stream, SEP-2575). Delivery still depends on a client actually holding that stream open; see [Serverless sessions](https://vercel-mcp-reference.vercel.app/internals/serverless-sessions/) before relying on it.
- Per-backend [progress notifications](https://vercel-mcp-reference.vercel.app/glossary/#progress-notification) and [cancellation](https://vercel-mcp-reference.vercel.app/glossary/#cancellation) travel on the originating request's response stream; the facade must route them by the originating request id.
- 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.

## Vercel mapping

- **One project, one route, a configurable front door.** [Routing Middleware](https://vercel-mcp-reference.vercel.app/glossary/#routing-middleware) or `vercel.json` [rewrites](https://vercel.com/docs/rewrites) map a stable public path to the MCP handler. That indirection is worth having: it lets you later split a backend out into its own project without breaking the URL clients were approved against.
- **The Firewall attaches at the same edge.** [Vercel Firewall](https://vercel.com/docs/vercel-firewall) rate-limit rules and custom WAF rules run before your function is invoked, so discovery floods and brute-force invocation traffic are dropped at the edge instead of billed as compute. 2026-07-28 requires `Mcp-Method` and `Mcp-Name` headers on every Streamable HTTP POST (SEP-2243), which lets WAF and rate-limit rules key on the exact method and tool being called, per-tool throttles for the facade's hottest backend, without inspecting request bodies. The facade is the choke point; put the throttle on the choke point.
- **The caveat that shapes everything: one process spans all backend credentials.** Every backend's environment variable is readable by the same function invocation. Vercel isolates per project, not per route, so a facade collapses the per-backend credential boundary by construction. If any credential is too sensitive to share a process, move that backend into its own project and let the host compose it via the [orchestrator](https://vercel-mcp-reference.vercel.app/patterns/orchestrator/) pattern.
- **Namespacing is code, not infrastructure.** The backend registry, the tool-name prefix, and the routing table live in `src/`; the platform sees one handler. Keep the registry data-driven so adding a backend is a table entry plus its tools, not a rewrite.

## Security considerations

- The facade is a single credential vault for many backends: a compromise of the facade is a compromise of every credential it holds. Compensate with strict [least privilege](https://vercel-mcp-reference.vercel.app/patterns/least-privilege/) on each backend credential. See [Authorization & scoping](https://vercel-mcp-reference.vercel.app/security/checklist/#authorization--scoping).
- Per-user authorization must be enforced server-side from the verified token's principal (the `AuthInfo` that `withMcpAuth` passes into handlers), never from a user identifier supplied by the client or the model. See [Authorization & scoping](https://vercel-mcp-reference.vercel.app/security/checklist/#authorization--scoping).
- Filter `tools/list`, `resources/list`, and `prompts/list` per principal so users only see what they may invoke. A facade that returns the union of every backend's capabilities to every user has effectively no access control. See [Authorization & scoping](https://vercel-mcp-reference.vercel.app/security/checklist/#authorization--scoping).
- Centralize input validation and output sanitization in the facade; it is the choke point, so a gap here is a gap for every backend at once. See [Input validation](https://vercel-mcp-reference.vercel.app/security/checklist/#input-validation) and [Output trust](https://vercel-mcp-reference.vercel.app/security/checklist/#output-trust).
- There is no per-backend egress allowlist inside one function (Static IPs on Pro and Enterprise give backends a fixed source to allowlist, and Secure Compute on Enterprise adds private connectivity, but neither filters what the function may call), so the routing table in code is your allowlist: never derive a backend target from model input, or prompt injection turns the facade into a network scanner. See [Trust boundaries](https://vercel-mcp-reference.vercel.app/security/checklist/#trust-boundaries).
- Log every tool invocation with the resolved backend, principal, and an argument hash, never raw secrets or results, and ship the log off-platform via log drains. See [Monitoring & audit](https://vercel-mcp-reference.vercel.app/security/checklist/#monitoring--audit).
- Put Firewall rate limits in front of the MCP endpoint and keep [preview deployments](https://vercel-mcp-reference.vercel.app/glossary/#preview-deployment) behind [Deployment Protection](https://vercel-mcp-reference.vercel.app/glossary/#deployment-protection); a public preview of a facade previews every backend at once. See [Deployment posture](https://vercel-mcp-reference.vercel.app/security/checklist/#deployment-posture).

A facade that fronts everything and filters nothing is not a gateway; it is a bigger attack surface with better ergonomics.

## Example implementation

- `examples/facade-server` (in the repository) - one namespaced surface over two in-process backends: a data-driven `BACKENDS` registry, underscore-namespaced tool names (`weather_get`, `directory_lookup`; tool names must be identifiers, so the dotted form above stays illustrative), a single `dispatch` choke point, and an audit log that records the backend and scope of every call but never keys or results. Its `BackendError` boundary contains an unexpected backend exception without forwarding it: `dispatch` mints a correlation id, hands the raw message and stack to an injectable `FaultLogger` (`setFaultLogger`; the default is `console.error`, which a Vercel log drain ships off-platform), and returns only the fixed text `backend "<name>" failed; see server logs for correlation id <uuid>` as an `isError` tool result. Expected failures (an unknown key, an unknown backend) pass through as ordinary `BackendError`s with no id and nothing logged. The tests `replaces the raw fault text with an opaque message and a correlation id` and `keeps the raw fault text out of the tool result and logs it server-side` inject a recording logger and assert the exception text is absent from every byte of the result while the logger received it under the same id, and `contains a fault as an isError result and the session survives` proves the shared handler keeps serving. It is explicit that single-process exception containment is not isolation: a crashing native dependency would still take down siblings, which is what the [sidecar](https://vercel-mcp-reference.vercel.app/patterns/sidecar/) shape is for.
- `examples/secure-tools-server` (in the repository) - the hardened single-surface skeleton: input validation, default-deny authorization, and output minimization on one tool. That is the per-tool discipline a facade must replicate across every backend it fronts.

## Trade-offs

| Pros | Cons |
|---|---|
| One project to deploy, monitor, version, and authenticate. | One process to lose: compromise blast radius spans every backend credential. |
| Central enforcement of policy, audit, and naming; one edge for Firewall and rate limits. | Backends share a process; one bad backend can degrade all. |
| Smaller client surface; one endpoint, one discovery round. | Lost per-backend revocation; removing one backend redeploys the whole surface. |
| One credential broker, one rotation cadence. | Coordination cost when multiple teams own the backends. |

## Related patterns

- [adapter](https://vercel-mcp-reference.vercel.app/patterns/adapter/) - the per-backend alternative; a facade is internally a collection of adapters.
- [sidecar](https://vercel-mcp-reference.vercel.app/patterns/sidecar/) - when per-backend isolation matters more than client simplicity.
- [orchestrator](https://vercel-mcp-reference.vercel.app/patterns/orchestrator/) - the host-side counterpart that composes multiple servers without collapsing their credentials into one process.
- [least-privilege](https://vercel-mcp-reference.vercel.app/patterns/least-privilege/) - required discipline for every credential a facade holds.
- [trust-boundaries](https://vercel-mcp-reference.vercel.app/patterns/trust-boundaries/) - explains the trust collapse a facade introduces and how to compensate.

## Vercel deployment (Terraform)

An illustrative Vercel expression of this pattern lives in `terraform/patterns/facade` (in the repository): one project whose rewrites fan out to the handler, with a Firewall configuration (rate limits and custom rules) at the front door, built with the official `vercel/vercel` provider. It is `tofu validate`-checked, never applied in CI. See `terraform/README.md` (in the repository) for scope and caveats.

## Bibliography

- Model Context Protocol Specification, *Architecture overview*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/architecture>
- Model Context Protocol Specification, *Server features*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/server>
- Model Context Protocol Specification, *Tools*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/server/tools>
- Model Context Protocol Specification, *Streamable HTTP transport*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http>
- Vercel Documentation, *Deploy MCP servers to Vercel* - <https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel>
- Vercel Documentation, *Routing Middleware* - <https://vercel.com/docs/routing-middleware>
- Vercel Documentation, *Rewrites* - <https://vercel.com/docs/rewrites>
- Vercel Documentation, *Vercel Firewall* - <https://vercel.com/docs/vercel-firewall>
- Vercel Documentation, *Static IPs* - <https://vercel.com/docs/networking/static-ips>
- Vercel Documentation, *Secure Compute* - <https://vercel.com/docs/networking/secure-compute>
- OWASP Top 10 for Large Language Model Applications - <https://owasp.org/www-project-top-10-for-large-language-model-applications/>
