Raxol.MCP.Deployment (Raxol MCP v2.7.0)

Copy Markdown View Source

Fail-closed environment gate for MCP authorization.

Mirrors the posture the payments stack uses (require_policy): outside a dev/test build, a network-exposed MCP transport must not serve tools without an authorizer configured. stdio is exempt (it inherits the OS process boundary); this guards SSE.

Summary

Functions

Fail-closed boot check. Raises when authorization is required in this environment but the fronted server has no authorizer configured; a no-op otherwise. context names the caller for the error message.

Fail-closed boot check for the READ seam. Raises when authorization is required in this environment but the fronted server has no :read_authorizer; a no-op otherwise. context names the caller.

True in any non-dev/test build.

Whether a network transport must refuse to expose tools without an authorizer.

Functions

enforce_authorization!(authorizer_configured?, context \\ "MCP transport")

@spec enforce_authorization!(boolean(), String.t()) :: :ok

Fail-closed boot check. Raises when authorization is required in this environment but the fronted server has no authorizer configured; a no-op otherwise. context names the caller for the error message.

enforce_read_authorization!(read_authorizer_configured?, context \\ "MCP transport")

@spec enforce_read_authorization!(boolean(), String.t()) :: :ok

Fail-closed boot check for the READ seam. Raises when authorization is required in this environment but the fronted server has no :read_authorizer; a no-op otherwise. context names the caller.

Separate from enforce_authorization!/2 because the two seams are separately configured and a network deployment needs both. The tool gate is satisfied by naming :mcp_authorizer or :mcp_allowed_tools, neither of which says anything about reads -- and resources/read serves live model state, completion/complete enumerates live session ids. Satisfying the tool gate alone used to be enough to expose them.

production?()

@spec production?() :: boolean()

True in any non-dev/test build.

Read at RUNTIME, deliberately not captured at compile time. This module compiles as a dependency, and a path dependency compiles under :prod whatever the umbrella's environment is, so a compile-time capture answers for how raxol_mcp itself was built rather than for the application it protects. The two disagreed in exactly the case that matters: the same predicate was false under this package's own mix test and true inside a dev session of an application depending on it, so a dev session read as production.

Mix being unusable is the signal for a release; a dev or test session has a RUNNING Mix and answers for that session. Called at transport boot rather than per request, so reaching the loader here costs nothing.

Note this is no longer a compile-time constant, which the previous shape hoisted to a module attribute to avoid: inlined, @mix_env not in [:dev, :test] compared two atoms the type checker knew were disjoint and warned on every build. Mix.env/0 is opaque to it, so the comparison is now an ordinary one.

Loadable is not the same as started

Code.ensure_loaded?(Mix) alone was wrong: it asks whether the MODULE is reachable, while Mix.env/0 reads :ets.lookup(Mix.State, :env) and so needs the :mix APPLICATION to be running. Any node that has Elixir's standard lib on its code path but has not started :mix -- elixir -e, an escript, a release that ships :mix without starting it -- satisfies the guard and then raises ArgumentError: the table identifier does not refer to an existing ETS table. That is on Raxol.Application.start/2's :mcp path, so the failure mode was a boot crash in exactly the deployment shape the else branch was written to answer for.

Unusable Mix now resolves to true rather than raising, which is also the fail-closed direction: an environment this predicate cannot identify is treated as production.

require_authorization?()

@spec require_authorization?() :: boolean()

Whether a network transport must refuse to expose tools without an authorizer.

Defaults to production?/0; override with config :raxol_mcp, require_authorization: boolean.

This got looser in dev, deliberately

production?/0 used to be captured at compile time, which read :prod for a path dependency whatever the umbrella's environment was. That made this true inside a raxol DEV session, so SSE refused to boot there without an authorizer. Reading the environment at runtime corrects the misreading and, as a consequence, stops the gate biting in dev and test.

That is the intended posture -- a dev session is not production and should not be gated as one -- but it is a relaxation as well as a correction. A dev or staging deployment that wants the production posture has to ask for it: config :raxol_mcp, require_authorization: true.