Raxol.MCP.Authorizer (Raxol MCP v2.7.0)

Copy Markdown View Source

The authorization seam in front of MCP tools/call.

An authorizer is a 3-arity function (tool_name, arguments, context) -> decision, evaluated by Raxol.MCP.Server before a tool runs. raxol_mcp deliberately owns only the seam, not a policy engine: richer ALLOW/ASK/DENY logic (e.g. Raxol.Agent.Authorization.Engine) lives upstream and plugs in as a closure, so the engine and this enforcement point share one vocabulary without raxol_mcp depending on raxol_agent (which would be a dependency cycle).

Decisions

  • :allow -- run the tool.
  • {:ask, prompt} -- the action needs interactive approval. What the server does with it depends on whether the client can be asked:
    • a client that advertised the elicitation capability at initialize (and has a subscribed transport) is sent a real elicitation/create request carrying prompt, and the call is parked until it answers;
    • anything else gets the machine-readable authorization_required result (deny-on-ASK), which is also what a decline, a cancel, an error, and a timeout resolve to. Only an explicit approval runs the tool. See Raxol.MCP.Server's "Elicitation" section.
  • {:deny, reason} -- refuse, machine-readably.

A nil authorizer means allow: a stdio transport already inherits the OS process boundary. The SSE transport, which exposes tools over the network, is guarded separately -- see Raxol.MCP.Deployment.

Two seams: tools and reads

Raxol.MCP.Server evaluates authorizers at two independent seams:

  • :authorizer -- guards tools/call (this module's original purpose). Receives the TOOL name.
  • :read_authorizer -- guards the read/metadata surfaces (resources/read, resources/subscribe, resources/unsubscribe, resources/list, prompts/get, prompts/list, tools/list, completion/complete). Receives the METHOD name in the tool-name position, e.g. "resources/read" with %{"uri" => uri} as the arguments.

They are deliberately separate: a tool allowlist knows nothing about method names and would otherwise deny every read. On read surfaces an {:ask, _} decision resolves to deny (no elicitation for reads) and the prompt is not echoed to the client. Network deployments must configure BOTH seams: a nil :read_authorizer serves model state to any connected client, so the SSE boot guard (Raxol.MCP.Deployment) refuses to boot without one, exactly as it refuses without a tool authorizer.

Summary

Functions

Allow every call. The implicit behaviour when no authorizer is configured.

Allow only tools whose name is in names; deny the rest. This is the allowlist exposure mode: pair it with a narrow set to expose a safe subset of the auto-derived tool surface over an untrusted transport.

Evaluate an authorizer, treating nil as :allow.

Deny every call with reason.

Types

context()

@type context() :: map()

decision()

@type decision() :: :allow | {:ask, String.t()} | {:deny, term()}

t()

@type t() :: (String.t(), map(), context() -> decision())

Functions

allow_all()

@spec allow_all() :: t()

Allow every call. The implicit behaviour when no authorizer is configured.

allowlist(names)

@spec allowlist([String.t()]) :: t()

Allow only tools whose name is in names; deny the rest. This is the allowlist exposure mode: pair it with a narrow set to expose a safe subset of the auto-derived tool surface over an untrusted transport.

decide(fun, tool, args, ctx)

@spec decide(t() | nil, String.t(), map(), context()) :: decision()

Evaluate an authorizer, treating nil as :allow.

deny_all(reason \\ :denied)

@spec deny_all(term()) :: t()

Deny every call with reason.