Set up an MCP server

Copy Markdown View Source

This guide starts with one working tool, then adds a transport, authentication, application policy, and deployment settings. Use the Livebook to execute the local HTTP path from beginning to end. Use the usage reference when you need a complete option contract or a more specialized deployment. Download the .livemd file and open it in Livebook with a dedicated runtime; run its cells in order, including cleanup.

You need Elixir 1.18 or later and a compatible Erlang/OTP installation. Initial dependency installation needs access to Hex. The HTTP examples use Bandit; the library itself uses Plug and does not require a particular HTTP adapter.

Choose your starting point

SituationFollow this path
Learn the API without changing an applicationRun the Livebook, or start with the working tool below.
Add a remote endpoint to an existing Phoenix application using AttestoPhoenixStart with the Phoenix installer, then connect a client.
A desktop client will launch a local processUse stdio; the launcher supplies trusted identity.
Serve HTTP from another Plug applicationFollow explicit HTTP wiring and supply an executable Attesto verifier.
Upgrade an existing MCP serverRead the migration guide before changing configuration.

For HTTP, the host's OAuth authorization server registers clients, authenticates users, grants scopes, and issues tokens. attesto_mcp_server verifies protected requests and implements MCP; it does not create that authorization service. The Livebook supplies a temporary key and mints a local token so you can exercise the boundary without setting up a login flow.

Get one tool working

In an existing Elixir project, add the dependency and fetch it:

{:attesto_mcp_server, "~> 2.8"}
mix deps.get
iex -S mix

In IEx, start a server with one tool installed atomically:

alias AttestoMCP.Server.{API, Test}

{:ok, server} =
  API.start_link(
    registrations: [
      {:tool, "echo",
       %{
         description: "Return a message",
         input_schema: %{
           "type" => "object",
           "properties" => %{"message" => %{"type" => "string"}},
           "required" => ["message"],
           "additionalProperties" => false
         },
         handler: fn arguments, _context -> {:ok, arguments} end
       }}
    ]
  )

response = Test.call_tool(server, "echo", %{"message" => "hello"})
%{"result" => %{"structuredContent" => %{"message" => "hello"}}} = response

%{"error" => _} = Test.call_tool(server, "echo", %{"message" => 123})

You now have a registered tool whose valid input produces a structured result and whose invalid input is rejected before the handler runs. Test dispatches through the server's schema, policy, handler, and result checks. It supplies a test identity and does not authenticate an HTTP token or perform transport negotiation. Stop this temporary server with GenServer.stop(server) when done.

In an application, supervise the server and install registrations at startup so a restarted process restores its catalog. Use API.register_all/2 when definitions are assembled in an application-owned registration module. The startup reference covers both forms and catalog replacement.

Add a transport

Install in an existing Phoenix application

The shortest deployed HTTP path assumes a working Phoenix application with AttestoPhoenix already issuing tokens. Run inside the Phoenix child application, with Igniter available and a direct Hex dependency on attesto_phoenix compatible with >= 2.14.1 and < 5.0.0.

If the Igniter task is not available, add it to the host's development dependencies and run mix deps.get first:

{:igniter, "~> 0.8.4", only: [:dev, :test], runtime: false}

If the host does not issue tokens yet, follow the AttestoPhoenix setup before connecting remote clients. With these prerequisites ready, install the server:

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

The base URL is the public origin without /mcp. The installer adds a supervised application-owned server, a server_status tool and its test, the protected /mcp forward, and public resource metadata. It reuses the authorization server's live verification, principal, revocation, DPoP, and mTLS configuration. Review its notices, including parser-order checks.

If it selects the bundled PostgreSQL session store, run the migration commands it prints. The installer does not run migrations for you:

mix attesto_mcp_server.gen.migration --repo MyApp.Repo
mix ecto.migrate

Then run the generated test and start the application:

mix test
mix phx.server

For loopback development, use --base-url http://127.0.0.1:4000 --allow-http-loopback. Production origins use HTTPS. Applications with multiple named authorization profiles should use AttestoPhoenix 3.x or 4.x. See the Phoenix reference for prerequisites, manual wiring, CIMD, metadata reuse, and installer recovery.

