Auth is application-owned. FastestMCP keeps a small runtime contract that turns credentials or framework state into normalized request context:

  • ctx.principal
  • ctx.auth
  • ctx.capabilities
  • Context.client_id/1

Your application verifies sessions, tokens, cookies, or upstream identity using its normal stack. FastestMCP only needs the normalized result.

Function Auth

Pass a function directly when auth is specific to the host application:

FastestMCP.server("app")
|> FastestMCP.add_auth(fn input, _ctx ->
  case MyApp.Auth.verify_mcp_request(input) do
    {:ok, user} ->
      {:ok,
       %{
         principal: %{"sub" => to_string(user.id)},
         auth: %{source: :app, user_id: user.id},
         capabilities: MyApp.MCPScopes.for_user(user)
       }}

    :error ->
      {:error, :unauthorized}
  end
end)

The function may have arity 2 or 3. Arity 3 receives the configured auth options as the third argument.

Module Auth

Use the behaviour when you want a reusable authenticator module:

defmodule MyApp.MCPAuth do
  @behaviour FastestMCP.Auth

  @impl true
  def authenticate(input, _ctx, opts) do
    with {:ok, user} <- MyApp.Auth.verify(input, opts) do
      {:ok,
       %FastestMCP.Auth.Result{
         principal: %{"sub" => to_string(user.id)},
         auth: %{source: :app, user_id: user.id},
         capabilities: MyApp.MCPScopes.for_user(user)
       }}
    end
  end
end

FastestMCP.server("app")
|> FastestMCP.add_auth(MyApp.MCPAuth, audience: "mcp")

Auth errors should return {:error, :unauthorized}, {:error, :forbidden}, {:error, {code, message}}, or {:error, %FastestMCP.Error{}}.

Phoenix Assigns

When the HTTP transport runs behind Plug or Phoenix authentication, copy selected conn.assigns into auth input with auth_assigns:. Assigns are available only to the auth function or module under "assigns"; they are not added to normal handler request metadata.

pipeline :mcp do
  plug :fetch_session
  plug MyAppWeb.UserAuth, :fetch_current_user
end

scope "/" do
  pipe_through :mcp

  forward "/mcp", FastestMCP.Transport.HTTPApp,
    server_name: MyApp.MCPServer,
    path: "/mcp",
    auth_assigns: [:current_user]
end

FastestMCP.Auth.from_assign/2 turns one assign into a normalized auth result:

FastestMCP.server(MyApp.MCPServer)
|> FastestMCP.add_auth(
  FastestMCP.Auth.from_assign(:current_user,
    principal: fn user -> %{"sub" => to_string(user.id)} end,
    capabilities: fn user -> MyApp.MCPScopes.for_user(user) end,
    auth: fn user -> %{source: :phoenix, user_id: user.id} end
  )
)

auth_assigns: accepts:

  • false or nil to copy no assigns
  • [:current_user, :account] to copy specific assigns
  • :all to copy every assign

The default is false.

Static Token

FastestMCP.Auth.StaticToken is kept for local development, integration tests, and hermetic tooling:

FastestMCP.server("dev")
|> FastestMCP.add_auth(FastestMCP.Auth.StaticToken,
  tokens: %{
    "dev-token" => %{
      client_id: "local-client",
      scopes: ["tools:call"],
      principal: %{"sub" => "local-client"}
    }
  },
  required_scopes: ["tools:call"]
)
|> FastestMCP.add_tool("whoami", fn _arguments, ctx ->
  %{principal: ctx.principal, auth: ctx.auth}
end)

Static tokens can be supplied as an HTTP bearer token, as "authorization" in direct auth_input, or as "token" in direct auth_input.

Component Authorization

Authentication identifies the caller. Component authorization decides which tools, resources, prompts, and templates the caller may see or call.

FastestMCP.server("app")
|> FastestMCP.add_tool("admin_report", &MyApp.Report.run/2,
  auth: FastestMCP.Authorization.require_scopes(["admin:reports"])
)

Authorization rules can also filter list results with tags:

FastestMCP.Authorization.restrict_tag("internal")

Authorization is fail closed. A component check authorizes only when it returns true or :ok. false, nil, malformed return values, exceptions, throws, and exits all deny access; a binary {:error, message} also denies with that message. When multiple versions share an identity, an unauthorized higher version is skipped so an authorized lower version can remain visible, while an explicit request for the unauthorized version is rejected.

HTTP Behavior

When a server configures auth, FastestMCP authenticates every inbound HTTP initialize, request, notification, client response, POST stream, GET stream, and DELETE before dispatch. The successful initialize identity is bound to the session; a different principal cannot reuse the session id. Component authorization still runs in the operation pipeline after transport authentication.

Without protected-resource configuration, HTTP auth failures use a plain bearer challenge:

WWW-Authenticate: Bearer error="invalid_token", error_description="missing credentials"

RFC 9728 Protected Resource Metadata

Configure protected_resource: when standards-aware clients must discover the authorization server and authoritative scopes for the MCP endpoint:

server =
  FastestMCP.server("documents",
    auth: MyApp.MCPAuth,
    protected_resource: [
      resource: "https://mcp.example.com/mcp",
      authorization_servers: ["https://auth.example.com"],
      scopes_supported: ["documents:read", "documents:write"],
      required_scopes: ["documents:read"]
    ]
  )

The public HTTP app serves the path-derived metadata document on the same resource origin. For the example above it is:

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

Authentication failures include both discovery and authoritative scope:

WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp", scope="documents:read", error="invalid_token"

The configured resource must be the exact absolute MCP resource URI. Non-loopback resources and every authorization-server URI must use HTTPS, and the authorization-server list cannot be empty. Access tokens in query strings are rejected before the authenticator runs. The authenticator receives "expected_resource" and "expected_scopes" in its input and the same values in request metadata.

For a protected resource, successful authentication must return verified evidence, not merely untrusted token claims:

{:ok,
 %FastestMCP.Auth.Result{
   principal: %{"sub" => subject},
   audiences: ["https://mcp.example.com/mcp"],
   scopes: ["documents:read"]
 }}

audiences identifies the resource audiences the host authenticator actually verified, and scopes identifies the granted scopes it actually verified. When protected-resource auth is enabled, FastestMCP fails closed unless the configured resource is present in audiences and every required scope is present in scopes. Those values survive the request-context handoff for component authorization; a session id is never accepted as authentication.

The older verified_audiences and verified_scopes struct/map keys remain accepted as compatibility aliases. New authenticators should use audiences and scopes; conflicting values are rejected.

FastestMCP remains the protected resource server. Authorization-server token issuance, signing, introspection, consent UI, and authorization-server operation stay application-owned or external. Signature/opaque-token verification is the authenticator's responsibility; FastestMCP enforces the verified audience and scope evidence returned by that boundary. Configure FastestMCP.Auth.ProtectedResource only together with an authenticator; protected-resource HTTP fails closed when no authenticator exists.

The connected-client OAuth flow and its host-owned browser/token-store boundaries are documented in Client. They do not turn FastestMCP into an authorization server.

Why This Shape

Phoenix applications usually already own authentication, sessions, user loading, authorization policy, and audit metadata. Keeping FastestMCP auth as a small contract avoids a second identity stack while preserving consistent context for handlers, middleware, tasks, transports, and component visibility.