Protocol Extensions

Copy Markdown View Source

MCP 2026-07-28 makes extensions an explicit capability surface. Configure them by exact reverse-DNS identifier:

alias FastestMCP.Protocol.Extensions

server =
  FastestMCP.server("extended",
    extensions: %{
      Extensions.apps() => %{},
      Extensions.tasks() => %{}
    }
  )

The connected client has the same extensions: %{identifier => settings} option. Unknown extension settings remain open data; FastestMCP validates only the extension fields it consumes.

Tasks and authorization-extension declarations affect only the modern profile. Apps capability metadata is also preserved during legacy initialization, but it does not retrofit modern core wire shapes onto a 2025-11-25 session.

Support Matrix

ExtensionIdentifierMaturity/versionFastestMCP boundary
MCP Appsio.modelcontextprotocol/uistable specification 2026-01-26; Apps v1.0.0server resource/tool metadata and connected-client preservation
Tasksio.modelcontextprotocol/tasksexperimental draftfull modern Tasks v2 server/client wire surface
OAuth Client Credentialsio.modelcontextprotocol/oauth-client-credentialsdraftconnected-client grant only
Enterprise-Managed Authorizationio.modelcontextprotocol/enterprise-managed-authorizationstableconnected-client grant and host identity-provider callback

Experimental and draft extensions may change independently of the core protocol. Configure their identifiers deliberately and review their versioned extension contracts when upgrading.

Active Server Extensions

extensions: is an open capability map: FastestMCP preserves and advertises its settings but does not execute arbitrary behavior from it. Use the ordered active_extensions: list when an extension owns server methods or intercepts negotiated tool calls:

extension =
  FastestMCP.ServerExtension.new("com.example/reports",
    settings: %{"revision" => 1},
    methods: [
      FastestMCP.ServerExtension.method(
        "reports/run",
        fn params, context ->
          %{"report" => MyApp.Reports.run(params, context.principal)}
        end,
        params_schema: %{
          "type" => "object",
          "properties" => %{"name" => %{"type" => "string"}},
          "required" => ["name"]
        }
      )
    ]
  )

server =
  FastestMCP.server("reports",
    active_extensions: [extension]
  )

Active methods are available only on 2026-07-28 requests whose current client-capability metadata advertises the same extension identifier. They run through the ordinary authentication, middleware, telemetry, supervised-call, and request-cleanup boundaries. The optional parameter schema is applied after the generic JSON-RPC and MCP metadata checks. A handler receives (params, context) and must return a JSON object.

An extension can also declare one lifespan: and one tool_interceptor:. Its lifespan state is available at context.lifespan_context[extension_id]. The interceptor has the same two-arity contract as middleware and runs only for a modern tools/call request that advertises that extension. Because it is installed on the root server, it also wraps mounted tools.

FastestMCP rejects duplicate extension identifiers, passive/active identifier collisions, duplicate method ownership, and attempts to shadow core, Tasks, or other built-in methods. Mounted child servers cannot own active extensions; configure them on the consuming root server so negotiation has one boundary. Apps and Tasks remain specialized implementations rather than generic active extensions.

Clients advertise configured extension settings on every modern request. Use the low-level request API for an extension method whose result has no dedicated FastestMCP normalizer:

client =
  FastestMCP.Client.connect!(endpoint,
    extensions: %{"com.example/reports" => %{"revision" => 1}}
  )

result = FastestMCP.Client.request(client, "reports/run", %{"name" => "daily"})

Active extension notifications, subscription types, generic output schemas, macros, and dynamic plugin loading are outside this API.

MCP Apps

FastestMCP.Apps builds canonical ui:// resource content, CSP/permission metadata, and tool-to-resource links. Both sides must opt in. Enable Apps on the server:

server =
  FastestMCP.server("apps",
    extensions: %{FastestMCP.Apps.extension_id() => %{}}
  )

Advertise the supported media type from the client:

extensions: %{
  FastestMCP.Apps.extension_id() => FastestMCP.Apps.client_settings()
}

