Wymcp.Plugs.Dispatch (Wymcp v0.8.7)

View Source

The method surface: routes a validated message to the module that answers it. The last plug in Wymcp.Plugs.Pipeline.

Two branches, in clause order, read from message classification. A notification never reaches a method: the core defines no client→server notification over HTTP, and JSON-RPC forbids an error response to a notification — so it is answered with the acceptance, Wymcp.Response.send_accepted/1, whatever its body. That is also why Wymcp.Plugs.Validate lets it through unvalidated. A request takes the table.

The method table

server/discover, tools/list and tools/call — the whole method surface. An unknown method answers HTTP 404 plus -32601: the spec's MUST for a method the server does not implement, which is where a request naming ping, logging/setLevel or notifications/cancelled lands (initialize never reaches here — Wymcp.Plugs.ProtocolFields answers it -32022 first). That 404 is a rejection, so this plug records it through Wymcp.Response.record_rejection/5 before the body is built.

The tool list this plug passes on is the mount module's mount tools — its :tools option — together with the definitions Wymcp.Router.init/1 built for them, which are tools/list's alone: tools/call is handed the tool list and nothing else. Wymcp.Router copies its init options into conn.assigns[:wymcp], and this plug reads both from there. The list is the effective one: the server is stateless, so nothing registers a tool after the registration moment.

Method containment

A fault past any method answers a -32603 error envelope carrying the request's id and a fixed message, and emits [:wymcp, :method, :error] carrying the fault's crash_reason whole. That rule is method containment and this section is its home. Nothing propagates: the envelope is the whole answer, the operator's account of the cause is the event, and Wymcp.Telemetry.Logger renders it at :error. Answering and then re-raising would buy the host a log line at the price of the connection — the answer lands and the socket is dropped, so every keep-alive client that meets a fault pays for it.

That containment — the seat — wraps the method call and nothing ahead of it. The plugs above this one are not covered, and neither is the table lookup here that chooses the method: a fault in either is a framework bug on a path carrying no consumer input. The consumer-boundary containments that sit inside a method keep their own dialects — Wymcp.Tool's fault containment around a tool's Wymcp.Tool.run/2 — and this rule answers what escapes it, wymcp's own faults included.

Wymcp.Plugs.Auth's containment around Wymcp.Auth.authenticate/1 is not one of them. It runs ahead of this plug in Wymcp.Plugs.Pipeline, so what escapes it never reaches the seat, and it rescues without catching: an exit or throw out of a consumer's auth module still leaves the mount with no envelope and no event.

The status is HTTP 200: the spec fixes no status for a request's internal error and both reference SDK servers answer one in a 200, while a client that treats every non-2xx as a transport failure would discard the body and with it the id that correlates the answer to the call. Only a request reaches a method — a notification is answered with the acceptance ahead of the table, and Wymcp.Plugs.Validate refuses every other kind — so no other kind's status arises.

A method does no fallible work after its send. This plug holds the connection as it was before the call, so it cannot tell a sent answer from an unsent one — and a second send on that connection does not raise, it puts a second HTTP response on the socket. Post-send work on a method path is :telemetry.execute/3 and nothing else, which never raises.

Not a Plug.ErrorHandler on Wymcp.Router, and the reason is a mechanism rather than a preference: Plug.Router's own dispatch wraps a route body's raise in a Plug.Conn.WrapperError carrying the connection as it received it — before Wymcp.Plugs.Pipeline parsed the body or set any assign — so a handler there has no request id and no message kind to answer with. Only a try inside a plug that already holds the parsed connection has both, which is what puts the seat here.