AttestoMCP
Plug/Phoenix helpers for protecting HTTP-based Model Context Protocol servers with attesto.
Where it fits
attesto_mcp is a narrow integration layer. It does not implement MCP, JSON-RPC,
tools, prompts, resources, transports, or server lifecycle. It wraps the HTTP
endpoint that an MCP server implementation exposes and connects that endpoint to
Attesto's OAuth/OIDC token verification, DPoP proof verification, mTLS
certificate binding, scope algebra, and metadata builders.
Use it when your MCP server is a Plug or Phoenix endpoint and you want:
- Bearer and DPoP authorization scheme handling.
- Rejection of DPoP-bound tokens that are presented as plain Bearer tokens.
- Rejection of mTLS-bound tokens unless the request has matching certificate thumbprint context.
-
Verified claims, scopes, and sender context in
conn.assigns. - A host callback that maps verified token claims into your own principal.
- RFC 9728 protected-resource metadata for MCP OAuth discovery.
- OAuth-compatible errors with host-controlled rendering.
Relationship to attesto and attesto_phoenix
attesto is the protocol engine: JWT access tokens, DPoP, mTLS, PKCE, JWKS,
discovery, and scopes. attesto_mcp reuses those checks and adds MCP-facing
Plug ergonomics.
attesto_phoenix is the Phoenix/Ecto authorization-server layer: routes,
controllers, registration, stores, and Phoenix-friendly configuration. MCP
servers that need dynamic client registration should expose it through the
authorization server layer rather than duplicate RFC 7591 here.
MCP authorization
The MCP authorization spec treats a protected HTTP MCP server as an OAuth resource server. Clients discover authorization information through OAuth Protected Resource Metadata (RFC 9728), then use Authorization Server Metadata (RFC 8414) for issuer endpoints.
This package provides builders for:
/.well-known/oauth-protected-resourcemetadata.authorization_servershandoff to one or more issuers.issuer,jwks_uri,authorization_endpoint, andtoken_endpointmetadata via Attesto's authorization-server metadata builder.-
Resource identifier handling through the explicit
:resourcevalue you pass.
It intentionally avoids a hard dependency on a specific Elixir MCP SDK. Existing packages have different license and maintenance profiles, and the auth boundary is a normal Plug boundary.
Installation
The package is prepared for Hex but has not been published yet.
def deps do
[
{:attesto_mcp, "~> 0.1.0"}
]
endMinimal Plug/Phoenix usage
Protect the mounted MCP endpoint before forwarding to whichever MCP server plug you use:
pipeline :mcp_auth do
plug AttestoMCP.Plug.Authenticate,
config: &MyApp.Attesto.config/0,
htu: fn _conn -> "https://mcp.example.com/mcp" end,
replay_check: &MyApp.DPoPReplay.check_and_record/2,
resource_path: "/mcp",
principal: fn claims, sender ->
MyApp.Principals.from_token(claims, sender)
end
plug AttestoMCP.Plug.RequireScopes,
scopes: [AttestoMCP.Scopes.tools_call()]
end
scope "/" do
pipe_through [:mcp_auth]
forward "/mcp", to: MyApp.MCPServerPlug
endAfter authentication, downstream code can read:
conn.assigns.attesto_mcp_claimsconn.assigns.attesto_mcp_scopesconn.assigns.attesto_mcp_senderconn.assigns.attesto_mcp_principal, if:principalis configured
For mTLS-bound access tokens, supply certificate context from your TLS layer:
plug AttestoMCP.Plug.Authenticate,
config: &MyApp.Attesto.config/0,
cert_der: fn conn ->
MyApp.TLS.client_certificate_der(conn)
end
The callback must return the DER-encoded certificate that the TLS layer already
authenticated, or nil when no certificate was presented.
Metadata
Serve protected-resource metadata from the well-known location derived from your MCP resource identifier:
metadata =
AttestoMCP.Metadata.protected_resource(conn, "/mcp",
authorization_servers: ["https://auth.example.com"],
resource_name: "Example MCP server",
scopes_supported: AttestoMCP.Scopes.all(),
tls_client_certificate_bound_access_tokens: true
)Authorization-server metadata belongs at the issuer:
AttestoMCP.Metadata.authorization_server(config,
authorization_endpoint: "https://auth.example.com/oauth/authorize",
token_endpoint_auth_methods_supported: ["client_secret_basic", "private_key_jwt"],
registration_endpoint: "https://auth.example.com/oauth/register"
)
Dynamic client registration should be exposed by the authorization server. When
using attesto_phoenix, enable its registration route and callbacks there. Only
advertise registration response fields such as client_secret_expires_at,
registration_access_token, and registration_client_uri if the authorization
server implementation returns and persists them correctly.
Scope conventions
The package ships common MCP-style scope strings as conventions:
mcp:tools:readmcp:tools:callmcp:resources:readmcp:prompts:read
Server-specific prefixes are available:
AttestoMCP.Scopes.server("search", :tools_call)
# "search:mcp:tools:call"These helpers are not policy. The authorization server decides what to issue and each MCP route decides what to require.
DPoP nonce and replay
DPoP proof replay protection is required for protected-resource requests. Pass a
shared :replay_check callback, such as an ETS store for a single node or a
database-backed store for clustered deployments. Without that callback, DPoP
requests fail closed through Attesto unless you explicitly acknowledge the risk
with Attesto's lower-level option.
If the server requires DPoP nonces, also pass :nonce_check and :nonce_issue.
Nonce failures produce use_dpop_nonce with a fresh DPoP-Nonce header so the
client can retry.
Security notes
- Use HTTPS for HTTP MCP servers.
- Validate token audience/resource identifiers for the exact MCP endpoint.
- Do not accept access tokens in the URI query string.
- Do not pass inbound MCP access tokens through to unrelated upstream services.
- Keep access tokens short-lived and scoped to the smallest MCP capability that can satisfy the request.
- Prefer DPoP or mTLS sender-constrained tokens for MCP servers exposed beyond a trusted local environment.
Development
mix deps.get
mix format --check-formatted
mix credo --strict
mix test
mix docsLicense
MIT. See LICENSE.