diff --git a/README.md b/README.md index d75e022..932ace3 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@ For details on how to use this package, check out our [documentation](.docs). ## Versions -| State | Version | Branch | Nette | PHP | -|-------------|---------|----------|--------|---------| -| dev | `^0.1` | `master` | `3.2+` | `>=8.4` | +| State | Version | Branch | mcp/sdk | Nette | PHP | +|-------------|---------|----------|------------|--------|---------| +| dev | `^0.3` | `master` | `^0.8` | `3.2+` | `>=8.4` | +| stable | `^0.2` | `master` | `dev-main` | `3.2+` | `>=8.4` | ## Development diff --git a/composer.json b/composer.json index fc902ec..e9881b4 100644 --- a/composer.json +++ b/composer.json @@ -1,18 +1,17 @@ { "name": "contributte/mcp", - "description": "Doctrine DBAL for Nette Framework", + "description": "Model Context Protocol (MCP) server integration for Nette Framework", "keywords": [ "nette", - "doctrine", - "dbal", - "database", - "mysql", - "postgres", - "sqlite" + "mcp", + "model-context-protocol", + "ai", + "llm", + "server" ], "type": "library", "license": "MIT", - "homepage": "https://github.com/contributte/doctrine-dbal", + "homepage": "https://github.com/contributte/mcp", "authors": [ { "name": "Milan Felix Ć ulc", @@ -22,10 +21,12 @@ "require": { "php": ">=8.4", "guzzlehttp/guzzle": "^7.10.0", - "psr/container": "^2.0.2", + "mcp/sdk": "^0.8.1", + "nette/di": "^3.2.5", "psr/cache": "^3.0.0", + "psr/container": "^2.0.2", "psr/simple-cache": "^3.0.0", - "mcp/sdk": "dev-main#369932378f7bcfa29a00efc1530b70c4d501a24b" + "symfony/finder": "^7.4 || ^8.0" }, "require-dev": { "ext-pdo": "*", @@ -34,11 +35,16 @@ "contributte/tester": "^0.5.0", "mockery/mockery": "^1.6.12", "nette/application": "^3.2.8", - "nette/di": "^3.2.5", "nette/http": "^3.3.3", "symfony/console": "^7.4.1 || ^8.0.1", "tracy/tracy": "^2.11.0" }, + "suggest": { + "nette/application": "Required for McpPresenter and McpRouter (HTTP transport)", + "nette/http": "Required for McpPresenter and GuzzleBridge (HTTP transport)", + "symfony/console": "Required for McpCommand (stdio transport)", + "tracy/tracy": "Required for the Tracy debug panel" + }, "autoload": { "psr-4": { "Contributte\\Mcp\\": "src/" @@ -63,7 +69,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.1.x-dev" + "dev-master": "0.3.x-dev" } } } diff --git a/src/DI/Helpers/BuilderMan.php b/src/DI/Helpers/BuilderMan.php index 233b183..a7fa7d1 100644 --- a/src/DI/Helpers/BuilderMan.php +++ b/src/DI/Helpers/BuilderMan.php @@ -2,6 +2,7 @@ namespace Contributte\Mcp\DI\Helpers; +use Contributte\Mcp\Exception\LogicalException; use Nette\DI\CompilerExtension; use Nette\DI\Definitions\Definition; use Nette\DI\Definitions\Reference; @@ -31,6 +32,10 @@ public function getServiceDefinitionsByTag(string $tag): array $definitions = []; foreach ($builder->findByTag($tag) as $serviceName => $tagValue) { + if (!is_string($tagValue) && !is_int($tagValue)) { + throw new LogicalException(sprintf('Tag "%s" of service "%s" must have a string value', $tag, $serviceName)); + } + $definitions[(string) $tagValue] = $builder->getDefinition($serviceName); } diff --git a/src/DI/McpExtension.php b/src/DI/McpExtension.php index ff7f127..426de0b 100644 --- a/src/DI/McpExtension.php +++ b/src/DI/McpExtension.php @@ -17,7 +17,7 @@ use Mcp\Server\Builder; use Mcp\Server\Session\FileSessionStore; use Mcp\Server\Session\InMemorySessionStore; -use Mcp\Server\Session\Psr16StoreSession; +use Mcp\Server\Session\Psr16SessionStore; use Nette\Application\IPresenterFactory; use Nette\DI\CompilerExtension; use Nette\DI\Definitions\ServiceDefinition; @@ -178,7 +178,8 @@ private function loadServerConfiguration(string $serverName, stdClass $serverCon // Server:Session switch ($serverConfig->session->type) { case 'file': - $path = $serverConfig->session->path ?? (isset($builder->parameters['tempDir']) ? $builder->parameters['tempDir'] . '/mcp' : null); + $tempDir = $builder->parameters['tempDir'] ?? null; + $path = $serverConfig->session->path ?? (is_string($tempDir) ? $tempDir . '/mcp' : null); if ($path === null) { throw new LogicalException( sprintf('Session path must be configured for file sessions (server "%s"). Either set session.path or ensure %%tempDir%% is available.', $serverName) @@ -198,7 +199,7 @@ private function loadServerConfiguration(string $serverName, stdClass $serverCon } $cacheService = BuilderMan::of($this)->resolveService($serverConfig->session->cache); - $sessionStore = new Statement(Psr16StoreSession::class, [$cacheService, $serverConfig->session->prefix, $serverConfig->session->ttl]); + $sessionStore = new Statement(Psr16SessionStore::class, [$cacheService, $serverConfig->session->prefix, $serverConfig->session->ttl]); break; default: $sessionStore = null; diff --git a/src/Http/GuzzleBridge.php b/src/Http/GuzzleBridge.php index d2bd371..686b14b 100644 --- a/src/Http/GuzzleBridge.php +++ b/src/Http/GuzzleBridge.php @@ -22,7 +22,7 @@ public static function fromNette(IRequest $netteRequest): ServerRequestInterface $headers = []; foreach ($netteRequest->getHeaders() as $name => $value) { - $headers[$name] = is_array($value) ? $value : [$value]; + $headers[$name] = [$value]; } $body = $netteRequest->getRawBody(); diff --git a/src/Http/StreamableTransportFactory.php b/src/Http/StreamableTransportFactory.php index e64cbe1..815ec0e 100644 --- a/src/Http/StreamableTransportFactory.php +++ b/src/Http/StreamableTransportFactory.php @@ -36,7 +36,6 @@ public function create(mixed ...$args): TransportInterface $serverRequest, $this->responseFactory, $this->streamFactory, - [], $this->logger, ); } diff --git a/src/Registry/TraceableRegistry.php b/src/Registry/TraceableRegistry.php index e8d0dbe..e625c0f 100644 --- a/src/Registry/TraceableRegistry.php +++ b/src/Registry/TraceableRegistry.php @@ -2,7 +2,6 @@ namespace Contributte\Mcp\Registry; -use Mcp\Capability\Discovery\DiscoveryState; use Mcp\Capability\Registry\PromptReference; use Mcp\Capability\Registry\ResourceReference; use Mcp\Capability\Registry\ResourceTemplateReference; @@ -10,7 +9,7 @@ use Mcp\Capability\RegistryInterface; use Mcp\Schema\Page; use Mcp\Schema\Prompt; -use Mcp\Schema\Resource; +use Mcp\Schema\ResourceDefinition; use Mcp\Schema\ResourceTemplate; use Mcp\Schema\Tool; @@ -39,14 +38,14 @@ public function getCalls(): array return $this->calls; } - public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void + public function registerTool(Tool $tool, callable|array|string $handler): ToolReference { - $this->registry->registerTool($tool, $handler, $isManual); + return $this->registry->registerTool($tool, $handler); } - public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void + public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference { - $this->registry->registerResource($resource, $handler, $isManual); + return $this->registry->registerResource($resource, $handler); } /** @@ -56,10 +55,9 @@ public function registerResourceTemplate( ResourceTemplate $template, callable|array|string $handler, array $completionProviders = [], - bool $isManual = false, - ): void + ): ResourceTemplateReference { - $this->registry->registerResourceTemplate($template, $handler, $completionProviders, $isManual); + return $this->registry->registerResourceTemplate($template, $handler, $completionProviders); } /** @@ -69,25 +67,49 @@ public function registerPrompt( Prompt $prompt, callable|array|string $handler, array $completionProviders = [], - bool $isManual = false, - ): void + ): PromptReference { - $this->registry->registerPrompt($prompt, $handler, $completionProviders, $isManual); + return $this->registry->registerPrompt($prompt, $handler, $completionProviders); } - public function clear(): void + public function unregisterTool(string $name): void { - $this->registry->clear(); + $this->registry->unregisterTool($name); } - public function getDiscoveryState(): DiscoveryState + public function unregisterResource(string $uri): void { - return $this->registry->getDiscoveryState(); + $this->registry->unregisterResource($uri); } - public function setDiscoveryState(DiscoveryState $state): void + public function unregisterResourceTemplate(string $uriTemplate): void { - $this->registry->setDiscoveryState($state); + $this->registry->unregisterResourceTemplate($uriTemplate); + } + + public function unregisterPrompt(string $name): void + { + $this->registry->unregisterPrompt($name); + } + + public function hasTool(string $name): bool + { + return $this->registry->hasTool($name); + } + + public function hasResource(string $uri): bool + { + return $this->registry->hasResource($uri); + } + + public function hasResourceTemplate(string $uriTemplate): bool + { + return $this->registry->hasResourceTemplate($uriTemplate); + } + + public function hasPrompt(string $name): bool + { + return $this->registry->hasPrompt($name); } public function hasTools(): bool diff --git a/tests/Cases/Tracy/TraceableRegistry.phpt b/tests/Cases/Tracy/TraceableRegistry.phpt index ae948f8..3afefc4 100644 --- a/tests/Cases/Tracy/TraceableRegistry.phpt +++ b/tests/Cases/Tracy/TraceableRegistry.phpt @@ -5,6 +5,7 @@ namespace Tests\Cases\Tracy; use Contributte\Mcp\Registry\TraceableRegistry; use Contributte\Tester\Toolkit; use Mcp\Capability\Registry; +use Mcp\Capability\Registry\ToolReference; use Mcp\Capability\RegistryInterface; use Mcp\Schema\Page; use Mcp\Schema\Tool; @@ -71,14 +72,24 @@ Toolkit::test(function (): void { $tool = new Tool( name: 'test-tool', + title: null, inputSchema: ['type' => 'object', 'properties' => [], 'required' => []], description: 'A test tool', annotations: null, ); - $traceableRegistry->registerTool($tool, fn () => 'result'); + $reference = $traceableRegistry->registerTool($tool, fn () => 'result'); + Assert::type(ToolReference::class, $reference); + Assert::same($tool, $reference->tool); Assert::true($traceableRegistry->hasTools()); Assert::true($innerRegistry->hasTools()); + Assert::true($traceableRegistry->hasTool('test-tool')); + Assert::false($traceableRegistry->hasTool('missing-tool')); + + $traceableRegistry->unregisterTool('test-tool'); + + Assert::false($traceableRegistry->hasTool('test-tool')); + Assert::false($innerRegistry->hasTool('test-tool')); }); // Test: TraceableRegistry accumulates multiple calls diff --git a/tests/Toolkit/ServerInspector.php b/tests/Toolkit/ServerInspector.php index 067315c..400aa48 100644 --- a/tests/Toolkit/ServerInspector.php +++ b/tests/Toolkit/ServerInspector.php @@ -4,7 +4,7 @@ use Mcp\Capability\RegistryInterface; use Mcp\Schema\Prompt; -use Mcp\Schema\Resource; +use Mcp\Schema\ResourceDefinition; use Mcp\Schema\Tool; use Mcp\Server; use Mcp\Server\Configuration; @@ -43,7 +43,7 @@ public static function hasTools(Server $server): bool } /** - * @return array + * @return array */ public static function getResources(Server $server): array {