Virtual MCP Servers
A Virtual MCP server is the endpoint AI clients connect to. It's not an upstream MCP server itself — it's the gateway's curated front for one Origin MCP. Each Virtual MCP has a slug, a public URL, a chosen subset of the Origin's tools, and an optional list of teams allowed to use it.
When a client like Claude Desktop, Cursor, ChatGPT, or Claude Code calls a Virtual MCP, the gateway:
- Authenticates the client against the project's OAuth identity provider.
- Checks that the user's team membership allows access to this Virtual MCP.
- Resolves the upstream credential (per-user OAuth, API key, or none) for the Origin behind this Virtual MCP.
- Forwards the MCP JSON-RPC call to the upstream and filters the response based on the configured tool selection.
Each Virtual MCP routes to exactly one upstream MCP server. To expose two upstreams, publish two Virtual MCPs. This 1:1 mapping keeps tool names unambiguous, makes upstream credentials cleanly scoped per route, and is how the gateway is built end to end — both in the Portal and in code config.
This page walks through creating and configuring a Virtual MCP in the portal,
then shows the equivalent route in routes.oas.json for projects configured in
code.
Anatomy
Every Virtual MCP has four pieces of state:
- Name — what humans call it. Shown in the catalog and on the card in the Virtual MCP list.
- Description — a one-line summary of what the server exposes.
- Slug — the URL-safe identifier. The public URL is built from the slug as
${deploymentUrl}/v1/mcp/{slug}. - Tools — the subset of tools, drawn from one or more Origins, that this Virtual MCP exposes.
Teams can be assigned separately and govern who is allowed to authenticate to the Virtual MCP. See Teams for the access-control model.
The public URL
The URL a client connects to has the form:
Code
deploymentUrl is the project's deployment URL (the one shown in the project
overview), and {slug} is the Virtual MCP's slug. A slug like linear-prod in
a project deployed at https://gateway-main-abc123.zuplosite.com produces:
Code
That's the URL clients paste into their MCP configuration. The gateway's catalog page generates the snippet for each supported client (Claude Desktop, ChatGPT, Cursor, VS Code) and copies it ready-to-use.
Configure in the portal
The portal flow has three parts: create the Virtual MCP, pick tools, and assign teams. Origins should already exist — see Origin MCP Servers if not.
Step 1: Create the Virtual MCP
Open MCP Virtual MCP in the Zuplo Portal. When the project has no Virtual MCPs yet, the empty state reads "Let's add your first Virtual MCP" with an Add Virtual MCP button. Once at least one exists, the same button is in the top-right.
-
Click Add Virtual MCP.
-
In the Create Virtual MCP dialog, fill in:
Field Required Notes Name Yes The display name in the catalog and the Virtual MCP list. Description No Shown on the card and in client connection instructions. Slug Yes Lowercase letters and hyphens only. Auto-derived from the name; override it when needed. -
Click Save.
The new Virtual MCP appears as a card in the list. Initially the card has no tools — that's the next step.
Step 2: Select tools
On the Virtual MCP card, click Select tools (or the N tools chip if some tools are already selected). The Tools dialog opens as a split view:
- Left column — Origin MCPs. Every Origin in the project is listed, collapsible. Each tool appears as a card with a toggle. A search box filters tools across all Origins. An Enable all button selects every tool from an Origin at once.
- Right column — Virtual MCP. The tools currently exposed by this Virtual MCP. Hover a card to reveal a Remove button. A Disable all button drops every tool from a single Origin.
Pick tools by toggling them on the left; they appear on the right. The picker lets you browse every Origin, but a Virtual MCP binds to exactly one upstream Origin. To expose two upstreams, publish two Virtual MCPs.
When the selection is complete, click Save. The tool count chip on the card updates.
If the left column shows "No origins found. Please add an origin MCP to get started.", the project doesn't have any Origins yet. Add one from the Origin MCP page first.
Step 3: Assign teams
By default, members of all project teams can connect to a Virtual MCP. To restrict access:
- On the Virtual MCP card, click the Teams chip in the footer.
- In the popover, check each team that should have access. Indented entries are sub-teams.
- Click Save.
Only members of the checked teams can authenticate to this Virtual MCP. See Teams for the full access-control model, including roles and the team tree.
If the project has no teams yet, the popover shows a link to create one.
Step 4: Connect a client
Once tools are selected and teams are assigned, the Virtual MCP is ready to serve clients. From the Catalog, click the Virtual MCP's Configuring the MCP chip to open the connection instructions dialog. The dialog has tabs for Claude, ChatGPT, Cursor, and VS Code; each tab shows a ready-to-paste config snippet with the Virtual MCP's URL filled in. Cursor also shows a one-click install button.
The first connect for each user triggers the gateway's OAuth flow — the client opens a browser, the user authenticates with the configured identity provider, and the gateway issues a bearer token bound to the Virtual MCP's URL. Subsequent calls reuse that token until it expires.
Configure with code
Every Virtual MCP in the portal maps to a route in routes.oas.json plus a
small set of inbound policies in policies.json. Projects that want full policy
control — including capability filtering,
per-route rate limits, or composition with other Zuplo policies — configure
routes directly.
The canonical layout — the same one Zuplo's own MCP Gateway dogfood project uses — pairs one route per upstream MCP server with two policies: an OAuth policy that authenticates inbound clients, and a token-exchange policy that resolves the upstream credential.
Route shape
A Virtual MCP route is a multi-method operation (get,post) that uses
McpProxyHandler as its handler and lists the inbound policies that handle
OAuth and upstream credentials.
Code
Required elements:
- Path —
/mcp/{slug}matches the portal pattern. The path can be anything, but/mcp/{slug}keeps the URL consistent with portal-managed Virtual MCPs. operationId— required for every MCP route. The operationId is the stable identity used for OAuth audience binding, upstream auth state, and analytics. Two MCP routes can't share anoperationId.get,post— Zuplo's multi-method shorthand.McpProxyHandlerrejectsGETwith HTTP 405 (no SSE streams) and forwardsPOSTto the upstream.handler—McpProxyHandlerexported from@zuplo/runtime/mcp-gateway.handler.options.rewritePattern— the upstream MCP server URL. The string must be static or use a single$env(...)reference; dynamic patterns derived from the request are rejected.policies.inbound— the OAuth policy (required for any authenticated MCP route) and, when the upstream needs credentials, the token exchange or header policy that resolves them.
Required policies
Two policies almost every MCP route uses:
Code
mcp-auth0-oauth-inbound(or the genericmcp-oauth-inboundfor non-Auth0 providers) authenticates the inbound client request and turns the gateway into an OAuth 2.1 Resource Server. Only one such policy can exist per project, shared across every MCP route.mcp-token-exchange-inboundresolves the upstream credential for this route. Each MCP route can have one of these; the policy reference covers the full options surface.
When the upstream uses an API key instead of OAuth, replace
mcp-token-exchange-* with
set-upstream-api-key-inbound
or set-headers-inbound. See
Origin MCP Servers for
a worked example.
Plugin registration
Code-configured projects need to load the MCP Gateway plugin once in
modules/zuplo.runtime.ts:
Code
The plugin registers the OAuth, callback, and upstream connection endpoints the gateway needs. It's a no-op if the project doesn't have any MCP-related policies, so it's safe to add even in projects that aren't yet using MCP.
Compatibility date
MCP Gateway routes require a compatibilityDate of 2026-03-01 or later in
zuplo.jsonc:
Code
The mcp-token-exchange-inbound policy uses chained response hooks for the
upstream 401 retry semantics, which landed on that compatibility date. Older
dates may silently drop the retry hook, leading to confusing intermittent
failures the first time an upstream token needs refreshing.
How portal Virtual MCPs map to routes
Each Virtual MCP managed in the portal corresponds to a route the runtime generates for you. The mapping is direct:
| Portal field | Route equivalent |
|---|---|
| Slug | The path segment in /mcp/{slug} |
| Name + Description | OpenAPI summary and description |
| Selected tools | A capability filter applied to the upstream tools/list |
| Assigned teams | Portal-side metadata; enforcement is a near-term roadmap item |
| Origin URL | McpProxyHandler.options.rewritePattern |
The runtime generates the underlying route from the portal configuration so that
the same Virtual MCP behaves identically whether it was created in the portal or
written by hand. The portal abstraction is convenience over the same
routes.oas.json + policies.json foundation.
One Virtual MCP, one Origin
A Virtual MCP routes to exactly one Origin. The Portal's tool picker shows every tool from every Origin in the project so you can browse, but the saved Virtual MCP binds to a single upstream and exposes a curated subset of that upstream's tools.
To expose two upstreams, publish two Virtual MCPs — one per upstream. The multi-upstream pattern walks through the worked example with Linear and Stripe.
The 1:1 mapping is intentional: composite servers (one Virtual MCP fronting tools from multiple unrelated upstreams) make tool names ambiguous, scatter upstream credentials across one route, and create a maintenance burden for a use case that's better served by two clear routes. The Portal UI for multi-Origin selection is evolving; configure one Origin per Virtual MCP today.
Current portal limitations
Three places in the Virtual MCP UI today are placeholders. Plan around them or use code-config:
- Per-Virtual-MCP policies popover. The Select Policies chip on each
card opens a popover that reads "Policies will be available soon". To
apply policies — capability filtering, rate limits, request validation —
configure them on the route in
routes.oas.jsoninstead. See Capability Filtering for the most useful policy in this category. - Reload Tools menu item. The Reload Tools entry in the card's overflow menu is disabled. Tools refresh on a background schedule; there's no manual trigger yet.
- OAuth 2.0 Client ID popover. The OAuth 2.0 Info popover on each catalog card shows a placeholder client ID, not a real one. Treat it as scaffolding. Real OAuth client IDs are issued through Dynamic Client Registration when an MCP client connects.
These will be wired up as the product evolves. Until then, anything in the list above is best done in code-config.
Reference
- Origin MCP Servers — the upstream servers a Virtual MCP draws tools from.
- Teams — group members and restrict which Virtual MCPs each team can connect to.
- Capability Filtering — narrow the tools and resources a route exposes downstream.
- Policy reference:
- For a full working layout, see the route and policy patterns demonstrated throughout this page — one route per upstream, one OAuth policy shared across all MCP routes, and one token-exchange or header policy per route.