PhoenixKit.API.MCP (phoenix_kit v2.63.0)

Copy Markdown View Source

The MCP transport of the shop API: Model Context Protocol over Streamable HTTP, tools only, stateless, JSON answers only. A plug that the router forwards to, once per audience:

forward "/mcp/admin", PhoenixKit.API.MCP, audience: :admin
forward "/mcp/shop", PhoenixKit.API.MCP, audience: :public

phoenix_kit_routes/0 emits both. The admin endpoint takes the same bearer key as the REST API (clients that let you set a request header for a remote MCP server can use it); the public endpoint needs no key.

What it speaks

POST with one JSON-RPC message per request:

  • initialize: the revision is negotiated (PhoenixKit.API.MCP.Versions), the capabilities are tools only;
  • notifications/* and responses from the client: 202, no body, and nothing is run, whatever the method;
  • ping: {};
  • tools/list: the tools the caller may use, with _ in their names;
  • tools/call: runs a tool through PhoenixKit.API.Executor. Both name forms are accepted. A failure of the tool is a result with isError, not a JSON-RPC error;
  • anything else: JSON-RPC -32601.

GET, DELETE and every other method are 405 with Allow: POST: there is no event stream and no session to close. No session id is issued; every request carries its own key. No CORS headers are sent.

Protocol revisions

The revisions it speaks, and what differs between them, are the table of PhoenixKit.API.MCP.Versions; config :phoenix_kit, :mcp_protocol_versions narrows or orders them without a code change. No header means 2025-03-26 (the MCP rule) while that revision is spoken. An unsupported MCP-Protocol-Version is HTTP 400 with JSON-RPC -32600, whatever revision the client speaks; initialize answers a revision it does not speak with the most preferred one it does.

The order of the checks

PhoenixKit.API.Auth first (API switch, website gate, credential in the URL, the Origin check of PhoenixKit.API.MCP.Origin, key), then the HTTP method, Accept (must allow application/json), the JSON-RPC shape of the body (PhoenixKit.API.MCP.Rpc), the MCP-Protocol-Version header, and last the method of the message.

initialize, ping, notifications, responses and unknown methods cost one rate-limit unit each, so that a flood of cheap messages is throttled like any other traffic. So do a tools/list and a tools/call with invalid params. A valid tools/list costs one unit through PhoenixKit.API.Executor.visible/2. A refusal is HTTP 429 with Retry-After.

A tools/call is priced by the executor. An admin call that the executor refuses before its limiter (an unknown, hidden, forbidden or disabled-module tool) is charged one unit here; the public audience is charged by the executor before the lookup and before the name is mapped from its MCP form (PhoenixKit.API.MCP.Names.resolve/2). A rate limit is HTTP 429, every other failure of the tool is an isError result.

Malformed JSON never reaches this plug: PhoenixKit.API.BodyGuard answers it with a JSON-RPC parse error in the endpoint.

Summary

Functions

The protocol revisions this server speaks, most preferred first (PhoenixKit.API.MCP.Versions.supported/0).

Functions

supported_versions()

@spec supported_versions() :: [String.t()]

The protocol revisions this server speaks, most preferred first (PhoenixKit.API.MCP.Versions.supported/0).