FastestMCP emits _meta.ui only when the server is enabled for Apps and the current client advertises text/html;profile=mcp-app. Without that negotiated pair, the same tool remains a normal MCP tool and its UI metadata is removed. The connected client preserves negotiated descriptors and resource documents for a consuming Host.

An advertised tool link is checked through the same resource visibility and authorization boundary as resources/read. Its target must exist as an accessible ui:// resource with the Apps MIME type. Every Apps-linked tool must still return a non-empty ordinary content array, including to clients that do not support Apps; structuredContent can carry the UI-oriented data.

Apps.result/3 treats HTML as opaque bytes. Supply a complete valid HTML5 document yourself:

FastestMCP.Apps.result(
  "ui://reports/summary.html",
  "<!doctype html><html><head><meta charset=\"utf-8\"></head><body>Report</body></html>"
)

FastestMCP validates the ui:///MIME/content envelope, but deliberately does not implement an HTML parser. CSP entries are schema-shaped source strings; values such as wss://live.example.com and https://*.example.com are preserved for Host enforcement. _meta.ui.visibility is Apps Host metadata and does not change FastestMCP's internal component visibility: policy.

The consuming browser or native MCP Host remains responsible for creating the View, iframe sandboxing, CSP enforcement, permission and consent UI, and the Host/View postMessage bridge. FastestMCP does not render untrusted HTML or claim to be an Apps Host.

Tasks: Two Deliberately Separate Eras

For 2026-07-28, enable io.modelcontextprotocol/tasks and register task support on components. Tasks v2 is server-directed and uses tasks/get, tasks/update, and tasks/cancel; terminal results are inlined by tasks/get. Durations use ttlMs and pollIntervalMs. Multi-round-trip input uses InputRequiredResult plus inputResponses.

For 2025-11-25, the existing experimental core Tasks v1 surface remains: tasks/get, tasks/list, tasks/result, and tasks/cancel, with legacy task augmentation and field names. It is kept for backwards compatibility and is not wire-compatible with Tasks v2.

The local Elixir background-task API is independent of either remote wire profile. See Background Tasks for the three-way split.

OAuth Client Credentials

This grant is for machine clients. It stays under the existing oauth: client configuration:

oauth: [
  grant:
    {:client_credentials,
     client_id: "worker",
     token_endpoint_auth_method: "client_secret_basic",
     client_secret: System.fetch_env!("MCP_CLIENT_SECRET")}
]

The tagged grant automatically declares io.modelcontextprotocol/oauth-client-credentials; a separate extensions: entry is not required.

For private_key_jwt, provide assertion_provider: instead of a secret. The arity-one callback or FastestMCP.Client.OAuth.ClientAssertionProvider implementation receives the discovered client id, token endpoint, authorization server, signing algorithms, and grant type. It returns {:ok, jwt}. FastestMCP does not load keys or implement a second JOSE stack.

Enterprise-Managed Authorization

Enterprise-managed authorization delegates the workforce identity assertion to the host:

oauth: [
  grant:
    {:enterprise_managed,
     registration: {:pre_registered, [client_id: "enterprise-client"]},
     provider: &MyApp.MCPIdentity.assertion/1}
]

The tagged grant automatically declares io.modelcontextprotocol/enterprise-managed-authorization on modern MCP requests.

The arity-one provider or FastestMCP.Client.OAuth.EnterpriseManagedProvider implementation receives the protected resource, authorization server, and scopes. It returns an assertion map with token_endpoint, subject_token, subject_token_type, and optional headers and form fields. FastestMCP performs the defined token exchange; the host owns sign-in, IdP/SAML/OIDC credential acquisition, secure storage, and user policy.

Client Credentials and Enterprise-Managed Authorization change how the connected client obtains a bearer token. They do not turn the FastestMCP server into an authorization server. See Auth and Phoenix Deployment.

Verification Boundary

The official runner has executable Tasks and authorization-extension scenarios. CI invokes them explicitly. Its Apps package currently supplies no server/client conformance scenario, so Apps is gated by native metadata, capability filtering, content, and round-trip tests. The runner's optional Tasks status-notification scenario is currently always skipped pending its subscriptions/listen fixture rewrite; native transport tests own that gate.