Skip to content

Conversation

CodeWithKyrian
Copy link
Contributor

Hey team,

I've been working on the Streamable HTTP transport and wanted to share a proof-of-concept to get everyone's thoughts. While building it, I realized that to properly support different transport lifecycles (like blocking STDIO vs. single-request HTTP), we might need a more flexible architecture.

This PR introduces that new architecture along with a foundational implementation of the StreamableHttpTransport.

The Core Idea: Decoupling the Server from the Transport Lifecycle

The main change here is a shift in responsibility. The previous Server::connect() method owned the message processing loop, which worked well for a simple, generator-based transport.

However, this model creates a challenge for different transport types:

  • STDIO needs a long-running, blocking loop.
  • A standard PHP HTTP server (FPM) handles a single, non-blocking request and then terminates.
  • An async PHP server (ReactPHP, Swoole) would need to start its own event loop.

Forcing the Server class to manage these different lifecycles feels complex. This PR proposes a cleaner pattern:

The Server is only responsible for processing messages. The Transport is responsible for its own execution lifecycle.

Now, the flow is:

  1. $server->connect($transport) simply registers the server's message handler with the transport.
  2. $transport->listen() is called to start the transport's specific lifecycle.

This decouples the two components, making the whole system much more flexible and easier to extend.

What Changed?

To achieve this, I've made a few key changes:

  1. TransportInterface Rework:

    • Removed receive() and isConnected().
    • Added setMessageHandler(callable $handler) for the server to register its callback.
    • Added a new listen() method, which serves as the main entry point for any transport's execution.
  2. Server Refactor:

    • The connect() method is now very lightweight. It just wires up the message handler.
    • The internal processing loop is gone.
  3. StdioTransport Update:

    • Updated to implement the new interface.
    • Its listen() method now contains the blocking while loop that continuously reads from STDIN.
  4. New StreamableHttpTransport:

    • This is a foundational, stateless implementation designed for a typical request/response environment.
    • It's constructed with a PSR-7 ServerRequest and PSR factories.
    • Its listen() method is non-blocking...it basically processes the single request and immediately returns a PSR-7 Response.

I've included a basic example (see 10-simple-http-transport) showing how to wire this up in a vanilla PHP setup using nyholm/psr7 showing the clean separation between connecting the server and listening with the transport.

Points for Discussion

I intentionally kept the StreamableHttpTransport very minimal to start a conversation on the next steps.

1. Naming the Execution Method
I've introduced a new method on the TransportInterface called listen(). This name felt descriptive for a transport, but I'm open to other suggestions if the team thinks another name like run() or start() would be clearer. What are your thoughts?

2. Event Emitter vs. Callback?
I've used a simple setMessageHandler callback to connect the server and transport. An alternative could be to use an event emitter pattern (e.g., with evenement/evenement), where the transport emits a message event. (which I was using in the PHP MCP project). I find the event pattern a bit cleaner, as the transport doesn't need to know if a handler is attached. Would there be any appetite for bringing in a lightweight event emitter for this, or is the direct callback approach preferred for its simplicity and lack of dependencies?

3. Next Steps for StreamableHttpTransport
This implementation is deliberately bare-bones. It doesn't handle:

  • Session Management: I left this out so we can discuss the best way to manage state. Should the transport accept a PSR Cache instance? Or should we create a separate SessionManager?
  • Routing: Currently, it assumes it's being hit at the root. We need to decide how users should configure the MCP endpoint path (e.g., /mcp).

My goal with this PR is to get a "vibe check" on this architectural direction before diving deeper into features like session management. I believe this new structure gives us a solid and highly flexible foundation for supporting all kinds of PHP environments. What do you think?

@CodeWithKyrian
Copy link
Contributor Author

Following up on this, I introduced a formal session management system.

Why a Dedicated Session System?

The MCP spec details a stateful lifecycle that goes beyond a simple request/response. To properly support it, the server needs to track client state across multiple interactions (regardless of transport). A dedicated session system is crucial for handling:

  1. The Initialization Handshake: We need to track whether a client has completed the initialize flow. Subsequent requests from an uninitialized client (in a stateful context) should be rejected.
  2. Storing Negotiated State: After initialization, the server needs to remember the client's capabilities, the negotiated protocol version, and other contextual information for the duration of the session.
  3. Future Stateful Features: This foundation is essential for features like resources/subscribe, logging levels per-client, and any other tool or resource that requires remembering previous interactions.

