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: :publicphoenix_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 aretoolsonly;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 throughPhoenixKit.API.Executor. Both name forms are accepted. A failure of the tool is a result withisError, 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
@spec supported_versions() :: [String.t()]
The protocol revisions this server speaks, most preferred first (PhoenixKit.API.MCP.Versions.supported/0).