├── .gitignore ├── .php-cs-fixer.php ├── .vscode └── settings.json ├── README.md ├── composer.json ├── phpstan.neon └── src ├── Annotation ├── Description.php ├── McpAnnotation.php ├── Prompt.php ├── Resource.php └── Tool.php ├── Capabilities.php ├── ConfigProvider.php ├── Constants.php ├── Contract ├── IdGeneratorInterface.php └── TransportInterface.php ├── IdGenerator └── UniqidIdGenerator.php ├── McpCollector.php ├── Server ├── Annotation │ └── Server.php ├── Exception │ └── Handler │ │ └── McpSseExceptionHandler.php ├── Listener │ ├── RegisterCommandListener.php │ └── RegisterSseRouterListener.php ├── McpHandler.php ├── McpServer.php ├── Protocol │ └── Packer.php └── Transport │ ├── SseTransport.php │ └── StdioTransport.php ├── TypeCollection.php └── Types └── Message ├── ErrorResponse.php ├── MessageInterface.php ├── Notification.php ├── Request.php └── Response.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea/ 3 | *.lock -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setParallelConfig(new ParallelConfig($maxProcesses, 20)) 29 | ->setRiskyAllowed(true) 30 | ->setRules([ 31 | '@PSR2' => true, 32 | '@Symfony' => true, 33 | '@DoctrineAnnotation' => true, 34 | '@PhpCsFixer' => true, 35 | 'header_comment' => [ 36 | 'comment_type' => 'PHPDoc', 37 | 'header' => $header, 38 | 'separate' => 'none', 39 | 'location' => 'after_declare_strict', 40 | ], 41 | 'array_syntax' => [ 42 | 'syntax' => 'short', 43 | ], 44 | 'list_syntax' => [ 45 | 'syntax' => 'short', 46 | ], 47 | 'concat_space' => [ 48 | 'spacing' => 'one', 49 | ], 50 | 'blank_line_before_statement' => [ 51 | 'statements' => [ 52 | 'declare', 53 | ], 54 | ], 55 | 'general_phpdoc_annotation_remove' => [ 56 | 'annotations' => [ 57 | 'author', 58 | ], 59 | ], 60 | 'ordered_imports' => [ 61 | 'imports_order' => [ 62 | 'class', 'function', 'const', 63 | ], 64 | 'sort_algorithm' => 'alpha', 65 | ], 66 | 'single_line_comment_style' => [ 67 | 'comment_types' => [ 68 | ], 69 | ], 70 | 'yoda_style' => [ 71 | 'always_move_variable' => false, 72 | 'equal' => false, 73 | 'identical' => false, 74 | ], 75 | 'phpdoc_align' => [ 76 | 'align' => 'left', 77 | ], 78 | 'multiline_whitespace_before_semicolons' => [ 79 | 'strategy' => 'no_multi_line', 80 | ], 81 | 'constant_case' => [ 82 | 'case' => 'lower', 83 | ], 84 | 'global_namespace_import' => [ 85 | 'import_classes' => true, 86 | 'import_constants' => true, 87 | 'import_functions' => true, 88 | ], 89 | 'class_attributes_separation' => true, 90 | 'combine_consecutive_unsets' => true, 91 | 'declare_strict_types' => true, 92 | 'linebreak_after_opening_tag' => true, 93 | 'lowercase_static_reference' => true, 94 | 'no_useless_else' => true, 95 | 'no_unused_imports' => true, 96 | 'not_operator_with_successor_space' => true, 97 | 'not_operator_with_space' => false, 98 | 'ordered_class_elements' => true, 99 | 'php_unit_strict' => false, 100 | 'phpdoc_separation' => false, 101 | 'single_quote' => true, 102 | 'standardize_not_equals' => true, 103 | 'multiline_comment_opening_closing' => true, 104 | // Since PHP 8.3, default null values can be declared as nullable. 105 | 'nullable_type_declaration_for_default_null_value' => true, 106 | 'single_line_empty_body' => false, 107 | ]) 108 | ->setFinder( 109 | Finder::create() 110 | ->exclude('bin') 111 | ->exclude('public') 112 | ->exclude('runtime') 113 | ->exclude('vendor') 114 | ->exclude('src/nacos/src/Protobuf/GPBMetadata') 115 | ->in(__DIR__) 116 | ) 117 | ->setUsingCache(false); 118 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "php.suggest.basic": false, 3 | "[php]": { 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "junstyle.php-cs-fixer", 6 | }, 7 | "php-cs-fixer.executablePath": "${workspaceRoot}/vendor/bin/php-cs-fixer", 8 | "php-cs-fixer.executablePathWindows": "php-cs-fixer.bat", 9 | "php-cs-fixer.onsave": true, 10 | "php-cs-fixer.rules": "@PSR2", 11 | "php-cs-fixer.config": ".php-cs-fixer.php;.php-cs-fixer.dist.php", 12 | "php-cs-fixer.allowRisky": false, 13 | "php-cs-fixer.pathMode": "override", 14 | "php-cs-fixer.exclude": [], 15 | "php-cs-fixer.autoFixByBracket": true, 16 | "php-cs-fixer.autoFixBySemicolon": false, 17 | "php-cs-fixer.formatHtml": true, 18 | "php-cs-fixer.documentFormattingProvider": true, 19 | "intelephense.files.exclude": [ 20 | "**/.git/**", 21 | "**/.svn/**", 22 | "**/.hg/**", 23 | "**/CVS/**", 24 | "**/.DS_Store/**", 25 | "**/node_modules/**", 26 | "**/bower_components/**", 27 | "**/vendor/**/{Tests,tests}/**", 28 | "**/.history/**", 29 | "**/vendor/**/vendor/**", 30 | "**/vendor/google/crc32/**", 31 | "**/vendor/mockery/mockery/library/helpers.php", 32 | ] 33 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Model Context Protocol (MCP) 2 | 3 | 开始使用模型上下文协议 Model Context Protocol (MCP)。 4 | 5 | ## 安装 6 | 7 | ```bash 8 | composer require hyperf/mcp-incubator 9 | ``` 10 | 11 | ## 使用 12 | 13 | ### Stdio 14 | 15 | 16 | ```php 17 | Hyperf\Server\CoroutineServer::class, # 建议协程风格 50 | 'servers' => [ 51 | 'mcp-sse' => [ 52 | 'type' => Server::SERVER_HTTP, 53 | 'host' => '0.0.0.0', 54 | 'port' => 3000, 55 | 'sock_type' => SWOOLE_SOCK_TCP, 56 | 'callbacks' => [ 57 | Event::ON_REQUEST => [McpServer::class, 'onRequest'], 58 | Event::ON_CLOSE => [McpServer::class, 'onClose'], 59 | ], 60 | 'options' => [ 61 | 'mcp_path' => '/sse', 62 | ], 63 | ], 64 | ], 65 | ]; 66 | ``` 67 | 68 | ```php 69 | =8.1", 13 | "hyperf/coordinator": "^3.1", 14 | "hyperf/di": "^3.1", 15 | "hyperf/http-server": "^3.1", 16 | "hyperf/json-rpc": "^3.1", 17 | "hyperf/rpc": "^3.1", 18 | "hyperf/framework": "^3.1", 19 | "hyperf/command": "^3.1" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.73", 23 | "phpstan/phpstan": "^2.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Hyperf\\Mcp\\": "src/" 28 | } 29 | }, 30 | "extra": { 31 | "hyperf": { 32 | "config": "Hyperf\\Mcp\\ConfigProvider" 33 | } 34 | }, 35 | "scripts": { 36 | "cs-fix": "php-cs-fixer fix $1", 37 | "analyse": "@php vendor/bin/phpstan analyse --memory-limit 512M -c phpstan.neon" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | 3 | parameters: 4 | level: 6 5 | parallel: 6 | jobSize: 20 7 | maximumNumberOfProcesses: 32 8 | minimumNumberOfJobsPerProcess: 2 9 | reportUnmatchedIgnoredErrors: false 10 | paths: 11 | - app/ 12 | - config/ 13 | - storage/languages 14 | scanFiles: 15 | excludePaths: 16 | ignoreErrors: 17 | - 18 | identifier: missingType.iterableValue 19 | - 20 | identifier: missingType.generics 21 | - 22 | '#invalid type Swow\\Psr7\\Server\\ServerConnection#' 23 | -------------------------------------------------------------------------------- /src/Annotation/Description.php: -------------------------------------------------------------------------------- 1 | getAttributes() as $attribute) { 36 | if ($attribute->getName() === Description::class) { 37 | return $attribute->newInstance()->description; 38 | } 39 | } 40 | 41 | return ''; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Annotation/Prompt.php: -------------------------------------------------------------------------------- 1 | className = $className; 34 | $this->target = $target; 35 | McpCollector::collectMethod($className, $target, $this->name, $this); 36 | } 37 | 38 | public function toSchema(): array 39 | { 40 | return [ 41 | 'name' => $this->name, 42 | 'description' => $this->description, 43 | 'arguments' => $this->generateArguments(), 44 | ]; 45 | } 46 | 47 | private function generateArguments(): array 48 | { 49 | $reflection = ReflectionManager::reflectMethod($this->className, $this->target); 50 | $parameters = $reflection->getParameters(); 51 | $arguments = []; 52 | 53 | foreach ($parameters as $parameter) { 54 | $arguments[] = [ 55 | 'name' => $parameter->getName(), 56 | 'description' => self::getDescription($parameter), 57 | 'required' => ! $parameter->isOptional(), 58 | ]; 59 | } 60 | return $arguments; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Annotation/Resource.php: -------------------------------------------------------------------------------- 1 | uri, $this); 34 | } 35 | 36 | public function toSchema(): array 37 | { 38 | return [ 39 | 'name' => $this->name, 40 | 'uri' => $this->uri, 41 | 'mimeType' => $this->mimeType, 42 | 'description' => $this->description, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Annotation/Tool.php: -------------------------------------------------------------------------------- 1 | className = $className; 35 | $this->target = $target; 36 | McpCollector::collectMethod($className, $target, $this->name, $this); 37 | } 38 | 39 | public function toSchema(): array 40 | { 41 | if (! preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) { 42 | throw new InvalidArgumentException('Tool name must be alphanumeric and underscores.'); 43 | } 44 | 45 | return [ 46 | 'name' => $this->name, 47 | 'description' => $this->description, 48 | 'inputSchema' => $this->generateInputSchema(), 49 | ]; 50 | } 51 | 52 | private function generateInputSchema(): array 53 | { 54 | $reflection = ReflectionManager::reflectMethod($this->className, $this->target); 55 | $parameters = $reflection->getParameters(); 56 | $properties = []; 57 | 58 | foreach ($parameters as $parameter) { 59 | $type = $parameter->getType()?->getName() ?? 'string'; // @phpstan-ignore method.notFound 60 | $type = match ($type) { 61 | 'int' => 'integer', 62 | 'float' => 'number', 63 | 'bool' => 'boolean', 64 | default => $type, 65 | }; 66 | $properties[$parameter->getName()] = [ 67 | 'type' => $type, 68 | 'description' => self::getDescription($parameter), 69 | ]; 70 | } 71 | 72 | $required = array_filter( 73 | array_map(fn (ReflectionParameter $parameter) => $parameter->isOptional() ? null : $parameter->getName(), $parameters) 74 | ); 75 | 76 | return array_filter([ 77 | 'type' => 'object', 78 | 'properties' => $properties, 79 | 'required' => $required, 80 | 'additionalProperties' => false, 81 | '$schema' => 'http://json-schema.org/draft-07/schema#', 82 | ]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Capabilities.php: -------------------------------------------------------------------------------- 1 | hasTools) { 31 | $capabilities->tools = new stdClass(); 32 | } 33 | if ($this->hasResources) { 34 | $capabilities->resources = new stdClass(); 35 | } 36 | if ($this->hasPrompts) { 37 | $capabilities->prompts = new stdClass(); 38 | } 39 | return $capabilities; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 26 | IdGeneratorInterface::class => UniqidIdGenerator::class, 27 | ], 28 | 'listeners' => [ 29 | RegisterCommandListener::class, 30 | RegisterSseRouterListener::class, 31 | ], 32 | 'annotations' => [ 33 | 'scan' => [ 34 | 'collectors' => [ 35 | McpCollector::class, 36 | ], 37 | ], 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Constants.php: -------------------------------------------------------------------------------- 1 | serverName]['_index'][$index] ?? null) { 29 | throw new InvalidArgumentException("{$annotation} index {$index} is exist on {$value->serverName} !"); 30 | } 31 | static::$container[$value->serverName][$annotation][$class][$method] = $value; 32 | static::$container[$value->serverName]['_index'][$index] = ['class' => $class, 'method' => $method, 'annotation' => $value]; 33 | } 34 | 35 | /** 36 | * @param class-string $annotation 37 | */ 38 | public static function getMethodsByAnnotation(string $annotation, string $serverName = Constants::DEFAULT_SERVER_NAME): array 39 | { 40 | $result = []; 41 | foreach (static::$container[$serverName][$annotation] ?? [] as $class => $metadata) { 42 | foreach ($metadata as $method => $value) { 43 | $result[] = ['class' => $class, 'method' => $method, 'annotation' => $value]; 44 | } 45 | } 46 | return $result; 47 | } 48 | 49 | public static function getMethodByIndex(string $index, string $serverName = Constants::DEFAULT_SERVER_NAME): ?array 50 | { 51 | return static::$container[$serverName]['_index'][$index] ?? null; 52 | } 53 | 54 | public static function clear(?string $key = null): void 55 | { 56 | if (! self::$init) { 57 | self::$init = true; 58 | static::$container = []; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Server/Annotation/Server.php: -------------------------------------------------------------------------------- 1 | transport->sendMessage(new ErrorResponse($this->transport->getRequestId(), '2.0', $throwable)); 34 | return (new HttpResponse())->setStatus(202)->setBody(new Stream('Accepted')); 35 | } 36 | 37 | public function isValid(Throwable $throwable): bool 38 | { 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Server/Listener/RegisterCommandListener.php: -------------------------------------------------------------------------------- 1 | $classes */ 44 | $classes = AnnotationCollector::getClassesByAnnotation(Server::class); 45 | 46 | foreach ($classes as $annotation) { 47 | if (! $annotation->signature) { 48 | continue; 49 | } 50 | 51 | $asCommand = new class($this->container, $annotation) extends Command { 52 | protected bool $coroutine = false; 53 | 54 | protected string $serverName; 55 | 56 | public function __construct(protected ContainerInterface $container, Server $annotation) 57 | { 58 | $this->signature = $annotation->signature; 59 | $this->description = $annotation->description; 60 | $this->serverName = $annotation->name; 61 | parent::__construct(); 62 | } 63 | 64 | public function handle(): void 65 | { 66 | $transport = new StdioTransport( 67 | $this->input, 68 | $this->output, 69 | $this->container->get(Packer::class) 70 | ); 71 | $handler = new McpHandler($this->serverName, $this->container); 72 | 73 | while (true) { // @phpstan-ignore while.alwaysTrue 74 | $request = $transport->readMessage(); 75 | if ($response = $handler->handle($request)) { 76 | $transport->sendMessage($response); 77 | } 78 | } 79 | } 80 | }; 81 | 82 | $hash = spl_object_hash($asCommand); 83 | $this->container->set($hash, $asCommand); // @phpstan-ignore method.notFound 84 | $this->appendConfig('commands', $hash); 85 | } 86 | } 87 | 88 | private function appendConfig(string $key, mixed $configValues): void 89 | { 90 | $configs = $this->config->get($key, []); 91 | $configs[] = $configValues; 92 | $this->config->set($key, $configs); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Server/Listener/RegisterSseRouterListener.php: -------------------------------------------------------------------------------- 1 | config->get('server.servers', []) as $name => $server) { 45 | $serverName = $server['name'] ?? $name; 46 | $path = $server['options']['mcp_path'] ?? '/'; 47 | 48 | foreach ($server['callbacks'] ?? [] as $event => $callback) { 49 | [$class] = $callback; 50 | if (is_a($class, McpServer::class, true)) { 51 | $this->registerRouter($serverName, $path); 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | 58 | protected function registerRouter(string $serverName, string $path): void 59 | { 60 | $handler = new McpHandler($serverName, ApplicationContext::getContainer()); 61 | Router::addServer($serverName, function () use ($path, $handler) { 62 | Router::addRoute(['GET', 'POST'], $path, function () use ($path, $handler) { 63 | match (RequestContext::get()->getMethod()) { 64 | 'GET' => $this->transport->register($path), 65 | 'POST' => $this->transport->sendMessage($handler->handle($this->transport->readMessage())), 66 | default => null, 67 | }; 68 | }); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Server/McpHandler.php: -------------------------------------------------------------------------------- 1 | serverName; 33 | switch ($request->method) { 34 | case 'initialize': 35 | $result = [ 36 | 'protocolVersion' => '2024-11-05', 37 | 'capabilities' => new Capabilities( 38 | TypeCollection::getTools($serverName)->isNotEmpty(), 39 | TypeCollection::getResources($serverName)->isNotEmpty(), 40 | TypeCollection::getPrompts($serverName)->isNotEmpty(), 41 | ), 42 | 'serverInfo' => [ 43 | 'name' => $serverName, 44 | 'version' => '1.0.0', 45 | ], 46 | ]; 47 | break; 48 | case 'tools/call': 49 | ['class' => $class, 'method' => $method] = McpCollector::getMethodByIndex($request->params['name'], $serverName); 50 | $class = $this->container->get($class); 51 | $result = $class->{$method}(...$request->params['arguments']); 52 | 53 | $result = ['content' => [['type' => 'text', 'text' => $result]]]; 54 | break; 55 | case 'tools/list': 56 | $result = ['tools' => TypeCollection::getTools($serverName)]; 57 | break; 58 | case 'resources/list': 59 | $result = ['resources' => TypeCollection::getResources($serverName)]; 60 | break; 61 | case 'resources/read': 62 | /** @var Annotation\Resource $annotation */ 63 | ['class' => $class, 'method' => $method, 'annotation' => $annotation] = McpCollector::getMethodByIndex($request->params['uri'], $serverName); 64 | $class = $this->container->get($class); 65 | $result = $class->{$method}(); 66 | 67 | $result = ['content' => [['uri' => $annotation->uri, 'mimeType' => $annotation->mimeType, 'text' => $result]]]; 68 | break; 69 | case 'prompts/list': 70 | $result = ['prompts' => TypeCollection::getPrompts($serverName)]; 71 | break; 72 | case 'prompts/get': 73 | /** @var Annotation\Prompt $annotation */ 74 | ['class' => $class, 'method' => $method, 'annotation' => $annotation] = McpCollector::getMethodByIndex($request->params['name'], $serverName); 75 | $class = $this->container->get($class); 76 | $result = $class->{$method}(...$request->params['arguments']); 77 | 78 | $result = ['messages' => [['role' => $annotation->role, 'content' => ['type' => 'text', 'text' => $result]]]]; 79 | break; 80 | case 'notifications/initialized': 81 | default: 82 | return null; 83 | } 84 | 85 | return new Response($request->id, $request->jsonrpc, $result); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Server/McpServer.php: -------------------------------------------------------------------------------- 1 | resume(); 27 | } 28 | 29 | public function getVersion(): string 30 | { 31 | return $this->version; 32 | } 33 | 34 | protected function getDefaultExceptionHandler(): array 35 | { 36 | return [ 37 | McpSseExceptionHandler::class, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Server/Protocol/Packer.php: -------------------------------------------------------------------------------- 1 | > 38 | */ 39 | public array $fdMaps = []; 40 | 41 | public function __construct( 42 | protected RequestInterface $request, 43 | protected ResponseInterface $response, 44 | protected Packer $packer, 45 | protected IdGeneratorInterface $idGenerator, 46 | protected StdoutLoggerInterface $logger, 47 | ) { 48 | } 49 | 50 | public function sendMessage(?MessageInterface $message): void 51 | { 52 | if (! $message) { 53 | return; 54 | } 55 | $result = $this->packer->pack($message); 56 | $fd = $this->fdMaps[$this->getServerName()][$this->request->input('sessionId')]; 57 | $this->connections[$this->getServerName()][$fd]->write("event: message\ndata: {$result}\n\n"); 58 | } 59 | 60 | public function readMessage(): Notification|Request 61 | { 62 | $message = $this->packer->unpack($this->request->getBody()->getContents()); 63 | if (! isset($message['id'])) { 64 | return new Notification(...$message); 65 | } 66 | return new Request(...$message); 67 | } 68 | 69 | public function register(string $path): void 70 | { 71 | $serverName = $this->getServerName(); 72 | $sessionId = $this->idGenerator->generate(); 73 | $fd = $this->getFd(); 74 | 75 | $this->logger->debug("McpSSE Request {$serverName} {$fd} {$sessionId}"); 76 | 77 | $eventStream = (new EventStream($this->response->getConnection())) // @phpstan-ignore method.notFound 78 | ->write('event: endpoint' . PHP_EOL) 79 | ->write("data: {$path}?sessionId={$sessionId}" . PHP_EOL . PHP_EOL); 80 | 81 | $this->connections[$serverName][$fd] = $eventStream; 82 | $this->fdMaps[$serverName][$sessionId] = $fd; 83 | 84 | CoordinatorManager::until("fd:{$fd}")->yield(); 85 | 86 | unset($this->connections[$serverName][$fd], $this->fdMaps[$serverName][$sessionId]); 87 | } 88 | 89 | public function getRequestId(): int 90 | { 91 | $data = $this->packer->unpack($this->request->getBody()->getContents()); 92 | return $data['id']; 93 | } 94 | 95 | private function getFd(): int 96 | { 97 | return RequestContext::get()->getSwooleRequest()->fd; // @phpstan-ignore method.notFound 98 | } 99 | 100 | private function getServerName(): string 101 | { 102 | return $this->request->getAttribute(Dispatched::class)->serverName; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Server/Transport/StdioTransport.php: -------------------------------------------------------------------------------- 1 | helper = new QuestionHelper(); 35 | } 36 | 37 | public function sendMessage(MessageInterface $message): void 38 | { 39 | $this->output->writeln($this->packer->pack($message)); 40 | } 41 | 42 | public function readMessage(): Notification|Request 43 | { 44 | $message = $this->helper->ask($this->input, $this->output, new Question('')); 45 | $message = $this->packer->unpack($message); 46 | if (! isset($message['id'])) { 47 | return new Notification(...$message); 48 | } 49 | return new Request(...$message); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/TypeCollection.php: -------------------------------------------------------------------------------- 1 | $annotation 30 | */ 31 | public static function getCollection(string $serverName, string $annotation): Collection 32 | { 33 | if (isset(self::$collections[$serverName][$annotation])) { 34 | return self::$collections[$serverName][$annotation]; 35 | } 36 | 37 | $classes = McpCollector::getMethodsByAnnotation($annotation, $serverName); 38 | 39 | self::$collections[$serverName][$annotation] = new Collection(); 40 | 41 | foreach ($classes as $class) { 42 | /* @var array{class: string, method: string, annotation: AbstractMcpAnnotation} $class */ 43 | self::$collections[$serverName][$annotation]->push($class['annotation']->toSchema()); 44 | } 45 | return self::$collections[$serverName][$annotation]; 46 | } 47 | 48 | public static function getTools(string $serverName): Collection 49 | { 50 | return self::getCollection($serverName, Tool::class); 51 | } 52 | 53 | public static function getResources(string $serverName): Collection 54 | { 55 | return self::getCollection($serverName, Resource::class); 56 | } 57 | 58 | public static function getPrompts(string $serverName): Collection 59 | { 60 | return self::getCollection($serverName, Prompt::class); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Types/Message/ErrorResponse.php: -------------------------------------------------------------------------------- 1 | result['content']) 27 | ) { 28 | $this->result['content'] = array_map(function ($item) { 29 | if (isset($item['text'], $item['type']) && $item['type'] === 'text') { 30 | $item['text'] = json_encode($item['text'], JSON_UNESCAPED_UNICODE); 31 | } 32 | 33 | return $item; 34 | }, $this->result['content']); 35 | } 36 | 37 | return [ 38 | 'id' => $this->id, 39 | 'jsonrpc' => $this->jsonrpc, 40 | 'result' => $this->result, 41 | ]; 42 | } 43 | } 44 | --------------------------------------------------------------------------------