[Server] Add outgoing (elicitation, sampling) request and client response events - #386
[Server] Add outgoing (elicitation, sampling) request and client response events#386omouren wants to merge 10 commits into
Conversation
Introduce `OutgoingRequestEvent` and `ClientResponseEvent` to provide full observability over the server's interaction with the client. This includes requests sent by the server (e.g., elicitation, sampling) and the client's replies to those requests. Additionally, update `ResponseEvent` and `ErrorEvent` to be dispatched when a suspended Fiber completes. This ensures that deferred responses from long-running, asynchronous operations are also observable via the existing event mechanisms.
chr-hertel
left a comment
There was a problem hiding this comment.
Thanks @omouren - looks like a valuable addition to me, left some comments and didn't test yet.
Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
…age factory The `OutgoingRequestEvent` is renamed to `ServerRequestEvent` to improve clarity, as it is dispatched when the server sends a request to the client. Additionally, the `MessageFactory` has been refactored. The `createMessage` and `createFromArray` methods are consolidated into a single public `createFromArray`, simplifying message creation and error handling logic.
|
Thanks for the review @chr-hertel 👍 |
|
Hey @omouren this needs a rebase - and more importantly, please double-check the handling in context of spec version 2026-07-28 with StatelessProtocol |
dbf4c37 to
a626b04
Compare
|
Hello @chr-hertel I haven’t looked at spec revision 2026-07-28 / StatelessProtocol yet, and I’m not entirely sure how to test the new spec for now. I retested with the previous spec and the events still work as expected there. |
|
Hello @chr-hertel Follow-up on 2026-07-28 / StatelessProtocol. I did not map elicitation onto ServerRequestEvent / ClientResponseEvent as there is no server-initiated JSON-RPC on that revision, so those events would lie about the wire. They stay for the handshake-era. I updated the StatelessProtocol to dispatch the same lifecycle events as the handshake path:
|
Introduces `RequestEvent`, `ResponseEvent`, and `ErrorEvent` for the stateless 2026-07-28 protocol revision. This provides extensibility points to observe and modify server operations during request processing. `RequestEvent` now includes `InputContext` for multi-round trip retries, and `ResponseEvent` dispatches `InputRequiredResult` for elicitation on the modern era. `ErrorEvent` is dispatched for all handler-related exceptions. Documentation is updated to clarify event behavior across protocol eras.
a0e32c9 to
7aa5c7c
Compare
| } | ||
| } | ||
|
|
||
| $session->save(); |
There was a problem hiding this comment.
should be moved into the if (null !== $parentRequest), no?
or maybe just invert that to keep the if small
if (null === $parentRequest) {
return $finalResult;
}There was a problem hiding this comment.
@chr-hertel
save() has to stay even when $parentRequest is null. pull() already forgets SESSION_FIBER_PARENT_REQUEST. Without a save that deletion never hits the session store, so the next createWithId() would see the parent request again.
…ling Introduce `ServerRequestEvent` and `ClientResponseEvent` to provide observability for handshake-era server-to-client requests like `elicitation/create` and `sampling/createMessage`, and their corresponding client replies. Dispatch `ResponseEvent` and `ErrorEvent` when a suspended Fiber completes, ensuring a consistent event lifecycle for multi-round trip operations. This also applies to modern protocol elicitation. Update `ClientResponseEvent::getId()` to correctly return `null` for error responses that lack an ID. Enhance fiber termination to always persist session state, preventing data loss even when a parent request is absent. This includes a BC break requiring custom `TransportInterface` implementations to include `setFiberTerminationHandler()`.
|
Thanks for the review @chr-hertel, all done, except for the |
Introduce
ServerRequestEventandClientResponseEventto provide full observability over the server's interaction with the client on handshake-era connections. This includes requests sent by the server (e.g.elicitation/create,sampling/createMessage) and the client's replies to those requests.Additionally, update
ResponseEventandErrorEventto be dispatched when a suspended Fiber completes. This ensures that deferred responses from long-running, asynchronous operations are also observable via the existing event mechanisms.On the modern protocol revision (
2026-07-28), there is no server-initiated JSON-RPC over HTTP, soServerRequestEvent/ClientResponseEventare not dispatched there, they would misrepresent the wire. Instead,StatelessProtocoldispatches the same lifecycle events as the handshake path:RequestEvent: including multi-round-trip retries, when the client sendsinputResponses, they are already lifted onto the session asInputContextResponseEvent: an elicitation ask is a successful result (InputRequiredResult,resultType: input_required)ErrorEvent: handler-related failuresDocumentation is updated in
docs/advanced/events.md.Motivation and Context
We wanted to be able to log every step of our MCP usage, including elicitation, across both protocol eras.
How Has This Been Tested?
ProtocolTest,StatelessProtocolTest, andMessageFactoryTestBreaking Changes
TransportInterfacegainssetFiberTerminationHandler(callable $handler): void. Custom transport implementations must implement it, SDK transports (StdioTransport,StreamableHttpTransport) already do viaManagesTransportCallbacks.Types of changes
Checklist