Current section
Files
Jump to
Current section
Files
attesto_mcp
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - 2026-08-25
### Added
- Add an explicit two-phase `ProtectResource` API for endpoints that classify a
bounded request envelope before selecting per-operation scopes. It performs
Attesto authentication once, then enforces the selected scopes through
`RequireScopes` before dispatch.
## [1.1.0] - 2026-08-11
### Security
- Require `attesto >= 1.12.2`, so MCP resource servers cannot resolve a core
release predating raw RSA-key denial-of-service bounds and the current token,
DPoP, parser, and wallet hardening.
- Remove the stale, unused Bandit lock entry affected by the WebSocket
fragmentation advisory and refresh Postgrex past its stream-comment advisory.
Neither dependency enters the package's runtime closure, but CI and
integration environments no longer retain an affected release.
## [1.0.5] - 2026-07-25
### Security
- Encode the security-fixed floor for every supported Plug minor line from
1.16 through 1.20, retaining broad compatibility without permitting affected
patch releases between a single lower and upper bound.
- Require Postgrex 0.22.3 or newer in the package's test environment, excluding
releases affected by the notification reconnect replay vulnerability. This
dependency remains test-only and does not enter a consumer's dependency tree.
### Changed
- Requires `attesto >= 1.3.0 and < 2.0.0`. The floor carries the same optional
Plug security branches and JOSE 1.11.12 boundary as 1.2.5, and adds key-bound
algorithm validation to every verification path this package uses: a DPoP
proof key must be an RSA key of at least 2048 bits for every accepted RSASSA
and RSA-PSS algorithm, and a trusted algorithm can no longer be paired with an
incompatible key type or curve.
- The optional installer integration now requires Igniter 0.6.0 or newer, and
the optional Anubis bridge requires Anubis MCP 1.7.0 or newer. Both retain a
next-major upper bound.
### Documentation
- Describe the optional Horde registry in terms of its cluster-wide session
uniqueness and routing. Anubis MCP 1.7.0 fixed its own dynamic session-name
fallback, so Horde is no longer presented as compensating for that issue.
## [1.0.4] - 2026-07-16
### Security
- Per-resource audience confinement now delegates to Attesto 1.2.2's strict
`:trusted_audiences` policy. A scalar token audience must match the protected
resource, and every member of an array-valued audience must match, so adding
an untrusted sibling audience can no longer bypass route confinement.
- `:resource_audience` and a caller-supplied core `:trusted_audiences` policy
are now mutually exclusive and fail closed during plug initialization instead
of allowing one policy to replace the other. Invalid static policies fail at
initialization, and a callback returning `nil` or another malformed value can
no longer disable route confinement for that request.
- Require patched Plug branches (`1.19.5+` within 1.19 or `1.20.3+`) and,
when the optional Phoenix integration is used, patched Phoenix branches
(`1.7.24+` within 1.7 or `1.8.9+`). This excludes the intervening releases
affected by published denial-of-service and cookie-injection advisories even
when an existing consumer lock would otherwise retain them.
### Changed
- Requires `attesto >= 1.2.2 and < 2.0.0` for strict trusted-audience
verification.
## [1.0.3] - 2026-07-15
### Fixed
- `mix attesto_mcp.install` now emits the public protected-resource metadata
macro with an explicit safe `root: false`, makes real repeated installs
idempotent, keeps distinct resource paths on collision-resistant protection
pipelines, and generates `resource:` plus `resource_audience: :resource` so
metadata, requested resources, token audiences, and validated audiences stay
aligned. Its notice now calls out request-derived origins and reverse-proxy
pinning.
- The installer now rejects empty scopes and degenerate resource paths, and
stops safely when an existing metadata declaration uses a non-literal path
that cannot be compared without evaluating host code.
- Multi-resource routers now reject a later `root: false` declaration when an
earlier resource already auto-mounted the compatibility root, closing the
remaining declaration-order-dependent root case.
### Documentation
- Corrected multi-resource root ownership and added the combined
`attesto_phoenix` authorization-server plus multiple MCP resource pattern.
## [1.0.2] - 2026-07-13
### Changed
- Docs only: RFC 9728 discovery guidance for combined AS+RS apps. The router
macro and README now spell out how clients actually discover
protected-resource metadata (the §3.1 path-inserted probe first, then the
`WWW-Authenticate` `resource_metadata` fallback) and how to give each PRM
route exactly one owner next to `attesto_phoenix`'s `attesto_routes` — whose
new `protected_resource_paths`/`protected_resource_root` options
(attesto_phoenix 1.3.0) cover the single-resource AS+RS case.
## [1.0.1] - 2026-07-08
### Changed
- Requires `attesto ~> 1.2`, picking up the 1.2.x line — including the
`Attesto.Keystore.Static` fix that labels the signing key's `kid` from
`:signing_alg` (attesto 1.2.1), so a PS256-signing host verifies its own
tokens without a redundant `:key_algs` entry. No functional change in
attesto_mcp itself.
## [1.0.0] - 2026-07-04
First stable release; the public API is now under semantic versioning. No
functional change from 0.11.0. Requires `attesto ~> 1.0`.
## [0.11.0] - 2026-06-23
### Added
- **Clustered + persistent Anubis MCP session infrastructure** (optional,
compile-guarded — an RS-only consumer pays nothing):
- `AttestoMCP.Anubis.SessionStore.Ecto` — a Postgres-backed
`Anubis.Server.Session.Store`. Anubis ships only a Redis adapter; this
persists the session map (keyed by `Mcp-Session-Id`) so a client reconnects
after a deploy/node-replacement with its initialized state restored instead
of re-initializing. Stateless (`start_link/1` is `:ignore`, rides the host
repo), with lazy + scheduled (`cleanup_expired/1`) TTL reaping. Backed by the
`AttestoMCP.Anubis.Session` schema; `mix attesto_mcp.gen.session_migration`
creates the `attesto_mcp_sessions` table. Compile-guarded on `Ecto.Schema`.
- `AttestoMCP.Anubis.JSONSafe.sanitize/1` — strips non-JSON-encodable values
(bare structs, pids, refs, tuples, functions) from session state before the
`jsonb` insert, since Anubis folds the whole request `assigns` into the
persisted frame; without it the driver raises mid-`initialize` and kills the
session process.
- `AttestoMCP.Anubis.Registry.Horde` — a cluster-wide `Anubis.Server.Registry`
that keeps the client-supplied `session_id` as CRDT/ETS data (a `:via`
tuple), coordinates cluster-wide session-name ownership, and routes a
request landing on any node to the node holding the session. Compile-guarded
on `Horde.Registry`.
- `mix attesto_mcp.install.sessions` — an Igniter installer that writes the
`config :anubis_mcp, :session_store` block (and, with `--registry`, adds the
`horde` dependency), and prints next-step notices for the migration and the
supervision-tree registry wiring.
## [0.10.0] - 2026-06-22
### Added
- **RFC 9470 Step-Up Authentication.** `AttestoMCP.Plug.ProtectResource` /
`Plug.Authenticate` accept a per-route `:step_up` requirement
(`[acr_values: [...], max_age: ...]`); after the token is verified its
`acr` / `auth_time` claims must satisfy it or the request is refused 401
`insufficient_user_authentication` naming what the client must re-request.
- `attesto_mcp_protected_resource_metadata/2` accepts `root: true | false` to
control the unsuffixed RFC 9728 root compatibility document explicitly.
### Changed
- The root `/.well-known/oauth-protected-resource` compatibility document is now
auto-mounted only for a **single-resource** router. Once more than one
resource is declared its resource is ambiguous, so the host must choose with
`root: true` (nominate the resource) or `root: false` (serve no root); a second
resource that leaves the root implicit is a compile error. Previously the root
silently resolved to whichever resource was declared first, which could publish
a sensitive resource's metadata at a guessable path by accident.
## [0.9.0] - 2026-06-22
### Added
- **RFC 8707 / RFC 9728 per-resource audience confinement.**
`AttestoMCP.Plug.ProtectResource` / `Plug.Authenticate` gain a
`:resource_audience` option: with `:resource` (or a string / `(conn -> uri)` /
`{m, f}` value) the access token's `aud` is validated against THIS resource's
identifier rather than the host's global `config.audience`, so a token minted
for a sibling resource is rejected (audience confinement, RFC 8707 §1).
- `AttestoMCP.Metadata.resource_identifier/3` — the single canonical
origin-plus-path identifier shared by the advertised metadata `resource` and
the validated audience, so `metadata.resource == requested resource == minted
aud == validated aud` holds by construction.
### Changed
- Requires `attesto ~> 0.10`. `:resource_audience: :resource` raises (fails
closed) when no resource path is configured to derive the identifier from.
## [0.8.0] - 2026-06-21
### Changed
- **MCP protected-resource authentication defaults to header-only bearer
credentials.** `AttestoMCP.Plug.Authenticate` and `ProtectResource` pass
`bearer_methods: [:header]` to attesto core unless the host opts in with
`bearer_methods: [:header, :body]`. This matches the MCP protected-resource
metadata default of `bearer_methods_supported: ["header"]`. **Requires
`attesto ~> 0.9`.**
## [0.7.0] - 2026-06-21
### Added
- **`AttestoMCP.Anubis.put_auth/1`** — an optional bridge that projects the
verified `:attesto_context` into an Anubis MCP server's `frame.context.auth`,
so Anubis's framework-level authorization (the `tools/list` visibility filter,
per-tool scope gates) sees the resource-server identity. `anubis_mcp` is an
optional dependency and the module is compile-guarded on `Anubis.Server.Frame`,
so a resource server that does not use Anubis never compiles it or pulls it in.
The projection is purely mechanical — no scope-superset, role, or visibility
policy, which stay in the host app.
- **`AttestoMCP.Plug.Authenticate` now assigns `:attesto_context`** — a single
protocol-shaped context map (`%{subject, client_id, scope, claims, cnf,
principal}`), identical to the one `AttestoPhoenix.Plug.Authenticate` assigns,
under the new `:context_key` option (default `:attesto_context`). This is the
canonical cross-plug auth context the Anubis bridge reads, so the bridge works
whether a request was authenticated by the MCP plug or the Phoenix plug.
### Changed
- **MCP scope-rejection rendering now delegates fully to attesto core.**
`AttestoMCP.Plug.RequireScopes` renders its 403 `insufficient_scope` through
`Attesto.Plug.OAuthError.insufficient_scope/4`, which now honors the
`:send_error` / `:www_authenticate` / `:no_store` transport hooks. This removes
the local 403 gap-filler and the `AttestoMCP.Plug.Error` shim (the on-the-wire
challenge and body are unchanged). **Requires `attesto ~> 0.8.1`.**
- **The `:no_store` transport hook** now threads through `RequireScopes` and
`ProtectResource`, at parity with `:send_error` / `:www_authenticate`.
### Fixed
- The `mix attesto_mcp.install` tests declare `phx_new` (test-only) so
`Igniter.Test.phx_test_project/1` has the Phoenix project generator
(Igniter.Phoenix.Single, compile-guarded on Phx.New.Project) it needs.
## [0.6.2] - 2026-06-14
### Fixed
- **The `mix attesto_mcp.install` task no longer references `Igniter.Mix.Task`
unconditionally, so attesto_mcp compiles in a consumer that does not depend on
`igniter`.** `igniter` is an optional dependency, but the install task did
`use Igniter.Mix.Task` at the top level. In a consumer application that
depends on attesto_mcp without also pulling in `igniter` (the common prod
case), compiling attesto_mcp failed with `module Igniter.Mix.Task is not
loaded and could not be found`. The task is now wrapped in a top-level
`if Code.ensure_loaded?(Igniter)` guard: the Igniter-backed installer compiles
only when `igniter` is present, and a `use Mix.Task` fallback that prints a
"install igniter to use this task" message takes its place otherwise. Behavior
is unchanged when `igniter` is available.
## [0.6.1] - 2026-06-14
### Fixed
- **`AttestoMCP.Plug.ProtectResource` can again be used as a compile-time router
pipeline plug (`plug_init_mode: :compile`).** 0.6.0 made `init/1` bake the
generated `resource_metadata` `WWW-Authenticate` closure into the
`RequireScopes` transport. Under `:compile` mode (the Phoenix router /
production default) a plug's `init/1` result is embedded via `Macro.escape`,
which rejects anonymous functions, so a router carrying the plug failed to
compile. The generated challenge is now built at **call** time from an
escape-safe spec (strings / `{m, f}` tuples), so `init/1` returns no closures
and the plug compiles in a `:compile`-mode pipeline. Behavior is unchanged: an
insufficient-scope 403 still carries the `resource_metadata` pointer, and a
host-supplied `:www_authenticate` still wins. (Callbacks the host passes to the
plug must be remote captures or MFA tuples, not anonymous `fn`, to be embedded
under `:compile` mode — the same constraint Plug imposes on every plug.)
## [0.6.0] - 2026-06-14
### Added
- **Origin pinning for protected-resource metadata behind a proxy.** A host can
now pin the advertised `resource`/challenge origin and `authorization_servers`
instead of always deriving them from the live request connection, which behind
a TLS-terminating proxy is both fragile (`http`/internal host) and an
`X-Forwarded-Host` spoofing vector into the metadata document a client trusts
to find its authorization server.
- `AttestoMCP.Metadata.resolve_origin/2` resolves the resource server origin:
an explicit `:base_url`/`:origin` (a `String.t()` or `(conn -> url)`) wins
over the request connection. It drives the `resource` identifier and the
`protected_resource_url/3` challenge URL.
- `AttestoMCP.Metadata.protected_resource/3` now defaults
`authorization_servers` to the `:config` issuer (or an explicit `:issuer`
string) when one is given, rather than the resource origin — the issuer is
the authorization server the host already trusts. The `resource` identifier
is **not** derived from the issuer (RFC 9728 keeps the two distinct).
- The router macro `attesto_mcp_protected_resource_metadata`,
`AttestoMCP.MetadataController`, `AttestoMCP.Plug.Authenticate`, and
`AttestoMCP.Plug.ProtectResource` all accept and thread `:base_url`/`:origin`
(and `:issuer`/`:config`), so the served metadata and the
`WWW-Authenticate` `resource_metadata` challenge stay aligned when pinned.
`:base_url`/`:origin` accept a string, a `(conn -> url)` callback, or a
`{module, fun}` / `{module, fun, args}` tuple (so a dynamic origin works in a
compiled router macro, where an anonymous fn cannot).
- The generated `resource_metadata` challenge is now emitted consistently on
every rejection the MCP plugs render - token failure, principal-callback
rejection (a 401 from `AttestoMCP.Plug.Authenticate`), and insufficient-scope
rejection (a 403 from `AttestoMCP.Plug.RequireScopes` via `ProtectResource`)
- so a client is pointed at metadata on all of them, not just token failures.
- New `guides/proxy_origin.md` documents the pinned-origin recipe so consumers
do not reinvent a canonical-host guard plug.
### Fixed
- `authorization_servers` advertises the issuer **verbatim**: it is no longer
trailing-slash-trimmed, since an issuer identifier is compared by exact string
match (a trimmed path-based issuer would break discovery). The resource origin
is still trimmed (it is joined with a path). A blank explicit `:issuer` is
ignored rather than publishing `[""]` or overriding a configured issuer.
- A blank, relative, or non-binary `:base_url`/`:origin` pin (`""`, `"/"`,
`"/prefix"`, `"mcp.example.com"`, …) is treated as "not configured" and falls
back (`:base_url` → `:origin` → request origin) instead of producing relative
or empty security metadata (no more `resource: "/mcp"` or
`authorization_servers: [""]`). A pin must be an absolute `scheme://…` origin.
- An explicit `:resource` / `:authorization_servers` now short-circuits the
derivation, so a lower-precedence origin/issuer callback is never invoked (and
cannot fail) for a value the host supplied outright. The resource origin is
also resolved at most once per document and shared with the
`authorization_servers` fallback, so a stateful origin callback cannot make
the `resource` and `authorization_servers` disagree.
### Changed
- `AttestoMCP.Metadata.protected_resource/3` now sets `resource` with `put_new`
(was `put`), so an explicit `:resource` opt overrides the derived identifier —
matching how `:authorization_servers` already behaved.
- With no pinning options supplied, behavior is unchanged: both origins derive
from the request connection.
## [0.5.2] - 2026-05-31
### Fixed
- Correct the README installation snippet now that the package is published on
Hex.
## [0.5.1] - 2026-05-31
### Changed
- Reuse `Attesto.Test.DPoP` for MCP DPoP proof fixtures so downstream MCP tests
stay aligned with Attesto's published DPoP helper API.
## [0.5.0] - 2026-05-31
### Added
- `AttestoMCP.Plug.ProtectResource`: a single plug composing
`AttestoMCP.Plug.Authenticate` then `AttestoMCP.Plug.RequireScopes` into a
correctly ordered, halt-respecting pipeline, with the RFC 9728
`resource_metadata` `WWW-Authenticate` challenge auto-wired from the resource
path.
- `AttestoMCP.Router` with the `attesto_mcp_protected_resource_metadata/2`
Phoenix router macro, and `AttestoMCP.MetadataController`, serving
per-resource `/.well-known/oauth-protected-resource/<path>` metadata plus a
backwards-compatible root `/.well-known/oauth-protected-resource` route. The
served `resource` identifier matches the `ProtectResource` challenge.
- `AttestoMCP.Test.DPoPAssertions`: shipped ExUnit assertions for host apps
proving a DPoP-bound token presented as a plain Bearer is rejected and is
accepted with a valid DPoP proof.
- `guides/mcp_wiring.md`: copy-pasteable end-to-end wiring guide.
- `phoenix` as an optional dependency (only needed by `AttestoMCP.Router` and
`AttestoMCP.MetadataController`).
- Initial Plug/Phoenix authentication wrapper for protecting HTTP MCP endpoints
with Attesto access-token verification, DPoP proof checks, and mTLS
certificate-bound token checks.
- MCP scope convention helpers.
- OAuth protected-resource metadata builder and authorization-server metadata
delegation.
- Focused tests for Bearer, DPoP, mTLS, scope enforcement, principal mapping,
custom error rendering, and public assign names.