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.
Transport#
POST /mcp, one JSON-RPC 2.0 message per request, answers inapplication/json.- No sessions and no server sent events: no
Mcp-Session-Idis minted.GETandDELETEanswer 405. - Every request carries
Authorization: Bearer <token>. Origin: absent, the app's own, or one listed inmcp.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
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
tokenKeyand its collection's auth secret. The API under/api/never accepts it, and/mcprefuses one whose audience is another endpoint. - Registered clients live in
_mcpClientsand grants in_mcpGrants: plain tables ofapp.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.