barrel_mcp features

View Source

Tracks 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) — MCP 2025-11-25 with downward negotiation to 2025-06-18, 2025-03-26, 2024-11-05. POST (JSON or SSE), GET (SSE), DELETE, OPTIONS. Default bind 127.0.0.1; public binds require allowed_origins. Origin is validated structurally on every method (POST/GET/DELETE/OPTIONS); literal Origin: null is rejected unless explicitly allowed. CORS echoes the validated origin (no wildcard); Access-Control-Allow-Headers is 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-Id on a non-initialize request → 400; unknown id → 404.
  • MCP-Protocol-Version validated server-side: missing falls back to the session-stored negotiated version; unsupported → 400.
  • JSON-RPC id must be string or integer; null and other shapes rejected with -32600. Top-level JSON arrays (batches) explicitly rejected — MCP removed batching.
  • notifications/cancelled aborts 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-ID replay; out-of-window ids surface a synthetic notifications/replay_truncated event.

Registries

  • Tools — handlers may be arity 1 or arity 2. Arity-2 handlers receive a Ctx map with session_id, request_id, progress_token, the inbound meta map (the spec's _meta extension hook), and an emit_progress function. Meta-bearing return shapes ({result_meta, Result, Map}, {structured_meta, Data, Content, Map}, {tool_error, Content, Map}) attach _meta to the response envelope.
  • Resources — text/binary content, MIME types, notifications/resources/updated for live updates. Handlers may return a single block (#{text := _} / #{blob := _, mimeType := _}, with optional mimeType and annotations) or a list of pre-built content blocks for multi-part responses.
  • Resource templates — RFC 6570 URI templates, surfaced via resources/templates/list. resources/read against 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 in Args.
  • Prompts — multi-message conversation templates with arguments.
  • Completions — keyed by {prompt, Name, Arg} or {resource_template, Uri, Arg}; advertised via the completions capability when at least one is registered.
  • All registrations accept optional title and icons.
  • Tool, resource, prompt, and resource-template registrations also accept annotations — a free-form map surfaced verbatim under annotations in the matching */list payload. Tools use readOnlyHint, destructiveHint, idempotentHint, openWorldHint; resources/prompts/templates use audience (["user" | "assistant"]) and priority (0..1).

Tool features

  • Return shapes: plain (text / map / list / image), {tool_error, Content} (→ isError: true), {structured, Data} / {structured, Data, Content} (→ structuredContent).
  • validate_input and validate_output opt-in schema validation via barrel_mcp_schema.
  • long_running => true returns a taskId immediately and runs the worker in the background. Backed by barrel_mcp_tasks — surfaces tasks/list, tasks/get, tasks/cancel, and notifications/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 use barrel_mcp:notify_progress/3,4.

Sessions

  • ETS tables are protected; mutators run in barrel_mcp_session's gen_server.
  • Mcp-Session-Id lifecycle 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,2 defaults to PBKDF2-SHA256 (100k iterations, 16-byte salt). barrel_mcp_auth_apikey:hash_key/2 produces 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} to start_http_stream/1 / start_http/1 to expose /.well-known/oauth-protected-resource and have the bearer challenge emit WWW-Authenticate: Bearer ... resource_metadata="<URL>" so MCP clients auto-discover the authorization server.

Server-to-client primitives

FaçadeEffect
barrel_mcp:notify_resource_updated/1,2notifications/resources/updated to every subscriber.
barrel_mcp:notify_progress/3,4notifications/progress to a session.
barrel_mcp:notify_log/3,4notifications/message (server log stream) to a session, filtered against the session's logging/setLevel.
barrel_mcp:notify_list_changed/1notifications/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/3Server→client sampling/createMessage (requires the client to declare sampling capability).
barrel_mcp:elicit_create/3Server→client elicitation/create to ask the host for structured user input (requires the client to declare elicitation capability).
barrel_mcp:roots_list/1,2Server→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/2Long-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

TransportModuleNotes
Streamable HTTPbarrel_mcp_client_httpPOST 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.
stdiobarrel_mcp_client_stdioSubprocess line-delimited JSON-RPC.

Protocol coverage (Phase A — shipped)

  • Targets 2025-11-25; negotiates downward through 2025-06-18, 2025-03-26, 2024-11-05.
  • initialize with 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 => true to follow paging by hand). The sugar helpers list_tools_all/1, list_resources_all/1, list_resource_templates_all/1, list_prompts_all/1, tasks_list_all/1 walk every page via barrel_mcp_pagination:walk/1.
  • Cancellation: barrel_mcp_client:cancel/2 sends notifications/cancelled and unblocks the caller.
  • Roots changes: barrel_mcp_client:notify_roots_list_changed/1 emits notifications/roots/list_changed to inform the server the host's roots have changed. The server may re-issue roots/list.
  • Progress: pass progress_token to call_tool/4 and the caller receives {mcp_progress, Token, Params} for every matching notifications/progress until the request settles.
  • Periodic ping: opt-in via ping_interval (and ping_failure_threshold) in the connect spec; the connection is closed with reason ping_failed after the configured number of consecutive failures.
  • Server→client requests dispatched through the barrel_mcp_client_handler behaviour. {reply, _, _}, {error, _, _, _}, and {async, Tag, _} reply forms; the host later calls barrel_mcp_client:reply_async/3.
  • Server→client notifications routed to handler; notifications/resources/updated also forwarded to subscribers.

Federation

Auth

  • barrel_mcp_client_auth behaviour.
  • 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 8707 resource parameter; confidential clients use HTTP Basic.
    • Dynamic Client Registration (RFC 7591): register_client/2 posts client metadata to the AS's registration_endpoint and returns the AS's response (client_id, optional client_secret, ...). Hosts then feed the credentials into one of the connect-spec auth entries.
    • Client Credentials grant (MCP ext-auth extension) for unattended agent hosts: pass auth => {oauth_client_credentials, Config} on the connect spec. Required keys: token_endpoint, client_id. Authenticate with client_secret (HTTP Basic) or client_assertion (private_key_jwt, RFC 7523). Optional scopes, resource. The library fetches the token eagerly during init/1 and re-acquires via the same grant on 401 — no refresh_token needed.
    • Enterprise-Managed Authorization (MCP ext-auth EMA) for SSO-driven hosts: pass auth => {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 attaches Authorization: Bearer ... on every request and runs the refresh-token grant on 401 if a refresh_token was supplied. The interactive authorization-code redirect stays a host concern.

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 — aggregated tools/list across 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 infinity timeout. In practice the client applies ?DEFAULT_REQUEST_TIMEOUT (30s) per request unless the caller explicitly passes timeout => infinity, so the default loop is already time-bounded. Overriding an explicit infinity from a background sweep would surprise callers who deliberately disabled the deadline; the per-request timer is the right hook.

  • Client-side Last-Event-ID resume. 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_done schedules reopen_sse which preserves sse_last_event_id, and start_get_sse re-adds the last-event-id header 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.