All notable changes to this project are documented here. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[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-backedAnubis.Server.Session.Store. Anubis ships only a Redis adapter; this persists the session map (keyed byMcp-Session-Id) so a client reconnects after a deploy/node-replacement with its initialized state restored instead of re-initializing. Stateless (start_link/1is:ignore, rides the host repo), with lazy + scheduled (cleanup_expired/1) TTL reaping. Backed by theAttestoMCP.Anubis.Sessionschema;mix attesto_mcp.gen.session_migrationcreates theattesto_mcp_sessionstable. Compile-guarded onEcto.Schema.AttestoMCP.Anubis.JSONSafe.sanitize/1— strips non-JSON-encodable values (bare structs, pids, refs, tuples, functions) from session state before thejsonbinsert, since Anubis folds the whole requestassignsinto the persisted frame; without it the driver raises mid-initializeand kills the session process.AttestoMCP.Anubis.Registry.Horde— a cluster-wideAnubis.Server.Registrythat keeps the client-suppliedsession_idas CRDT/ETS data (a:viatuple), closing the atom-table-exhaustion DoS in the bundled:pgadapter (which derives a never-GC'd atom per session id), and routes a request landing on any node to the node holding the session. Compile-guarded onHorde.Registry.mix attesto_mcp.install.sessions— an Igniter installer that writes theconfig :anubis_mcp, :session_storeblock (and, with--registry, adds thehordedependency), 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.Authenticateaccept a per-route:step_uprequirement ([acr_values: [...], max_age: ...]); after the token is verified itsacr/auth_timeclaims must satisfy it or the request is refused 401insufficient_user_authenticationnaming what the client must re-request. attesto_mcp_protected_resource_metadata/2acceptsroot: true | falseto control the unsuffixed RFC 9728 root compatibility document explicitly.
Changed
- The root
/.well-known/oauth-protected-resourcecompatibility 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 withroot: true(nominate the resource) orroot: 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.Authenticategain a:resource_audienceoption: with:resource(or a string /(conn -> uri)/{m, f}value) the access token'saudis validated against THIS resource's identifier rather than the host's globalconfig.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 metadataresourceand the validated audience, sometadata.resource == requested resource == minted aud == validated audholds by construction.
Changed
- Requires
attesto ~> 0.10.:resource_audience: :resourceraises (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.AuthenticateandProtectResourcepassbearer_methods: [:header]to attesto core unless the host opts in withbearer_methods: [:header, :body]. This matches the MCP protected-resource metadata default ofbearer_methods_supported: ["header"]. Requiresattesto ~> 0.9.
[0.7.0] - 2026-06-21
Added
AttestoMCP.Anubis.put_auth/1— an optional bridge that projects the verified:attesto_contextinto an Anubis MCP server'sframe.context.auth, so Anubis's framework-level authorization (thetools/listvisibility filter, per-tool scope gates) sees the resource-server identity.anubis_mcpis an optional dependency and the module is compile-guarded onAnubis.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.Authenticatenow assigns:attesto_context— a single protocol-shaped context map (%{subject, client_id, scope, claims, cnf, principal}), identical to the oneAttestoPhoenix.Plug.Authenticateassigns, under the new:context_keyoption (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.RequireScopesrenders its 403insufficient_scopethroughAttesto.Plug.OAuthError.insufficient_scope/4, which now honors the:send_error/:www_authenticate/:no_storetransport hooks. This removes the local 403 gap-filler and theAttestoMCP.Plug.Errorshim (the on-the-wire challenge and body are unchanged). Requiresattesto ~> 0.8.1. - The
:no_storetransport hook now threads throughRequireScopesandProtectResource, at parity with:send_error/:www_authenticate.
Fixed
- The
mix attesto_mcp.installtests declarephx_new(test-only) soIgniter.Test.phx_test_project/1has 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.installtask no longer referencesIgniter.Mix.Taskunconditionally, so attesto_mcp compiles in a consumer that does not depend onigniter.igniteris an optional dependency, but the install task diduse Igniter.Mix.Taskat the top level. In a consumer application that depends on attesto_mcp without also pulling inigniter(the common prod case), compiling attesto_mcp failed withmodule Igniter.Mix.Task is not loaded and could not be found. The task is now wrapped in a top-levelif Code.ensure_loaded?(Igniter)guard: the Igniter-backed installer compiles only whenigniteris present, and ause Mix.Taskfallback that prints a "install igniter to use this task" message takes its place otherwise. Behavior is unchanged whenigniteris available.
[0.6.1] - 2026-06-14
Fixed
AttestoMCP.Plug.ProtectResourcecan again be used as a compile-time router pipeline plug (plug_init_mode: :compile). 0.6.0 madeinit/1bake the generatedresource_metadataWWW-Authenticateclosure into theRequireScopestransport. Under:compilemode (the Phoenix router / production default) a plug'sinit/1result is embedded viaMacro.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), soinit/1returns no closures and the plug compiles in a:compile-mode pipeline. Behavior is unchanged: an insufficient-scope 403 still carries theresource_metadatapointer, and a host-supplied:www_authenticatestill wins. (Callbacks the host passes to the plug must be remote captures or MFA tuples, not anonymousfn, to be embedded under:compilemode — 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 andauthorization_serversinstead of always deriving them from the live request connection, which behind a TLS-terminating proxy is both fragile (http/internal host) and anX-Forwarded-Hostspoofing vector into the metadata document a client trusts to find its authorization server.AttestoMCP.Metadata.resolve_origin/2resolves the resource server origin: an explicit:base_url/:origin(aString.t()or(conn -> url)) wins over the request connection. It drives theresourceidentifier and theprotected_resource_url/3challenge URL.AttestoMCP.Metadata.protected_resource/3now defaultsauthorization_serversto the:configissuer (or an explicit:issuerstring) when one is given, rather than the resource origin — the issuer is the authorization server the host already trusts. Theresourceidentifier 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, andAttestoMCP.Plug.ProtectResourceall accept and thread:base_url/:origin(and:issuer/:config), so the served metadata and theWWW-Authenticateresource_metadatachallenge stay aligned when pinned.:base_url/:originaccept 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_metadatachallenge is now emitted consistently on every rejection the MCP plugs render - token failure, principal-callback rejection (a 401 fromAttestoMCP.Plug.Authenticate), and insufficient-scope rejection (a 403 fromAttestoMCP.Plug.RequireScopesviaProtectResource)- so a client is pointed at metadata on all of them, not just token failures.
- New
guides/proxy_origin.mddocuments the pinned-origin recipe so consumers do not reinvent a canonical-host guard plug.
Fixed
authorization_serversadvertises 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:issueris ignored rather than publishing[""]or overriding a configured issuer.- A blank, relative, or non-binary
:base_url/:originpin ("","/","/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 moreresource: "/mcp"orauthorization_servers: [""]). A pin must be an absolutescheme://…origin. - An explicit
:resource/:authorization_serversnow 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 theauthorization_serversfallback, so a stateful origin callback cannot make theresourceandauthorization_serversdisagree.
Changed
AttestoMCP.Metadata.protected_resource/3now setsresourcewithput_new(wasput), so an explicit:resourceopt overrides the derived identifier — matching how:authorization_serversalready 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.DPoPfor 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 composingAttestoMCP.Plug.AuthenticatethenAttestoMCP.Plug.RequireScopesinto a correctly ordered, halt-respecting pipeline, with the RFC 9728resource_metadataWWW-Authenticatechallenge auto-wired from the resource path.AttestoMCP.Routerwith theattesto_mcp_protected_resource_metadata/2Phoenix router macro, andAttestoMCP.MetadataController, serving per-resource/.well-known/oauth-protected-resource/<path>metadata plus a backwards-compatible root/.well-known/oauth-protected-resourceroute. The servedresourceidentifier matches theProtectResourcechallenge.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.phoenixas an optional dependency (only needed byAttestoMCP.RouterandAttestoMCP.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.