attesto_mcp_server

Add an authenticated MCP endpoint to an Elixir or Phoenix SaaS application. attesto_mcp_server combines the MCP server, Streamable HTTP transport, tool catalog, request validation, and Attesto authorization in one Apache-2.0 package.

For a Phoenix application already using attesto_phoenix, the batteries-included setup is one Igniter command. It reuses the application's existing issuer, token verification, revocation, principal loading, DPoP, and mTLS policy; it does not create a second authentication system.

Want to exercise the full request path before changing an application? The runnable Livebook walkthrough creates an ephemeral local issuer, starts Bandit, registers a tool, and makes authenticated MCP requests against it.

Install in a Phoenix application

Run this inside the Phoenix child application—not an umbrella root—with Igniter available and attesto_phoenix declared directly from Hex at a version compatible with >= 2.14.1 and < 3.0.0:

mix igniter.install attesto_mcp_server --base-url https://mcp.example.com

--base-url is the externally reachable HTTPS origin, without /mcp; the installer mounts the MCP path itself.

The installer:

Review every installer notice, including any exact manual verification emitted when endpoint source is unavailable. After a successful install, run the generated starter test before editing the sample:

mix test

Then replace the starter registration in <App>.MCP with application tools and update the generated registration test to assert those tools. The generated module uses the stable host-facing API:

alias AttestoMCP.Server.API
def register(server) do
API.register_all(server, [
{:tool, "customer_lookup",
%{
description: "Look up a customer by ID",
input_schema: %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "string", "minLength" => 1}
},
"required" => ["id"],
"additionalProperties" => false
},
handler: fn %{"id" => id}, context ->
customer = MyApp.Customers.fetch!(context.principal, id)
{:ok, %{"id" => customer.id, "name" => customer.name}}
end
}}
])
end

Run the updated tests and start the application normally:

mix test
mix phx.server

MCP clients connect to https://mcp.example.com/mcp and authorize against the same Attesto authorization server as the rest of the application.

Connect a client

First make the client known to the application's Attesto authorization server: pre-register a known client in the host's client store, or enable Client ID Metadata Documents (CIMD) when the client identifies itself with an HTTPS metadata URL. See AttestoPhoenix's CIMD guidance. Then point the client at the /mcp URL.

The generated endpoint uses secure generic MCP scope defaults: mcp:tools:read, mcp:tools:call, mcp:resources:read, and mcp:prompts:read. Make sure the authorization server grants those scopes, or set the mount's scopes_supported and default_scopes to scopes the application already issues.

With package-generated routes, add scopes_supported to both the generated metadata forward and MCP forward, and add default_scopes to the MCP forward. With --reuse-metadata-route, AttestoPhoenix owns the public metadata, so configure its advertised scopes there and the enforcement defaults on the MCP forward.

Some clients identify themselves with an HTTPS Client ID Metadata Document. If the application already supports CIMD, its configuration is reused. Otherwise, run the attesto_client_id_metadata migration when using the default Ecto cache, or prepare the selected custom cache, then rerun the same installer command with --enable-cimd. Preserve its other flags, including --reuse-metadata-route when used. The installer does not silently enable dynamic client registration or invent client persistence. See Phoenix installation for those host-owned authorization-server choices.

What application code owns

The package handles protocol negotiation, authenticated transport, catalog discovery, request validation, bounded execution, result validation, sessions, subscriptions, and neutral authorization failures. Application code supplies the useful part: registered tools, resources, prompts, completion handlers, and business-policy callbacks.

Registrations can be installed atomically at startup or through register_all/2. The public API supports:

Handler inputs are specific to each primitive. An arity-2 handler receives the decoded input followed by an authenticated context containing the principal, tenant, scopes, claims, sender constraints, request metadata, and optional application context. One-arity and MFA handlers are also supported. See the registration and handler contract for every input form.

Handler results

Handlers can return simple strings or valid string-key maps. Public Content and Result constructors are available for text, structured tool results, resources, prompts, images, audio, and canonical Base64 blobs. They catch malformed output before it reaches a client; raw maps remain supported for extensions. See registration and handler results.

JSON Schema default values are annotations and are not inserted during normal dispatch. Applications that intentionally need bounded direct-property defaults can call AttestoMCP.Server.Schema.apply_property_defaults/2.

For a business failure that is safe to disclose, return {:error, AttestoMCP.Server.Result.error(message, code)}. Other errors and exceptions remain generic at the protocol boundary.

