Hex.pm Hexdocs.pm Elixir CI License: MIT Elixir

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-resource metadata.
  • authorization_servers handoff to one or more issuers.
  • issuer, jwks_uri, authorization_endpoint, and token_endpoint metadata via Attesto's authorization-server metadata builder.
  • Resource identifier handling through the explicit :resource value 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

def deps do
  [
    {:attesto_mcp, "~> 0.5"}
  ]
end

Minimal 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
end

After authentication, downstream code can read:

  • conn.assigns.attesto_mcp_claims
  • conn.assigns.attesto_mcp_scopes
  • conn.assigns.attesto_mcp_sender
  • conn.assigns.attesto_mcp_principal, if :principal is 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:read
  • mcp:tools:call
  • mcp:resources:read
  • mcp: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 docs

License

MIT. See LICENSE.