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.
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:
- creates and supervises an application-owned
<App>.MCPserver; - mounts the protected
/mcpendpoint and its public OAuth resource metadata; - connects the endpoint to the live
attesto_phoenixconfiguration; - preserves token revocation, principal loading, DPoP replay/nonce, canonical request, and mTLS certificate checks;
- installs the parser bypass when it can prove a direct standard parser, refuses custom or ambiguous parser setups before editing, and reports the narrower cases that still need manual verification; and
- adds a starter
server_statustool and registration test.
Review every installer notice. A custom or ambiguous parser setup stops the installation before any edit. When endpoint source is unavailable but the remaining setup is safe to generate, the installer reports the exact manual verification instead of guessing. 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
For a client already registered with the application's Attesto authorization
server, point it 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:
- tools with JSON Schema inputs and optional output schemas;
- static resources and bounded multi-expression URI templates;
- prompts and completions;
- catalog replacement for generated or reloaded catalogs; and
- modern and legacy notifications and subscriptions.
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 supervise 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.
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.