DocsReference

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 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)#

partvalue
methodsserver/discover, tools/list, tools/call, ping
request _metaio.modelcontextprotocol/protocolVersion
headersMCP-Protocol-Version, Mcp-Method, and Mcp-Name on tools/call
every resultresultType: 'complete' and _meta['io.modelcontextprotocol/serverInfo']
tools/listadds ttlMs: 0 and cacheScope: 'private': the list depends on the caller
errorwhen
400 -32020a header is missing or disagrees with the body
400 -32022an unknown protocol version; data.supported lists the known ones
404 -32601an 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#

codemeaning
-32700parse error
-32600invalid request
-32601unknown method
-32602invalid 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):

routewhat
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-serverRFC 8414: issuer <origin>, the endpoints, code_challenge_methods_supported: ['S256'], grants authorization_code and refresh_token, authorization_response_iss_parameter_supported: true
POST /oauth/registerRFC 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/authorizethe consent page, on the app's own origin
POST /oauth/authorizethe page's decision, signed in with the app's session: { redirect }
POST /oauth/tokenauthorization_code (PKCE S256 required) and refresh_token

Tokens#

tokenformlifedies when
app session tokenJWT of kind auth12 hours by defaultthe password or email changes
MCP access tokenJWT of kind mcp, aud the endpoint URL1 hourthe password or email changes
refresh tokenopaque, stored hashed30 days, rotated at each useused once, or the password changes
authorization codeopaque, in memory5 minutes, single useused 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.

This page as markdown