Scopes and application policy

scopes_supported is the public scope list MCP clients request for a mount. default_scopes is the scope set enforced for protected operations without an explicit method override. Keep both aligned with grants the application's Attesto authorization server can issue.

Definitions can add narrow required_scopes, bounded alternative scope sets, and an authorize callback for business rules that are not grants. These rules apply consistently to catalog visibility and direct invocation.

Only literal true from authorize permits access; failures deny access without disclosing whether the definition exists. An optional HTTP context_builder can add application data under context.host_context without replacing the authenticated identity or claims.

Applications needing definition-scoped HTTP authorization can enable the bounded scope_policy modes documented in definition authorization. Omitting that option retains the secure method-level defaults.

Installer options

The automatic path above is intended for most Phoenix SaaS applications. These options cover less common installations:

Enable Client ID Metadata Documents

The installer leaves CIMD disabled unless explicitly requested, because the default attesto_phoenix cache may require the attesto_client_id_metadata migration. After verifying that storage, run:

mix igniter.install attesto_mcp_server \
--base-url https://mcp.example.com \
--enable-cimd

Existing cache, repository, table-prefix, allowlist, native-app, and disabled settings remain authoritative.

Reuse an existing metadata route

If the router already exposes matching attesto_phoenix protected-resource metadata for /mcp, retain it and add only the MCP endpoint with:

mix igniter.install attesto_mcp_server \
--base-url https://mcp.example.com \
--reuse-metadata-route

Ambiguous or mismatched routes are left unchanged and reported with manual remediation.

Use a loopback origin for local development

HTTP remains disabled for deployed origins. For local development, explicitly allow a loopback origin:

mix igniter.install attesto_mcp_server \
--base-url http://127.0.0.1:4000 \
--allow-http-loopback

Connect local clients to http://127.0.0.1:4000/mcp; an unauthenticated probe should reach the boundary and return 401 rather than 404.

When the AttestoPhoenix native-app callback setting is absent, the installer also enables localhost callback matching so a registered portless callback can use a client's ephemeral local port. It preserves any existing true or false choice.

Use Attesto without attesto_phoenix

An application with its own Attesto configuration callback can still use the installer:

mix igniter.install attesto_mcp_server \
--base-url https://mcp.example.com \
--attesto-config MyApp.Attesto.config/0

The task validates dependency, router, route, and parser ownership before editing. It refuses ambiguous or custom parser arrangements instead of guessing. All installer options and recovery steps are in Phoenix installation.

Other hosts and transports

Non-Phoenix Plug hosts can add the package directly:

def deps do
[{:attesto_mcp_server, "~> 0.12.2"}]
end

Supervise AttestoMCP.Server, register definitions through AttestoMCP.Server.API, and mount AttestoMCP.Server.Plug directly. The examples/bandit.exs program demonstrates direct server startup, registration, and the protected Plug; the usage guide documents the transport and authentication options. Router and supervision wiring remain specific to the host.

The production library depends on Plug rather than a particular HTTP server. Bandit is the documented development/test adapter. The loopback example returns 401 until given a valid credential. The stdio adapter is available through AttestoMCP.Server.Stdio.run/2 and examples/stdio.exs.

Operations and limits

Secure defaults bound JSON values, outputs, queues, concurrency, and execution time. Applications with larger tool inputs or Base64 resources can raise the finite max_json_bytes budget together with the relevant max_body_bytes and max_message_bytes transport ceilings. Result constructors may also need an explicit higher limit for oversized content. Atomic catalogs, durable session-store adapters, clustered routing, cache policy, telemetry, and exception reporting are documented in the usage guide.

The server prefers MCP 2026-07-28 and also negotiates 2025-11-25 and 2025-06-18. Exact runner and SDK evidence is recorded in CONFORMANCE.md.

At this package's protected HTTP boundary, clients sending a 2026-07-28 POST must include the required version, method, and selected-definition mirror headers. Missing, duplicate, or mismatched mirrors return a neutral HTTP 400. See modern HTTP mirror headers for the complete contract and request examples. The earlier session-bound MCP revisions use their negotiated session rules instead.

Package boundaries

attesto_mcp supplies the protected resource boundary used before body decoding. In the batteries-included Phoenix path, attesto_phoenix remains the OAuth authorization server and token issuer; attesto_mcp_server supplies the MCP protocol server and transports. The installer connects them using the host application's validated runtime configuration.

More documentation