Skip to content

Conversation

@CodeWithKyrian
Copy link
Contributor

@CodeWithKyrian CodeWithKyrian commented Oct 15, 2025

This PR introduces full bidirectional communication between MCP servers and clients, letting tools surface outbound requests, logging, and progress updates while awaiting client responses (for server requests).

Motivation and Context

  • Needed a way for tool/prompt/resource handlers to invoke client-side functionality (logging, progress, sampling) while executing without blocking the transport, and continuing their execution effectively.
  • Required implementation to work for all transport shapes (single-process STDIO, multi-process PHP-FPM style HTTP, and async event-loop transports) so transports always control the blocking portions of the flow; the server/protocol now relies on transports to manage cooperative multitasking regardless of architecture.
  • Wanted a developer-friendly façade to make emitting notifications and requests from tool/prompt/resource handlers straightforward.

What’s Changed

  • Wrapped request handlers in Fibers and extended Protocol with callbacks so transports can track pending requests, resume suspended handlers (immediate for notifications and with client responses for pending requests), and flush queued messages for a session.
  • Added session-scoped queues for outgoing messages, pending requests, and stored responses; transports now consume these via new protocol-provided callbacks.
  • Introduced ClientGateway with a straightforward API to send server-initiated requests and notifications. with helpers for logging, progress and sampling. ReferenceHandler auto-injects it while SchemaGenerator hides it from generated tool schemas so type-hinted parameters stay internal.
  • Factored shared transport infrastructure into BaseTransport and ManagesTransportCallbacks, reducing boilerplate for message/session/fiber wiring in Transports.
  • Updated StdioTransport to poll input without blocking using stream_set_blocking($stdin, false), enabling it to process suspended fibers, flush queued messages, and then return to polling input without stalling.
  • Enhanced StreamableHttpTransport with SSE support via a new CallbackStream, allowing suspended handlers to stream notifications and await client responses over the same request.
  • Strengthened typing across handlers (RequestHandlerInterface, generic Response) and augmented Request with meta/id accessors and setters so server-originated requests can be cloned safely.
  • Added example servers (custom method handlers, stdio/http client communication) demonstrating the new workflow.
  • Refreshed unit tests to cover session-bound argument injection and queued responses.

Additional Notes

  • ClientGateway is adapted from [Server] Introduce ClientAwareInterface and Trait #101 so I could actually test the bidirectional flow; its interface and injection strategy is not set in stone, and can still evolve there if this is merged. This patch just wires it in via ReferenceHandler purely to validate the transport/protocol changes.
  • I built a ReactPHP HTTP transport proof-of-concept (https://github.com/CodeWithKyrian/mcp-reactphp-transport) to validate the Transport-agnostic behavior and confirm the protocol’s fiber/callback approach works in async event-loop environments with no issues.
  • Handlers can suspend and resume their Fiber repeatedly. This means they can send multiple log entries, progress updates, or sampling requests, and execution pauses and resumes accurately each time (waiting for responses before resuming if its a request or resuming immediately if its a notification), as demonstrated in the new examples.
  • Server-initiated requests and notifications that occur outside the normal client request cycle (for example, list-changed notifications) are not yet supported in StreamableHttpTransport. This functionality depends on the GET streaming endpoint which isn't implemented yet.

Breaking Changes

None. Existing transports and handlers continue to work; new behavior is opt-in through the injected gateway.

Comment on lines +62 to +68
$meta = $data['params']['_meta'];
if ($meta instanceof \stdClass) {
$meta = (array) $meta;
}
if (\is_array($meta)) {
$request->meta = $meta;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if that's something we should be able to control instead of having it ambigous?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm 🤔. What do you mean by control in this case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it an array or an object? isn't that something we can handle more strict?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the spec, there’s no strict structure defined for request metadata. The progress token is the only documented key so far, but servers and clients are allowed to attach arbitrary metadata to their requests.

@chr-hertel chr-hertel added the Server Issues & PRs related to the Server component label Oct 20, 2025
@CodeWithKyrian CodeWithKyrian force-pushed the feature/bidirectional-client-communication branch from 69000ec to 5db83d4 Compare October 26, 2025 21:32
Copy link
Member

@chr-hertel chr-hertel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could pull out some changes here into separate PRs to get the diff smaller, or debate on the size of the TransportInterface - but that's also fine as a follow up to get this in and get going with the next features on top of it.

Thanks for tackling that mind-twisting exercise @CodeWithKyrian!

@CodeWithKyrian CodeWithKyrian merged commit f1b471a into modelcontextprotocol:main Oct 26, 2025
10 checks passed
@CodeWithKyrian CodeWithKyrian deleted the feature/bidirectional-client-communication branch October 26, 2025 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants