Current section

Files

Jump to
Raw

README.md

# 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](https://hex.pm/packages/attesto)
authorization in one Apache-2.0 package.
For a Phoenix application already using
[`attesto_phoenix`](https://hex.pm/packages/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`:
```sh
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>.MCP` server;
- mounts the protected `/mcp` endpoint and its public OAuth resource metadata;
- connects the endpoint to the live `attesto_phoenix` configuration;
- 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_status` tool 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:
```sh
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:
```elixir
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:
```sh
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](docs/usage.md#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](docs/usage.md#registration) 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](docs/usage.md#registration).
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](docs/usage.md#per-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:
```sh
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:
```sh
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:
```sh
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:
```sh
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](docs/usage.md#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`](examples/bandit.exs) program demonstrates direct server
startup, registration, and the protected Plug; the [usage guide](docs/usage.md)
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`](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](docs/usage.md).
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`](CONFORMANCE.md).
## Package boundaries
[`attesto_mcp`](https://hex.pm/packages/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
- [Usage and deployment](docs/usage.md)
- [Migration runbook](docs/migration.md)
- [Security policy](SECURITY.md)
- [Conformance evidence](CONFORMANCE.md)