From dbb29ca51611276d37c7aac2a4739419a56cc3bb Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Wed, 8 Oct 2025 12:51:27 +0100 Subject: [PATCH 1/4] feat: replace connect/listen flow with run lifecycle --- README.md | 10 ++--- docs/transports.md | 37 ++++++------------- examples/custom-method-handlers/server.php | 6 +-- .../http-combined-registration/server.php | 4 +- examples/http-complex-tool-schema/server.php | 4 +- .../http-discovery-userprofile/server.php | 4 +- examples/http-schema-showcase/server.php | 4 +- examples/stdio-cached-discovery/server.php | 6 +-- examples/stdio-custom-dependencies/server.php | 6 +-- .../stdio-discovery-calculator/server.php | 6 +-- examples/stdio-env-variables/server.php | 6 +-- .../stdio-explicit-registration/server.php | 6 +-- src/Server.php | 8 +++- src/Server/Transport/InMemoryTransport.php | 4 +- src/Server/Transport/StdioTransport.php | 9 ++++- src/Server/Transport/TransportInterface.php | 1 + tests/Unit/ServerTest.php | 7 ++-- 17 files changed, 59 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index f441b3ca..8d4aaa81 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ $server = Server::builder() ->build(); $transport = new StdioTransport(); -$server->connect($transport); -$transport->listen(); + +$server->run($transport); ``` ### 3. Configure Your MCP Client @@ -175,15 +175,13 @@ $server = Server::builder() **STDIO Transport** (Command-line integration): ```php $transport = new StdioTransport(); -$server->connect($transport); -$transport->listen(); +$server->run($transport); ``` **HTTP Transport** (Web-based communication): ```php $transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); -$server->connect($transport); -$response = $transport->listen(); +$response = $server->run($transport); // Handle $response in your web application ``` diff --git a/docs/transports.md b/docs/transports.md index dc0f50a2..b8fe2623 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -22,9 +22,7 @@ $server = Server::builder() $transport = new SomeTransport(); -$server->connect($transport); - -$transport->listen(); // For STDIO, or handle response for HTTP +$result = $server->run($transport); // Blocks for STDIO, returns a response for HTTP ``` ## STDIO Transport @@ -70,9 +68,9 @@ $server = Server::builder() $transport = new StdioTransport(); -$server->connect($transport); +$status = $server->run($transport); -$transport->listen(); +exit($status); // 0 on clean shutdown, non-zero if STDIN errored ``` ### Client Configuration @@ -138,24 +136,20 @@ use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7Server\ServerRequestCreator; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; -// Create PSR-7 request from globals $psr17Factory = new Psr17Factory(); $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $creator->fromGlobals(); -// Build server $server = Server::builder() ->setServerInfo('HTTP Server', '1.0.0') ->setDiscovery(__DIR__, ['.']) ->setSession(new FileSessionStore(__DIR__ . '/sessions')) // HTTP needs persistent sessions ->build(); -// Process request and get response $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); -$server->connect($transport); -$response = $transport->listen(); -// Emit response +$response = $server->run($transport); + (new SapiEmitter())->emit($response); ``` @@ -199,8 +193,7 @@ class McpController // Process with MCP $transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory); - $mcpServer->connect($transport); - $psrResponse = $transport->listen(); + $psrResponse = $mcpServer->run($transport); // Convert PSR-7 response back to Symfony return $httpFoundationFactory->createResponse($psrResponse); @@ -234,13 +227,12 @@ class McpController { $psr17Factory = new Psr17Factory(); - // Create and connect the MCP HTTP transport + // Create the MCP HTTP transport $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); - $mcpServer->connect($transport); // Process MCP request and return PSR-7 response // Laravel automatically handles PSR-7 responses - return $transport->listen(); + return $mcpServer->run($transport); } } @@ -255,7 +247,6 @@ Slim Framework works natively with PSR-7. Create a route handler using Slim's built-in factories and container: ```php -use Psr\Container\ContainerInterface; use Slim\Factory\AppFactory; use Slim\Psr7\Factory\ResponseFactory; use Slim\Psr7\Factory\StreamFactory; @@ -263,25 +254,19 @@ use Mcp\Server; use Mcp\Server\Transport\StreamableHttpTransport; $app = AppFactory::create(); -$container = $app->getContainer(); -$container->set('mcpServer', function (ContainerInterface $container) { - return Server::builder() +$app->any('/mcp', function ($request, $response) { + $mcpServer =Server::builder() ->setServerInfo('My MCP Server', '1.0.0') ->setDiscovery(__DIR__, ['.']) ->build(); -}); - -$app->any('/mcp', function ($request, $response) { - $mcpServer = $this->get('mcpServer'); $responseFactory = new ResponseFactory(); $streamFactory = new StreamFactory(); $transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); - $mcpServer->connect($transport); - return $transport->listen(); + return $mcpServer->run($transport); }); ``` diff --git a/examples/custom-method-handlers/server.php b/examples/custom-method-handlers/server.php index b1db5f8f..3c40f5c4 100644 --- a/examples/custom-method-handlers/server.php +++ b/examples/custom-method-handlers/server.php @@ -137,8 +137,8 @@ public function handle(CallToolRequest|HasMethodInterface $message, SessionInter $transport = new StdioTransport(logger: logger()); -$server->connect($transport); +$result = $server->run($transport); -$transport->listen(); +logger()->info('Server listener stopped gracefully.', ['result' => $result]); -logger()->info('Server listener stopped gracefully.'); +exit((int) $result); diff --git a/examples/http-combined-registration/server.php b/examples/http-combined-registration/server.php index 6eefcfd3..660cf3c1 100644 --- a/examples/http-combined-registration/server.php +++ b/examples/http-combined-registration/server.php @@ -42,8 +42,6 @@ $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); -$server->connect($transport); - -$response = $transport->listen(); +$response = $server->run($transport); (new SapiEmitter())->emit($response); diff --git a/examples/http-complex-tool-schema/server.php b/examples/http-complex-tool-schema/server.php index fbbe45a8..a3795a6c 100644 --- a/examples/http-complex-tool-schema/server.php +++ b/examples/http-complex-tool-schema/server.php @@ -35,8 +35,6 @@ $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); -$server->connect($transport); - -$response = $transport->listen(); +$response = $server->run($transport); (new SapiEmitter())->emit($response); diff --git a/examples/http-discovery-userprofile/server.php b/examples/http-discovery-userprofile/server.php index b1cba000..6f859c0a 100644 --- a/examples/http-discovery-userprofile/server.php +++ b/examples/http-discovery-userprofile/server.php @@ -77,8 +77,6 @@ function (): array { $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); -$server->connect($transport); - -$response = $transport->listen(); +$response = $server->run($transport); (new SapiEmitter())->emit($response); diff --git a/examples/http-schema-showcase/server.php b/examples/http-schema-showcase/server.php index 8b35b6a2..e8d6d176 100644 --- a/examples/http-schema-showcase/server.php +++ b/examples/http-schema-showcase/server.php @@ -35,8 +35,6 @@ $transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); -$server->connect($transport); - -$response = $transport->listen(); +$response = $server->run($transport); (new SapiEmitter())->emit($response); diff --git a/examples/stdio-cached-discovery/server.php b/examples/stdio-cached-discovery/server.php index dcd849ba..2b7cb0fc 100644 --- a/examples/stdio-cached-discovery/server.php +++ b/examples/stdio-cached-discovery/server.php @@ -31,8 +31,8 @@ $transport = new StdioTransport(logger: logger()); -$server->connect($transport); +$result = $server->run($transport); -$transport->listen(); +logger()->info('Server listener stopped gracefully.', ['result' => $result]); -logger()->info('Server listener stopped gracefully.'); +exit((int) $result); diff --git a/examples/stdio-custom-dependencies/server.php b/examples/stdio-custom-dependencies/server.php index 42d5b053..a5b1afda 100644 --- a/examples/stdio-custom-dependencies/server.php +++ b/examples/stdio-custom-dependencies/server.php @@ -39,8 +39,8 @@ $transport = new StdioTransport(logger: logger()); -$server->connect($transport); +$result = $server->run($transport); -$transport->listen(); +logger()->info('Server listener stopped gracefully.', ['result' => $result]); -logger()->info('Server listener stopped gracefully.'); +exit((int) $result); diff --git a/examples/stdio-discovery-calculator/server.php b/examples/stdio-discovery-calculator/server.php index ad5c1cf7..de06d1c6 100644 --- a/examples/stdio-discovery-calculator/server.php +++ b/examples/stdio-discovery-calculator/server.php @@ -28,8 +28,8 @@ $transport = new StdioTransport(logger: logger()); -$server->connect($transport); +$result = $server->run($transport); -$transport->listen(); +logger()->info('Server listener stopped gracefully.', ['result' => $result]); -logger()->info('Server listener stopped gracefully.'); +exit((int) $result); diff --git a/examples/stdio-env-variables/server.php b/examples/stdio-env-variables/server.php index 08848bba..e0e4bc20 100644 --- a/examples/stdio-env-variables/server.php +++ b/examples/stdio-env-variables/server.php @@ -57,8 +57,8 @@ $transport = new StdioTransport(logger: logger()); -$server->connect($transport); +$result = $server->run($transport); -$transport->listen(); +logger()->info('Server listener stopped gracefully.', ['result' => $result]); -logger()->info('Server listener stopped gracefully.'); +exit((int) $result); diff --git a/examples/stdio-explicit-registration/server.php b/examples/stdio-explicit-registration/server.php index f225f989..f7f620f4 100644 --- a/examples/stdio-explicit-registration/server.php +++ b/examples/stdio-explicit-registration/server.php @@ -31,8 +31,8 @@ $transport = new StdioTransport(logger: logger()); -$server->connect($transport); +$result = $server->run($transport); -$transport->listen(); +logger()->info('Server listener stopped gracefully.', ['result' => $result]); -logger()->info('Server listener stopped gracefully.'); +exit((int) $result); diff --git a/src/Server.php b/src/Server.php index ec3d6542..b39ec82d 100644 --- a/src/Server.php +++ b/src/Server.php @@ -35,7 +35,7 @@ public static function builder(): Builder return new Builder(); } - public function connect(TransportInterface $transport): void + public function run(TransportInterface $transport): mixed { $transport->initialize(); @@ -56,5 +56,11 @@ public function connect(TransportInterface $transport): void $transport->onSessionEnd(function (Uuid $sessionId) { $this->jsonRpcHandler->destroySession($sessionId); }); + + try { + return $transport->listen(); + } finally { + $transport->close(); + } } } diff --git a/src/Server/Transport/InMemoryTransport.php b/src/Server/Transport/InMemoryTransport.php index 2da8d215..3c6dd40b 100644 --- a/src/Server/Transport/InMemoryTransport.php +++ b/src/Server/Transport/InMemoryTransport.php @@ -60,9 +60,10 @@ public function listen(): mixed if (\is_callable($this->sessionDestroyListener) && null !== $this->sessionId) { \call_user_func($this->sessionDestroyListener, $this->sessionId); + $this->sessionId = null; } - return null; + return 0; } public function onSessionEnd(callable $listener): void @@ -74,6 +75,7 @@ public function close(): void { if (\is_callable($this->sessionDestroyListener) && null !== $this->sessionId) { \call_user_func($this->sessionDestroyListener, $this->sessionId); + $this->sessionId = null; } } } diff --git a/src/Server/Transport/StdioTransport.php b/src/Server/Transport/StdioTransport.php index 6b4a337a..b602c02c 100644 --- a/src/Server/Transport/StdioTransport.php +++ b/src/Server/Transport/StdioTransport.php @@ -63,9 +63,14 @@ public function listen(): mixed { $this->logger->info('StdioTransport is listening for messages on STDIN...'); + $status = 0; while (!feof($this->input)) { $line = fgets($this->input); if (false === $line) { + if (!feof($this->input)) { + $status = 1; + } + break; } @@ -82,9 +87,10 @@ public function listen(): mixed if (\is_callable($this->sessionEndListener) && null !== $this->sessionId) { \call_user_func($this->sessionEndListener, $this->sessionId); + $this->sessionId = null; } - return null; + return $status; } public function onSessionEnd(callable $listener): void @@ -96,6 +102,7 @@ public function close(): void { if (\is_callable($this->sessionEndListener) && null !== $this->sessionId) { \call_user_func($this->sessionEndListener, $this->sessionId); + $this->sessionId = null; } if (\is_resource($this->input)) { diff --git a/src/Server/Transport/TransportInterface.php b/src/Server/Transport/TransportInterface.php index a8040a0f..879ff3b4 100644 --- a/src/Server/Transport/TransportInterface.php +++ b/src/Server/Transport/TransportInterface.php @@ -63,6 +63,7 @@ public function onSessionEnd(callable $listener): void; * * This method should be called when the transport is no longer needed. * It should clean up any resources and close any connections. + * `Server::run()` calls this automatically after `listen()` exits. */ public function close(): void; } diff --git a/tests/Unit/ServerTest.php b/tests/Unit/ServerTest.php index 1abba9f6..1c58f58b 100644 --- a/tests/Unit/ServerTest.php +++ b/tests/Unit/ServerTest.php @@ -32,16 +32,15 @@ public function testJsonExceptions() $transport = $this->getMockBuilder(InMemoryTransport::class) ->setConstructorArgs([['foo', 'bar']]) - ->onlyMethods(['send']) + ->onlyMethods(['send', 'close']) ->getMock(); $transport->expects($this->exactly(2))->method('send')->willReturnOnConsecutiveCalls( null, null ); + $transport->expects($this->once())->method('close'); $server = new Server($handler); - $server->connect($transport); - - $transport->listen(); + $server->run($transport); } } From c0629f3298fa535a22da2caef1fdff71de93477b Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Wed, 8 Oct 2025 21:45:42 +0100 Subject: [PATCH 2/4] feat(transport): add phpstan generics for typed transport returns --- examples/custom-method-handlers/server.php | 2 +- examples/stdio-cached-discovery/server.php | 2 +- examples/stdio-custom-dependencies/server.php | 2 +- examples/stdio-discovery-calculator/server.php | 2 +- examples/stdio-env-variables/server.php | 2 +- examples/stdio-explicit-registration/server.php | 2 +- src/Server.php | 7 +++++++ src/Server/Transport/InMemoryTransport.php | 7 ++++++- src/Server/Transport/StdioTransport.php | 4 +++- src/Server/Transport/StreamableHttpTransport.php | 4 +++- src/Server/Transport/TransportInterface.php | 4 +++- 11 files changed, 28 insertions(+), 10 deletions(-) diff --git a/examples/custom-method-handlers/server.php b/examples/custom-method-handlers/server.php index 3c40f5c4..ef47f32d 100644 --- a/examples/custom-method-handlers/server.php +++ b/examples/custom-method-handlers/server.php @@ -141,4 +141,4 @@ public function handle(CallToolRequest|HasMethodInterface $message, SessionInter logger()->info('Server listener stopped gracefully.', ['result' => $result]); -exit((int) $result); +exit($result); diff --git a/examples/stdio-cached-discovery/server.php b/examples/stdio-cached-discovery/server.php index 2b7cb0fc..2f10de0e 100644 --- a/examples/stdio-cached-discovery/server.php +++ b/examples/stdio-cached-discovery/server.php @@ -35,4 +35,4 @@ logger()->info('Server listener stopped gracefully.', ['result' => $result]); -exit((int) $result); +exit($result); diff --git a/examples/stdio-custom-dependencies/server.php b/examples/stdio-custom-dependencies/server.php index a5b1afda..743fd78b 100644 --- a/examples/stdio-custom-dependencies/server.php +++ b/examples/stdio-custom-dependencies/server.php @@ -43,4 +43,4 @@ logger()->info('Server listener stopped gracefully.', ['result' => $result]); -exit((int) $result); +exit($result); diff --git a/examples/stdio-discovery-calculator/server.php b/examples/stdio-discovery-calculator/server.php index de06d1c6..fe223240 100644 --- a/examples/stdio-discovery-calculator/server.php +++ b/examples/stdio-discovery-calculator/server.php @@ -32,4 +32,4 @@ logger()->info('Server listener stopped gracefully.', ['result' => $result]); -exit((int) $result); +exit($result); diff --git a/examples/stdio-env-variables/server.php b/examples/stdio-env-variables/server.php index e0e4bc20..62c03501 100644 --- a/examples/stdio-env-variables/server.php +++ b/examples/stdio-env-variables/server.php @@ -61,4 +61,4 @@ logger()->info('Server listener stopped gracefully.', ['result' => $result]); -exit((int) $result); +exit($result); diff --git a/examples/stdio-explicit-registration/server.php b/examples/stdio-explicit-registration/server.php index f7f620f4..1efcba8b 100644 --- a/examples/stdio-explicit-registration/server.php +++ b/examples/stdio-explicit-registration/server.php @@ -35,4 +35,4 @@ logger()->info('Server listener stopped gracefully.', ['result' => $result]); -exit((int) $result); +exit($result); diff --git a/src/Server.php b/src/Server.php index b39ec82d..54ba8103 100644 --- a/src/Server.php +++ b/src/Server.php @@ -35,6 +35,13 @@ public static function builder(): Builder return new Builder(); } + /** + * @template TResult + * + * @param TransportInterface $transport + * + * @return TResult + */ public function run(TransportInterface $transport): mixed { $transport->initialize(); diff --git a/src/Server/Transport/InMemoryTransport.php b/src/Server/Transport/InMemoryTransport.php index 3c6dd40b..a1bd2946 100644 --- a/src/Server/Transport/InMemoryTransport.php +++ b/src/Server/Transport/InMemoryTransport.php @@ -14,6 +14,8 @@ use Symfony\Component\Uid\Uuid; /** + * @implements TransportInterface + * * @author Tobias Nyholm */ class InMemoryTransport implements TransportInterface @@ -50,6 +52,9 @@ public function send(string $data, array $context): void } } + /** + * @return null + */ public function listen(): mixed { foreach ($this->messages as $message) { @@ -63,7 +68,7 @@ public function listen(): mixed $this->sessionId = null; } - return 0; + return null; } public function onSessionEnd(callable $listener): void diff --git a/src/Server/Transport/StdioTransport.php b/src/Server/Transport/StdioTransport.php index b602c02c..be69dd04 100644 --- a/src/Server/Transport/StdioTransport.php +++ b/src/Server/Transport/StdioTransport.php @@ -16,6 +16,8 @@ use Symfony\Component\Uid\Uuid; /** + * @implements TransportInterface + * * @author Kyrian Obikwelu */ class StdioTransport implements TransportInterface @@ -59,7 +61,7 @@ public function send(string $data, array $context): void fwrite($this->output, $data.\PHP_EOL); } - public function listen(): mixed + public function listen(): int { $this->logger->info('StdioTransport is listening for messages on STDIN...'); diff --git a/src/Server/Transport/StreamableHttpTransport.php b/src/Server/Transport/StreamableHttpTransport.php index 7f5035fe..070657de 100644 --- a/src/Server/Transport/StreamableHttpTransport.php +++ b/src/Server/Transport/StreamableHttpTransport.php @@ -21,6 +21,8 @@ use Symfony\Component\Uid\Uuid; /** + * @implements TransportInterface + * * @author Kyrian Obikwelu */ class StreamableHttpTransport implements TransportInterface @@ -78,7 +80,7 @@ public function send(string $data, array $context): void ]); } - public function listen(): mixed + public function listen(): ResponseInterface { return match ($this->request->getMethod()) { 'OPTIONS' => $this->handleOptionsRequest(), diff --git a/src/Server/Transport/TransportInterface.php b/src/Server/Transport/TransportInterface.php index 879ff3b4..c910154f 100644 --- a/src/Server/Transport/TransportInterface.php +++ b/src/Server/Transport/TransportInterface.php @@ -14,6 +14,8 @@ use Symfony\Component\Uid\Uuid; /** + * @template TResult + * * @author Christopher Hertel * @author Kyrian Obikwelu */ @@ -38,7 +40,7 @@ public function onMessage(callable $listener): void; * - For a single-request transport like HTTP, this will process the request * and return a result (e.g., a PSR-7 Response) to be sent to the client. * - * @return mixed the result of the transport's execution, if any + * @return TResult the result of the transport's execution, if any */ public function listen(): mixed; From c792cb5abe9ee99ebbd8b76c1409b04e84a53cfd Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Fri, 10 Oct 2025 18:38:04 +0100 Subject: [PATCH 3/4] refactor: standardize variable naming for MCP server instances in transport handling --- docs/transports.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/transports.md b/docs/transports.md index b8fe2623..75902c93 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -181,7 +181,7 @@ use Mcp\Server\Transport\StreamableHttpTransport; class McpController { #[Route('/mcp', name: 'mcp_endpoint'] - public function handle(Request $request, Server $mcpServer): Response + public function handle(Request $request, Server $server): Response { // Create PSR-7 factories $psr17Factory = new Psr17Factory(); @@ -193,7 +193,7 @@ class McpController // Process with MCP $transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory); - $psrResponse = $mcpServer->run($transport); + $psrResponse = $server->run($transport); // Convert PSR-7 response back to Symfony return $httpFoundationFactory->createResponse($psrResponse); @@ -223,7 +223,7 @@ use Nyholm\Psr7\Factory\Psr17Factory; class McpController { - public function handle(ServerRequestInterface $request, Server $mcpServer): ResponseInterface + public function handle(ServerRequestInterface $request, Server $server): ResponseInterface { $psr17Factory = new Psr17Factory(); @@ -232,7 +232,7 @@ class McpController // Process MCP request and return PSR-7 response // Laravel automatically handles PSR-7 responses - return $mcpServer->run($transport); + return $server->run($transport); } } @@ -256,7 +256,7 @@ use Mcp\Server\Transport\StreamableHttpTransport; $app = AppFactory::create(); $app->any('/mcp', function ($request, $response) { - $mcpServer =Server::builder() + $server = Server::builder() ->setServerInfo('My MCP Server', '1.0.0') ->setDiscovery(__DIR__, ['.']) ->build(); @@ -266,7 +266,7 @@ $app->any('/mcp', function ($request, $response) { $transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory); - return $mcpServer->run($transport); + return $server->run($transport); }); ``` From d7f50a1414fa8c6c66935e6017d7fa1bc12cc14a Mon Sep 17 00:00:00 2001 From: Kyrian Obikwelu Date: Fri, 10 Oct 2025 18:47:51 +0100 Subject: [PATCH 4/4] test: remove unnecessary phpstan-ignore annotation in DocBlockTestFixture --- tests/Unit/Capability/Discovery/DocBlockTestFixture.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Capability/Discovery/DocBlockTestFixture.php b/tests/Unit/Capability/Discovery/DocBlockTestFixture.php index 0f015ce4..a218ad63 100644 --- a/tests/Unit/Capability/Discovery/DocBlockTestFixture.php +++ b/tests/Unit/Capability/Discovery/DocBlockTestFixture.php @@ -75,7 +75,7 @@ public function methodWithReturn(): string * @deprecated use newMethod() instead * @see DocBlockTestFixture::newMethod() */ - public function methodWithMultipleTags(float $value): bool /* @phpstan-ignore throws.unusedType */ + public function methodWithMultipleTags(float $value): bool { return true; }