# Origin MCP Servers

An **Origin MCP** is the upstream MCP server the gateway proxies to. It's the
real thing — a remote MCP server hosted by Linear, Stripe, GitHub, Grafana, your
own internal service, or any other provider — that exposes tools, prompts, and
resources over MCP's Streamable HTTP transport.

The gateway's job is to sit in front of one or more Origins, handle the
client-facing auth and policy, then forward each call upstream. Every Virtual
MCP server you publish through the gateway points at exactly one Origin — one
upstream MCP server per route — and exposes a curated subset of that upstream's
tools. Without an Origin, a Virtual MCP has nothing to call.

## Origin vs. Virtual MCP

The two concepts are easy to confuse. The split is deliberate:

|                        | **Origin MCP**                                          | **Virtual MCP**                            |
| ---------------------- | ------------------------------------------------------- | ------------------------------------------ |
| **Who configures it**  | Gateway operator                                        | Gateway operator                           |
| **Who connects to it** | The gateway (server-to-server)                          | AI clients (Claude, Cursor, ChatGPT, etc.) |
| **What it points at**  | A real upstream MCP server URL                          | A curated subset of one Origin's tools     |
| **Auth surface**       | Upstream provider's OAuth, API keys, or no auth         | Gateway-issued OAuth bearer tokens         |
| **URL**                | The provider's URL (e.g., `https://mcp.linear.app/mcp`) | `${deploymentUrl}/v1/mcp/{slug}`           |

See [Virtual MCP Servers](./virtual-mcp-servers.mdx) for the client-facing side.

## Configure in the portal

