A Plug implementing the MCP Streamable HTTP transport (revision 2025-11-25).
Mount it at a single endpoint path serving POST, GET and DELETE:
forward "/mcp", to: Urchin.Transport.StreamableHTTP, init_opts: [server: MyServer]or run it standalone via Urchin.start_link/2 / Urchin.Endpoint.
Options
:server(required) - a module implementingUrchin.Server:init_arg- argument passed toUrchin.Server.init/1once per session (defaultnil):allowed_origins-:all, a list of allowedOriginvalues, ornilto allow missing origins plus localhost (defaultnil):require_session- reject post-initialize requests without a session id (defaulttrue):enable_get- offer the GET SSE stream (defaulttrue):allow_delete- allow client session termination via DELETE (defaulttrue):min_log_level- default minimum log level for new sessions (default"info"):request_timeout- per-request handler timeout in ms (default60_000):validate_protocol_version- validate theMCP-Protocol-Versionheader (defaulttrue):max_sessions- reject new sessions with503once this many are active (defaultnil, unlimited). The cap is enforced atomically before the server'sinit/1runs, and is global across all sessions in the app.:session_idle_timeout- terminate a session after this many ms without client activity (defaultnil, never). A session serving a request is not reaped.:session_max_lifetime- terminate a session this many ms after it was created, regardless of activity (defaultnil, never). Set it above your longest expected tool run, since it can expire a session mid-request.:expose_internal_errors- return raised-exception messages to the client instead of a generic error (defaultfalse). Exceptions are always logged; enable only in development.:sse_buffer_limit- the maximum number of recent general-stream (GET SSE) events each session keeps for resumption replay. Defaults tonil, which preserves the session's internal default of100. A positive integer ornil.:auth- anUrchin.Auth(or keyword options) to require OAuth 2.1 bearer tokens on every request;nil(default) serves MCP unauthenticated. The metadata discovery endpoint is served byUrchin.Endpoint/Urchin.Auth.Metadata, not this plug.
The transport enforces the spec by default and these behaviors are not configurable: it
validates a DSL tool's tools/call arguments against its input schema (a hand-written
call_tool/3 validates its own arguments), rejects operation requests received before
notifications/initialized (only ping is allowed pre-init), and surfaces a tool handler's
{:error, binary} as an isError CallToolResult.
The plug reads the raw request body itself, so mount it before any JSON body parser.