# MCP API
URL: https://openship.io/docs/api/mcp.md

The Streamable-HTTP JSON-RPC endpoint that exposes Openship's API to AI clients as tools, re-authenticating every call.

import { TypeTable } from 'fumadocs-ui/components/type-table';

The MCP API is a single **Model Context Protocol** endpoint that lets AI agents (Claude, Cursor, and any
MCP-compatible client) drive Openship as a set of tools. Each tool maps onto a permission-tagged REST route,
and every call re-runs the full auth and permission stack — so an agent only ever sees and does what its
token allows. For client setup (OAuth flow, config snippets), see the [MCP overview](/docs/mcp).

<Callout title="Base path & auth">
The endpoint lives at **`/api/mcp`** on your instance — e.g. `https://your-host/api/mcp`. It accepts
**either** a personal access token (`Authorization: Bearer <token>`, created with
[`openship token create`](/docs/cli/access)) **or** an OAuth 2.1 access token that an MCP client obtains for
you. There is no separate MCP credential. See the [API overview](/docs/api) and
[auth model](/docs/security/auth) for details.
</Callout>

Available on both self-hosted and Openship Cloud (mounted in the shared route set).

## Endpoints

| Method & path | Permission | What it does |
|---|---|---|
| `POST /api/mcp` | public | Stateless Streamable-HTTP JSON-RPC 2.0 endpoint. Authenticates the bearer itself, then handles `initialize`, `ping`, `tools/list`, and `tools/call`. |
| `GET /api/mcp` | public | Returns `405` — this server never pushes messages, so there is no SSE stream to open. |

Both routes are declared `public` (no auto-injected auth middleware): the `POST` handler authenticates the
bearer credential itself, and each `tools/call` dispatches an internal request that re-authenticates through
the real permission stack.

## How authentication works

A missing or invalid bearer returns **`401`** with a `WWW-Authenticate` header pointing at
`/.well-known/oauth-protected-resource`, so OAuth-2.1 MCP clients can discover the authorization server and
start the PKCE flow. A valid credential resolves the caller's effective capability — this is used only to
**filter `tools/list`** so the endpoint doesn't advertise tools the token can't use.

The capability is **not** the authorization gate. Every `tools/call` builds an internal HTTP request and
dispatches it through the real Hono app, forwarding the caller's bearer, so routing, validation, auth, and
the per-resource permission check all run exactly as they do over HTTP. A read-only token can only reach
read tools; a scoped token stays confined to the projects, servers, and repositories it was granted.

The endpoint is rate-limited per-IP (the `mcp` policy: 300 requests/minute) because even an unauthenticated
probe costs a credential lookup.

## Request body (JSON-RPC 2.0 envelope)

The `POST` body is a single JSON-RPC 2.0 message (batching was removed in protocol `2025-06-18` — an array
body returns `-32600`).

<TypeTable
  type={{
    jsonrpc: { type: 'string', description: 'Protocol identifier. Must be "2.0".', required: true },
    method: { type: 'string', description: 'One of: initialize, ping, tools/list, tools/call, notifications/initialized.', required: true },
    id: { type: 'string | number', description: 'Request id, echoed back in the response. Omit entirely for a notification — the server replies 202 with no body.' },
    params: { type: 'object', description: 'Method arguments — e.g. { protocolVersion } for initialize, { name, arguments } for tools/call.' },
  }}
/>

### Methods

| Method | Result |
|---|---|
| `initialize` | Negotiates the protocol version (default `2025-06-18`; also speaks `2025-03-26` and `2024-11-05`) and returns `capabilities: { tools: { listChanged: false } }` and `serverInfo: { name: "openship", version: "1.0.0" }`. |
| `ping` | Returns `{}`. |
| `tools/list` | Returns the tools the caller's token can actually use (filtered by read-only flag, role, and grants). |
| `tools/call` | Dispatches the named tool through the real API and returns its response as JSON text content. |
| `notifications/initialized` | Acknowledged only (notification, no reply → `202`). |

## List available tools

```
POST /api/mcp
```

```bash
curl -s https://your-host/api/mcp \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

```bash
# Register the same endpoint with an MCP client (Claude Code):
claude mcp add --transport http openship https://your-host/api/mcp \
  --header "Authorization: Bearer $OPENSHIP_TOKEN"
```

The tool set is derived from Openship's permission-tagged routes that opt in with an `mcp` block; the
credential/auth surfaces (`tokens`, `auth`, `mcp` itself) are never exposed.

## Call a tool

`params.name` is the tool name from `tools/list`; `params.arguments` carries any path parameters, an optional
`query` object, an optional `body` object (for `POST`/`PUT`/`PATCH` tools), and an optional
`organizationId` (sent as the `x-organization-id` header).

```bash
curl -s https://your-host/api/mcp \
  -H "Authorization: Bearer $OPENSHIP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"get_projects","arguments":{}}}'
```

The result wraps the underlying API response as text content, with `isError: true` when the dispatched
request failed.

## Errors you might see

<Callout title="401 — missing or invalid access token" type="error">
No bearer, or one that's expired/revoked. The response carries a `WWW-Authenticate` header so OAuth clients
can start discovery; PAT clients should mint a fresh token with `openship token create`.
</Callout>

<Callout title="405 on GET /api/mcp" type="warn">
Expected. This is a request/response server with no server→client stream, so `GET` is not supported — always
`POST` your JSON-RPC message.
</Callout>

<Callout title="-32600 / -32700 — bad request" type="warn">
`-32700` is a JSON parse error; `-32600` is an invalid envelope or a batch (array) body, which this server
does not accept. `-32601` means the JSON-RPC method is unknown, and `-32602` means the tool name in
`tools/call` doesn't exist.
</Callout>

<Callout title="A tool call returns 404 for a resource" type="info">
`tools/list` succeeded but a specific `tools/call` reports `404` — that's the token's scope working as
intended: it wasn't granted that project, server, or repository.
</Callout>