Open [**Origin MCP**](https://portal.zuplo.com/+/account/project/mcp/origin-mcp)
in the Zuplo Portal. When the project has no origins yet, the empty state reads
**"Let's add your first MCP"** with an **Add Origin MCP** button. Once at least
one origin exists, the same button appears in the top-right.

### Add an Origin

1. Click **Add Origin MCP**.
2. In the **Create Origin MCP** dialog, fill in the fields:

   | Field           | Required | Notes                                                           |
   | --------------- | :------: | --------------------------------------------------------------- |
   | **Name**        |   Yes    | A short label shown in the origins list and tool picker.        |
   | **Description** |    No    | A one-line summary of what this origin provides.                |
   | **Origin URL**  |   Yes    | The upstream MCP server URL, e.g. `https://mcp.linear.app/mcp`. |

3. Click **Create**.

The new origin appears in the list. The gateway calls the upstream's
`tools/list` to fetch the tools the origin exposes; once they load, the card's
**"N Tools available"** badge updates.

Click the badge to open the **Tools available** dialog, which lists each tool
the upstream advertises with its name and description.

### Edit or delete an Origin

On any origin card:

- The **Copy URL** button copies the upstream URL.
- The overflow menu has:
  - **Edit** — reopens the form with the existing values.
  - **Delete** — removes the origin. Tools that came from this origin are
    removed from any Virtual MCP that referenced them.
- The **Reload Tools** menu item is a placeholder in the current release and is
  disabled. Reloads happen automatically; there is no manual button yet.

## Authenticated upstreams

Many upstream MCP servers require authentication — most commonly OAuth or an API
key on every request. The portal form on the Origin dialog has fields for an
**API Key** and **Manual Headers**, but **these fields are not yet persisted on
submit**. Only the **Name**, **Description**, and **Origin URL** are saved.

Until upstream auth is wired through the portal, configure authenticated
upstreams in code. Two patterns cover most cases:

### Pattern 1: API key in a header

Use a standard Zuplo header policy on the route. The gateway adds the header on
its way to the upstream, and the client never sees the key.

```jsonc
// config/policies.json
{
  "name": "set-firecrawl-api-key-header",
  "policyType": "set-upstream-api-key-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime)",
    "export": "SetUpstreamApiKeyInboundPolicy",
    "options": {
      "value": "Bearer $env(FIRECRAWL_API_KEY)",
    },
  },
}
```

```jsonc
// config/routes.oas.json
"/mcp/firecrawl-v1": {
  "get,post": {
    "operationId": "firecrawl-mcp-server",
    "x-zuplo-route": {
      "corsPolicy": "none",
      "handler": {
        "module": "$import(@zuplo/runtime/mcp-gateway)",
        "export": "McpProxyHandler",
        "options": { "rewritePattern": "https://mcp.firecrawl.dev/v2/mcp" }
      },
      "policies": {
        "inbound": [
          "auth0-managed-oauth",
          "set-firecrawl-api-key-header"
        ]
      }
    }
  }
}
```

The route uses `McpProxyHandler` for the forwarding, an OAuth policy for the
client-facing auth, and `set-upstream-api-key-inbound` to attach the API key
before the request goes upstream. The `$env(...)` reference reads the value from
the project's environment variables — never check raw keys into source control.

For arbitrary header values (not necessarily API keys), use
[`set-headers-inbound`](../policies/set-headers-inbound.mdx) instead.

### Pattern 2: Per-user OAuth to the upstream

When the upstream provider supports OAuth and you want each end user to connect
with their own identity, use the
[`mcp-token-exchange-inbound`](../policies/mcp-token-exchange-inbound.mdx)
policy. The gateway runs the upstream OAuth flow on behalf of the user, stores
the resulting tokens encrypted, and injects an `Authorization: Bearer` header on
every request to the upstream.

```jsonc
// config/policies.json
{
  "name": "mcp-token-exchange-linear",
  "policyType": "mcp-token-exchange-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime/mcp-gateway)",
    "export": "McpTokenExchangeInboundPolicy",
    "options": {
      "displayName": "Linear",
      "protectedResourceMetadataUrl": "https://mcp.linear.app/.well-known/oauth-protected-resource",
      "authMode": "user-oauth",
      "scopes": [],
      "clientRegistration": { "mode": "auto" },
    },
  },
}
```

```jsonc
// config/routes.oas.json
"/mcp/linear-v1": {
  "get,post": {
    "operationId": "linear-mcp-server",
    "x-zuplo-route": {
      "corsPolicy": "none",
      "handler": {
        "module": "$import(@zuplo/runtime/mcp-gateway)",
        "export": "McpProxyHandler",
        "options": { "rewritePattern": "https://mcp.linear.app/mcp" }
      },
      "policies": {
        "inbound": [
          "auth0-managed-oauth",
          "mcp-token-exchange-linear"
        ]
      }
    }
  }
}
```

When a client makes its first call to `/mcp/linear-v1` without a stored upstream
connection, the gateway returns a JSON-RPC connect-required error with a URL the
user opens in a browser to complete the upstream OAuth dance. After the user
authorizes, the gateway stores the tokens and the same call succeeds. See the
[`mcp-token-exchange-inbound` policy reference](../policies/mcp-token-exchange-inbound.mdx)
for the full lifecycle, including per-user vs. shared OAuth, scope handling, and
the `clientRegistration` modes.

## Tool discovery

The gateway calls the upstream's `tools/list` to populate the **Tools
available** view in the portal. The list refreshes on its own schedule.

If a tool you expect doesn't show up:

- **Confirm the upstream exposes it.** Some MCP servers gate certain tools
  behind authentication state, scopes, or admin-only features. A `tools/list`
  from an unauthenticated context returns less than a `tools/list` from an
  authenticated one.
- **Confirm the Origin URL is reachable.** A misconfigured URL or a network
  block returns no tools. The [browser error page](#troubleshooting) details
  which gateway error fired and the underlying upstream response.
- **Confirm the upstream returns valid MCP.** If the upstream returns HTML or a
  non-MCP JSON response, the gateway treats it as a failure rather than
  pretending it succeeded with zero tools.

## What an Origin is _not_

A few common misreadings:

- **An Origin is not a route in the gateway's OpenAPI.** Routes are exposed by
  Virtual MCPs (`/mcp/{slug}`); Origins are upstream targets. One Origin can
  back many Virtual MCPs (for example, a read-only Linear route and a full-power
  Linear route), but each Virtual MCP points at exactly one Origin.
- **An Origin is not an auth boundary by itself.** The gateway authenticates
  inbound traffic at the Virtual MCP route; the Origin describes where to
  forward authenticated requests. Use route policies to add upstream auth.
- **An Origin doesn't host MCP tools.** It points at a server that does. The
  tool definitions, input schemas, and execution all live in the upstream.

## Code-configured origins

When configuring the gateway entirely in code (no portal), the Origin is
expressed by two things in the route definition:

1. The **`rewritePattern`** on `McpProxyHandler.options`, which is the upstream
   URL.
2. The **inbound policies** that handle upstream credentials and any request
   transformation.

Once the route is in `routes.oas.json` and the gateway deploys, the upstream is
reachable through the route's path. There's no portal-managed Origin record in
this mode — the route itself is the configuration.

If you started in the portal and want to move to code-config, copy the upstream
URL from the Origin card into the `rewritePattern` of the new route, recreate
the upstream auth policy, and remove the portal Origin once the code route is
live. See [Virtual MCP Servers](./virtual-mcp-servers.mdx#configure-with-code)
for the full route shape — one route per upstream, each with its own
token-exchange or API-key policy.

## Troubleshooting

| Symptom                                          | Likely cause                                                                                                                            |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| **"N Tools available" is 0 after a few seconds** | Upstream is unreachable, returns HTML, or rejects unauthenticated `tools/list`. Verify the URL and any upstream auth.                   |
| **Connect-required popup loops**                 | Upstream OAuth client registration is failing. Set explicit `scopes` or switch to `clientRegistration: { mode: "manual" }`.             |
| **Headers from the portal form vanish**          | Portal headers/API key fields are not persisted today. Move upstream auth to a code-config policy.                                      |
| **A tool name changed upstream**                 | Tools are matched by name throughout the system. Update any references in Virtual MCP tool selections or capability filter allow-lists. |

For deeper auth debugging, the gateway's browser error page surfaces the gateway
error code and request ID along with the raw upstream HTML when relevant.
Include those in any support request.

## Reference

- [Virtual MCP Servers](./virtual-mcp-servers.mdx) — how to select tools from
  Origins and expose them to clients.
- [Capability Filtering](./capability-filtering.mdx) — narrow what an Origin
  exposes downstream.
- [`mcp-token-exchange-inbound`](../policies/mcp-token-exchange-inbound.mdx) —
  per-user OAuth to upstream MCP servers.
- [`set-upstream-api-key-inbound`](../policies/set-upstream-api-key-inbound.mdx)
  and [`set-headers-inbound`](../policies/set-headers-inbound.mdx) — set
  upstream credentials when the provider uses API keys or static headers.
- [`McpProxyHandler`](./virtual-mcp-servers.mdx#configure-with-code) — the route
  handler that does the forwarding.
