diff --git a/README.md b/README.md index f441b3c..8d4aaa8 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 dc0f50a..75902c9 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 b1db5f8..ef47f32 100644 --- a/examples/custom-method-handlers/server.php +++ b/examples/custom-method-handlers/server.php @@ -137,8 +137,8 @@ $server = Server::builder() $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 6eefcfd..660cf3c 100644 --- a/examples/http-combined-registration/server.php +++ b/examples/http-combined-registration/server.php @@ -42,8 +42,6 @@ $server = Server::builder() $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 fbbe45a..a3795a6 100644 --- a/examples/http-complex-tool-schema/server.php +++ b/examples/http-complex-tool-schema/server.php @@ -35,8 +35,6 @@ $server = Server::builder() $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 b1cba00..6f859c0 100644 --- a/examples/http-discovery-userprofile/server.php +++ b/examples/http-discovery-userprofile/server.php @@ -77,8 +77,6 @@ $server = Server::builder() $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 8b35b6a..e8d6d17 100644 --- a/examples/http-schema-showcase/server.php +++ b/examples/http-schema-showcase/server.php @@ -35,8 +35,6 @@ $server = Server::builder() $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 dcd849b..2f10de0 100644 --- a/examples/stdio-cached-discovery/server.php +++ b/examples/stdio-cached-discovery/server.php @@ -31,8 +31,8 @@ $server = Server::builder() $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 42d5b05..743fd78 100644 --- a/examples/stdio-custom-dependencies/server.php +++ b/examples/stdio-custom-dependencies/server.php @@ -39,8 +39,8 @@ $server = Server::builder() $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 ad5c1cf..fe22324 100644 --- a/examples/stdio-discovery-calculator/server.php +++ b/examples/stdio-discovery-calculator/server.php @@ -28,8 +28,8 @@ $server = Server::builder() $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 08848bb..62c0350 100644 --- a/examples/stdio-env-variables/server.php +++ b/examples/stdio-env-variables/server.php @@ -57,8 +57,8 @@ $server = Server::builder() $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 f225f98..1efcba8 100644 --- a/examples/stdio-explicit-registration/server.php +++ b/examples/stdio-explicit-registration/server.php @@ -31,8 +31,8 @@ $server = Server::builder() $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 ec3d654..54ba810 100644 --- a/src/Server.php +++ b/src/Server.php @@ -35,7 +35,14 @@ final class Server 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 @@ final class Server $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 2da8d21..a1bd294 100644 --- a/src/Server/Transport/InMemoryTransport.php +++ b/src/Server/Transport/InMemoryTransport.php @@ -14,6 +14,8 @@ namespace Mcp\Server\Transport; use Symfony\Component\Uid\Uuid; /** + * @implements TransportInterface + * * @author Tobias Nyholm */ class InMemoryTransport implements TransportInterface @@ -50,6 +52,9 @@ class InMemoryTransport implements TransportInterface } } + /** + * @return null + */ public function listen(): mixed { foreach ($this->messages as $message) { @@ -60,6 +65,7 @@ class InMemoryTransport implements TransportInterface if (\is_callable($this->sessionDestroyListener) && null !== $this->sessionId) { \call_user_func($this->sessionDestroyListener, $this->sessionId); + $this->sessionId = null; } return null; @@ -74,6 +80,7 @@ class InMemoryTransport implements TransportInterface { 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 6b4a337..be69dd0 100644 --- a/src/Server/Transport/StdioTransport.php +++ b/src/Server/Transport/StdioTransport.php @@ -16,6 +16,8 @@ use Psr\Log\NullLogger; use Symfony\Component\Uid\Uuid; /** + * @implements TransportInterface + * * @author Kyrian Obikwelu */ class StdioTransport implements TransportInterface @@ -59,13 +61,18 @@ class StdioTransport implements TransportInterface 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 @@ class StdioTransport implements TransportInterface 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 @@ class StdioTransport implements TransportInterface { 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 7f5035f..070657d 100644 --- a/src/Server/Transport/StreamableHttpTransport.php +++ b/src/Server/Transport/StreamableHttpTransport.php @@ -21,6 +21,8 @@ use Psr\Log\NullLogger; use Symfony\Component\Uid\Uuid; /** + * @implements TransportInterface + * * @author Kyrian Obikwelu */ class StreamableHttpTransport implements TransportInterface @@ -78,7 +80,7 @@ class StreamableHttpTransport implements TransportInterface ]); } - 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 a8040a0..c910154 100644 --- a/src/Server/Transport/TransportInterface.php +++ b/src/Server/Transport/TransportInterface.php @@ -14,6 +14,8 @@ namespace Mcp\Server\Transport; use Symfony\Component\Uid\Uuid; /** + * @template TResult + * * @author Christopher Hertel * @author Kyrian Obikwelu */ @@ -38,7 +40,7 @@ interface TransportInterface * - 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 @@ interface TransportInterface * * 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 0f015ce..a218ad6 100644 --- a/tests/Unit/Capability/Discovery/DocBlockTestFixture.php +++ b/tests/Unit/Capability/Discovery/DocBlockTestFixture.php @@ -75,7 +75,7 @@ class DocBlockTestFixture * @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 1abba9f..1c58f58 100644 --- a/tests/Unit/ServerTest.php +++ b/tests/Unit/ServerTest.php @@ -32,16 +32,15 @@ class ServerTest extends TestCase $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); } }