Skip to main content

guides/features.md

# barrel_mcp features

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ç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 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

- `barrel_mcp_clients` registers one supervised client per
  caller-chosen `ServerId`. Looked up via:
  - `barrel_mcp:start_client/2`
  - `barrel_mcp:stop_client/1`
  - `barrel_mcp:whereis_client/1`
  - `barrel_mcp:list_clients/0`
- Tool-name namespacing across servers is host policy and is not
  enforced by the library.

### 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.