# MCP Architecture

This page explains where the MCP Gateway boundary is drawn: hosted tools versus federation tools,
the inbound and outbound request paths, and which capabilities the self-hosted Node runtime and the
hosted edge runtime provide.

## Management routes versus tool gateway

```mermaid
flowchart LR
  Client[MCP client] -->|Bearer mcp:invoke| GW[Tool gateway]
  Mgmt[Management routes] --> Catalog[Catalog and policies]
  GW --> Catalog
  GW --> Hosted[Hosted tools]
  GW --> Up[Registered upstream]
```

Management routes register servers, inspect the catalog, seal credentials, publish tools, and write
policies. The tool gateway speaks the streamable-HTTP MCP protocol to external clients and enforces
the catalog and policy on every call.

- On the hosted edge, management routes and the tool gateway run as separate services; the tool
  gateway owns the MCP origin (`/v1?mcp=<id>`).
- On the self-hosted Node runtime, several products share one origin, so the tool gateway keeps the
  `/mcp/v1?mcp=<id>` prefix.
- The `?mcp=<id>` parameter selects a built-in target (`provon`, `duckduckgo`) or a registered
  server id; omitting it returns `400 MCP_ID_REQUIRED` before authentication.

## Hosted versus federation

|                  | Hosted tools                                                | Federation tools                          |
| ---------------- | ----------------------------------------------------------- | ----------------------------------------- |
| Origin           | Built-in read-only capabilities with no upstream connection | Proxied to a registered upstream server   |
| Execution        | Executed by the gateway itself                              | Outbound over streamable-HTTP MCP or REST |
| Mutations        | Read-only                                                   | Read or write, per upstream               |
| Discovery gating | Registered allowlist; nothing wildcarded                    | Catalog publish state                     |
| Default access   | Usable out of the box by an `mcp:invoke` caller             | Default-deny; needs an allow policy       |
| Current tools    | Web search; trace and Finding readers                       | Whatever you register and sync            |

A tool's publish state (`draft` / `published` / `deprecated` / `archived`) decides eligibility.
Only `published` and `deprecated` tools are eligible for listing and invocation; `draft` and
`archived` are closed. Deprecated tools stay eligible but surface a migration notice. Publish
state is orthogonal to principal authorization: a federation tool can be published yet invisible
to a principal with no allow policy, and it cannot be called until that principal is authorized.
Hosted default-allow tools are the exception.

Hosted tools ship with a default-allow flag, so an `mcp:invoke` caller reaches them even without a
matching access policy; an explicit deny still overrides. Federation tools stay closed until a
policy allows them.

## Inbound path (external client to gateway)

1. The tool gateway resolves the caller from the Bearer credential. The ingress accepts only a
   project API key carrying `mcp:invoke`; a session or any non-API-key caller receives
   `401 UNAUTHORIZED`, and a caller the project cannot place receives `403 FORBIDDEN`.
2. Per request, the gateway evaluates the project's policies for that principal for the `read` or
   `invoke` action. There is no cached allow-list: every call re-checks authorization, publish
   state, and the current catalog revision. A tool listed at `tools/list` is not automatically
   callable.
3. `tools/list` returns only `published` or `deprecated` tools inside the principal's scope, plus
   the default-allow hosted tools.
4. `tools/call` re-adjudicates, runs the input guardrail over the arguments, executes the hosted
   tool or proxies to the upstream, then runs the output guardrail over the result.

## Outbound path (gateway to registered service)

- For a native MCP server, the gateway opens a streamable-HTTP connection and calls
  `tools/list` / `tools/call`.
- For an OpenAPI server, the call is a direct REST request: path parameters come from the tool
  arguments, remaining arguments become the query string for GET or a JSON body for write methods.
- The sealed upstream credential is injected server-side. The inbound Bearer presented by the
  caller is never forwarded upstream.
- Every outbound target must be a permitted public host; see [Security](./security.md#transport-hardening).
- When a credential fails authentication, the cached connection is dropped so the next call uses
  the current sealed secret.

## Runtime capability matrix

| Capability                                                         | Self-hosted Node | Hosted edge |
| ------------------------------------------------------------------ | ---------------- | ----------- |
| List, create, update, delete servers                               | Yes              | Yes         |
| Create, list, pause, resume, promote, and revoke credentials       | Yes              | Yes         |
| Create, list, and delete access policies                           | Yes              | Yes         |
| Project-wide read-only tool catalog                                | Yes              | Yes         |
| Set tool publish state                                             | Yes              | Yes         |
| OpenAPI document import                                            | Yes              | Yes         |
| Catalog sync for OpenAPI servers                                   | Yes              | Yes         |
| Catalog sync for native HTTP MCP servers                           | Yes              | Yes         |
| Upstream reachability test                                         | Yes              | Yes         |
| Hosted tools (web search, trace and Finding readers)               | Yes              | Yes         |
| Tool gateway `tools/list` and `tools/call`                         | Yes              | Yes         |
| Guardrail budgets, rate limits, and audit attributes on tool calls | Yes              | Yes         |

Management and invocation are both wired on the hosted edge; the matrix below lists default limits so
you can size a self-hosted deployment honestly.

## Default runtime limits

These are per-runtime defaults; the rate-limit bucket dimension differs between the two runtimes.

| Limit                                      | Default                            | Notes                                                                                                               |
| ------------------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Federation calls per minute, fixed window  | 120                                | Node: per `(server, tool)` in one process; edge: per `(project, principal, tool)` in shared project state           |
| Hosted-tool calls per minute, fixed window | 60 on Node; shares the edge bucket | Node keeps a separate per-process hosted bucket; on the edge hosted calls share the 120 calls/minute project bucket |
| Concurrent in-flight calls per server      | 4                                  | Node runtime only                                                                                                   |
| Per-call timeout                           | 30 s                               | Applies to outbound calls                                                                                           |
| Upstream connect timeout                   | 15 s                               | Applies to the MCP handshake                                                                                        |
| Tool arguments guardrail budget            | 64 KiB                             | Both runtimes                                                                                                       |
| Tool output guardrail budget               | 256 KiB                            | Both runtimes                                                                                                       |
| OpenAPI response cap                       | 1 MB                               | Both runtimes; larger bodies are withheld rather than truncated                                                     |
| OpenAPI direct-fetch timeout               | 30 s                               | Both runtimes                                                                                                       |

On Node, the federation and hosted rate gates live in one process and are not distributed across
multiple Node instances. On the edge, the fixed-window bucket is shared across the project's
requests and bucketed per principal and tool. Upstream `429` responses are surfaced independently of
these local limiters.
