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.