Use another Plug host

Supply these pieces through the host application's supervision and routing:

PieceResponsibility
AttestoMCP.Server and registrationsSupervise the server and restore its catalog at startup.
AttestoMCP.Server.PlugMount /mcp with an executable Attesto configuration and a pinned public resource identifier.
OAuth protected-resource metadataPublish the canonical resource, authorization server, and supported scopes for client discovery.
HTTP adapter and proxy configurationServe the Plug, normalize trusted proxy information, and preserve authentication before body parsing.

Start from the Livebook for a complete authenticated Bandit example. The Bandit reference explains direct wiring; examples/bandit.exs is a credential-free boundary example that returns 401 until configured with a valid key and credential. Setting an issuer URL alone does not supply a token verifier. See Attesto and resource metadata for canonical audience, runtime configuration, revocation, and sender constraints.

Use stdio for a local client

From a checkout of this repository, the runnable example starts an echo tool and reads newline-delimited JSON-RPC from stdin. This single request exercises the modern protocol without an HTTP token or an initialization handshake:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"hello":"world"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' \
  | elixir examples/stdio.exs

Expect a JSON-RPC result containing structuredContent: {"hello":"world"}. The launcher routes install diagnostics to stderr so stdout carries protocol messages. For an MCP client that launches processes, configure elixir as the command and the absolute path to examples/stdio.exs as its argument.

For your own application, run AttestoMCP.Server.Stdio.run/2 against its registered server and supply a trusted context. This context represents the local launcher's identity and permissions; it is not populated from an OAuth bearer token. The stdio reference covers supervision, framing, limits, and session-bound client compatibility.

Connect an HTTP client

First make the client known to your authorization server. Pre-register it in the host's client store, or enable CIMD when it identifies itself with an HTTPS Client ID Metadata Document. Give it an appropriate grant and consent policy. Dynamic client registration is a separate host decision. Point the MCP client at the public /mcp URL; its OAuth support discovers the authorization server and obtains a token for that resource.

Issue a resource-specific token whose aud matches the exact canonical MCP resource URL. An existing token for another application API is not sufficient. If this is a new resource, include it in the authorization server's issuance policy. In AttestoPhoenix, that policy can include:

resource_indicators: [allowed_resources: ["https://mcp.example.com/mcp"]]

Merge this entry with the host's existing allowed resources or per-client policy. The client's OAuth requests select this resource through the resource parameter. See AttestoPhoenix resource indicators for issuance configuration; keep MCP audience verification pinned to its own resource.

The default HTTP method policies need these scopes:

OperationDefault scope
List toolsmcp:tools:read
Call toolsmcp:tools:call
List or read resourcesmcp:resources:read
List or retrieve promptsmcp:prompts:read

Ensure the authorization server can grant them and the resource metadata advertises them. Alternatively, configure the mount's scopes_supported and default_scopes to use your application's scopes. With generated routes, set scopes_supported on both metadata and MCP forwards, and default_scopes on the MCP forward. With a reused metadata route, its authorization-server owner advertises the scopes. See scopes and application policy.

Verify the boundary and first request

Check public discovery and a protected endpoint without a token:

curl --include https://mcp.example.com/.well-known/oauth-protected-resource/mcp
curl --include https://mcp.example.com/mcp

The metadata route should return the configured resource description. The unauthenticated MCP request should return 401 with an OAuth challenge. A 404 usually means the route is not mounted at that path.

After your authorization server issues a token for https://mcp.example.com/mcp with mcp:tools:read, use it as ACCESS_TOKEN to make a complete modern request. This example assumes an unbound bearer token. A DPoP-bound token needs the DPoP authorization scheme and a valid proof; an mTLS-bound token needs its matching client certificate. Use the client's OAuth implementation to provide the sender constraints your host requires.

curl --include https://mcp.example.com/mcp \
  --request POST \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, text/event-stream" \
  --header "Mcp-Protocol-Version: 2026-07-28" \
  --header "Mcp-Method: tools/list" \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'

A successful body lists the installed tools. Calling one also needs mcp:tools:call, Mcp-Method: tools/call, and Mcp-Name equal to its name; use the complete call example. Check the JSON-RPC error member and a tool result's isError, as well as HTTP status. Tool execution failures can use HTTP 200.

