diff --git a/CHANGELOG.md b/CHANGELOG.md index c484c44d..29c89d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,3 @@ -CHANGELOG -========= +# Changelog -0.1 ---- - - * Add Model Context Protocol (MCP) implementation for LLM-application communication - * Add JSON-RPC based protocol handling with `JsonRpcHandler` - * Add three core MCP capabilities: - - Resources: File-like data readable by clients (API responses, file contents) - - Tools: Functions callable by LLMs (with user approval) - - Prompts: Pre-written templates for specific tasks - * Add multiple transport implementations: - - Symfony Console Transport for testing and CLI applications - - Stream Transport supporting Server-Sent Events (SSE) and HTTP streaming - - STDIO transport for command-line interfaces - * Add capability chains for organizing features: - - `ToolChain` for tool management - - `ResourceChain` for resource management - - `PromptChain` for prompt template management - * Add Server component managing transport connections - * Add request/notification handlers for MCP operations - * Add standardized interface enabling LLMs to interact with external systems - * Add support for building LLM "plugins" with extra context capabilities \ No newline at end of file +All notable changes to `mcp/sdk` will be documented in this file. \ No newline at end of file diff --git a/examples/09-standalone-cli/README.md b/examples/09-standalone-cli/README.md deleted file mode 100644 index b64295b2..00000000 --- a/examples/09-standalone-cli/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Standalone example app with CLI - -This is just for testing and debugging purposes. Different from the other examples, this one does not use the same -autoloader, but installs the SDK via path repository and therefore has mostly decoupled dependencies. - -Install dependencies: - -```bash -cd /path/to/your/project/examples/09-standalone-cli -composer update -``` - -Run the CLI with: - -```bash -DEBUG=1 php index.php -``` - -You will see debug outputs to help you understand what is happening. - -In this terminal you can now test by adding some JSON strings. See `example-requests.json`. - -Run with Inspector: - -```bash -npx @modelcontextprotocol/inspector php index.php -``` diff --git a/examples/09-standalone-cli/composer.json b/examples/09-standalone-cli/composer.json deleted file mode 100644 index 0a99e8b0..00000000 --- a/examples/09-standalone-cli/composer.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "mcp/cli-server-example", - "description": "An example application for CLI", - "license": "MIT", - "type": "project", - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - } - ], - "require": { - "php": ">=8.1", - "mcp/sdk": "@dev", - "symfony/console": "^7.2" - }, - "minimum-stability": "stable", - "autoload": { - "psr-4": { - "App\\": "src/" - } - }, - "repositories": [ - { "type": "path", "url": "../../" } - ] -} - diff --git a/examples/09-standalone-cli/example-requests.json b/examples/09-standalone-cli/example-requests.json deleted file mode 100644 index eaff960b..00000000 --- a/examples/09-standalone-cli/example-requests.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - {"jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": []}, - {"jsonrpc": "2.0", "id": 2, "method": "resources/read", "params": {"uri": "file:///project/src/main.rs"}}, - - {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}, - {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Current time"}}, - {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Current time","arguments": {"format": "Y-m-d"}}}, - - {"jsonrpc": "2.0", "id": 1, "method": "prompts/list"}, - {"jsonrpc": "2.0", "id": 2 ,"method": "prompts/get", "params": {"name": "Greet"}}, - {"jsonrpc": "2.0", "id": 2 ,"method": "prompts/get", "params": {"name": "Greet", "arguments": { "firstName": "Tobias" }}} -] diff --git a/examples/09-standalone-cli/index.php b/examples/09-standalone-cli/index.php deleted file mode 100644 index 9ef61b7e..00000000 --- a/examples/09-standalone-cli/index.php +++ /dev/null @@ -1,35 +0,0 @@ -setServerInfo('Standalone CLI', '1.0.0') - ->setLogger($logger) - ->setDiscovery(__DIR__, ['.']) - ->build(); - -$transport = new StdioTransport(logger: $logger); - -$server->connect($transport); - -$transport->listen(); diff --git a/examples/09-standalone-cli/src/Builder.php b/examples/09-standalone-cli/src/Builder.php deleted file mode 100644 index 77ea7b75..00000000 --- a/examples/09-standalone-cli/src/Builder.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -class Builder -{ - /** - * @return list - */ - public static function buildMethodHandlers(): array - { - $promptManager = new PromptChain([ - new ExamplePrompt(), - ]); - - $resourceManager = new ResourceChain([ - new ExampleResource(), - ]); - - $toolManager = new ToolChain([ - new ExampleTool(), - ]); - - return [ - new NotificationHandler\InitializedHandler(), - new RequestHandler\InitializeHandler(), - new RequestHandler\PingHandler(), - new RequestHandler\ListPromptsHandler($promptManager), - new RequestHandler\GetPromptHandler($promptManager), - new RequestHandler\ListResourcesHandler($resourceManager), - new RequestHandler\ReadResourceHandler($resourceManager), - new RequestHandler\CallToolHandler($toolManager), - new RequestHandler\ListToolsHandler($toolManager), - ]; - } -} diff --git a/examples/09-standalone-cli/src/ExamplePrompt.php b/examples/09-standalone-cli/src/ExamplePrompt.php deleted file mode 100644 index 7c4b0927..00000000 --- a/examples/09-standalone-cli/src/ExamplePrompt.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class ExamplePrompt implements MetadataInterface, PromptGetterInterface -{ - public function get(GetPromptRequest $request): GetPromptResult - { - $firstName = $request->arguments['firstName'] ?? null; - - return new GetPromptResult( - [new PromptMessage( - Role::User, - new TextContent(\sprintf('Hello %s', $firstName ?? 'World')), - )], - $this->getDescription(), - ); - } - - public function getName(): string - { - return 'Greet'; - } - - public function getDescription(): ?string - { - return 'Greet a person with a nice message'; - } - - public function getArguments(): array - { - return [ - [ - 'name' => 'first name', - 'description' => 'The name of the person to greet', - 'required' => false, - ], - ]; - } -} diff --git a/examples/09-standalone-cli/src/ExampleResource.php b/examples/09-standalone-cli/src/ExampleResource.php deleted file mode 100644 index 66cbdc3f..00000000 --- a/examples/09-standalone-cli/src/ExampleResource.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ -class ExampleResource implements MetadataInterface, ResourceReaderInterface -{ - public function read(ReadResourceRequest $request): ReadResourceResult - { - return new ReadResourceResult([ - new TextResourceContents($this->getUri(), null, 'Content of My Resource'), - ]); - } - - public function getUri(): string - { - return 'file:///project/src/main.rs'; - } - - public function getName(): string - { - return 'my-resource'; - } - - public function getDescription(): ?string - { - return 'This is just an example'; - } - - public function getMimeType(): ?string - { - return null; - } - - public function getSize(): ?int - { - return null; - } -} diff --git a/examples/09-standalone-cli/src/ExampleTool.php b/examples/09-standalone-cli/src/ExampleTool.php deleted file mode 100644 index 559de51d..00000000 --- a/examples/09-standalone-cli/src/ExampleTool.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class ExampleTool implements MetadataInterface, ToolCallerInterface -{ - public function call(CallToolRequest $request): CallToolResult - { - $format = $request->arguments['format'] ?? 'Y-m-d H:i:s'; - - return new CallToolResult([ - new TextContent( - (new \DateTime('now', new \DateTimeZone('UTC')))->format($format), - ), - ]); - } - - public function getName(): string - { - return 'Current time'; - } - - public function getDescription(): string - { - return 'Returns the current time in UTC'; - } - - public function getInputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'format' => [ - 'type' => 'string', - 'description' => 'The format of the time, e.g. "Y-m-d H:i:s"', - 'default' => 'Y-m-d H:i:s', - ], - ], - 'required' => [], - ]; - } -} diff --git a/examples/10-simple-http-transport/McpElements.php b/examples/10-simple-http-transport/McpElements.php deleted file mode 100644 index 6ac08427..00000000 --- a/examples/10-simple-http-transport/McpElements.php +++ /dev/null @@ -1,118 +0,0 @@ - $a + $b, - 'subtract', '-' => $a - $b, - 'multiply', '*' => $a * $b, - 'divide', '/' => 0 != $b ? $a / $b : 'Error: Division by zero', - default => 'Error: Unknown operation. Use: add, subtract, multiply, divide', - }; - } - - /** - * Server information resource. - * - * @return array{status: string, timestamp: int, version: string, transport: string, uptime: int} - */ - #[McpResource( - uri: 'info://server/status', - name: 'server_status', - description: 'Current server status and information', - mimeType: 'application/json' - )] - public function getServerStatus(): array - { - return [ - 'status' => 'running', - 'timestamp' => time(), - 'version' => '1.0.0', - 'transport' => 'HTTP', - 'uptime' => time() - $_SERVER['REQUEST_TIME'], - ]; - } - - /** - * Configuration resource. - * - * @return array{debug: bool, environment: string, timezone: string, locale: string} - */ - #[McpResource( - uri: 'config://app/settings', - name: 'app_config', - description: 'Application configuration settings', - mimeType: 'application/json' - )] - public function getAppConfig(): array - { - return [ - 'debug' => $_SERVER['DEBUG'] ?? false, - 'environment' => $_SERVER['APP_ENV'] ?? 'production', - 'timezone' => date_default_timezone_get(), - 'locale' => 'en_US', - ]; - } - - /** - * Greeting prompt. - * - * @return array{role: string, content: string} - */ - #[McpPrompt( - name: 'greet', - description: 'Generate a personalized greeting message' - )] - public function greetPrompt(string $firstName = 'World', string $timeOfDay = 'day'): array - { - $greeting = match (strtolower($timeOfDay)) { - 'morning' => 'Good morning', - 'afternoon' => 'Good afternoon', - 'evening', 'night' => 'Good evening', - default => 'Hello', - }; - - return [ - 'role' => 'user', - 'content' => "# {$greeting}, {$firstName}!\n\nWelcome to our MCP HTTP Server example. This demonstrates how to use the Model Context Protocol over HTTP transport.", - ]; - } -} diff --git a/examples/10-simple-http-transport/README.md b/examples/10-simple-http-transport/README.md deleted file mode 100644 index aa06a67e..00000000 --- a/examples/10-simple-http-transport/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# HTTP MCP Server Example - -This example demonstrates how to use the MCP SDK with HTTP transport using the StreamableHttpTransport. It provides a complete HTTP-based MCP server that can handle JSON-RPC requests over HTTP POST. - -## Usage - -**Step 1: Start the HTTP server** - -```bash -cd examples/10-simple-http-transport -php -S localhost:8000 server.php -``` - -**Step 2: Connect with MCP Inspector** - -```bash -npx @modelcontextprotocol/inspector http://localhost:8000 -``` - -## Available Features - -- **Tools**: `current_time`, `calculate` -- **Resources**: `info://server/status`, `config://app/settings` -- **Prompts**: `greet` diff --git a/examples/10-simple-http-transport/server.php b/examples/10-simple-http-transport/server.php deleted file mode 100644 index 0ce83404..00000000 --- a/examples/10-simple-http-transport/server.php +++ /dev/null @@ -1,40 +0,0 @@ -fromGlobals(); - -$server = Server::make() - ->setServerInfo('HTTP MCP Server', '1.0.0', 'MCP Server over HTTP transport') - ->setContainer(container()) - ->setSession(new FileSessionStore(__DIR__.'/sessions')) - ->setDiscovery(__DIR__, ['.']) - ->build(); - -$transport = new StreamableHttpTransport($request, $psr17Factory, $psr17Factory); - -$server->connect($transport); - -$response = $transport->listen(); - -(new SapiEmitter())->emit($response); diff --git a/examples/bootstrap.php b/examples/bootstrap.php index ca332791..fef29802 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -44,9 +44,9 @@ public function log($level, Stringable|string $message, array $context = []): vo if ($_SERVER['FILE_LOG'] ?? false) { file_put_contents('dev.log', $logMessage, \FILE_APPEND); + } elseif (defined('STDERR')) { + fwrite(\STDERR, $logMessage); } - - fwrite(\STDERR, $logMessage); } }; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f7b29633..a70cf02d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -321,25 +321,6 @@ parameters: count: 1 path: examples/08-schema-showcase-streamable/SchemaShowcaseElements.php - - - - message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListPromptsHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\PromptChain given\.$#' - identifier: argument.type - count: 1 - path: examples/09-standalone-cli/src/Builder.php - - - - message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListResourcesHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\ResourceChain given\.$#' - identifier: argument.type - count: 1 - path: examples/09-standalone-cli/src/Builder.php - - - - message: '#^Parameter \#1 \$registry of class Mcp\\Server\\RequestHandler\\ListToolsHandler constructor expects Mcp\\Capability\\Registry\\ReferenceProviderInterface, Mcp\\Capability\\ToolChain given\.$#' - identifier: argument.type - count: 1 - path: examples/09-standalone-cli/src/Builder.php - - message: '#^PHPDoc tag @return with type array is incompatible with native type object\.$#' identifier: return.phpDocType diff --git a/src/Capability/Discovery/Discoverer.php b/src/Capability/Discovery/Discoverer.php index a2bdd3cd..e8362a76 100644 --- a/src/Capability/Discovery/Discoverer.php +++ b/src/Capability/Discovery/Discoverer.php @@ -98,7 +98,7 @@ public function discover(string $basePath, array $directories, array $excludeDir $this->processFile($file, $discoveredCount); } } catch (\Throwable $e) { - $this->logger->error('Error during file finding process for MCP discovery', [ + $this->logger->error('Error during file finding process for MCP discovery'.json_encode($e->getTrace(), \JSON_PRETTY_PRINT), [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); diff --git a/src/Capability/Prompt/CollectionInterface.php b/src/Capability/Prompt/CollectionInterface.php deleted file mode 100644 index 8498576f..00000000 --- a/src/Capability/Prompt/CollectionInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface CollectionInterface -{ - /** - * @param int $count the number of metadata items to return - * - * @return iterable - * - * @throws InvalidCursorException if no item with $lastIdentifier was found - */ - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable; -} diff --git a/src/Capability/Prompt/IdentifierInterface.php b/src/Capability/Prompt/IdentifierInterface.php deleted file mode 100644 index 8fea7919..00000000 --- a/src/Capability/Prompt/IdentifierInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ -interface IdentifierInterface -{ - public function getName(): string; -} diff --git a/src/Capability/Prompt/MetadataInterface.php b/src/Capability/Prompt/MetadataInterface.php deleted file mode 100644 index 45c07ab7..00000000 --- a/src/Capability/Prompt/MetadataInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface MetadataInterface extends IdentifierInterface -{ - public function getDescription(): ?string; - - /** - * @return list - */ - public function getArguments(): array; -} diff --git a/src/Capability/PromptChain.php b/src/Capability/PromptChain.php deleted file mode 100644 index f7edf62a..00000000 --- a/src/Capability/PromptChain.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -class PromptChain implements PromptGetterInterface, CollectionInterface -{ - public function __construct( - /** - * @var IdentifierInterface[] - */ - private readonly array $items, - ) { - } - - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable - { - $found = null === $lastIdentifier; - foreach ($this->items as $item) { - if (!$item instanceof MetadataInterface) { - continue; - } - - if (false === $found) { - $found = $item->getName() === $lastIdentifier; - continue; - } - - yield $item; - if (--$count <= 0) { - break; - } - } - - if (!$found) { - throw new InvalidCursorException($lastIdentifier); - } - } - - public function get(GetPromptRequest $request): GetPromptResult - { - foreach ($this->items as $item) { - if ($item instanceof PromptGetterInterface && $request->name === $item->getName()) { - try { - return $item->get($request); - } catch (\Throwable $e) { - throw new PromptGetException($request, $e); - } - } - } - - throw new PromptNotFoundException($request); - } -} diff --git a/src/Capability/Resource/CollectionInterface.php b/src/Capability/Resource/CollectionInterface.php deleted file mode 100644 index 612b0361..00000000 --- a/src/Capability/Resource/CollectionInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface CollectionInterface -{ - /** - * @param int $count the number of metadata items to return - * - * @return iterable - * - * @throws InvalidCursorException if no item with $lastIdentifier was found - */ - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable; -} diff --git a/src/Capability/Resource/IdentifierInterface.php b/src/Capability/Resource/IdentifierInterface.php deleted file mode 100644 index 0daa39e8..00000000 --- a/src/Capability/Resource/IdentifierInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ -interface IdentifierInterface -{ - public function getUri(): string; -} diff --git a/src/Capability/Resource/MetadataInterface.php b/src/Capability/Resource/MetadataInterface.php deleted file mode 100644 index 757dfa1a..00000000 --- a/src/Capability/Resource/MetadataInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface MetadataInterface extends IdentifierInterface -{ - public function getName(): string; - - public function getDescription(): ?string; - - public function getMimeType(): ?string; - - /** - * Size in bytes. - */ - public function getSize(): ?int; -} diff --git a/src/Capability/ResourceChain.php b/src/Capability/ResourceChain.php deleted file mode 100644 index 1d40d492..00000000 --- a/src/Capability/ResourceChain.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -class ResourceChain implements CollectionInterface, ResourceReaderInterface -{ - public function __construct( - /** - * @var IdentifierInterface[] - */ - private readonly array $items, - ) { - } - - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable - { - $found = null === $lastIdentifier; - foreach ($this->items as $item) { - if (!$item instanceof MetadataInterface) { - continue; - } - - if (false === $found) { - $found = $item->getUri() === $lastIdentifier; - continue; - } - - yield $item; - if (--$count <= 0) { - break; - } - } - - if (!$found) { - throw new InvalidCursorException($lastIdentifier); - } - } - - public function read(ReadResourceRequest $request): ReadResourceResult - { - foreach ($this->items as $item) { - if ($item instanceof ResourceReaderInterface && $request->uri === $item->getUri()) { - try { - return $item->read($request); - } catch (\Throwable $e) { - throw new ResourceReadException($request, $e); - } - } - } - - throw new ResourceNotFoundException($request); - } -} diff --git a/src/Capability/Tool/CollectionInterface.php b/src/Capability/Tool/CollectionInterface.php deleted file mode 100644 index 297e2036..00000000 --- a/src/Capability/Tool/CollectionInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -interface CollectionInterface -{ - /** - * @param int $count the number of metadata items to return - * - * @return iterable - * - * @throws InvalidCursorException if no item with $lastIdentifier was found - */ - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable; -} diff --git a/src/Capability/Tool/IdentifierInterface.php b/src/Capability/Tool/IdentifierInterface.php deleted file mode 100644 index 0ad3f8c9..00000000 --- a/src/Capability/Tool/IdentifierInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ -interface IdentifierInterface -{ - public function getName(): string; -} diff --git a/src/Capability/Tool/MetadataInterface.php b/src/Capability/Tool/MetadataInterface.php deleted file mode 100644 index bebc48b6..00000000 --- a/src/Capability/Tool/MetadataInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -interface MetadataInterface extends IdentifierInterface -{ - public function getDescription(): string; - - /** - * @return array{ - * type?: string, - * required?: list, - * properties?: array, - * } - */ - public function getInputSchema(): array; -} diff --git a/src/Capability/Tool/ToolCollectionInterface.php b/src/Capability/Tool/ToolCollectionInterface.php deleted file mode 100644 index 1c71eea1..00000000 --- a/src/Capability/Tool/ToolCollectionInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ -interface ToolCollectionInterface -{ - /** - * @return MetadataInterface[] - */ - public function getMetadata(): array; -} diff --git a/src/Capability/ToolChain.php b/src/Capability/ToolChain.php deleted file mode 100644 index e500ff00..00000000 --- a/src/Capability/ToolChain.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -class ToolChain implements ToolCallerInterface, CollectionInterface -{ - public function __construct( - /** - * @var IdentifierInterface[] $items - */ - private readonly array $items, - ) { - } - - public function getMetadata(int $count, ?string $lastIdentifier = null): iterable - { - $found = null === $lastIdentifier; - foreach ($this->items as $item) { - if (!$item instanceof MetadataInterface) { - continue; - } - - if (false === $found) { - $found = $item->getName() === $lastIdentifier; - continue; - } - - yield $item; - if (--$count <= 0) { - break; - } - } - - if (!$found) { - throw new InvalidCursorException($lastIdentifier); - } - } - - public function call(CallToolRequest $request): CallToolResult - { - foreach ($this->items as $item) { - if ($item instanceof ToolCallerInterface && $request->name === $item->getName()) { - try { - return $item->call($request); - } catch (\Throwable $e) { - throw new ToolCallException($request, $e); - } - } - } - - throw new ToolNotFoundException($request); - } -}