# MCP API

The MCP API has two route groups: a **project-scoped management route set** and the
**streamable-HTTP tool gateway** an MCP client speaks. All management routes live under
`/v1/projects/:projectId/mcp/...`; there is deliberately no global (non-project-scoped) route.

| Route group  | Base path                                     | Runtime           | Auth                                                                            |
| ------------ | --------------------------------------------- | ----------------- | ------------------------------------------------------------------------------- |
| Management   | `<api-origin>/v1/projects/:projectId/mcp/...` | Node + API Worker | Bearer key with `mcp:manage`, or session with project read/`project:mcp:manage` |
| Tool gateway | `<mcp-origin>/v1?mcp=<target>`                | Node + MCP Worker | Project API key with `mcp:invoke` only (sessions rejected)                      |

`?mcp=<target>` is required on every tool-gateway request (otherwise `400 MCP_ID_REQUIRED`); see
[Tool gateway](#tool-gateway-streamable-http-mcp) for the `provon` / `duckduckgo` / `<serverId>`
values. On Node the shared-origin path is `/mcp/v1?mcp=<target>`.

Local Node: `http://127.0.0.1:3000` (both groups on one origin; the gateway at `/mcp/v1`).
Hosted: management under `https://api.provon.dev/v1`, the tool gateway under
`https://mcp.provon.dev/v1`. When the management service is not wired on a runtime, management
routes return `501 MCP_MANAGEMENT_UNAVAILABLE`. On the edge, the API Worker hosts the management
routes and the MCP Worker hosts the HTTP tool gateway.

## Servers

### List servers

```http
GET /v1/projects/:projectId/mcp/servers
```

Management read. Returns `{ "servers": McpServer[] }`.

### Create a server

```http
POST /v1/projects/:projectId/mcp/servers
```

Management write. `201`. Returns `{ "server": McpServer }`.

```json
{
  "displayName": "Acme internal tools",
  "sourceKind": "mcp",
  "baseUrl": "https://mcp.acme.example.com",
  "openapiSpec": null,
  "enabled": true
}
```

| Field         | Required                     | Notes                  |
| ------------- | ---------------------------- | ---------------------- |
| `displayName` | yes                          | non-empty string       |
| `sourceKind`  | yes                          | `"mcp"` or `"openapi"` |
| `baseUrl`     | yes for MCP                  | HTTP upstream endpoint |
| `openapiSpec` | when `sourceKind: "openapi"` | the raw OpenAPI object |
| `enabled`     | no                           | defaults `true`        |

The server record carries `connectionStatus` (`unknown` | `reachable` | `unreachable` | `disabled`)
separately from `enabled`; connection health and authentication posture are distinct states and are
never collapsed into one `status`.

### Update a server

```http
PATCH /v1/projects/:projectId/mcp/servers/:sid
```

Management write. Body may include `displayName`, `baseUrl`, `openapiSpec`, and/or `enabled`.
Returns `{ "server": McpServer }`. Unknown server → `404 MCP_SERVER_NOT_FOUND`.

### Delete a server

```http
DELETE /v1/projects/:projectId/mcp/servers/:sid
```

Management write. `204` on success.

### Test connection

```http
POST /v1/projects/:projectId/mcp/servers/:sid/test
```

Management write. Opens an outbound HTTP connection and records status. Returns
`{ "ok": bool, "connectionStatus": "unknown"|"reachable"|"unreachable"|"disabled", "message": string|null, "toolCount": number|null }`.

### Discover (read-only)

```http
POST /v1/projects/:projectId/mcp/servers/:sid/discover
```

Management write. Lists the upstream's tools **without** writing the catalog, publishing, or
changing the stored connection status. Returns `{ "reachable": bool, "classification": string,
"message": string|null, "tools": Tool[] }`; the provisional tools become catalog entries only after
an explicit **sync**.

### Sync catalog

```http
POST /v1/projects/:projectId/mcp/servers/:sid/sync
```

Management write. Works on Node and Worker for OpenAPI servers (pure normalization) and native
HTTP MCP servers (edge outbound fetch). Returns
`{ "catalogVersion": number, "tools": Tool[], "archivedTools": Tool[] }`.

## Credentials

Sealed upstream secrets. Credentials are typed by `kind` (`static_secret` today; `oauth2` reserved)
and by the whitelisted outbound `authMethod`.

### List credentials

```http
GET /v1/projects/:projectId/mcp/servers/:sid/credentials
```

Management read. Returns `{ "credentials": Credential[] }` where each credential exposes
`id`, `serverId`, `label`, `kind` (`static_secret` | `oauth2`), `authMethod`
(`authorization_bearer` | `x_api_key`), `role` (`primary` | `backup`), `enabled`,
`lastVerifiedAt`, and `createdAt` — **never the secret**. Exactly one credential is the active
`primary`; every other credential is a `backup` held for cutover. There is no numeric selection
priority.

### Create a credential

```http
POST /v1/projects/:projectId/mcp/servers/:sid/credentials
```

Management write. `201`. Returns `{ "credential": Credential }`.

```json
{
  "label": "acme-robot",
  "secret": "<upstream token>",
  "authMethod": "authorization_bearer",
  "enabled": true
}
```

The secret is sealed at rest. `authMethod` selects the outbound header whitelist
(`Authorization: Bearer` or `X-Api-Key`). New credentials are `backup` until an isolation test
promotes them.

### Test a credential

```http
POST /v1/projects/:projectId/mcp/servers/:sid/credentials/:cid/test
```

Management write. Isolation-tests the sealed secret against the upstream WITHOUT making it the
primary. Returns `{ "ok": bool, "classification": string, "message": string|null }`.

### Promote a credential

```http
POST /v1/projects/:projectId/mcp/servers/:sid/credentials/:cid/promote
```

Management write. Atomically cuts over an isolation-verified credential as the server's primary —
the store demotes the current primary and promotes this one in one batch. Returns
`{ "credential": Credential }`.

### Pause / resume a credential

```http
POST /v1/projects/:projectId/mcp/servers/:sid/credentials/:cid/pause
POST /v1/projects/:projectId/mcp/servers/:sid/credentials/:cid/resume
```

Management write. Holds a credential (never used on the outbound path) or re-enables it without
revoking.

### Revoke a credential

```http
DELETE /v1/projects/:projectId/mcp/servers/:sid/credentials/:cid
```

Management write. `204`; unknown → `404 MCP_CREDENTIAL_NOT_FOUND`.

## Tool catalog

### Project-wide catalog (Workbench)

```http
GET /v1/projects/:projectId/mcp/tools
```

Management read. Merges code-defined hosted tools with persisted federation tools into one read-only
view. Returns `{ "tools": CatalogToolView[], "catalogVersion": number }`. Each view carries
`origin` (`hosted` | `federation`), `serverId` (`null` for hosted), `normalizedToolName`,
`title`, `description`, `inputSchema`, `publishState`, `catalogVersion`, `lastSyncedAt`,
`httpMethod`/`httpPath` (OpenAPI only), `connection` display facts (never the upstream URL),
`defaultAllowed` (only hosted tools are pre-approved), and `availability`
(`{ state: "available" }` or `{ state: "unavailable", reason: "draft"|"archived"|"server_disabled"|"server_unreachable" }`).
A federation row stays on the page for management even when its upstream is unreachable; callers
cannot invoke it while the auth posture is `missing`, `invalid`, or `revoked`.

### List tools for a server

```http
GET /v1/projects/:projectId/mcp/servers/:sid/tools
```

Management read. Returns `{ "tools": Tool[] }` — every cataloged tool for the server, scoped to the
project and server regardless of publish state.

### Set publish state

```http
PATCH /v1/projects/:projectId/mcp/servers/:sid/tools/:tid
```

Management write. Body `{ "publishState": "draft"|"published"|"deprecated"|"archived" }`. Only
`published` and `deprecated` are listable and invocable. Returns `{ "tool": Tool }`. Unknown →
`404 MCP_TOOL_NOT_FOUND`.

## OpenAPI import

```http
POST /v1/projects/:projectId/mcp/openapi/import
```

Management write. `201`. The body supplies the OpenAPI document by reference **or** inline, but
exactly one of:

```json
{ "displayName": "Optional name", "specUrl": "https://example.com/openapi.json" }
```

```json
{ "displayName": "Optional name", "specText": "<uploaded OpenAPI document contents>" }
```

Omitting both or sending both returns `400 INVALID_FIELD`. There is no inline `spec` object field.
Returns `{ "server": Server, "tools": Tool[] }`. Each imported operation becomes a **`draft`** tool;
it is not discoverable or callable until explicitly published with the `PATCH .../tools/:tid` route.
See [OpenAPI import](../mcp/openapi.md) for operation-to-tool mapping.

## Access policies

Explicit principal allow/deny. Default-deny, deny-overrides, narrowing-only. The built-in
hosted tools (the four `provon` tools plus `duckduckgo` `web_search`) are pre-approved and need
no policy; federation tools stay closed until allowed.

### List policies

```http
GET /v1/projects/:projectId/mcp/policies
```

Management read. `{ "policies": Policy[] }`.

### Create a policy

```http
POST /v1/projects/:projectId/mcp/policies
```

Management write. `201`. Returns `{ "policy": Policy }`.

```json
{
  "effect": "allow",
  "priority": 100,
  "principalKind": "api_key",
  "principalRef": "<invoking api key id>",
  "serverIds": null,
  "toolNames": null,
  "actions": ["read", "invoke"]
}
```

| Field           | Notes                                                                |
| --------------- | -------------------------------------------------------------------- |
| `effect`        | `"allow"` or `"deny"`                                                |
| `priority`      | integer; higher evaluated first under deny-overrides (default `100`) |
| `principalKind` | `user` \| `api_key`                                                  |
| `principalRef`  | an opaque id (user id or API key id)                                 |
| `actions`       | one or more of `read`, `invoke` (there is no `manage` tool action)   |
| `serverIds`     | `null` = all, or an explicit array                                   |
| `toolNames`     | `null` = all, or an explicit array                                   |

### Delete a policy

```http
DELETE /v1/projects/:projectId/mcp/policies/:pid
```

Management write. `204`; unknown → `404 MCP_POLICY_NOT_FOUND`.

## Gateway enablement

The project MCP tool gateway can be paused or resumed as a whole:

```http
GET  /v1/projects/:projectId/mcp/settings
POST /v1/projects/:projectId/mcp/pause
POST /v1/projects/:projectId/mcp/resume
```

`GET` requires a management read; `pause` and `resume` require `mcp:manage`. Each returns the
current `{ "settings": ... }`.

## Tool gateway (streamable HTTP MCP)

```http
POST /v1?mcp=<target>      # Hosted MCP Worker
POST /mcp/v1?mcp=<target>  # Node, shared origin
```

The `?mcp=` query parameter is **required** on every request; omitting it returns `400
MCP_ID_REQUIRED`. Its value selects which tools are exposed:

- `?mcp=provon` — the hosted Provon tools (`search_traces`, `get_trace`, `list_findings`, `get_finding`);
- `?mcp=duckduckgo` — the hosted `web_search` tool;
- `?mcp=<serverId>` — a federated MCP server's published tools.

On Node (shared origin) the endpoint is `POST /mcp/v1`; the Worker also answers `GET`/`DELETE`
session probes on the same path.

Runs on Node and the MCP Worker. Speak JSON-RPC 2.0 (stateless) with an `mcp:invoke`-bearing key.
This ingress accepts **project API keys only**: a Workspace session (or any non-API-key caller)
receives `401 UNAUTHORIZED`; a key without `mcp:invoke` is rejected. A paused project gateway
returns `503 MCP_PAUSED`. Supported methods:

| Method                      | Purpose                                                        |
| --------------------------- | -------------------------------------------------------------- |
| `initialize`                | handshake; advertises the server name/version (`provon-mcp`)   |
| `notifications/initialized` | post-init notification                                         |
| `ping`                      | liveness                                                       |
| `tools/list`                | discover hosted + published tools inside the principal's scope |
| `tools/call`                | invoke a tool by name                                          |

Errors use the reserved JSON-RPC range: `-32601` method not found, `-32000` session expired,
`-32001` permission denied, `-32002` rate limited, `-32003` upstream failure, `-32004` gateway
paused, `-32603` internal. (On the HTTP ingress a paused gateway returns `503 MCP_PAUSED` before
the JSON-RPC body; `-32004` is the JSON-RPC code surfaced on bypass paths such as the Workbench
playground.)

Upstream HTTP-status → JSON-RPC mapping differs by transport:

- **Native MCP** (both runtimes): `401`/`403` → `-32001`, `404` → `-32601`, `429` → `-32002`,
  other HTTP → `-32003`, transport/timeout → `-32003`.
- **OpenAPI direct-fetch on Node**: every non-2xx (including `401`/`403`/`404`/`500`) → `-32001`;
  fetch abort / network error → `-32000`.
- **OpenAPI direct-fetch on the Worker**: every non-2xx **and** every transport / blocked-host /
  over-size error → `-32003`.

See [Security](../mcp/security.md) for guardrail and audit behavior.
