# MCP protocol and OAuth

> The wire of an app's MCP server: the methods, the headers of each protocol revision, the error codes, the OAuth routes and the tokens.

An app with `createApp({ mcp })` serves the Model Context Protocol at `/mcp`. It follows revision **2026-07-28**, the current one, and also answers the handshake of revisions 2025-11-25, 2025-06-18 and 2025-03-26. How to turn it on: [The MCP server](https://golocomotive.fr/docs/developers/mcp.md).

## Transport

- `POST /mcp`, one JSON-RPC 2.0 message per request, answers in `application/json`.
- No sessions and no server sent events: no `Mcp-Session-Id` is minted. `GET` and `DELETE` answer 405.
- Every request carries `Authorization: Bearer <token>`.
- `Origin`: absent, the app's own, or one listed in `mcp.origins`. Any other is 403.

## The current revision (2026-07-28)

| part | value |
|---|---|
| methods | `server/discover`, `tools/list`, `tools/call`, `ping` |
| request `_meta` | `io.modelcontextprotocol/protocolVersion` |
| headers | `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name` on `tools/call` |
| every result | `resultType: 'complete'` and `_meta['io.modelcontextprotocol/serverInfo']` |
| `tools/list` | adds `ttlMs: 0` and `cacheScope: 'private'`: the list depends on the caller |

| error | when |
|---|---|
| 400 `-32020` | a header is missing or disagrees with the body |
| 400 `-32022` | an unknown protocol version; `data.supported` lists the known ones |
| 404 `-32601` | an unknown method |

## Earlier revisions (2025-11-25 and before)

`initialize` answers the version asked when it is supported, else 2025-11-25. Then `notifications/initialized` (202), `tools/list`, `tools/call`, `ping`. Errors answer with status 200.

## JSON-RPC errors

| code | meaning |
|---|---|
| `-32700` | parse error |
| `-32600` | invalid request |
| `-32601` | unknown method |
| `-32602` | invalid params: an unknown tool, arguments that are not an object |

A tool that the app refuses (a rule, a 404, a validation error, the write rate limit) is not a JSON-RPC error. It is a tool result with `isError: true` and the route's `{ status, message, data }` as text.

## Server info

`{ name: <the app's name>, version: <the locosdk version> }`, plus the `instructions` paragraph when the app sets one.

## Unauthenticated requests

No token, or a dead one: 401 with

```text
WWW-Authenticate: Bearer resource_metadata="https://orders.example.com/.well-known/oauth-protected-resource/mcp", scope="mcp"
```

A superuser's token gets 403 unless `mcp.superusers` is true.

## OAuth routes

With `mcp.oauth` on (the default):

| route | what |
|---|---|
| `GET /.well-known/oauth-protected-resource/mcp` (and without `/mcp`) | RFC 9728: `resource`, `authorization_servers: [<origin>]`, `scopes_supported: ['mcp']`, `bearer_methods_supported: ['header']` |
| `GET /.well-known/oauth-authorization-server` | RFC 8414: issuer `<origin>`, the endpoints, `code_challenge_methods_supported: ['S256']`, grants `authorization_code` and `refresh_token`, `authorization_response_iss_parameter_supported: true` |
| `POST /oauth/register` | RFC 7591 dynamic client registration. `redirect_uris` in https, or http on loopback. `token_endpoint_auth_method`: `none` (default), `client_secret_post` or `client_secret_basic` (a secret is minted). 10 per minute per IP |
| `GET /oauth/authorize` | the consent page, on the app's own origin |
| `POST /oauth/authorize` | the page's decision, signed in with the app's session: `{ redirect }` |
| `POST /oauth/token` | `authorization_code` (PKCE S256 required) and `refresh_token` |

## Tokens

| token | form | life | dies when |
|---|---|---|---|
| app session token | JWT of kind `auth` | 12 hours by default | the password or email changes |
| MCP access token | JWT of kind `mcp`, `aud` the endpoint URL | 1 hour | the password or email changes |
| refresh token | opaque, stored hashed | 30 days, rotated at each use | used once, or the password changes |
| authorization code | opaque, in memory | 5 minutes, single use | used once |

- An MCP access token is signed with the record's `tokenKey` and its collection's auth secret. The API under `/api/` never accepts it, and `/mcp` refuses one whose audience is another endpoint.
- Registered clients live in `_mcpClients` and grants in `_mcpGrants`: plain tables of `app.db`, outside the schema, carried by backups.

## Limits

Writes through tools are limited per signed in record by the `mcp:write` rate limit rule: 60 per 60 seconds by default. `create`, `update`, `delete` and app tools not marked `readOnly` count one per call; `batch` counts one per request inside it. Past it, the tool answers an error result with status 429.
