Changelog
View SourceAll notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[2.0.2] - 2026-05-23
A security release from a release-time review of the HTTP transport and the auth providers. No public API changes.
Security
- Authentication is enforced on every Streamable HTTP verb.
Previously only POST ran the configured auth provider; GET (open
SSE stream) and DELETE (terminate session) were gated by the
Mcp-Session-Idalone. A caller holding a leaked session id could read a session's server-to-client SSE traffic (including replayed buffered events) or terminate sessions without presenting a credential. Both verbs now run the same auth gate as POST, before any session lookup. Withbarrel_mcp_auth_none(the default) behaviour is unchanged. - Resource, prompt and completion handler crashes no longer leak
exception terms to the client. The 2.0.1 change that returns a
generic
Internal tool errorcovered onlytools/call. The synchronousresources/read,prompts/getandcompletion/completepaths still serialised the caughtClass:Reason(which can carry internal paths, argument values or secret-bearing terms) into the JSON-RPC error. They now log the class, reason, stack, request id and handler name vialogger:errorand return a generic message. - The built-in listener caps concurrent connections. Because
idle_timeoutisinfinity(so long-lived SSE GETs are never reaped), a connection lived until the peer closed it, so a flood of connections or slow/idle keep-alive clients could exhaust file descriptors and memory. Each listener now bounds established connections (default 16384, override with themax_connectionsstart option) and drops connections past the cap. h1's default 60srequest_timeoutalready bounds slow request headers. - Basic-auth unknown-user timing matches the configured mode.
When
hash_passwordswasfalsethe unknown-user path still ran the slow PBKDF2 stand-in while the configured-user path ran a fast SHA-256 compare, so response timing could reveal whether a username existed. The stand-in now does the same work as the active comparison mode.
Fixed
- Client reports its real version. The default
client_infosent ininitializewas pinned at2.0.0; it now matches the library version. The README dependency example also pinned the stalev1.3.0tag.
[2.0.1] - 2026-05-21
Follow-up hardening from a review of the 2.0.0 transport.
Fixed
_authno longer leaks into inbound responses. The Streamable HTTP transport tagged the authenticated principal (_auth) on every decoded message before splitting requests from responses, so a client-posted JSON-RPC response (answering a serversampling/elicitationrequest) carried_authinto the delivered map. It is now attached only on the request dispatch path, matching 1.x behaviour. Server-internal only; no data was sent to clients.- Accept loop no longer spins on persistent errors.
barrel_mcp_http_listenernow backs off briefly on a non-closedaccept error, so a system error such as file-descriptor exhaustion (emfile) throttles the acceptor instead of burning CPU.
[2.0.0] - 2026-05-21
A dependency-restructuring release. The HTTP server transport is rebuilt on the h1 and h2 libraries and Cowboy is removed from the library, so barrel_mcp can be embedded next to web frameworks (such as Livery) that bring their own HTTP stack without dragging Cowboy into the runtime. The protocol core and the public start/stop API are unchanged.
Changed (breaking)
- No Cowboy in the runtime. The
barrel_mcpapplication'sapplicationslist is now[kernel, stdlib, crypto, h1, h2, hackney](was[..., cowboy, hackney]). The built-in HTTP server (barrel_mcp:start_http/1,barrel_mcp:start_http_stream/1) runs onh1/h2: a cleartext bind speaks HTTP/1.1, and a TLS bind serves HTTP/1.1 and HTTP/2 on the same port via ALPN. Start/stop options and the protocol-core entry points are unchanged. Hosts that relied onbarrel_mcptransitively starting Cowboy must drop that assumption and add the apps they need to their own release. - Dependencies. Added
h1(hex packageerlang_h1) 0.2.2 andh20.6.0; removedcowboy. The MCP HTTP client still useshackney, bumped to 4.0.0. Cowboy is now a test-only dependency (the OAuth DCR and EMA suites mock an authorization server with it).
Added
barrel_mcp_http_engine: a transport-neutral implementation of the Streamable HTTP and simple HTTP protocol logic (routing, sessions, CORS, Origin validation, authentication, the OAuth protected-resource-metadata endpoint, async tool calls). It drives response I/O through a smallRespondermap of closures, so the built-inh1/h2server and external adapters (for example a Livery handler) can both reuse it.barrel_mcp_http_listener: the built-in single-porth1/h2server (cleartext h1, TLS h1+h2 via ALPN). A listenerstopnow tears down its in-flight connection processes.
Removed
barrel_mcp_prm_handler: the/.well-known/oauth-protected-resourceroute is now served directly bybarrel_mcp_http_engine.
[1.3.0] - 2026-05-10
A feature release that completes the OAuth surface vs MCP 2025-11-25 and modelcontextprotocol/ext-auth: Enterprise-Managed Authorization (EMA) for SSO-driven hosts, Dynamic Client Registration (RFC 7591) including the section-3 protected variant, plus four security follow-ups from review (scope fail-closed, no exception leakage, capped client buffers, no redirect-following on discovery).
Security follow-ups
- Scope checks now fail closed. When
required_scopesis configured but a custom auth provider returns anAuthInfomap without ascopeskey (or with a non-list value), the request is rejected with{error, insufficient_scope}. Previously these requests were admitted because the catch-allcheck_scopes/2clause returned{ok, AuthInfo}. Behaviour is unchanged forbarrel_mcp_auth_bearer(which always emits a list). - Tool crash details no longer leak to clients. When a tool handler raises,
barrel_mcp_registrylogs the class, reason, stack, request id, module and function vialogger:error. The wire-level error is now a generic<<"Internal tool error">>from bothbarrel_mcp_protocolandbarrel_mcp_http_stream; the previousio_lib:format("~p", [Reason])could disclose module/file/function names and exception terms. - Streamable-HTTP client buffers are capped.
barrel_mcp_client_httpnow bounds in-flight response buffers (16 MiB) and SSE event buffers (4 MiB). On overrun the client emits{mcp_closed, Pid, {response_too_large, Bytes}}and drops the request from tracking; a malicious or compromised MCP server can no longer drive unbounded memory growth in the host. - OAuth discovery no longer follows redirects.
barrel_mcp_client_auth_oauth:discover_protected_resource/1anddiscover_authorization_server/1previously passed{follow_redirect, true}, which let an untrusted MCP server redirect discovery into an SSRF-style probe. The flag is nowfalse; non-2xx surfaces as{error, {http_error, Status}}.
Enterprise-Managed Authorization grant
- New connect-spec entry
auth => {oauth_enterprise, Config}chains an IdP-issued ID Token (or SAML assertion) through RFC 8693 token-exchange (at the IdP) and RFC 7523 jwt-bearer (at the AS) into a short-lived MCP access token. Required Config keys:idp_token_endpoint,as_token_endpoint,client_id,subject_token,subject_token_type,audience,resource. Optionalclient_secret/client_assertion,scopes. Implements the second half ofmodelcontextprotocol/ext-authfor SSO-driven MCP hosts. - New public exchangers
barrel_mcp_client_auth_oauth:token_exchange/2andjwt_bearer/2for hosts that want to drive each step directly. - The handle re-walks the chain on every 401 (no
refresh_tokeninvolved). When the IdP returnsinvalid_grantthe library surfaces the typed{error, subject_token_expired}so the host can re-acquire from its IdP without parsing JSON.
Dynamic Client Registration (RFC 7591)
- New public
barrel_mcp_client_auth_oauth:register_client/2. Posts the supplied client metadata to the AS'sregistration_endpointand returns the response unchanged:client_id, optionalclient_secret,client_id_issued_at,client_secret_expires_at, plus any echoed metadata. Hosts feed the returned credentials into a subsequent{oauth, ...}/{oauth_client_credentials, ...}connect spec. - Stays a standalone exchanger: auto-wiring would need persistent storage of issued credentials, which is host policy. Documented in the OAuth section of the auth guide.
- New
register_client/3accepts anOptsmap.initial_access_token(RFC 7591 section 3) attachesAuthorization: Bearer ...so protected registration endpoints work.
_meta end-to-end propagation
- The MCP spec defines
_metaas the extensibility hook on every JSON-RPC envelope. Previously only_meta.progressTokenwas read ontools/call; everything else dropped on the floor. Now:- Inbound: tool handler
Ctxcarries the full inbound_metamap under themetakey.progress_tokenstays for back-compat. The async plan emitted bybarrel_mcp_protocol:handle/1fortools/callcarriesmetaso transports without their own_metaextraction (stdio, legacy HTTP) get it viabarrel_mcp_protocol:drive_async_plan/2. - Outbound: new return shapes on tool handlers —
{result_meta, Result, MetaMap},{structured_meta, Data, Content, MetaMap},{tool_error, Content, MetaMap}— surface_metaon the response. The existing tuple shapes are unchanged. - Envelope helpers: new
barrel_mcp_protocol:success_response/3anderror_response/4accept an optional_metamap. Empty map omits the field. Used by every transport's tool-outcome path so the wire shape is consistent.
- Inbound: tool handler
- 8 new eunit cases cover the envelope helpers,
drive_async_planfor the new result/structured/error meta variants, and an end-to-end_metaround-trip through a tool that echoes Ctx-supplied_metaback through the response.
Server-side OAuth Protected Resource Metadata + spec-correct WWW-Authenticate
- New
resource_metadatastart option onbarrel_mcp:start_http_stream/1andbarrel_mcp:start_http/1. When set, the server registers a cowboy route at/.well-known/oauth-protected-resourcethat returns the configured RFC 9728 PRM document as JSON, and threads the absolute PRM URL into the auth provider's challenge. - Wire change.
barrel_mcp_auth_bearer's 401 challenge now emitsBearer realm="...", resource_metadata="<URL>"(RFC 9728 / MCP auth sub-spec) instead of the previous non-conformantresource="..."parameter (which conflated the RFC 8707 audience claim with the metadata URL). MCP clients can now auto-discover a barrel_mcp deployment's authorization server end-to-end by parsingWWW-Authenticateand followingresource_metadata. - New
barrel_mcp_prm_handlercowboy handler. Two new helpers exported frombarrel_mcp_http_stream:normalize_resource_metadata/1,inject_resource_metadata_url/2(legacybarrel_mcp_httpreuses them). - New CT cases
prm_endpoint_serves_metadataandbearel_challenge_includes_resource_metadatainbarrel_mcp_http_stream_security_SUITE.
barrel_mcp_client:notify_roots_list_changed/1
- New client emitter for
notifications/roots/list_changed. Hosts that mutate their roots afterinitialize(user opened a new workspace, granted access to a new directory, …) call this so the server picks up the change without polling. The server may follow up withroots/listagainst the host's handler. - The server-side dispatch hook (
application:set_env(barrel_mcp, roots_changed_handler, {Mod, Fun})) was already in place; this PR closes the inverse direction. - New
test/barrel_mcp_client_roots_SUITEintegration test stands up a real Streamable HTTP server with a roots-changed handler that forwards to the test process; asserts the notification round-trips end-to-end.
resources/read runtime template expansion
- Registered
resource_templateentries are now matched against incomingresources/readURIs and routed to the template's handler. Previously templates only appeared inresources/templates/list; reading any URI matching one returnedResource not found. - New
barrel_mcp_uri_templatemodule implementing RFC 6570 Level 1 (simple{var}expansion).match/2returns a binary-keyed map of substituted values;expand/2is the inverse. 13 eunit cases cover single / multi-variable, literal-only, malformed templates, and round-trip. - The substituted variables flow into the handler's
Argsmap alongside the original requestparams, so afile:///{path}template matched againstfile:///etc/hostslets the handler read<<"path">>. - Direction A of the Python interop suite reads
file:///etc/hostsagainst thefile:///{path}fixture template and asserts the handler's expandedpathvalue round-trips.
[1.2.0] - 2026-05-03
A large release that consolidates everything since 1.1.0:
hardened security on both HTTP transports, full server-side
spec parity for MCP 2025-11-25 (including the new tasks/*
surface and the three server-to-client primitives), the
agent-host story (federation registry, multi-server aggregator,
LLM provider tool-shape bridge), a Python interop harness that
exercises every wire surface against mcp 1.27.0 in both
directions, server-side cursor pagination on every */list
endpoint, the OAuth Client Credentials grant from
modelcontextprotocol/ext-auth, and three runnable example
apps. The default protocol_version env is now 2025-11-25
(was 2025-03-26).
Breaking wire-level changes since 1.1.0 — hosts that produced or consumed these envelopes need to update:
notifications/tasks/changedwas renamed tonotifications/tasks/status(the spec method name).tools/callforlong_running => truetools wraps the immediate response asCreateTaskResult({<<"task">> => Task}) instead of the flat{taskId, status}.- Task envelopes use
lastUpdatedAt(wasupdatedAt) and include attlfield (alwaysnullfor now). tasks/cancelreturns the cancelledTaskinstead of{}.tasks/resultreturns aCallToolResult({<<"content">>, <<"structuredContent">>?}) instead of the raw stored value.Task status vocabulary is now
working | completed | failed | cancelled(wasrunning | success | error | cancelled).- Task timestamps are RFC 3339 strings (were integer milliseconds).
initializeadvertisestasks.list/get/cancel/resultas objects, not bare booleans.- POST
tools/callclients on Streamable HTTP must now list bothapplication/jsonandtext/event-streaminAccept(or*/*). Originis structurally validated on every Streamable HTTP method; public binds require explicitallowed_origins.barrel_mcp_http_streamdefaults to loopback ({127, 0, 0, 1}).- Top-level JSON-RPC arrays (batch requests) are rejected with
-32600. - JSON-RPC
idMUST be a string or integer;nulland other shapes are rejected.
Cancellation race fix in Streamable HTTP
wait_for_tool/2now does a 50ms lookahead after every tool outcome to absorb a pending{cancelled, _}message that races with the worker's response. A cooperative arity-2 handler that returns{tool_error, ...}on cancel could deliver its outcome to the waiter's mailbox before the session-emitted{cancelled, _}, depending on scheduler — which made the HTTP path emit a JSON-RPCisError: trueenvelope instead of the spec-mandated 200 + empty body. With the lookahead the cancel always wins.
OAuth Client Credentials grant (MCP ext-auth extension)
barrel_mcp_client_auth_oauthnow supports the OAuth 2.1client_credentialsgrant for unattended agent hosts. Passauth => {oauth_client_credentials, Config}on the connect spec; required keys aretoken_endpointandclient_id, plus eitherclient_secret(HTTP Basic per RFC 6749) orclient_assertion(private_key_jwt, RFC 7523). Optionalscopes,resource.- New public exchanger
barrel_mcp_client_auth_oauth:client_credentials/2for direct use outside the auth-handle flow. - The library fetches the token eagerly during
init/1(so a misconfigured client fails fast) and re-acquires via the same grant on every 401 — no refresh_token involved. Reuses the existing PRM + AS metadata discovery code. - Implements the OAuth Client Credentials extension from
modelcontextprotocol/ext-auth. The Enterprise-Managed Authorization extension (token-exchange + JWT bearer assertions) is left for follow-up; ask if you need it.
Doc cleanup: stale roadmap items + subscription session scope
guides/features.md's roadmap section called out a "periodic deadline timer" and "client-sideLast-Event-IDresume" as missing. Both turn out to be either by-design (default request timeout already bounds every call; explicitinfinityis a deliberate caller choice) or already shipped (the transport'sreopen_sseloop preservessse_last_event_idacross server-initiated SSE closes, and a full client restart re-initializes the session anyway). Replaced the roadmap section with notes explaining each.guides/tools-resources-prompts.mdnow calls out thatresources/subscribeis scoped to the callingMcp-Session-Id: when a client re-initializes, the new session id has no carry-over subscriptions and must subscribe again. Matches the spec's session-lifecycle model; previously implicit.
examples/agent_host — runnable multi-server federation demo
- New example app showing the
barrel_mcp_agentaggregator + router end-to-end.agent_host:run/0connects two clients to one in-process MCP server under differentServerIds, callsbarrel_mcp_agent:list_tools/0to surface the namespaced catalog, and routes a<<"beta:echo">>call through the right client. CT case asserts the namespaced names appear and the routed result round-trips. - Closes the docs loop for
barrel_mcp_agent(the module shipped without a runnable example). - Picked up by
make examples-testautomatically (the existingfor ex in examples/*/loop).
Server-side cursor pagination on */list
tools/list,resources/list,resources/templates/list,prompts/list, andtasks/listnow accept an opaquecursorparameter and emitnextCursorwhen more entries remain. Page size is 50, sorted by name (ortaskIdfor tasks). Existing single-shot callers see the first page transparently.- Direction A of the Python interop suite registers 60 dummy tools and walks
tools/listviacursoruntil exhausted, asserting at least onenextCursorwas emitted, no duplicates across pages, and that all fixture tools are visible across the walk.
Interop assertions tightened to value level
- Direction A's
list_tools,list_resources,read_resource,list_prompts,get_prompt,list_resource_templates, andcompletenow assert the actual field values returned by the Erlang server (descriptions, mime types, prompt argument names, content text, completion suggestions) instead of just presence checks. Same for Direction B against the Python FastMCP server.
Interop coverage: full feature surface
Direction A (Python client → Erlang server) now exercises every wire surface defined by the MCP spec:
ping,prompts/get,resources/templates/list,completion/complete.- Tool result variants:
structuredContentandisError: true. notifications/tools/list_changed(auto-emitted byreg/unreg).notifications/cancelledend-to-end: start a long-running task, cancel mid-flight viaexperimental.cancel_task, verify the cooperative arity-2 worker observes{cancel, RequestId}in its mailbox.notifications/tasks/statuscaptured via themessage_handlerwhile running other long-running tools.
Direction B (Erlang client → Python FastMCP server) now exercises:
tools/list,tools/call,resources/list,resources/read,prompts/list,prompts/get,ping.
Together with sampling / elicitation / roots / progress / subscribe / tasks already covered, every spec wire surface is now verified against the reference SDK on every CI run.
notifications/tasks/changed renamed to notifications/tasks/status
- The Task status-change notification was emitted under method
notifications/tasks/changed. The reference Python SDK'sServerNotificationdiscriminated union uses the spec method namenotifications/tasks/status. Renamed everywhere to match. Hosts that subscribed tonotifications/tasks/changedneed to switch to the new name.
Interop coverage: notifications/progress
- Direction A of the Python interop suite now exercises
notifications/progressend-to-end. A server-sideprogress_echotool emits three progress events through the arity-2 handler'sCtx.emit_progress; the reference Python SDK auto-attaches a progress token oncall_tooland routes the inbound notifications to aprogress_callback. Verifies the progress token plumbing, SSE delivery of progress notifications, and the SDK's progress dispatch.
Long-running tools: spec-shaped CreateTaskResult + CallToolResult on tasks/result
- Wire change.
tools/callforlong_running => truetools now returns the spec-shapedCreateTaskResultenvelope{<<"task">> => Task}(the full Task object with taskId, status, createdAt, lastUpdatedAt, ttl) instead of the flat{taskId, status}shape that was rejected by the reference Python SDK with a pydantic ValidationError. Hosts that previously readResult.taskIdneed to readResult.task.taskId. - Wire change. The task collector now stores tool results as the spec-shaped
CallToolResult({content, structuredContent?}) instead of the raw value, sotasks/resultreturns a payload that decodes asCallToolResultagainst the reference SDK. Previouslytasks/resultcould surface bare strings, which the JSON-RPC envelope validator rejected. - Direction A of the Python interop suite now exercises the full long-running flow end-to-end:
experimental.call_tool_as_task→ pollexperimental.get_taskuntilcompleted→ fetchexperimental.get_task_result(..., CallToolResult). Both wire shapes above are now validated againstmcp 1.27.0's pydantic models on every CI run.
Interop coverage: elicitation/create + roots/list
- Direction A now exercises the remaining two server-to-client primitives end-to-end against the reference SDK: a server-side tool calls
barrel_mcp:elicit_create/3(form-mode payload), the Pythonelicitation_callbackreturns anacceptaction with a fixed colour, and the tool surfaces that colour as text. Same shape forroots/list: a tool callsbarrel_mcp:roots_list/1, the Pythonlist_roots_callbackreturns one fixed root, and the tool surfaces its name. With sampling already covered, every server-to-client primitive is now wire-validated against the reference implementation on every CI run.
Interop coverage: server-to-client sampling/createMessage
- Direction A of the Python interop suite now exercises the full sampling round-trip against the reference SDK: a server-side tool calls
barrel_mcp:sampling_create_message/3, the Pythonsampling_callbackreturns a canned reply, the tool surfaces that text as its result. Verifies that the pending-request map, SSE delivery, response correlation, and capability gating all interoperate with the reference implementation.
Interop coverage: resources/subscribe + notifications/resources/updated
- Direction A of the Python interop suite now exercises the full subscribe round-trip: subscribe to a URI, trigger a server-side
notify_resource_updated/1, wait for the inboundnotifications/resources/updatedto arrive on the client SSE stream, unsubscribe. Verifies thatbarrel_mcp_session:subscribe_resource/2,notify_resource_updated/1, and the SSE delivery path all interoperate correctly with the reference implementation's notification handling.
Tasks Task-shape wire alignment
- Renamed the wire field
updatedAt→lastUpdatedAton every Task envelope (tasks/get,tasks/list,notifications/tasks/changed). The reference Python SDK models the field aslastUpdatedAt, and accepts no other name. - Added a
ttlfield to every Task envelope (alwaysnullfor now — we don't yet honour client-supplied TTLs). The Python SDK'sTaskmodel requires the field to be present. tasks/cancelnow returns the cancelled Task instead of{}. MatchesCancelTaskResultin the reference SDK; existing barrelmcp clients that pattern-match `{ok, }` are unaffected.- Extended the Direction A interop test to call
experimental.list_tasks(), exercising the Task wire shape end-to-end against the reference pydantic models.
Tasks capability wire shape fix
initializenow advertisestasks.list/tasks.get/tasks.cancel/tasks.resultas the spec-shaped empty objects (#{}) instead of baretruebooleans. Caught by the new Python interop tests; the reference Python SDK rejectsboolfor these fields with a pydanticValidationError.listChangedstays a boolean (the spec keeps that one as bool).
Python interop test harness
- New
test/interop/directory pairing a Python MCP client (client.py, Streamable HTTP) and a Python FastMCP server (server.py, stdio) with the officialmcpSDK pinned to~= 1.27.0. - New
test/barrel_mcp_python_interop_SUITECommon Test suite drives both directions: Python client → Erlang server (initialize, list_tools / call_tool, list_resources / read_resource, list_prompts, set_logging_level), and Erlang client → Python server (list_tools, call_tool round-trip). make interop-setupcreates the venv,make interop-testruns the suite. The CT cases skip cleanly whenINTEROP_PYTHONis unset, so the defaultrebar3 ctloop is unaffected by missing Python tooling.- New
interopCI job runs both directions on Linux with Python 3.12 + OTP 28.
barrel_mcp_agent — multi-server tool aggregator
- New module sitting on top of
barrel_mcp_clients. Aggregatestools/listacross every connected MCP client, rewrites each tool name to<<"ServerId<sep>ToolName">>(default separator:), and routes a namespacedcall_tool/2,3back to the correct client. to_anthropic/0,1andto_openai/0,1return the aggregated catalog directly in provider format, ready to hand to a model.- Closes the orchestration gap for hosts running an agent loop against multiple MCP servers.
barrel_mcp_tool_format — LLM provider tool-shape translator
- New module bridging MCP tool definitions and the tool shapes the LLM provider APIs expect.
to_anthropic/1,to_openai/1translate MCPtools/listentries (single map or list) to the Anthropic Messages API and OpenAI Chat Completions tool shapes.from_anthropic_call/1,from_openai_call/1translate a model's tool-call back to the(Name, Arguments)pairbarrel_mcp_client:call_tool/4expects. Accepts both parsed arguments (already a map) and the wire form (JSON string).- Closes the bridge an agent host needs when it hands MCP tools to a model and routes the model's tool calls back through an MCP client.
resources/read content-block flexibility
- Resource handlers may now return a list of pre-built content blocks; each block is passed through verbatim, with
uriauto-injected when the handler omits it. - The
#{text := _}and#{blob := _, mimeType := _}map shapes accept optionalmimeType(text only — blob already requires it) andannotationskeys, matching the spec's per-content metadata. Both flow through to the wire undermimeType/annotations.
Tool, resource, prompt, and resource-template annotations
reg_tool/4,reg_resource/4,reg_prompt/4, andreg_resource_template/4accept a newannotationsoption — a free-form map surfaced verbatim underannotationsin the matching*/listpayload. The MCP spec definesreadOnlyHint/destructiveHint/idempotentHint/openWorldHintfor tools, andaudience/priorityfor resources, prompts, and templates. Registrations without annotations omit the field on the wire.
logging/setLevel actually filters the log stream
- The previous
logging/setLevelhandler was a no-op stub that accepted any payload and returned{}. It now validates the requested level against the eight RFC 5424 levels (debug, info, notice, warning, error, critical, alert, emergency), persists the chosen level on the session, and rejects unknown levels with-32602. - New
barrel_mcp:notify_log/3,4façade. Emitsnotifications/messageto a session and is silently dropped when the event level is below the session's configured level. Default session level isinfo, matching the spec. - New helpers
barrel_mcp_session:set_log_level/2,get_log_level/1,log_level_priority/1.
Server-to-client roots/list
- New
barrel_mcp:roots_list/1,2façade. Sendsroots/listto the connected client behind a session id and returns the host's roots. Requires the client to have declaredrootscapability in itsinitialize' request and an active SSE stream. - New helpersbarrel_mcp:list_sessions_with_roots/0,barrel_mcp_session:has_roots/1,barrel_mcp_session:list_roots_capable/0. ### Server-to-clientelicitation/create- Newbarrel_mcp:elicit_create/3façade. Sendselicitation/createto the client behind a session id and blocks until the client responds (ortimeout_mselapses, default 30s). Requires the client to have declaredelicitationcapability in itsinitialize' request and an active SSE stream. Mirrors the existingsampling_create_message/3flow. - New helpers
barrel_mcp:list_sessions_with_elicitation/0,barrel_mcp_session:has_elicitation/1,barrel_mcp_session:list_elicitation_capable/0. - Internally, the server's pending-request map now carries the response tag, so sampling and elicitation responses route back to the correct caller without colliding.
Tasks spec parity (MCP 2025-11-25)
Status vocabulary aligned with spec. Internal
running | success | error | cancelledreplaced withworking | completed | failed | cancelledon the wire (intasks/get,tasks/list,notifications/tasks/changed, and the immediatetools/callresponse whenlong_running => true).- RFC 3339 timestamps.
createdAtandupdatedAtare emitted as ISO 8601 strings viacalendar:system_time_to_rfc3339/1instead of integer milliseconds. tasks/resultmethod. New JSON-RPC method to fetch the recorded result for acompletedtask (or the recorded error forfailed); returnsTask not yet completeforworking,Task cancelledforcancelled, andTask not foundotherwise. New client wrapperbarrel_mcp_client:tasks_result/2.- Tasks capability shape. Advertised as
#{list, get, cancel, result, listChanged}instead of the bare#{listChanged}placeholder.
Critical correctness and security fixes
- API-key auth verification.
barrel_mcp_auth_apikey:verify_key/2no longer returnsokfor any HMAC-formatted stored value (it was self-comparingStoredwith itself). The 2-arity helper now rejects HMAC formats with{error, pepper_required}; a newverify_key/3takes the pepper and does a constant-time HMAC compare. The provider state now keepspepper, sohash_keys => truewith a configured pepper actually verifies HMAC keys end to end. - Async tools/call works on stdio and legacy HTTP.
barrel_mcp_protocol:handle/2returns{async, AsyncPlan}fortools/call; both transports now drive the plan via the newbarrel_mcp_protocol:drive_async_plan/2helper. Tool calls over stdio went from broken to functional. - Session cleanup no longer self-calls. The cleanup timer in
barrel_mcp_sessionpreviously routed throughgen_server:call(?MODULE, ...)from inside its ownhandle_infoand would deadlock. The cleanup is now inlined inhandle_info(cleanup, _). - Basic auth unknown-user timing. The unknown-user fake check now runs the same PBKDF2 work as the configured-user path via a precomputed dummy hash. Previously the configured
hash_passwords => falsepath used a fast SHA-256 compare while the unknown-user path always did PBKDF2, leaking username existence. - Streamable HTTP: Accept strictness. POST clients must list both
application/jsonandtext/event-stream(or*/*).application/jsonalone now returns 406. - Streamable HTTP: initialize with unknown session id → 404. Previously silently created a fresh session; now forces the client to re-initialize without a session header.
- Legacy HTTP transport hardened. Reuses the Streamable HTTP
validate_origin/2,cors_response_headers/3, andextract_headers/2helpers. No more wildcardAccess-Control-Allow-Origin; auth headers come from the configured provider'sauth_headers/1callback (customheader_nameflows through CORS and into header extraction). tasks/cancelactually stops the worker. Long-running tools now record their worker pid on the task;tasks/cancelsends{cancel, RequestId}to the worker before transitioning the stored status. Cooperative arity-2 handlers can abort cleanly; arity-1 handlers still run to completion but their result is dropped because the task is in a terminal state.
Spec parity additives
- Long-running tools return a
taskIdimmediately; clients track them viatasks/list,tasks/get,tasks/cancelandnotifications/tasks/changed. Opt in withreg_tool/4'slong_running => true. - Tools can return structured output via
{structured, Data}or{structured, Data, Content}; the response includesstructuredContent. Opt-invalidate_output => trueschema-checks the output and surfaces failures asisError: true. completion/completeis backed by a registry. Hosts callbarrel_mcp:reg_completion(Ref, Mod, Fun, Opts)to provide suggestions for prompt or resource-template arguments. Thecompletionscapability is advertised when at least one is registered.- Tool, resource, prompt, and resource-template registrations accept
titleandicons; the matching*/listresponses surface them. - Streamable HTTP keeps a per-session ring buffer of recent SSE events. Reconnecting clients with
Last-Event-IDget every event newer than that id replayed before live mode; an out-of-window id yields a syntheticnotifications/replay_truncated. Buffer size configurable viastart/1'ssse_buffer_size.
Spec parity: protocol bump, async tools, list-changed, auth hardening
- Server protocol bumped to
2025-11-25.initialize' negotiates with the client: when the client requests a version we speak, we echo it; otherwise we reply with our preferred version. Capabilities advertised ininitialize' now includelistChanged: true' ontools',resources', andprompts'. - Async tool execution.
barrel_mcp_protocol:handle/2returns{async, AsyncPlan}fortools/call'; the transport invokes the spawn closure to start a worker, records the in-flight entry, and waits on its mailbox. Tool handlers may export arity 1 (legacy) or arity 2 ((Args, Ctx)— the new shape that receives session/progress context). - **notifications/cancelled' wired end-to-end.** Inbound cancel finds the in-flight worker viabarrel_mcp_session:cancel_in_flight/2, sends{cancel, RequestId}' to the worker and{cancelled, RequestId}' to the waiter. Per the MCP spec the cancelled HTTP request closes with 200 + empty body; no JSON-RPC response is emitted. - `notifications/progress' emit + handler context. New façades
barrel_mcp:notify_progress/3,4. Arity-2 tool handlers receiveCtxwith anemit_progressfunction bound to the session's progress token, so they can emit progress without knowing about sessions. - `notifications/roots/list_changed' dispatch hook. Configurable viaapplication:set_env(barrel_mcp, roots_changed_handler, {Mod, Fun}).. No-op when unset. - `resources/templates/list' real registry. New
barrel_mcp:reg_resource_template/4,unreg_resource_template/1,list_resource_templates/0. The protocol method now returns the registered templates instead of an empty stub. - Server-side input validation.reg_tool/4acceptsvalidate_input => true; the registry runsbarrel_mcp_schema:validate/2against the tool'sinput_schemabefore invoking the handler. Failures surface to the client asisError: truecontent. - Tool error reporting viaisError: true. Handlers may return{tool_error, Content}'; the transport wraps it as#{<<"content">> => Content, <<"isError">> => true}`. - `*/list_changed' notifications.
barrel_mcp_registry:reg/4,5andunreg/2automatically broadcast the matchingnotifications/<kind>/list_changedenvelope to every active SSE session. Newbarrel_mcp:notify_list_changed/1for out-of-band catalogue changes. - Auth hardening. -barrel_mcp_auth_basic:hash_password/1,2now defaults to PBKDF2-SHA256 (100k iterations, random salt). Stored formatpbkdf2-sha256$<iters>$<b64(salt)>$<b64(hash)>. Publicverify_password/2accepts the new format and the legacy hex SHA-256 digest (the latter logs a deprecation warning). -barrel_mcp_auth_apikey:hash_key/2adds an HMAC-SHA-256 keyed format (hmac-sha256$<b64(hash)>). Publicverify_key/2honours both formats with constant-time comparison. ### Security and spec conformance (Streamable HTTP + JSON-RPC) - Origin validation. Streamable HTTP and the legacybarrel_mcp_httpnow validate theOriginheader on POST/GET/DELETE/OPTIONS usinguri_string:parse/1(structural scheme/host/port match — no binary prefix matching). New optionsallowed_originsandallow_missing_origin. The literalOrigin: nullvalue is treated as a distinct present origin and is rejected unless explicitly allowed. - Default bind to loopback. Both transports default to{127, 0, 0, 1}. Public binds require an explicitallowed_origins; the start function refuses with{error, allowed_origins_required}otherwise. - CORS tightening.Access-Control-Allow-Originnow echoes the validatedOrigin(no wildcard) withVary: Origin, and is omitted entirely when noOriginis sent. TheAccess-Control-Allow-Headersallow-list is derived from the configured auth provider via a new optionalauth_headers/1callback onbarrel_mcp_auth. Custom API-key header names are honoured both in CORS and inextract_headers. - Streamable HTTP response shape. Notifications and POSTed responses to server-initiated requests now return 202 Accepted with empty body. MissingMcp-Session-Idon a non-initialize request returns 400 Bad Request; unknown/invalid id returns 404 Not Found.initializeis the only request that may run without a session. -MCP-Protocol-Versionserver validation. Present-but-unsupported header → 400 with the supported list. Missing header on a session that has completed initialize falls back to the session-stored negotiated version. Pre-init / no session falls back to2025-03-26per spec compatibility guidance. New?MCP_SUPPORTED_VERSIONSmacro. - JSON-RPC id strictness.barrel_mcp_protocol:handle/2anddecode_envelope/1now reject ids that are notbinaryorinteger(includingnull) with-32600 Invalid Request. - Batch rejection. Top-level JSON arrays are explicitly rejected with-32600 Batch requests are not supportedat both the HTTP boundary and insidehandle/2. - ETS visibility.barrel_mcp_sessions,barrel_mcp_resource_subs, andbarrel_mcp_pending_requestsare nowprotected. Every public mutator onbarrel_mcp_session(create, updateactivity, delete, set_client_capabilities, set_protocol_version, set_sse_pid, subscribe_resource, unsubscribe_resource, deliver_response, cleanup_expired) routes through the gen_server. - Newtest/barrel_mcp_http_stream_security_SUITE.erlcovers Origin matching, session lookup, version validation, response shape, batch / id strictness, and ETS protection. ### Added - Spec-conformant MCP client (barrel_mcp_client) - Rewritten as agen_statem(connecting→initializing→ready→closing). - Async transports forward inbound JSON-RPC envelopes as `{mcp_in, , _}messages. - Streamable HTTP client transport (barrel_mcp_client_http): POST withapplication/json, text/event-stream, parses SSE from POST and from a long-lived GET stream, capturesMcp-Session-Id, sendsMCP-Protocol-Versionafter init, DELETE on close, 401 retry through pluggable auth. - Stdio client transport (barrel_mcp_client_stdio) extracted into its own gen_server. - Targets MCP2025-11-25and negotiates downward through2025-06-18,2025-03-26,2024-11-05. - Server-initiated requests/notifications routed through abarrel_mcp_client_handlerbehaviour with sync, error, and async reply forms; default no-op handler ships inbarrel_mcp_client_handler_default. - Capability-shaped initialize payload (booleans become spec objects on the wire). - Resource subscription notifications routed back to the subscribing process. - Pagination, cancellation, and progress-token plumbing ontools/call. - **Federation registry** (barrel_mcp_clients): one supervised connection per server id, looked up viabarrel_mcp:start_client/2,whereis_client/1,list_clients/0,stop_client/1. - **Auth behaviour** (barrel_mcp_client_auth) with a static-bearer implementation; OAuth 2.1 + PKCE planned for a follow-up. - **JSON-RPC envelope helpers** (encode_request/3,encode_notification/2,encode_response/2,encode_error/3,decode_envelope/1) shared between client and server. - New tests:barrel_mcp_client_tests(loopback handshake / call_tool / version downgrade),barrel_mcp_client_handler_tests,barrel_mcp_clients_tests,barrel_mcp_protocol_envelope_tests. - New doc:guides/features.mdsummarising the client surface and roadmap. ### Changed -notifications/initializedis now the spec name; legacy bareinitializedstill accepted for one release. - CORS onbarrel_mcp_http_streamexposesmcp-protocol-versionandlast-event-id. ### Added (ergonomics) -barrel_mcp_pagination:walk/1,2: cursor walker shared by every/listpaged helper, with a configurable max-pages guard. -barrel_mcp_client:list_tools_all/1,list_resources_all/1,list_resource_templates_all/1,list_prompts_all/1: walk every page and return the union. -barrel_mcp_schema:validate/2: minimal JSON Schema validator covering type/properties/required/enum/items/oneOf/anyOf/allOf/min-max-length/pattern/min-max-items/uniqueItems/min-max/exclusive bounds. Returnsokor{error, [{Path, Reason}]}. Hosts use it to pre-flight LLM-generated tool args before calling the server. ### Added (control plane) - Progress dispatch: when a caller passesprogress_tokentocall_tool/4, the client registers the caller pid against that token and routes inboundnotifications/progressto it as{mcp_progress, Token, Params}. The mapping clears automatically when the request settles, is cancelled, or times out. - Periodic ping:ping_interval(defaultinfinity, opt-in) sendspingwhile inready. Afterping_failure_thresholdconsecutive failures (default3), the connection closes with reasonping_failed. ### Added (docs) -guides/building-a-client.md— task-oriented walkthrough for hosting MCP clients onbarrel_mcp(transport choice, connect spec, lifecycle, capability negotiation, tool calls, server-initiated requests via the handler behaviour, OAuth, federation, schema validation, error reference). -guides/internals.md— architecture and behaviour contracts (module map, supervision tree, state machine, message flow, transport/handler/auth contracts, ETS layout, wire format). -examples/echo_client/— minimal MCP host that boots a local server, lists tools, callsecho. Common-test suite asserts the round-trip. -examples/sampling_host/— host implementingbarrel_mcp_client_handlerto answersampling/createMessage. Common-test suite covers the full server-to-client round-trip. -test/snippet_check.escript+test/doc_snippets_SUITE.erl— extracts every `` ```erlang `` fenced block from the new guides and example READMEs and verifies it compiles. Wired intorebar3 ct. -Makefilewithexamples-setupandexamples-testtargets; CI runs example suites on OTP 27 + 28. - Per-function@docand-specon the public client surface (barrel_mcp_client,barrel_mcp_clients,barrel_mcp_client_handlerexample). - ex_doc sidebar reorganised: client modules grouped, new "Building a Client" / "Client Internals" pages. ### Added (auth) -barrel_mcp_client_auth_oauth: OAuth 2.1 + PKCE per the MCP authorization spec. - Discovery helpers hosts can use during initial token acquisition:parse_www_authenticate/1,discover_protected_resource/1(RFC 9728),discover_authorization_server/1(RFC 8414, with OpenID Connect fallback). - PKCE primitives:gen_code_verifier/0,code_challenge/1(S256),build_authorization_url/2. - Token endpoint:exchange_code/2(authorization-code grant) andrefresh_token/2(refresh grant). Both honour the RFC 8707resourceparameter and support confidential-client HTTP Basic. - Behaviour implementation that attachesAuthorization: Bearer ...and refreshes transparently on 401 when arefresh_tokenwas supplied. -barrel_mcp_client_auth:new({oauth, Config})is now wired through;Configacceptsaccess_token(required),refresh_token,token_endpoint,client_id,client_secret,resource,scopes. The interactive authorization-code redirect step stays a host concern; once the host has tokens it hands them to the client and the library handles refresh. ## [1.1.0] - 2025-01-27 ### Added - **MCP Streamable HTTP Transport** (barrel_mcp_http_stream) - Protocol version 2025-03-26 support for Claude Code integration - POST with JSON or SSE streaming responses - GET for server-to-client notification streams (SSE) - DELETE for session termination - OPTIONS for CORS preflight - HTTPS/TLS support - Seeguides/http-stream.mdfor usage - **Session Management** (barrel_mcp_session) - ETS-based session tracking for Streamable HTTP transport - Sessions identified viaMcp-Session-Idheader - Configurable TTL with automatic cleanup (default: 30 minutes) - SSE stream lifecycle management - **Custom Authentication Provider** (barrel_mcp_auth_custom) - Simplified interface for custom authentication modules - Only requiresinit/1andauthenticate/2callbacks - Automatically extracts tokens from Bearer and X-API-Key headers - Seeguides/custom-authentication.mdfor usage ### Changed - Protocol version updated to2025-03-26for Streamable HTTP transport - Supervisor now includes session manager child spec - Addedcryptoto application dependencies ## [1.0.0] - 2025-12-29 Initial release of barrel_mcp, an Erlang implementation of the Model Context Protocol (MCP) 2024-11-05. ### Added #### Core Features - **Tools** - Register and call tools with JSON Schema validation - **Resources** - Register and read resources with URI-based addressing - **Prompts** - Register and retrieve prompts with argument substitution - **Registry** - ETS + persistent_term based handler registry for fast lookups #### Transports - **HTTP Transport** - Cowboy-based HTTP server for MCP over HTTP - **stdio Transport** - stdin/stdout transport for Claude Desktop integration - Blocking mode viastart_stdio/0- Supervised mode viastart_stdio_link/0` #### Client - *MCP Client - Connect to external MCP servers - HTTP transport support via hackney - Tool listing and calling - Resource listing and reading - Prompt listing and retrieval #### Authentication - Pluggable authentication system viabarrel_mcp_authbehaviour - Built-in providers: -barrel_mcp_auth_none- No authentication (default) -barrel_mcp_auth_bearer- JWT/Bearer token authentication (HS256 built-in) -barrel_mcp_auth_apikey- API key authentication -barrel_mcp_auth_basic- HTTP Basic authentication - Scope-based authorization - Constant-time credential comparison #### Documentation - Comprehensive EDoc documentation for all public APIs - HexDocs integration via rebar3_ex_doc - Guides: - Getting Started - stdio Transport - Authentication - Tools, Resources & Prompts - MCP Client ### Protocol Support - JSON-RPC 2.0 - MCP 2024-11-05 specification - Methods: initialize, ping, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get [1.3.0]: https://github.com/barrel-platform/barrel_mcp/releases/tag/v1.3.0 [1.2.0]: https://github.com/barrel-platform/barrel_mcp/releases/tag/v1.2.0 [1.0.0]: https://github.com/barrel-db/barrel_mcp/releases/tag/v1.0.0