Skip to content

Releases: barrel-platform/barrel_mcp

v2.2.0

09 Jun 11:39
81b2c4f

Choose a tag to compare

Threads the authenticated principal into tool handlers.

Added

  • Arity-2 tool handlers (Mod:Fun(Args, Ctx)) now receive the auth provider's authenticate/2 result in Ctx under auth_info, so owner-scoped tools can identify the caller. Arity-1 handlers (Mod:Fun(Args)) are unchanged. With barrel_mcp_auth_none the value is the anonymous principal; on paths with no auth provider (stdio) it is undefined.
  • barrel_mcp_protocol:drive_async_plan/3, which threads auth_info into the synchronous tool-call path. drive_async_plan/2 is retained and delegates with auth_info set to undefined.

Full Changelog: https://github.com/barrel-platform/barrel_mcp/blob/main/CHANGELOG.md

v2.1.0

28 May 21:40
d6f5bb3

Choose a tag to compare

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 catch operator with try ... catch throughout, as OTP 29 deprecates catch ....
  • Updated dependencies: erlang_h1 0.2.3, h2 0.6.1, hackney 4.0.3; test deps meck 1.2.0 and cowboy 2.15.0.

v2.0.2

23 May 15:02
fe8b8dc

Choose a tag to compare

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-Id alone 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 under barrel_mcp_auth_none.
  • Resource, prompt and completion handler crashes no longer leak exception terms to the client. resources/read, prompts/get and completion/complete serialised the caught Class:Reason into the JSON-RPC error (only tools/call was fixed in 2.0.1). They now log server-side and return a generic message.
  • The built-in listener caps concurrent connections. With idle_timeout at infinity (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_connections option).
  • Basic-auth unknown-user timing matches the configured mode. When hash_passwords was false the 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.0 in client_info; it now matches the library version, and the README dependency example no longer pins the stale v1.3.0 tag.

Full Changelog: v2.0.1...v2.0.2

v2.0.1

21 May 00:10
89b8527

Choose a tag to compare

Follow-up hardening from a review of the 2.0.0 transport. No API changes.

  • Attach _auth only on the request dispatch path, so a client-posted JSON-RPC response (answering a server sampling/elicitation request) no longer carries it into the delivered map. Server-internal; matches 1.x.
  • barrel_mcp_http_listener backs off briefly on non-closed accept errors, so file-descriptor exhaustion (emfile) throttles the acceptor instead of spinning the CPU.

See CHANGELOG.md for full notes.

v2.0.0

20 May 23:33
724ec7c

Choose a tag to compare

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

  • applications is 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); stop drops in-flight connections.
  • Removed barrel_mcp_prm_handler (engine serves the protected-resource-metadata route).

See CHANGELOG.md for full notes.

v1.3.0

10 May 00:29
1a7a4c1

Choose a tag to compare

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/2 and jwt_bearer/2.
  • Re-walks the chain on every 401. IdP invalid_grant surfaces as {error, subject_token_expired}.

Dynamic Client Registration (RFC 7591)

  • New barrel_mcp_client_auth_oauth:register_client/2 posts client metadata to the AS's registration_endpoint and returns the response unchanged.
  • New register_client/3 accepts initial_access_token (RFC 7591 section 3) for protected registration endpoints.

Security follow-ups

  • Scope checks fail closed when a custom provider returns an AuthInfo without a scopes key.
  • 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

03 May 20:31
4ca18b7

Choose a tag to compare

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/changed was renamed to notifications/tasks/status (the spec method name).
  • tools/call for long_running => true tools wraps the immediate response as CreateTaskResult ({<<"task">> => Task}) instead of the flat {taskId, status}.
  • Task envelopes use lastUpdatedAt (was updatedAt) and include a ttl field (always null for now).
  • tasks/cancel returns the cancelled Task instead of {}.
  • tasks/result returns a CallToolResult ({<<"content">>, <<"structuredContent">>?}) instead of the raw stored value.
  • Task status vocabulary is now working | completed | failed | cancelled (was running | success | error | cancelled).
  • Task timestamps are RFC 3339 strings (were integer milliseconds).
  • initialize advertises tasks.list / get / cancel / result as objects, not bare booleans.
  • POST tools/call clients on Streamable HTTP must now list both application/json and text/event-stream in Accept (or */*).
  • Origin is structurally validated on every Streamable HTTP method; public binds require explicit allowed_origins.
  • barrel_mcp_http_stream defaults to loopback ({127, 0, 0, 1}).
  • Top-level JSON-RPC arrays (batch requests) are rejected with -32600.
  • JSON-RPC id MUST be a string or integer; null and other shapes are rejected.

Cancellation race fix in Streamable HTTP

  • wait_for_tool/2 now 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-RPC isError: true envelope 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_oauth now supports the OAuth 2.1 client_credentials grant for unattended agent hosts. Pass auth => {oauth_client_credentials, Config} on the connect spec; required keys are token_endpoint and client_id, plus either client_secret (HTTP Basic per RFC 6749) or client_assertion (private_key_jwt, RFC 7523). Optional scopes, resource.
  • New public exchanger barrel_mcp_client_auth_oauth:client_credentials/2 for 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-side Last-Event-ID resume" as missing. Both turn out to be either by-design (default request timeout already bounds every call; explicit infinity is a deliberate caller choice) or already shipped (the transport's reopen_sse loop preserves sse_last_event_id across 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.md now calls out that resources/subscribe is scoped to the calling Mcp-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_agent aggregator + router end-to-end. agent_host:run/0 connects two clients to one in-process MCP server under different ServerIds, calls barrel_mcp_agent:list_tools/0 to 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-test automatically (the existing for ex in examples/*/ loop).

Server-side cursor pagination on */list

  • tools/list, resources/list, resources/templates/list, prompts/list, and tasks/list now accept an opaque cursor parameter and emit nextCursor when more entries remain. Page size is 50, sorted by name (or taskId for tasks). Existing single-shot callers see the first page transparently.
  • Direction A of the Python interop suite registers 60 dummy tools and walks tools/list via cursor until exhausted, asserting at least one nextCursor was 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, and complete now 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: structuredContent and isError: true.
  • notifications/tools/list_changed (auto-emitted by reg/unreg).
  • notifications/cancelled end-to-end: start a long-running task, cancel mid-flight via experimental.cancel_task, verify the cooperative arity-2 worker observes {cancel, RequestId} in its mailbox.
  • notifications/tasks/status captured via the message_handler while 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's ServerNotification discriminated union uses the spec method name notifications/tasks/status. Renamed everywhere to match. Hosts that subscribed to notifications/tasks/changed need to switch to the new name.

Interop coverage: notifications/progress

  • Direction A of the Python interop suite now exercises notifications/progress end-to-end. A server-side progress_echo tool emits three progress events through the arity-2 handler's Ctx.emit_progress; the reference Python SDK auto-attaches a progress token on call_tool and routes the inbound notifications to a progress_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/call for long_running => true tools now returns the spec-shaped CreateTaskResult envelope {<<"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 read Result.taskId need to read Result.task.taskId.
  • Wire change. The task collector now stores tool results as the spec-shaped CallToolResult ({content, structuredContent?}) instead of the raw value, so tasks/result returns a payload that decodes as CallToolResult against the reference SDK. Previously tasks/result could 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 → poll experimental.get_task until completed → fetch experimental.get_task_result(..., CallToolResult). Both wire shapes above are now validated against mcp 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 Python elicitation_callback returns an accept action with a fixed colour, and the tool surfaces that colour as text. Same shape for roots/list: a tool calls barrel_mcp:roots_list/1, the Python list_roots_callback returns 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...
Read more