Wymcp. Plugs. Auth
(Wymcp v0.1.1)
View Source
Authentication plug for MCP requests.
Reads the auth module from router opts (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 the router's GET/DELETE wire-check call sites pass.
Observability
The plug emits two telemetry events (see Wymcp.Telemetry) alongside
the wire response:
[:wymcp, :auth, :reject]— the auth module returned{:error, reason}. Metadata includesauth_module,reason,request_id, andmethod.[:wymcp, :auth, :error]— the auth module raised. Metadata includesauth_module,exception,error,request_id, andmethod.
Both events carry http_method — the conn's HTTP verb ("POST",
"GET", "DELETE"). request_id and method come from the parsed
request body, so both are nil off POST. Both branches also emit a
structured Logger line with the same metadata so operators without a
telemetry handler still get attribution.
The telemetry request_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 four observability sites
and nowhere else.