Modern 2026-07-28 requests carry protocol metadata on every request and do not require initialize or a session ID. Clients selecting 2025-11-25 or 2025-06-18 use initialization and the negotiated session rules instead. Let the client SDK manage those differences. See protocol compatibility.

SymptomCheck
401Token expiry, issuer, resource audience, loaded principal, revocation, and any required DPoP or mTLS binding.
403Granted method scopes and the host's mount policy.
400 on a modern POSTMatching version/method/name headers and body metadata, without duplicate headers.
Tool absent or invocation deniedDefinition scopes and its application authorization callback.
JSON-RPC input errorThe registered schema and string-keyed arguments.

Replace the sample with application features

Tools should validate inputs and use the authenticated context to enforce business policy. Register definition-level required_scopes or authorize callbacks where appropriate; passing the HTTP method scope does not establish permission to read a particular record. Use a stable principal_binding when the host loads mutable principal structures. See definition authorization and Phoenix principal binding.

For richer output, use AttestoMCP.Server.Content and AttestoMCP.Server.Result. Inside a tool handler, Result.tool_from_context/2,3 uses the running server's output budget and canonicalization policy. Add an output_schema when the client depends on a particular structured result. See handler results.

Add other primitives only as needed:

FeatureWhen to use itReference
Resources and URI templatesExpose readable content with stable identifiers.Registration
Prompts and completionsProvide reusable message templates and argument suggestions.Registration
Caller-specific guidanceExplain workflow through instructions_provider.Server guidance
Tool presentationCustomize visible descriptions, titles, icons, or application metadata.Tool presentation
Request metadataRead client-supplied application metadata through context.request_meta.Request metadata
Identity and schema exportAdd server_icons or export_schema_dialect.Identity, dialects
Cache hintsSet bounded modern client freshness without caching HTTP responses.Cache hints

Guidance, tool presentation, icons, and client metadata do not authorize an operation. Treat request metadata as untrusted. Keep private cache hints for personalized content; public hints require an explicit policy and content that is safe to publish. These features are optional additions to the same registered server, not prerequisites for getting a tool working.

Use AttestoMCP.Server.Test for focused checks of tools, resources, prompts, completion, discovery, and lists. Keep transport tests for token enforcement, header mirroring, framing, and session negotiation. The Livebook demonstrates both a focused dispatch check and real loopback HTTP requests.

Prepare for deployment

Before moving the local example to a shared endpoint, complete these host-owned settings:

AreaDeployment decision
Issuer and keysUse the real authorization server, persistent key management, registered clients, and explicit grants/consent. Replace the notebook's temporary signer.
Public addressPin HTTPS resource/audience and trusted proxy normalization. Authenticate before parsing request bodies.
Identity and policyLoad valid principals, select stable session bindings, enforce revocation and application permissions, and configure sender constraints when used.
PersistenceChoose stores for the protocol revisions and approval flows you actually enable. Migrate before use.
Multiple nodesArrange routing and shared state/signing keys; a shared session table alone does not distribute local streams or pending requests.
LimitsKeep finite body, JSON, metadata, frame, timeout, queue, and concurrency limits aligned with expected data sizes.
OperationsMonitor telemetry, test refusal paths and restart behavior, and verify clients against the revisions you enable.

2026-07-28 is session-free; earlier supported revisions use client sessions. The default ETS session store is local and lost on restart. PostgreSQL can persist session records, while streaming and pending work retain separate routing needs. Shared cursor and request-state secrets must remain consistent where callers can move between nodes. See sessions and clustering and limits.

Add advanced interactions when required

Multi-round requests and subscriptions cover user input, URL approvals, sampling, roots, and resource updates. Use them when a specific tool or client workflow needs that interaction. Bind retries to the returned request state and echo the actual requested input keys; preserve the original operation and application metadata. URL approvals need their own persistence and authenticated host approval flow.

The reference also covers multiple MCP mounts, telemetry, and protocol-era separation. Tasks are disabled in this release. Review the conformance evidence for tested revisions and the limits of its claims, and the migration guide when upgrading.