Wymcp.Router (Wymcp v0.1.1)

View Source

Plug router for the Wymcp MCP server.

Usage in a Phoenix router

forward "/mcp", Wymcp.Router,
  tools: [MyApp.Tools.Events, MyApp.Tools.Tasks]

With authentication

forward "/mcp", Wymcp.Router,
  tools: [MyApp.Tools.Events, MyApp.Tools.Tasks],
  auth: MyApp.McpAuth

With OAuth discovery hints on the 401 challenge

forward "/mcp", Wymcp.Router,
  tools: [MyApp.Tools.Events, MyApp.Tools.Tasks],
  auth: MyApp.McpAuth,
  www_authenticate: [
    resource_metadata: {MyAppWeb.Endpoint, :url, []},
    scope: "mcp"
  ]

With origin allowlist (DNS rebinding protection)

forward "/mcp", Wymcp.Router,
  tools: [MyApp.Tools.Events, MyApp.Tools.Tasks],
  origin: ["http://localhost:4000"]

With server callbacks

forward "/mcp", Wymcp.Router,
  tools: [MyApp.Tools.Events, MyApp.Tools.Tasks],
  server: MyApp.McpServer

With server info and instructions

forward "/mcp", Wymcp.Router,
  tools: [MyApp.Tools.Events],
  instructions: "Search docs before answering questions.",
  server_info: %{
    title: "My App MCP",
    description: "Project management tools",
    website_url: "https://myapp.example.com"
  }

Options

  • :tools — list of modules implementing the Wymcp.Tool behaviour (required). Wymcp.Help is appended automatically: every server exposes the framework's introspection tool under the reserved name help, and no consumer tool may use that name (init/1 raises). Two tools declaring the same Wymcp.Tool.name/0 are likewise refused at init/1 rather than at request time — a duplicate would make a tools/call ambiguous, and boot is the only moment the whole list is visible at once.
  • :auth — module implementing the Wymcp.Auth behaviour (optional, defaults to Wymcp.Auth.Noop)
  • :www_authenticate — keyword list of RFC 6750 auth-params appended to the Bearer challenge in the 401 WWW-Authenticate header (optional; when absent the challenge is bare Bearer). Each {key, value} renders as key="value" with quoted-string escaping. A value may be a {module, function, args} tuple resolved per request — use this when the value is only known at runtime (e.g. a public URL from runtime config), since forward options are evaluated at compile time. Typical MCP use: an RFC 9728 resource_metadata pointer and a scope hint. If rendering an entry raises (e.g. a misconfigured MFA), the challenge degrades to bare Bearer for that request and an error naming this option is logged — the 401 contract survives misconfiguration.
  • :server — module implementing the Wymcp.Server behaviour for session lifecycle hooks (optional, defaults to nil)
  • :origin — list of allowed Origin header values for DNS rebinding protection (optional, defaults to allowing all origins). A request with no Origin header passes the check even when an allowlist is configured — non-browser clients (curl, SDKs) do not send one
  • :instructions — a string that guides how an LLM should interact with this server's tools, included in the initialize response (legacy era) and the server/discover result (modern era) (optional)
  • :server_info — a map of optional server identity fields displayed by MCP clients. Supported keys: :title (human-readable name), :description, :website_url, and :icons. Each icon is a map with :src (required URL or data: URI) and the optional keys :mime_type (e.g. "image/png"), :sizes (list of "WxH" strings or "any"), and :theme ("light" or "dark"). Any other key in an icon map is dropped and a warning is logged. These fields are merged with name and version from application config by Wymcp.ServerInfo; the legacy lane emits the result in initialize, the modern lane in every result's _meta (optional).

Every non-fallthrough route — POST, GET (the SSE stream), DELETE — runs all three wire checks before the request touches any session state: the origin check (Wymcp.Plugs.OriginCheck), the auth check (Wymcp.Plugs.Auth), then the singleton-header check (Wymcp.Plugs.SingletonHeaders). This rule is the wire-check invariant, and its ordering is load-bearing: 401/403 rejections win over the session answers, so an unauthenticated caller learns nothing about session existence — and a rejected request neither resets the session's idle timer nor displaces its registered SSE stream. The origin check stays first because nothing has validated Origin when it runs, which is also why that header's duplicate arm lives in the origin check rather than in the singleton-header check. The fallthrough (any other verb) runs no checks and touches nothing.

POST runs the checks as the first, fourth, and fifth plugs of Wymcp.Plugs.Pipeline's chain; that module owns the full order and the reasons for it, and this module does not restate them. Modern-classified requests and notifications pass through Wymcp.Plugs.Session untouched, and a JSON-RPC response resolves its session on either lane. GET and DELETE run the same three checks in the route body, before the Mcp-Session-Id header is read. A wire check's rejection speaks the error dialect of the route it runs on: the JSON-RPC dialect on POST, the plain-JSON dialect (%{error: message}) on GET and DELETE — with the 401 WWW-Authenticate challenge on every method.

flowchart TD
    subgraph Router
        R[Wymcp.Router] --> POST["POST / → Pipeline"]
        R --> WC["GET / DELETE / → wire checks"]
        WC --> GET["GET / → SSE stream"]
        WC --> DELETE["DELETE / → terminate"]
    end
    subgraph External
        POST --> P["Plugs.Pipeline (wire checks inside)"]
        WC --> OC[Plugs.OriginCheck]
        WC --> AU[Plugs.Auth]
        WC --> SH[Plugs.SingletonHeaders]
        GET --> S[Session]
        GET --> ST[Transport.Stream]
        DELETE --> S
    end

Summary

Functions

Callback implementation for Plug.call/2.

Callback implementation for Plug.init/1.

Functions

call(conn, opts)

Callback implementation for Plug.call/2.

init(opts)

Callback implementation for Plug.init/1.