Releases: barrel-platform/barrel_mcp
v2.2.0
Threads the authenticated principal into tool handlers.
Added
- Arity-2 tool handlers (
Mod:Fun(Args, Ctx)) now receive the auth provider'sauthenticate/2result inCtxunderauth_info, so owner-scoped tools can identify the caller. Arity-1 handlers (Mod:Fun(Args)) are unchanged. Withbarrel_mcp_auth_nonethe value is the anonymous principal; on paths with no auth provider (stdio) it isundefined. barrel_mcp_protocol:drive_async_plan/3, which threadsauth_infointo the synchronous tool-call path.drive_async_plan/2is retained and delegates withauth_infoset toundefined.
Full Changelog: https://github.com/barrel-platform/barrel_mcp/blob/main/CHANGELOG.md
v2.1.0
Erlang/OTP 29 support. No public API changes.
Changed
- Support OTP 29; CI now runs on OTP 28 and 29 (dropped 27) with rebar3 3.27.
- Replaced the deprecated bare
catchoperator withtry ... catchthroughout, as OTP 29 deprecatescatch .... - Updated dependencies:
erlang_h10.2.3,h20.6.1,hackney4.0.3; test depsmeck1.2.0 andcowboy2.15.0.
v2.0.2
A security release from a release-time review of the 2.0.x HTTP transport and the auth providers. No public API changes.
Security
- Authentication is enforced on every Streamable HTTP verb. GET (open SSE) and DELETE (terminate session) previously ran no auth provider; the
Mcp-Session-Idalone gated them, so a leaked session id let a caller read a session's server-to-client SSE traffic or terminate sessions without a credential. Both verbs now run the same auth gate as POST, before any session lookup. No-op underbarrel_mcp_auth_none. - Resource, prompt and completion handler crashes no longer leak exception terms to the client.
resources/read,prompts/getandcompletion/completeserialised the caughtClass:Reasoninto the JSON-RPC error (onlytools/callwas fixed in 2.0.1). They now log server-side and return a generic message. - The built-in listener caps concurrent connections. With
idle_timeoutatinfinity(so long-lived SSE GETs are never reaped) a connection lived until the peer closed it, so a connection flood or slow/idle clients could exhaust file descriptors and memory. Each listener now bounds established connections (default 16384,max_connectionsoption). - Basic-auth unknown-user timing matches the configured mode. When
hash_passwordswasfalsethe unknown-user path ran PBKDF2 while the configured path ran a fast SHA-256 compare, revealing username existence by timing. The stand-in now matches the active mode.
Fixed
- The client reported version
2.0.0inclient_info; it now matches the library version, and the README dependency example no longer pins the stalev1.3.0tag.
Full Changelog: v2.0.1...v2.0.2
v2.0.1
Follow-up hardening from a review of the 2.0.0 transport. No API changes.
- Attach
_authonly on the request dispatch path, so a client-posted JSON-RPC response (answering a serversampling/elicitationrequest) no longer carries it into the delivered map. Server-internal; matches 1.x. barrel_mcp_http_listenerbacks off briefly on non-closedaccept errors, so file-descriptor exhaustion (emfile) throttles the acceptor instead of spinning the CPU.
See CHANGELOG.md for full notes.
v2.0.0
HTTP server transport rebuilt on h1/h2; cowboy removed from the library, so barrel_mcp runs alongside frameworks (e.g. Livery) with their own HTTP stack. Protocol core and public start/stop API unchanged.
Breaking
applicationsis now[kernel, stdlib, crypto, h1, h2, hackney]; cowboy is test-only. Hosts that relied on barrel_mcp starting cowboy must add what they need to their own release.- Deps: +h1 0.2.2 (
erlang_h1), +h2 0.6.0, hackney 4.0.0.
Changes
- New
barrel_mcp_http_engine(transport-neutral) +barrel_mcp_http_listener(single port: cleartext h1, TLS h1+h2 via ALPN);stopdrops in-flight connections. - Removed
barrel_mcp_prm_handler(engine serves the protected-resource-metadata route).
See CHANGELOG.md for full notes.
v1.3.0
A feature release that completes the OAuth surface vs MCP 2025-11-25 and modelcontextprotocol/ext-auth, plus four security follow-ups from review.
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. - New public exchangers
barrel_mcp_client_auth_oauth:token_exchange/2andjwt_bearer/2. - Re-walks the chain on every 401. IdP
invalid_grantsurfaces as{error, subject_token_expired}.
Dynamic Client Registration (RFC 7591)
- New
barrel_mcp_client_auth_oauth:register_client/2posts client metadata to the AS'sregistration_endpointand returns the response unchanged. - New
register_client/3acceptsinitial_access_token(RFC 7591 section 3) for protected registration endpoints.
Security follow-ups
- Scope checks fail closed when a custom provider returns an
AuthInfowithout ascopeskey. - Tool handler crashes are logged server-side via
logger:error; the wire emits a generic<<"Internal tool error">>instead of~p-formatted exception terms. - Streamable-HTTP client caps in-flight response buffers (16 MiB) and SSE buffers (4 MiB).
- OAuth discovery (
discover_protected_resource/1,discover_authorization_server/1) no longer follows redirects.
See CHANGELOG.md for the full list.
barrel_mcp 1.2.0
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 implem...