### Reproduce conformance runner setup and execution Source: https://github.com/xukullc/attesto_mcp_server/blob/main/CONFORMANCE.md Downloads and verifies the pinned conformance runner archive, installs dependencies, builds, and runs the fixture script for both tested revisions. Requires curl, sha256sum, tar, and npm. The script scripts/run_conformance_fixture.sh must be present in the repository. ```sh RUNNER_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mcp-conformance.XXXXXX")" ARCHIVE="$RUNNER_DIR.tar.gz" curl -fsSL \ "https://github.com/modelcontextprotocol/conformance/archive/74edef34d674f563537be8c6587cebaa58e830ca.tar.gz" \ -o "$ARCHIVE" printf '%s %s\n' \ 28d22ae3a4541a9a68c208e6a5653486bfacd97df45cf63cd8f0f7f9d5938293 \ "$ARCHIVE" | sha256sum -c - mkdir -p "$RUNNER_DIR" tar -xzf "$ARCHIVE" -C "$RUNNER_DIR" --strip-components=1 ( cd "$RUNNER_DIR" npx --yes npm@12.0.2 ci npx --yes npm@12.0.2 run build ) scripts/run_conformance_fixture.sh "$RUNNER_DIR" 2026-07-28 scripts/run_conformance_fixture.sh "$RUNNER_DIR" 2025-11-25 ``` -------------------------------- ### Run tests after installation Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Run the generated starter test before editing the sample. This verifies the installation is correct. ```sh mix test ``` -------------------------------- ### Configure durable session storage with mix attesto_mcp_server.install Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/migration.md Commands to configure the PostgreSQL-backed session store for the Phoenix path. The installer configures the Repo when it finds exactly one statically confirmed PostgreSQL Repo supervised as a literal application child; ambiguous, dynamic, unsupervised, and non-PostgreSQL setups are never guessed. Run the generated migration before deploying the configuration. ```console mix attesto_mcp_server.install \ --base-url https://api.example.com \ --session-store ecto \ --repo MyApp.Repo mix attesto_mcp_server.gen.migration --repo MyApp.Repo mix ecto.migrate ``` -------------------------------- ### AttestoMCP.Server.Stdio.run/2 Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Starts the stdio adapter with a caller-supplied supervised server and context. Returns startup error tuples on failure. ```APIDOC ## [RUN] AttestoMCP.Server.Stdio.run/2 ### Description Starts the line-delimited stdio adapter with a caller-supplied supervised server and context. Returns startup error tuples on failure. ### Method run ### Endpoint AttestoMCP.Server.Stdio.run/2 ### Parameters - **server** (term) - Required - Supervised server to use. - **context** (term) - Required - Context for the adapter. ### Response #### Success Response - Returns the adapter result. #### Error Response - `{:error, :session_store_unavailable}` - Session store unavailable. - `{:error, :nonportable_binding}` - Non-portable binding. - `{:error, :binding_too_large}` - Binding too large. - `{:error, :record_too_large}` - Record too large. ``` -------------------------------- ### Run tests and start Phoenix server Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Run the updated tests and start the application normally. ```sh mix test ``` ```sh mix phx.server ``` -------------------------------- ### Registration API Examples Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Code examples for constructing tool results, applying schema defaults, and building HTTP context. ```APIDOC ## Result.tool_from_context/3 ### Description Constructs a complete tool result inside an arity-2 tool handler, inheriting the supervised server's JSON budget and output canonicalization settings. ### Method Function ### Endpoint AttestoMCP.Server.Result.tool_from_context(content, context, opts \\ []) ### Parameters - **content** (term) - Required - The content to include in the result. - **context** (map) - Required - The handler context, providing inherited settings. - **opts** (keyword list) - Optional - Options for the result, e.g., `structured_content`. ### Request Example ```elixir Result.tool_from_context(Content.text("loaded"), context, structured_content: record ) ``` ### Response Returns a complete tool result map with canonical string keys, validated against the server's limits. ``` ```APIDOC ## Result.tool/1,2 ### Description Constructs a complete tool result with standalone defaults: strict output validation and a 2,000,000-byte budget. Use when no handler context is available or for deliberate per-result overrides. ### Method Function ### Endpoint AttestoMCP.Server.Result.tool(content, opts \\ []) ### Parameters - **content** (term) - Required - The content to include in the result. - **opts** (keyword list) - Optional - Options such as `:strict` and output canonicalization settings. ### Response Returns a complete tool result map with canonical string keys, validated against the standalone limits. ``` ```APIDOC ## Result.resource/1,2 ### Description Constructs a complete resource result with canonical string keys and bounded validation. ### Method Function ### Endpoint AttestoMCP.Server.Result.resource(content, opts \\ []) ### Parameters - **content** (term) - Required - The content to include in the result. - **opts** (keyword list) - Optional - Options for the result. ### Response Returns a complete resource result map. ``` ```APIDOC ## Result.error/2 ### Description Creates a client-visible business failure result. The message and optional code are length-bounded and validated as UTF-8. ### Method Function ### Endpoint AttestoMCP.Server.Result.error(message, code \\ nil) ### Parameters - **message** (string) - Required - The error message. - **code** (string) - Optional - An error code, placed at `_meta["io.attesto/errorCode"]`. ### Request Example ```elixir {:error, AttestoMCP.Server.Result.error("account is read-only", "account_read_only")} ``` ### Response Returns an error tuple that becomes an `isError` tool result or a JSON-RPC application error. ``` ```APIDOC ## Schema.apply_property_defaults/2 ### Description Applies JSON Schema `default` values for optional direct properties. It applies at most 500 defaults, never replaces present values, and validates the completed object. ### Method Function ### Endpoint AttestoMCP.Server.Schema.apply_property_defaults(arguments, schema) ### Parameters - **arguments** (map) - Required - The request arguments to which defaults are applied. - **schema** (map) - Required - The JSON Schema object with `properties` containing `default` values. ### Request Example ```elixir schema = %{ "type" => "object", "properties" => %{ "format" => %{"type" => "string", "default" => "summary"} } } handler: fn arguments, context -> with {:ok, arguments} <- AttestoMCP.Server.Schema.apply_property_defaults(arguments, schema) do {:ok, AttestoMCP.Server.Result.tool_from_context( AttestoMCP.Server.Content.text(arguments["format"]), context )} end end ``` ### Response Returns `{:ok, arguments}` with defaults applied, or `{:error, reason}` if validation fails. ``` ```APIDOC ## context_builder/1 ### Description Derives application context after authentication for HTTP hosts. The function receives the connection and returns a context map. ### Method Function ### Endpoint context_builder: fn conn -> map end ### Parameters - **conn** (Plug.Conn) - Required - The connection after authentication. ### Request Example ```elixir context_builder: fn conn -> %{request_id: Plug.Conn.get_req_header(conn, "x-request-id") |> List.first()} end ``` ### Response Returns a map representing the application context. ``` -------------------------------- ### Install with HTTP loopback allowed Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Installs with an explicitly enabled loopback origin, allowing HTTP for 127.0.0.1:4000. HTTPS is mandatory except for this explicitly enabled loopback case. ```sh mix attesto_mcp_server.install \ --base-url http://127.0.0.1:4000 \ --allow-http-loopback \ --attesto-config MyApp.Attesto.config/0 ``` -------------------------------- ### AttestoMCP.Server.Stdio.main/1 Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Entry point for the stdio adapter executable. Accepts adapter controls, starts the owned server, and raises on startup failure. ```APIDOC ## [RUN] AttestoMCP.Server.Stdio.main/1 ### Description Accepts adapter-only identity, input, server-request, and EOF controls. Removes those controls before starting the owned server so core unknown-option validation stays strict. Stops the owned server after the adapter exits and raises for a startup failure. ### Method main ### Endpoint AttestoMCP.Server.Stdio.main/1 ### Parameters - **args** (list) - Required - Command-line arguments including adapter controls. ### Response #### Success Response - Returns the adapter result. #### Error Response - Raises on startup failure. ``` -------------------------------- ### Start Bandit development server with Attesto MCP Plug Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Starts a local Bandit development server with the Attesto MCP server Plug. Requires a valid Attesto configuration in `my_attesto_config`; the server listens on 127.0.0.1:4000 at the `/mcp` path. ```elixir Mix.install( [{:attesto_mcp_server, path: "."}, {:bandit, "~> 1.6"}], force: true, verbose: false ) {:ok, server} = AttestoMCP.Server.start_link(name: :bandit_example) plug = {AttestoMCP.Server.Plug, server: server, path: "/mcp", auth: [config: my_attesto_config, base_url: "http://127.0.0.1:4000"]} Bandit.start_link(plug: plug, scheme: :http, ip: {127, 0, 0, 1}, port: 4000) ``` -------------------------------- ### Register catalog at startup with AttestoMCP.Server Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/migration.md Registers the catalog during startup by passing the registrations to AttestoMCP.Server. start_link/1 returns only after the whole batch has been validated and installed. A duplicate or invalid entry rejects the whole batch, leaves the previous catalog revision unchanged, and emits no invalidation. ```elixir children = [ {AttestoMCP.Server, name: MyApp.MCP, registrations: registrations} ] ``` -------------------------------- ### Generate and run migration for Ecto store Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md When the installer selects the bundled Ecto store, run the migration generator and then migrate. The installer does not run these commands automatically. ```sh mix attesto_mcp_server.gen.migration --repo MyApp.Repo ``` ```sh mix ecto.migrate ``` -------------------------------- ### Run install task directly Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Calls the package task directly when the attesto_phoenix dependency is already present. Requires a canonical public origin; HTTPS is mandatory unless loopback is explicitly enabled. ```sh mix attesto_mcp_server.install \ --base-url https://mcp.example.com \ --attesto-config MyApp.Attesto.config/0 ``` -------------------------------- ### Enable Client ID Metadata Documents (CIMD) Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Run this command to enable CIMD during installation. It is disabled by default because the default attesto_phoenix cache may require the attesto_client_id_metadata migration. Verify storage first, then use this flag. ```sh mix igniter.install attesto_mcp_server \ --base-url https://mcp.example.com \ --enable-cimd ``` -------------------------------- ### Install attesto_mcp_server with Igniter Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Run this command inside the Phoenix child application (not umbrella root) with Igniter available and attesto_phoenix declared directly from Hex at a compatible version (>= 2.14.1 and < 4.0.0). --base-url is the externally reachable HTTPS origin without /mcp; the installer mounts the MCP path itself. ```sh mix igniter.install attesto_mcp_server --base-url https://mcp.example.com ``` -------------------------------- ### Register a tool with AttestoMCP.Server.API Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Replace the starter registration in .MCP with application tools. The generated module uses the stable host-facing API. The example registers a customer_lookup tool with input schema and handler that fetches a customer using the principal from context. ```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 ``` -------------------------------- ### Install with custom Attesto configuration callback Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Use this command when the application has its own Attesto configuration callback. The task validates dependency, router, route, and parser ownership before editing, and refuses ambiguous or custom parser arrangements instead of guessing. ```sh mix igniter.install attesto_mcp_server \ --base-url https://mcp.example.com \ --attesto-config MyApp.Attesto.config/0 ``` -------------------------------- ### GET /mcp/admin Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Handles GET requests for the mounted MCP server, subject to the same mount authorization policy. The policy runs once on each authenticated GET leg before protocol dispatch. ```APIDOC ## [GET] /mcp/admin ### Description Handles GET requests for the mounted MCP server, subject to the same mount authorization policy. The policy runs once on each authenticated GET leg before protocol dispatch. ### Method GET ### Endpoint /mcp/admin ### Parameters #### Path Parameters - **path** (string) - Required - The mount path for the MCP server. #### Query Parameters - **server** (module) - Required - The MCP server module. - **auth** (tuple) - Required - Authentication configuration. - **principal_binding** (tuple) - Required - Principal binding configuration. - **authorize** (tuple) - Required - Authorization callback. - **resource** (string) - Required - Resource identifier. - **base_url** (string) - Required - Base URL for the MCP server. ### Response #### Success Response (200) - **status** (integer) - HTTP status code. #### Response Example ```json { "status": 200 } ``` ``` -------------------------------- ### Start three named MCP servers in one Phoenix host Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Starts three named MCP servers in one Phoenix host, each with a distinct registered name and session namespace. The bundled Ecto adapter stores all sessions in the same `attesto_mcp_sessions` table while the `{namespace, session_id}` key prevents cross-mount session acceptance. ```elixir defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.Repo, mcp_child(MyApp.MCP.Catalog, "mcp-catalog", MyApp.MCP.Catalog.registrations()), mcp_child(MyApp.MCP.Operations, "mcp-operations", MyApp.MCP.Operations.registrations()), mcp_child(MyApp.MCP.Preview, "mcp-preview", MyApp.MCP.Preview.registrations()) ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) end defp mcp_child(name, namespace, registrations) do {:ok, store} = AttestoMCP.Server.SessionStore.Ecto.new( repo: MyApp.Repo, namespace: namespace ) {AttestoMCP.Server, name: name, registrations: registrations, session_store: {AttestoMCP.Server.SessionStore.Ecto, store}, session_namespace: namespace} end end ``` -------------------------------- ### Mount authorization with Plug authorize option Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Configures the HTTP Plug's `authorize` option for a single business-policy gate covering an entire mount. The callback accepts a one-argument function, `{module, function}`, or `{module, function, prefix_arguments}`; only literal `true` permits access, and any other return or raise produces a plain 403 without a `WWW-Authenticate` challenge. This gate runs once on each authenticated POST, GET, and DELETE leg before the body is read, and is not invoked for invalid credentials or metadata discovery. ```elixir forward "/mcp/admin", AttestoMCP.Server.Plug, server: MyApp.AdminMCP, path: "/mcp/admin", auth: {AttestoMCP.Server.Phoenix, :protected_resource_options, [:my_app]}, principal_binding: {MyApp.MCPIdentity, :binding}, authorize: {MyApp.MCPPolicy, :admin_mount?}, resource: "/mcp/admin", base_url: "https://mcp.example.com" defmodule MyApp.MCPPolicy do def admin_mount?(context), do: context.principal.role == :admin end ``` -------------------------------- ### Default configuration after CIMD opt-in Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Relevant defaults set when CIMD is explicitly enabled. The installer sets native_apps.loopback_include_localhost to true only when that key is absent, and existing configuration is not replaced. ```elixir config :my_app, AttestoPhoenix.Config, client_id_metadata: [enabled: true], native_apps: [loopback_include_localhost: true] ``` -------------------------------- ### Compute source fingerprint with git ls-files and shasum Source: https://github.com/xukullc/attesto_mcp_server/blob/main/CONFORMANCE.md Computes a SHA-256 fingerprint over tracked files in mix.exs, config, lib, examples, scripts, test, and fixtures. The output is a single hash used to identify the tested source state. ```sh git ls-files -z mix.exs config lib examples scripts test fixtures | xargs -0 shasum -a 256 | shasum -a 256 ``` -------------------------------- ### Dump Anubis component schemas with Jason Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/migration.md Dumps the JSON Schema maps produced by an existing Anubis component's public DSLs as a starting point for migration. Use this one-time pretty-printed dump to review and validate the schemas before registration. ```elixir schemas = %{ "input" => MyApp.Tools.GetItem.input_schema(), "output" => MyApp.Tools.GetItem.output_schema() } IO.puts(Jason.encode!(schemas, pretty: true)) ``` -------------------------------- ### Run external consumer check with mix commands Source: https://github.com/xukullc/attesto_mcp_server/blob/main/examples/consumer/README.md Run the external consumer check from the examples/consumer directory. It fetches dependencies, compiles with warnings as errors, and runs the stdio adapter after building the server and initializing Plug. ```text mix deps.get mix compile --warnings-as-errors mix run -e ' {:ok, server} = AttestoMCP.ExternalConsumer.build_server(); _plug = AttestoMCP.ExternalConsumer.plug(server); AttestoMCP.ExternalConsumer.stdio(server)' ``` -------------------------------- ### MCP Server HTTP Transport Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Documentation for the MCP server's HTTP transport, including mirror headers, request examples, and tool argument mirror declarations. ```APIDOC ## POST /mcp ### Description Sends a JSON-RPC request to the MCP server. The session-free `2026-07-28` transport requires mirror headers that must agree with the decoded JSON-RPC request. ### Method POST ### Endpoint https://mcp.example.com/mcp ### Headers - **Authorization** (string) - Required - Bearer token for authentication. - **Content-Type** (string) - Required - Must be `application/json`. - **Accept** (string) - Required - Must be `application/json, text/event-stream`. - **Mcp-Protocol-Version** (string) - Required - Must equal `params._meta["io.modelcontextprotocol/protocolVersion"]`. - **Mcp-Method** (string) - Required - Must equal the JSON-RPC `method`. - **Mcp-Name** (string) - Required for `tools/call`, `prompts/get`, `resources/read`, `tasks/get`, `tasks/update`, and `tasks/cancel`; must equal the corresponding body value. ### Request Body - **jsonrpc** (string) - Required - Must be `"2.0"`. - **id** (number) - Required - Request identifier. - **method** (string) - Required - JSON-RPC method name. - **params** (object) - Required - Method-specific parameters. - **params._meta** (object) - Required - Must include `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`. ### Request Example ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "customer_lookup", "arguments": {"id": "cus_123"}, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } } ``` ### Response #### Success Response (200) - **jsonrpc** (string) - Protocol version. - **id** (number) - Request identifier. - **result** (object) - Method-specific result. #### Error Response (400) - **jsonrpc** (string) - Protocol version. - **id** (number) - Request identifier. - **error** (object) - Error details. - **error.code** (number) - Error code, e.g., `-32020`. - **error.message** (string) - Error message. - **error.data** (object) - Additional error data. - **error.data.reason** (string) - Reason for the error, e.g., `body_header_mismatch`. #### Response Example ```json { "jsonrpc": "2.0", "id": 7, "error": { "code": -32020, "message": "Invalid MCP metadata or header", "data": {"reason": "body_header_mismatch"} } } ``` ``` ```APIDOC ## POST /mcp (resources/read) ### Description Reads a resource by URI. The `Mcp-Name` header must mirror the complete `params.uri` value. ### Method POST ### Endpoint https://mcp.example.com/mcp ### Headers - **Authorization** (string) - Required - Bearer token. - **Content-Type** (string) - Required - `application/json`. - **Accept** (string) - Required - `application/json, text/event-stream`. - **Mcp-Protocol-Version** (string) - Required - Must equal `params._meta["io.modelcontextprotocol/protocolVersion"]`. - **Mcp-Method** (string) - Required - Must be `resources/read`. - **Mcp-Name** (string) - Required - Must equal `params.uri`. ### Request Body - **jsonrpc** (string) - Required - `"2.0"`. - **id** (number) - Required - Request identifier. - **method** (string) - Required - `resources/read`. - **params** (object) - Required - Must include `uri` and `_meta`. - **params.uri** (string) - Required - Resource URI. - **params._meta** (object) - Required - Must include `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`. ### Request Example ```json { "jsonrpc": "2.0", "id": 2, "method": "resources/read", "params": { "uri": "urn:customer:cus_123", "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } } ``` ### Response #### Success Response (200) - **jsonrpc** (string) - Protocol version. - **id** (number) - Request identifier. - **result** (object) - Resource content. #### Error Response (400) - **jsonrpc** (string) - Protocol version. - **id** (number) - Request identifier. - **error** (object) - Error details. - **error.code** (number) - Error code. - **error.message** (string) - Error message. - **error.data** (object) - Additional error data. - **error.data.reason** (string) - Reason for the error. ``` ```APIDOC ## Tool Argument Mirror Declaration ### Description Declares a parameter header for a tool's input schema using the `x-mcp-header` annotation. The server constructs `Mcp-Param-{suffix}` from the annotation value. ### Usage In the tool's `input_schema` property, add `x-mcp-header` to a property to require a corresponding header. ### Example ```elixir input_schema: %{ "type" => "object", "properties" => %{ "account" => %{ "type" => "string", "x-mcp-header" => "account" } } } ``` ### Rules - The `x-mcp-header` value must be a nonempty RFC 9110 `tchar` suffix. - Valid only on statically reachable string, integer, or boolean properties. - If the property is absent or null, the client omits its header; otherwise exactly one header is required and its decoded value must equal the nested `params.arguments` value. - The normative Base64 sentinel is `=?base64?SGVsbG8=?=`. Padding and alphabet are strict; values missing either prefix or suffix are compared literally. - Mixed-case header names and RFC 9110 optional whitespace are handled safely. ``` -------------------------------- ### Construct standard MCP outputs with AttestoMCP.Server.Result and Content Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/migration.md Shows the public constructors for standard tool, resource, and prompt outputs. Use Result.tool/2 for a deliberate per-result override; prefer Result.tool_from_context/2,3 inside arity-2 handlers or shared adapters so the result inherits the server's output setting and JSON budget. ```elixir alias AttestoMCP.Server.{Content, Result} tool_result = Result.tool(Content.text("saved"), structured_content: %{"id" => "item-7"} ) resource_result = Result.resource( Content.resource_text("urn:item:7", "contents", mime_type: "text/plain") ) prompt_messages = [ Content.prompt_message(:user, Content.text("Review item 7")) ] ``` -------------------------------- ### Configure server options for atom tool arguments and Jason canonicalization Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Server startup options to opt in to atom-keyed tool arguments and Jason output canonicalization. The :atoms conversion applies only after the original string-keyed input passes its JSON Schema, converting literal keys under properties only when the corresponding atom already exists in the VM; undeclared keys and names without an existing atom remain strings. The default is tool_argument_keys: :strings. ```elixir server_options = [ tool_argument_keys: :atoms, output_canonicalization: :jason ] ``` -------------------------------- ### Registration Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md This section describes the registration process for tools, resources, URI templates, prompts, and completions, including handler callback signatures and server options for tool argument key handling. ```APIDOC ## Registration Register tools, resources, URI templates, prompts, and completions before serving traffic. Identity collisions return `{:error, {:duplicate, type, identity}}`. Registration rejects unsafe names/URIs/templates, malformed handlers, unsupported JSON Schema dialects/remote references, and schemas outside the bounded local 2020-12/draft-07 subset. Anchors, local dynamic references, tuple items, unevaluated items, and content annotations are validated without network fetches. Registry output is stable by identity and pagination cursors are opaque, signed, expiring, and bound to the principal, tenant, scopes, visible catalog content, page size, and negotiated era. Resource templates use bounded reverse matching for an RFC 6570 subset. A template may contain up to 16 separated named or reserved path expressions and 32 globally unique variables, including prefix modifiers such as `{id:3}`. Adjacent path expressions remain rejected because their capture boundary is ambiguous. A single query expression supports `{?q,limit}` and `{?keys*}`. Values are strictly percent-decoded and bounded; simple variables reject raw or encoded path separators, and every variable rejects traversal. Unsupported or ambiguous operator layouts are rejected at registration rather than accepted with nonfunctional matching. A matching `resources/read` handler receives both the requested `uri` and a `params` map of captured variables. Completion handlers should register an explicit `ref` matching the prompt or resource-template reference; only that handler is invoked, and returned string values preserve the handler's relevance order, are de-duplicated, and are capped at 100 with truthful `total`/`hasMore` metadata. Callback inputs are deliberately explicit and primitive-specific: ```elixir alias AttestoMCP.Server.{Content, Result} # Tool handler: fn %{"left" => left, "right" => right}, context -> total = left + right {:ok, Result.tool_from_context(Content.text("total: #{total}"), context, structured_content: %{"total" => total} )} end # Prompt whose definition declares "topic" as required handler: fn %{name: "review", arguments: %{"topic" => topic}}, _context -> {:ok, [Content.prompt_message(:user, Content.text(topic))]} end # Resource or resource template handler: fn %{uri: uri, params: template_params}, _context -> {:ok, Result.resource( Content.resource_text(uri, inspect(template_params), mime_type: "text/plain") )} end # Completion handler: fn %{value: value}, _context -> {:ok, [value]} end ``` Declared outer envelope fields use atom keys. A resource MRTR retry also places its string-keyed input-response entries at the envelope's top level. Nested MCP arguments, references, and completion context retain their JSON string keys. For optional prompt arguments, read the nested map with `Map.get/2` instead of requiring the key in the callback head. An arity-2 callback's second argument is the authenticated request context; a completion input's `:context` is the separate client-supplied completion context. Arity-1 and MFA handler forms are also accepted. Tool handlers receive string-keyed arguments by default. Applications porting handlers that already use atom keys may opt in at server startup: ```elixir server_options = [ tool_argument_keys: :atoms, output_canonicalization: :jason ] ``` `:atoms` is applied only after the original string-keyed input has passed its registered JSON Schema. It converts literal keys found under `properties` only when the corresponding atom already exists in the VM; no call creates an atom. Undeclared keys and declared names without an existing atom remain strings. The conversion follows nested literal `properties`, array `items`, and `prefixItems`. Keys available only through references, combinators, or conditional branches remain strings so ambiguous schema paths never broaden the conversion. The default `tool_argument_keys: :strings` preserves the wire map exactly. ``` -------------------------------- ### Register handler callbacks for tools, prompts, resources, and completions Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Shows the expected callback shapes for tool, prompt, resource/resource-template, and completion handlers. Tool handlers receive string-keyed arguments and a request context; prompt handlers receive a map with name and arguments; resource handlers receive uri and params; completion handlers receive a value. Return values use Content and Result helpers, and completion results preserve relevance order, are de-duplicated, and capped at 100. ```elixir alias AttestoMCP.Server.{Content, Result} # Tool handler: fn %{"left" => left, "right" => right}, context -> total = left + right {:ok, Result.tool_from_context(Content.text("total: #{total}"), context, structured_content: %{"total" => total} )} end # Prompt whose definition declares "topic" as required handler: fn %{name: "review", arguments: %{"topic" => topic}}, _context -> {:ok, [Content.prompt_message(:user, Content.text(topic))]} end # Resource or resource template handler: fn %{uri: uri, params: template_params}, _context -> {:ok, Result.resource( Content.resource_text(uri, inspect(template_params), mime_type: "text/plain") )} end # Completion handler: fn %{value: value}, _context -> {:ok, [value]} end ``` -------------------------------- ### Configure Ecto session store Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Configure the Ecto-backed session store for the MCP server. The store handle is validated by AttestoMCP.Server.SessionStore.Ecto.new/1 and should be created only after the Repo module is compiled, not during config.exs evaluation. ```elixir store = %{ repo: MyApp.Repo, namespace: "primary-mcp", schema_prefix: nil } server_options = [ session_store: {AttestoMCP.Server.SessionStore.Ecto, store}, session_namespace: "primary-mcp" ] ``` -------------------------------- ### Configure PhoenixParser with multiple MCP paths Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Replaces the endpoint's ordinary `Plug.Parsers` declaration with the wrapper, retaining the host's parser options. One wrapper accepts up to 32 unique static MCP paths, matching each decoded configured path as a segment prefix and bypassing the prefix, trailing/repeated-slash equivalents, and every deeper child path; parent paths and sibling segment prefixes still run the host parser. Ordinary routes below a configured prefix also bypass parsing and must not overlap it. ```elixir plug AttestoMCP.Server.PhoenixParser, mcp_path: ["/mcp/catalog", "/mcp/operations", "/mcp/preview"], parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library() ``` -------------------------------- ### Install with ETS session store Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Use this command to explicitly keep sessions in memory (ETS) even when a PostgreSQL Repo exists. The default --session-store auto behavior keeps the in-memory ETS store when no statically confirmed supervised PostgreSQL Repo is found; this flag makes that choice explicit. ```sh mix igniter.install attesto_mcp_server \ --base-url https://mcp.example.com \ --session-store ets ``` -------------------------------- ### Configure required_scopes and alternative_scope_sets in Elixir Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/migration.md Configure the primary all-of clause and up to seven bounded alternatives for a definition. Any one complete clause permits access; partial clauses do not. Alternatives require a non-empty primary clause, and limits of 128 total scope memberships, 8,192 aggregate scope bytes, and 256 bytes per scope apply. ```elixir required_scopes: ["documents.read"], alternative_scope_sets: [ ["documents.admin"], ["workspace.read", "documents.execute"] ] ``` -------------------------------- ### Allow loopback origin for local development Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Use this command to allow a loopback origin for local development. HTTP remains disabled for deployed origins; this flag explicitly permits http://127.0.0.1:4000. Connect local clients to http://127.0.0.1:4000/mcp; an unauthenticated probe should return 401 rather than 404. ```sh mix igniter.install attesto_mcp_server \ --base-url http://127.0.0.1:4000 \ --allow-http-loopback ``` -------------------------------- ### Configure principal_binding for AttestoMCP.Server.Plug Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Configure the MCP Plug with a principal_binding callback that returns a stable identity term from the loaded principal. The callback must return the binding itself (not an {:ok, value} wrapper), be non-nil, portable, and at most 64 KiB encoded. Invalid returns or callback failures stop the request with a generic internal error before the POST body is read. ```elixir forward "/mcp", AttestoMCP.Server.Plug, server: MyApp.MCP, path: "/mcp", auth: {AttestoMCP.Server.Phoenix, :protected_resource_options, [:my_app]}, principal_binding: {MyApp.MCPIdentity, :binding}, resource: "/mcp", base_url: "https://mcp.example.com" defmodule MyApp.MCPIdentity do def binding(%MyApp.Accounts.User{id: id}), do: {:user, id} end ``` -------------------------------- ### Run conformance fixture for frozen requirements sets Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Runs the authenticated conformance fixture for each frozen requirements set. These are preflight checks, not a substitute for the official conformance runner, and do not count disabled Tasks scenarios. ```sh MCP_REQUIREMENTS=2026-07-28 MIX_ENV=test mix test test/conformance_fixture_test.exs --seed 0 MCP_REQUIREMENTS=2025-11-25 MIX_ENV=test mix test test/conformance_fixture_test.exs --seed 0 ``` -------------------------------- ### Configure custom session store and namespace Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Configure a custom session store backend and namespace in the server configuration. The store must implement AttestoMCP.Server.SessionStore. The namespace should be globally unique when unrelated deployments share the store or Erlang cluster. ```elixir session_store: {MyApp.MCPSessions, store_handle}, session_namespace: "primary-mcp" ``` -------------------------------- ### Tool registration with handler returning result Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/migration.md Shows an AttestoMCP tool registration that retains a domain function while adapting the MCP handler. The handler returns {:ok, result} or {:error, reason} using Result.tool_from_context/3 and Result.error/2. Requires the server's output_canonicalization: :jason option when structured values include atoms or Jason-encoded structs. ```elixir alias AttestoMCP.Server.{Content, Result} {:tool, "get_item", %{ input_schema: input_schema, output_schema: output_schema, required_scopes: ["items.read"], handler: fn arguments, context -> case MyApp.Items.fetch(context.principal, arguments["id"]) do {:ok, item} -> value = %{"id" => item.id, "name" => item.name} {:ok, Result.tool_from_context(Content.text(item.name), context, structured_content: value )} :not_found -> {:error, Result.error("item not found", "item_not_found")} end end }} ``` -------------------------------- ### Define protected resource metadata and forwards for three MCP servers Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Publishes one path-specific protected-resource metadata document and one protected forward per server. `root: false` avoids assigning the ambiguous unsuffixed metadata document to any of the three resources. Keep these routes outside browser-session and CSRF pipelines; each metadata path, Plug `:path`, protected resource, public origin, advertised scopes, and server registration must agree for that mount. ```elixir defmodule MyAppWeb.Router do use MyAppWeb, :router use AttestoMCP.Router scope "/" do attesto_mcp_protected_resource_metadata "/mcp/catalog", scopes: ["catalog.mcp"], base_url: "https://mcp.example.com", root: false attesto_mcp_protected_resource_metadata "/mcp/operations", scopes: ["operations.mcp"], base_url: "https://mcp.example.com", root: false attesto_mcp_protected_resource_metadata "/mcp/preview", scopes: ["preview.mcp"], base_url: "https://mcp.example.com", root: false end forward "/mcp/catalog", AttestoMCP.Server.Plug, server: MyApp.MCP.Catalog, path: "/mcp/catalog", scopes_supported: ["catalog.mcp"], default_scopes: ["catalog.mcp"], auth: {AttestoMCP.Server.Phoenix, :protected_resource_options, [:my_app]}, resource: "/mcp/catalog", base_url: "https://mcp.example.com" forward "/mcp/operations", AttestoMCP.Server.Plug, server: MyApp.MCP.Operations, path: "/mcp/operations", scopes_supported: ["operations.mcp"], default_scopes: ["operations.mcp"], auth: {AttestoMCP.Server.Phoenix, :protected_resource_options, [:my_app]}, resource: "/mcp/operations", base_url: "https://mcp.example.com" forward "/mcp/preview", AttestoMCP.Server.Plug, server: MyApp.MCP.Preview, path: "/mcp/preview", scopes_supported: ["preview.mcp"], default_scopes: ["preview.mcp"], auth: {AttestoMCP.Server.Phoenix, :protected_resource_options, [:my_app]}, resource: "/mcp/preview", base_url: "https://mcp.example.com" end ``` -------------------------------- ### POST /mcp/admin Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Mounts the MCP server with an authorization gate that enforces a business policy before protocol dispatch. The callback receives the authenticated base context and must return literal true to permit access. ```APIDOC ## [POST] /mcp/admin ### Description Mounts the MCP server at the specified path with an authorization gate that enforces a business policy before any protocol dispatch. The gate runs on each authenticated POST, GET, and DELETE leg before the request body is read. ### Method POST ### Endpoint /mcp/admin ### Parameters #### Path Parameters - **path** (string) - Required - The mount path for the MCP server. #### Query Parameters - **server** (module) - Required - The MCP server module. - **auth** (tuple) - Required - Authentication configuration. - **principal_binding** (tuple) - Required - Principal binding configuration. - **authorize** (tuple) - Required - Authorization callback. - **resource** (string) - Required - Resource identifier. - **base_url** (string) - Required - Base URL for the MCP server. ### Request Example ```elixir forward "/mcp/admin", AttestoMCP.Server.Plug, server: MyApp.AdminMCP, path: "/mcp/admin", auth: {AttestoMCP.Server.Phoenix, :protected_resource_options, [:my_app]}, principal_binding: {MyApp.MCPIdentity, :binding}, authorize: {MyApp.MCPPolicy, :admin_mount?}, resource: "/mcp/admin", base_url: "https://mcp.example.com" ``` ### Response #### Success Response (200) - **status** (integer) - HTTP status code. #### Response Example ```json { "status": 200 } ``` ``` -------------------------------- ### Focused tool test with AttestoMCP.Server.Test.call_tool/4 Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Uses `AttestoMCP.Server.Test.call_tool/4` for a per-tool test that goes through protocol dispatch, checking operation scopes, tool scope clauses, `authorize` callback, input schema, handler, output schema, and final wire-output validation. The helper JSON-encodes and decodes its request before dispatch and defaults to protocol revision `2026-07-28`, principal `"test-principal"`, and no scopes. It does not exercise token, DPoP, mTLS, HTTP header, parser-order, or mount-policy checks. ```elixir response = AttestoMCP.Server.Test.call_tool( MyApp.MCP, "get_item", %{"id" => "item-7"}, request_id: "get-item-test", principal: %{id: "user-7"}, scopes: ["items.read"], host_context: %{account_id: "acct-1"} ) assert %{ "id" => "get-item-test", "result" => %{ "resultType" => "complete", "structuredContent" => %{"id" => "item-7"} } } = response ``` -------------------------------- ### Configure Attesto auth with trusted_audiences Source: https://github.com/xukullc/attesto_mcp_server/blob/main/docs/usage.md Configure the AttestoMCP.Plug.ProtectResource boundary with a dynamic config callback, a pinned canonical resource identifier, and an explicit list of trusted audiences for RFC 8707 gateway substitution. The canonical resource drives metadata and challenges; every unlisted audience is rejected. ```elixir auth: [ config: &MyApp.MCP.attesto_config/0, resource: "https://mcp.example.com/mcp", trusted_audiences: [ "https://mcp.example.com/mcp", "https://gateway.example.net/proxy/mcp" ] ] ``` -------------------------------- ### Add attesto_mcp_server dependency in mix.exs Source: https://github.com/xukullc/attesto_mcp_server/blob/main/README.md Adds the attesto_mcp_server package to a Mix project's deps. Use this in a non-Phoenix Plug host to include the library. The version constraint is "~> 2.0". ```elixir def deps do [{:attesto_mcp_server, "~> 2.0"}] end ```