Wymcp. Plugs. Auth
(Wymcp v0.6.2)
View Source
Authentication plug for MCP requests.
Reads the auth module from the mount module's options
(conn.assigns[:wymcp][:auth])
and calls its Wymcp.Auth.authenticate/1 callback. When no auth
module is configured, defaults to Wymcp.Auth.Noop (pass-through).
On authentication failure, returns HTTP 401 with a WWW-Authenticate
challenge as required by the MCP 2025-11-25 specification. By default the
challenge is bare Bearer; consumers may append RFC 6750 auth-params (an
RFC 9728 resource_metadata pointer, a scope hint) via the router's
:www_authenticate option — see Wymcp.Router. If rendering the configured
params raises (e.g. a misconfigured MFA), the challenge degrades to bare
Bearer for that request and the error is logged naming the option — the
401 contract survives misconfiguration. The response body follows the
route's error dialect (the :error_dialect init option): a JSON-RPC error
with code -32600 (Invalid Request) on the default :json_rpc dialect — the
POST pipeline — or the plain-JSON %{error: message} object under
:plain_json, which Wymcp.Router's GET and DELETE routes pass when they
run the wire checks its wire-check invariant names.
The plain-JSON half is legacy-only: that dialect exists for the GET and
DELETE rejection bodies, and it goes with those routes at the legacy
decommission.
Observability
A 401 from either branch is a rejection:
it carries the rejection mark and emits [:wymcp, :wire, :reject] like
every other rejection wymcp sends, with reason :unauthenticated when the
auth module returned {:error, reason} and :auth_error when it raised.
The consumer's reason — a string, or an atom the wire renders as its
string — rides that event as message.
The raise branch emits a second event, [:wymcp, :auth, :error] (see
Wymcp.Telemetry): the rejection records that the client got a 401, the
fault records that the consumer's module is broken, and the two are
orthogonal. Its metadata names the module, the exception class and
message, the conn's HTTP verb, the body's method and message_id, and
crash_reason — the exception and its stacktrace, which a handler cannot
rebuild from the strings.
The telemetry message_id is read straight from the body and deliberately
differs from the id the 401's envelope carries: the wire answer applies
Wymcp.Response.rejection_id/1, which is nil on every non-request
message, while an operator diagnosing a rejection wants the id the client
actually sent. That is why request_field/2 survives at the two
observability sites and nowhere else.
Neither branch writes a Logger line of its own: Wymcp.Telemetry.Logger
renders both events, and a consumer wanting different lines writes its own
handler rather than living with wymcp's.