barrel_mcp features
View SourceTracks notable capabilities and the spec-conformance status of the
Erlang MCP library. See CHANGELOG.md for release-by-release detail.
Server
Transports
- HTTP transport (
barrel_mcp_http) — JSON-RPC over POST. Legacy. - Streamable HTTP transport (
barrel_mcp_http_stream) — MCP2025-11-25with downward negotiation to2025-06-18,2025-03-26,2024-11-05. POST (JSON or SSE), GET (SSE), DELETE, OPTIONS. Default bind127.0.0.1; public binds requireallowed_origins.Originis validated structurally on every method (POST/GET/DELETE/OPTIONS); literalOrigin: nullis rejected unless explicitly allowed. CORS echoes the validated origin (no wildcard);Access-Control-Allow-Headersis derived from the configured auth provider. - stdio transport (
barrel_mcp_stdio).
Wire-level conformance
- POSTed JSON-RPC requests return either a JSON envelope or an SSE stream. Notifications and POSTed responses to server-initiated requests return HTTP 202 with empty body.
- Missing
Mcp-Session-Idon a non-initializerequest → 400; unknown id → 404. MCP-Protocol-Versionvalidated server-side: missing falls back to the session-stored negotiated version; unsupported → 400.- JSON-RPC
idmust be string or integer;nulland other shapes rejected with -32600. Top-level JSON arrays (batches) explicitly rejected — MCP removed batching. notifications/cancelledaborts the in-flight tool call; the cancelled HTTP request closes with 200 and an empty body (no JSON-RPC envelope, per spec).- Per-session SSE ring buffer (default 256 entries) for
Last-Event-IDreplay; out-of-window ids surface a syntheticnotifications/replay_truncatedevent.
Registries
- Tools — handlers may be arity 1 or arity 2. Arity-2
handlers receive a
Ctxmap withsession_id,request_id,progress_token, the inboundmetamap (the spec's_metaextension hook), and anemit_progressfunction. Meta-bearing return shapes ({result_meta, Result, Map},{structured_meta, Data, Content, Map},{tool_error, Content, Map}) attach_metato the response envelope. - Resources — text/binary content, MIME types,
notifications/resources/updatedfor live updates. Handlers may return a single block (#{text := _}/#{blob := _, mimeType := _}, with optionalmimeTypeandannotations) or a list of pre-built content blocks for multi-part responses. - Resource templates — RFC 6570 URI templates, surfaced via
resources/templates/list.resources/readagainst a URI matching a registered template auto-expands the variables (Level 1, simple{var}substitutions) and routes to the template handler with the substituted values inArgs. - Prompts — multi-message conversation templates with arguments.
- Completions — keyed by
{prompt, Name, Arg}or{resource_template, Uri, Arg}; advertised via thecompletionscapability when at least one is registered. - All registrations accept optional
titleandicons. - Tool, resource, prompt, and resource-template registrations
also accept
annotations— a free-form map surfaced verbatim underannotationsin the matching*/listpayload. Tools usereadOnlyHint,destructiveHint,idempotentHint,openWorldHint; resources/prompts/templates useaudience(["user" | "assistant"]) andpriority(0..1).
Tool features
- Return shapes: plain (text / map / list / image),
{tool_error, Content}(→isError: true),{structured, Data}/{structured, Data, Content}(→structuredContent). validate_inputandvalidate_outputopt-in schema validation viabarrel_mcp_schema.long_running => truereturns ataskIdimmediately and runs the worker in the background. Backed bybarrel_mcp_tasks— surfacestasks/list,tasks/get,tasks/cancel, andnotifications/tasks/status.- Cancellation: cooperative arity-2 handlers see
{cancel, RequestId}in their mailbox; arity-1 handlers run to completion but their result is discarded. - Progress: handlers call
(maps:get(emit_progress, Ctx))(Done, Total, MessageOrUndef); out-of-band code can usebarrel_mcp:notify_progress/3,4.
Sessions
- ETS tables are
protected; mutators run inbarrel_mcp_session's gen_server. Mcp-Session-Idlifecycle with TTL-based cleanup.- Server-to-client sampling (
sampling/createMessage), elicitation (elicitation/create), roots query (roots/list), and resource update notifications.
Authentication
- Providers:
barrel_mcp_auth_bearer,barrel_mcp_auth_apikey,barrel_mcp_auth_basic,barrel_mcp_auth_none,barrel_mcp_auth_custom. - Hashing:
barrel_mcp_auth_basic:hash_password/1,2defaults to PBKDF2-SHA256 (100k iterations, 16-byte salt).barrel_mcp_auth_apikey:hash_key/2produces a peppered HMAC-SHA-256 digest. Both verifiers accept legacy hex SHA-256 digests for one release. All comparisons are constant-time. - OAuth 2.0 Protected Resource Metadata (RFC 9728): pass
resource_metadata => #{resource, authorization_servers}tostart_http_stream/1/start_http/1to expose/.well-known/oauth-protected-resourceand have the bearer challenge emitWWW-Authenticate: Bearer ... resource_metadata="<URL>"so MCP clients auto-discover the authorization server.
Server-to-client primitives
| Façade | Effect |
|---|---|
barrel_mcp:notify_resource_updated/1,2 | notifications/resources/updated to every subscriber. |
barrel_mcp:notify_progress/3,4 | notifications/progress to a session. |
barrel_mcp:notify_log/3,4 | notifications/message (server log stream) to a session, filtered against the session's logging/setLevel. |
barrel_mcp:notify_list_changed/1 | notifications/tools/list_changed, .../resources/list_changed, or .../prompts/list_changed to every active SSE session. Auto-emitted on reg_*/unreg_*. |
barrel_mcp:sampling_create_message/3 | Server→client sampling/createMessage (requires the client to declare sampling capability). |
barrel_mcp:elicit_create/3 | Server→client elicitation/create to ask the host for structured user input (requires the client to declare elicitation capability). |
barrel_mcp:roots_list/1,2 | Server→client roots/list to enumerate the host's available roots (requires the client to declare roots capability). |
barrel_mcp_tasks:create/3, finish/3, fail/3, cancel/2 | Long-running operation lifecycle. |
Client (barrel_mcp_client)
barrel_mcp_client is a supervised gen_statem that holds one
connection to one MCP server and routes the protocol surface defined
by the spec.
Transports
| Transport | Module | Notes |
|---|---|---|
| Streamable HTTP | barrel_mcp_client_http | POST with application/json, text/event-stream, SSE on POST and on a long-lived GET, Mcp-Session-Id capture, MCP-Protocol-Version after init, DELETE on close, 401 retry through barrel_mcp_client_auth. |
| stdio | barrel_mcp_client_stdio | Subprocess line-delimited JSON-RPC. |
Protocol coverage (Phase A — shipped)
- Targets
2025-11-25; negotiates downward through2025-06-18,2025-03-26,2024-11-05. initializewith spec-shaped capability objects;notifications/initialized(the spec name).tools/list,tools/call,resources/list,resources/read,resources/templates/list,resources/subscribe,resources/unsubscribe,prompts/list,prompts/get,completion/complete,logging/setLevel,ping,tasks/list,tasks/get,tasks/cancel,tasks/result.- Task statuses on the wire:
working,completed,failed,cancelled. Task timestamps (createdAt,lastUpdatedAt) are RFC 3339 strings. - Pagination via
cursor/nextCursor. Single page by default (want_cursor => trueto follow paging by hand). The sugar helperslist_tools_all/1,list_resources_all/1,list_resource_templates_all/1,list_prompts_all/1,tasks_list_all/1walk every page viabarrel_mcp_pagination:walk/1. - Cancellation:
barrel_mcp_client:cancel/2sendsnotifications/cancelledand unblocks the caller. - Roots changes:
barrel_mcp_client:notify_roots_list_changed/1emitsnotifications/roots/list_changedto inform the server the host's roots have changed. The server may re-issueroots/list. - Progress: pass
progress_tokentocall_tool/4and the caller receives{mcp_progress, Token, Params}for every matchingnotifications/progressuntil the request settles. - Periodic ping: opt-in via
ping_interval(andping_failure_threshold) in the connect spec; the connection is closed with reasonping_failedafter the configured number of consecutive failures. - Server→client requests dispatched through the
barrel_mcp_client_handlerbehaviour.{reply, _, _},{error, _, _, _}, and{async, Tag, _}reply forms; the host later callsbarrel_mcp_client:reply_async/3. - Server→client notifications routed to handler;
notifications/resources/updatedalso forwarded to subscribers.
Federation
barrel_mcp_clientsregisters one supervised client per caller-chosenServerId. Looked up via:- Tool-name namespacing across servers is host policy and is not enforced by the library.
Auth
barrel_mcp_client_authbehaviour.barrel_mcp_client_auth_bearer: static token.barrel_mcp_client_auth_oauth: OAuth 2.1 + PKCE.- Discovery:
parse_www_authenticate/1,discover_protected_resource/1(RFC 9728),discover_authorization_server/1(RFC 8414 with OpenID Connect fallback). - PKCE:
gen_code_verifier/0,code_challenge/1(S256),build_authorization_url/2. - Token endpoint:
exchange_code/2,refresh_token/2,client_credentials/2,token_exchange/2,jwt_bearer/2. All attach the RFC 8707resourceparameter; confidential clients use HTTP Basic. - Dynamic Client Registration (RFC 7591):
register_client/2posts client metadata to the AS'sregistration_endpointand returns the AS's response (client_id, optionalclient_secret, ...). Hosts then feed the credentials into one of the connect-spec auth entries. - Client Credentials grant (MCP
ext-authextension) for unattended agent hosts: passauth => {oauth_client_credentials, Config}on the connect spec. Required keys:token_endpoint,client_id. Authenticate withclient_secret(HTTP Basic) orclient_assertion(private_key_jwt, RFC 7523). Optionalscopes,resource. The library fetches the token eagerly duringinit/1and re-acquires via the same grant on 401 — no refresh_token needed. - Enterprise-Managed Authorization (MCP
ext-authEMA) for SSO-driven hosts: passauth => {oauth_enterprise, Config}. Required keys:idp_token_endpoint,as_token_endpoint,client_id,subject_token(an IdP ID Token / SAML assertion the host obtained out of band),subject_token_type,audience,resource. The handle chains RFC 8693 token-exchange at the IdP through RFC 7523 jwt-bearer at the AS and re-walks the same chain on 401. An expired subject token surfaces as{error, subject_token_expired}so the host can re-acquire from the IdP. - As an auth handle: when used through
auth => {oauth, Config}on the client spec, the library attachesAuthorization: Bearer ...on every request and runs the refresh-token grant on 401 if arefresh_tokenwas supplied. The interactive authorization-code redirect stays a host concern.
- Discovery:
Multi-server agent aggregator (barrel_mcp_agent)
Sits on top of barrel_mcp_clients and turns the federation
registry into a single namespaced tool catalog the host can hand
to an LLM, plus a router that dispatches a model's tool call back
to the right MCP server.
list_tools/0,1— aggregatedtools/listacross every registered client; tool names rewritten to<<"ServerId<sep>ToolName">>(default separator:).to_anthropic/0,1,to_openai/0,1— the same catalog in the matching provider shape.call_tool/2,3— parses the namespaced name, routes to the right client. Errors{error, no_separator | unknown_server}cover the parse / lookup paths.
LLM provider bridge (barrel_mcp_tool_format)
Translates MCP tool maps to the shapes the major LLM provider
APIs expect, and translates a model's tool-call back into the
(Name, Arguments) pair barrel_mcp_client:call_tool/4 consumes.
to_anthropic/1,to_openai/1— MCP tool → provider tool.from_anthropic_call/1,from_openai_call/1— provider call →{Name, Args}. Accepts both parsed maps and JSON-string arguments (the OpenAI wire shape).
Schema validation (barrel_mcp_schema)
Pure-Erlang JSON Schema subset validator hosts can use to pre-flight
LLM-generated tool args before calling the server. Covers type,
properties, required, enum, items, oneOf/anyOf/allOf,
additionalProperties: false, string minLength/maxLength/pattern,
number bounds, and array minItems/maxItems/uniqueItems.
case barrel_mcp_schema:validate(Args, ToolInputSchema) of
ok -> barrel_mcp_client:call_tool(Pid, Name, Args);
{error, Errors} -> reject(Errors)
end.Notes on past roadmap items
Periodic deadline timer. Earlier docs flagged a missing global sweep for in-flight requests with
infinitytimeout. In practice the client applies?DEFAULT_REQUEST_TIMEOUT(30s) per request unless the caller explicitly passestimeout => infinity, so the default loop is already time-bounded. Overriding an explicitinfinityfrom a background sweep would surprise callers who deliberately disabled the deadline; the per-request timer is the right hook.Client-side
Last-Event-IDresume. Earlier docs said the transport tracked the id but did not replay on reconnect. This actually does work today within the transport process lifetime:handle_sse_doneschedulesreopen_ssewhich preservessse_last_event_id, andstart_get_ssere-adds thelast-event-idheader on the new GET. A full client restart (gen_statem crash) does lose the cursor, but that flow re-initializes the session anyway, so a fresh stream is the correct outcome.