What's New?

I've introduced a set of flexible, decoupled components:

  • SessionInterface & Session: A standard contract for a session object, with a default implementation that provides simple key-value storage (get, set, has, etc.).
  • SessionStoreInterface: An abstraction for persistence and an InMemorySessionStore as a default for simple use cases and testing. We can easily add a CacheSessionStore (using psr/simple-cache) or others later.
  • SessionFactoryInterface & SessionFactory: This decouples the creation of session objects from the server, making the system more extensible and easier to test. It defaults to creating sessions with a standard UUIDv4.
  • ServerBuilder Integration: The builder now has a withSession(...) method to allow users to provide their own custom session factory and store...or just configure the TTL.

Next Steps & Discussion

The next logical step is to integrate this session logic into the JsonRpc\Handler, Server and the transports. This will involve:

  • Enforcing the Handshake: The Handler will use the session to verify that a client is initialized before processing other requests.
  • Handling the Mcp-Session-Id Header: The StreamableHttpTransport will be responsible for reading the header and passing the ID to the Server, which will then use the session system to load the corresponding state.
  • Handling the StdioTransport Session: For the StdioTransport, since it's a single persistent process for one client, a single session will be created when the connection starts and assigned to the transport. The initialize handshake is still required per the spec, and the session will hold that state for the lifetime of the process, then dispose it afterwards.

IN the future, we could implement a stateless mode ("stateless session" sounds like an oxymoron 😄). It refers to creating a temporary, per-request session object that isn't persisted (or deleted at the end of the request) which can allow us to bypass the initialize requirement for simpler use cases(the Typescript and Python SDKs have something similar to this too even though its not part of the official spec)

