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..75902c93 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); ``` @@ -187,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(); @@ -199,8 +193,7 @@ class McpController // Process with MCP $transport = new StreamableHttpTransport($psrRequest, $psr17Factory, $psr17Factory); - $mcpServer->connect($transport); - $psrResponse = $transport->listen(); + $psrResponse = $server->run($transport); // Convert PSR-7 response back to Symfony return $httpFoundationFactory->createResponse($psrResponse); @@ -230,17 +223,16 @@ 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(); - // 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 $server->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) { + $server = 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 $server->run($transport); }); ``` diff --git a/examples/custom-method-handlers/server.php b/examples/custom-method-handlers/server.php index b1db5f8f..ef47f32d 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($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..2f10de0e 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($result); diff --git a/examples/stdio-custom-dependencies/server.php b/examples/stdio-custom-dependencies/server.php index 42d5b053..743fd78b 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($result); diff --git a/examples/stdio-discovery-calculator/server.php b/examples/stdio-discovery-calculator/server.php index ad5c1cf7..fe223240 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($result); diff --git a/examples/stdio-env-variables/server.php b/examples/stdio-env-variables/server.php index 08848bba..62c03501 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($result); diff --git a/examples/stdio-explicit-registration/server.php b/examples/stdio-explicit-registration/server.php index f225f989..1efcba8b 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($result); diff --git a/src/Server.php b/src/Server.php index ec3d6542..54ba8103 100644 --- a/src/Server.php +++ b/src/Server.php @@ -35,7 +35,14 @@ public static function builder(): Builder return new Builder(); } - public function connect(TransportInterface $transport): void + /** + * @template TResult + * + * @param TransportInterface $transport + * + * @return TResult + */ + public function run(TransportInterface $transport): mixed { $transport->initialize(); @@ -56,5 +63,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..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) { @@ -60,6 +65,7 @@ public function listen(): mixed if (\is_callable($this->sessionDestroyListener) && null !== $this->sessionId) { \call_user_func($this->sessionDestroyListener, $this->sessionId); + $this->sessionId = null; } return null; @@ -74,6 +80,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..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,13 +61,18 @@ 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...'); + $status = 0; while (!feof($this->input)) { $line = fgets($this->input); if (false === $line) { + if (!feof($this->input)) { + $status = 1; + } + break; } @@ -82,9 +89,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 +104,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/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 a8040a0f..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; @@ -63,6 +65,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/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; } 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); } }