I wanted to introduce these classes as a discrete step to get feedback on the session architecture itself before weaving it into the request handling logic. (Or should it be an entire different PR? INcluded it here since it's integral to getting the Http Transport up and running)

Copy link
Member

Choose a reason for hiding this comment

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

I'm more in favor of what you had before for the examples in php-mcp - one main composer setup and shared dependencies. makes it easier to follow the example code I'd say.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair. I went this route because this HTTP example needs specific PSR-7 implementations (Nyholm PSR-7, PSR-7 Server, and Laminas HTTP Handler Runner) to demonstrate the HTTP transport properly.

Adding these as dev dependencies to the main composer.json felt like a stretch since they're only relevant for this one HTTP example (unlike the STDIO examples which work with the base dependencies, or the former HTTP examples where the server was inbuilt)

Copy link
Member

Choose a reason for hiding this comment

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

why do we need this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the example has its own composer.json and installs dependencies locally so we wouldn't want to include that (unless we agree to move the dependencies to the base)

Copy link
Member

Choose a reason for hiding this comment

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

Okay, that's clear - sorry, should have been more explicit.
If the vendor path is really needed, let's add it in the root level one - but I'd be fine with adding the deps to root dev dependencies - we'll need them in other examples as well potentially

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool. Got it!

Run with the MCP Inspector for testing:

```bash
npx @modelcontextprotocol/inspector http://localhost:8000
Copy link
Member

Choose a reason for hiding this comment

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

currently doesn't work for me - am i missing something? it was running for you already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Works for me. Make sure you’ve pulled the latest commit and run composer update (I added a new dependency: psr/clock). Also, check that the inspector is set to Streamable HTTP instead of STDIO cos it sometimes defaults to STDIO.

Copy link
Member

Choose a reason for hiding this comment

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

Yea, did all of that ... will try again later

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting 🤔. Waiting for feedback then

@chr-hertel
Copy link
Member

Thanks @CodeWithKyrian - great to see this tackled! :)

Just dropping my thoughts here, while wrapping my head around it:
yes, session def needs to happen - we need this. not sure if it's even a higher level, instead of in server namespace - but it's a good start to have it here. it's anyhow a moving target here, since there are multiple discussion currently ongoing, see SEP-1359 and SEP-1364 - both open drafts.

other than that, i'd love to have the transport extension point that abstract in the long run, that we could have various kind of infrastructure adapters. http-foundation, reactphp, swool, mercure ... donno - not sure if realistic, but you pointed out the challenges on that layer already 👍

@chr-hertel
Copy link
Member

@Nyholm was just thinking if there is some synergy or conceptual similarity with that php-runtime idea ... just for MCP transports tho

@CodeWithKyrian
Copy link
Contributor Author

Thanks for the feedback

  1. On Session & the SEPs
    I read through one of them before working on this, and the approach I went with was intentionally transport-agnostic. The core Session and SessionStoreInterface are generalized — the only transport-specific bit is how the session ID gets sourced (from a header in HTTP vs generated once in STDIO). From what I can tell, this lines up with the direction those SEPs are heading so if/when they’re finalized, we should be able to adapt pretty easily (I think 🙂)

  2. On the Transport Extension Point
    Yep. 100%...that’s the main reason I refactored. I had something similar in the PHP MCP package, though the default there was opinionated, using a ReactPHP server. The idea was always that it could swap out for other (non-async) transports, like what’s shown in this Laravel adapter transport. So here, I'm going with a more generalized PSR-7–based transport as default since it makes sense for most PHP setups. Then, we could either ship officially maintained transports for Laravel, Swoole, ReactPHP, etc., or provide clear guides for people to roll their own.

Glad to see we’re aligned on this 👍🏽

@CodeWithKyrian CodeWithKyrian force-pushed the feat/rework-transport-architecture branch from 40ebb80 to 260f59e Compare September 14, 2025 21:32
…nsport

- `Server::connect()` no longer contains a processing loop.
- `TransportInterface` is updated with `setMessageHandler()` and `listen()`.
- `StdioTransport` is updated to implement the new interface.
- A new, minimal `StreamableHttpTransport` is added for stateless HTTP.
- Updated `TransportInterface` to use `onMessage` for handling incoming messages with session IDs.
- Refactored `Server`, `Handler`, and transport classes to accommodate session management using `Uuid`.
- Introduced methods for creating sessions with auto-generated and specific UUIDs in `SessionFactory` and `SessionFactoryInterface`.
- Added session support to the `Server` and `Handler` classes, allowing for session data to be managed during message processing.
- Updated `TransportInterface` to include session context in the `send` method.
- Refactored various request handlers to utilize session information, ensuring proper session handling for incoming requests.
- Introduced a file-based session store for persistent session data management
@CodeWithKyrian CodeWithKyrian force-pushed the feat/rework-transport-architecture branch from 9946beb to 25bec57 Compare September 20, 2025 13:40
@CodeWithKyrian
Copy link
Contributor Author

Quick updates on this:

  • Session management is now fully handled by the Handler, transports remain session-agnostic, while Server acts as a lightweight middleman: listens to transport → relays to handler → sends response through transport(with context).
  • Added FileSessionStore for persistence (the HTTP transport won't work so well with the InMemorySessionStore)
  • All method handlers now accept SessionInterface parameter for state management
  • Partial initialize request validation as per spec(no session ID with initialize)
  • Session persistence managed automatically after successful request processing

This PR touches quite a few files, so the sooner we can wrap up reviews and merge, the smoother things will be for everyone as we move forward.

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.

Yes agree, let's get this merged to move on 👍

Thanks @CodeWithKyrian - the pipeline still needs attention tho

@CodeWithKyrian
Copy link
Contributor Author

On it. Quick one: It's safe to nuke the Transport/Sse and its contents now right?

@chr-hertel
Copy link
Member

On it. Quick one: It's safe to nuke the Transport/Sse and its contents now right?

I'd say yes - it's deprecated and we have the new one in place than

@CodeWithKyrian CodeWithKyrian force-pushed the feat/rework-transport-architecture branch from b2a067f to ec99e1f Compare September 21, 2025 09:45
@CodeWithKyrian CodeWithKyrian force-pushed the feat/rework-transport-architecture branch from ec99e1f to 5605f07 Compare September 21, 2025 09:46
@CodeWithKyrian
Copy link
Contributor Author

Alright. All set @chr-hertel

@chr-hertel chr-hertel merged commit 719262b into modelcontextprotocol:main Sep 21, 2025
9 checks passed
@CodeWithKyrian CodeWithKyrian deleted the feat/rework-transport-architecture branch September 21, 2025 10:35
@chr-hertel chr-hertel changed the title feat(server): Rework transport architecture and add StreamableHttpTransport [Server] Rework transport architecture and add StreamableHttpTransport Sep 21, 2025
@chr-hertel chr-hertel added the Server Issues & PRs related to the Server component label Sep 21, 2025
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