*/ protected array $supportedProtocolVersion = [ '2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', ]; /** * @var array|stdClass|string> */ protected array $capabilities = [ self::CAPABILITY_TOOLS => [ 'listChanged' => false, ], self::CAPABILITY_RESOURCES => [ 'listChanged' => false, ], self::CAPABILITY_PROMPTS => [ 'listChanged' => false, ], ]; /** * @var array> */ protected array $tools = []; /** * @var array> */ protected array $resources = []; /** * @var array> */ protected array $prompts = []; public int $maxPaginationLength = 50; public int $defaultPaginationLength = 15; /** * @var array> */ protected array $methods = [ 'tools/list' => ListTools::class, 'tools/call' => CallTool::class, 'resources/list' => ListResources::class, 'resources/read' => ReadResource::class, 'resources/templates/list' => ListResourceTemplates::class, 'prompts/list' => ListPrompts::class, 'prompts/get' => GetPrompt::class, 'completion/complete' => CompletionComplete::class, 'ping' => Ping::class, ]; public function __construct( protected Transport $transport, ) { // } /** * Add or modify a server capability. * * Using dot notation like "feature.enabled" will create a nested capability array. * Passing a single key like "anotherFeature" will register an empty object capability. */ public function addCapability(string $key, bool $value = true): void { if (str_contains($key, '.')) { [$root, $child] = explode('.', $key, 2); $existing = $this->capabilities[$root] ?? []; if (! is_array($existing)) { $existing = []; } $existing[$child] = $value; $this->capabilities[$root] = $existing; return; } // Represent empty capability as an object when JSON encoded $this->capabilities[$key] = (object) []; } /** * Register a custom JSON-RPC method handler. * * @param class-string $handler */ public function addMethod(string $method, string $handler): void { $this->methods[$method] = $handler; } public function start(): void { $this->boot(); $this->detectUiCapability(); $this->transport->onReceive($this->handle(...)); } protected function boot(): void { // } public function handle(string $rawMessage): void { $context = $this->createContext(); try { $jsonRequest = json_decode($rawMessage, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new JsonRpcException('Parse error: Invalid JSON was received by the server.', -32700); } $request = isset($jsonRequest['id']) ? JsonRpcRequest::from($jsonRequest, $this->transport->sessionId()) : JsonRpcNotification::from($jsonRequest); if ($request instanceof JsonRpcNotification) { return; } if ($request->method === 'initialize') { $this->handleInitializeMessage($request, $context); return; } if (! isset($this->methods[$request->method])) { throw new JsonRpcException( "The method [{$request->method}] was not found.", -32601, $request->id, ); } $this->handleMessage($request, $context); } catch (JsonRpcException $e) { $this->transport->send($e->toJsonRpcResponse()->toJson()); } catch (Throwable $e) { report($e); $config = Container::getInstance()->make('config'); if ($config->get('app.debug', false)) { throw $e; } $jsonRpcResponse = JsonRpcResponse::error( $request->id ?? null, -32603, 'Something went wrong while processing the request.', ); $this->transport->send($jsonRpcResponse->toJson()); } } public function createContext(): ServerContext { $name = $this->resolveAttribute(Name::class); $version = $this->resolveAttribute(Version::class); $instructions = $this->resolveAttribute(Instructions::class); return new ServerContext( supportedProtocolVersions: $this->supportedProtocolVersion, serverCapabilities: $this->capabilities, serverName: $name !== null ? $name->value : $this->name, serverVersion: $version !== null ? $version->value : $this->version, instructions: $instructions !== null ? $instructions->value : $this->instructions, maxPaginationLength: $this->maxPaginationLength, defaultPaginationLength: $this->defaultPaginationLength, tools: $this->tools, resources: $this->resources, prompts: $this->prompts, ); } /** * @throws JsonRpcException */ protected function handleMessage(JsonRpcRequest $request, ServerContext $context): void { $response = $this->runMethodHandle($request, $context); if (! is_iterable($response)) { $this->transport->send($response->toJson()); return; } $this->transport->stream(function () use ($response): void { foreach ($response as $message) { $this->transport->send($message->toJson()); } }); } /** * @return iterable|JsonRpcResponse * * @throws JsonRpcException */ protected function runMethodHandle(JsonRpcRequest $request, ServerContext $context): iterable|JsonRpcResponse { $container = Container::getInstance(); /** @var Method $methodClass */ $methodClass = $container->make( $this->methods[$request->method], ); $container->instance('mcp.request', $request->toRequest()); try { $response = $methodClass->handle($request, $context); } finally { $container->forgetInstance('mcp.request'); } return $response; } protected function handleInitializeMessage(JsonRpcRequest $request, ServerContext $context): void { $response = (new Initialize)->handle($request, $context); $sessionId = $this->generateSessionId(); Container::getInstance()->make('events')->dispatch(new SessionInitialized( sessionId: $sessionId, clientInfo: $request->params['clientInfo'] ?? null, protocolVersion: $request->params['protocolVersion'] ?? null, clientCapabilities: $request->params['capabilities'] ?? null, )); $this->transport->send($response->toJson(), $sessionId); } protected function generateSessionId(): string { return Str::uuid()->toString(); } protected function detectUiCapability(): void { if (array_key_exists(self::CAPABILITY_UI, $this->capabilities)) { return; } foreach ($this->resources as $resource) { if (is_subclass_of($resource, AppResource::class)) { $this->addCapability(self::CAPABILITY_UI); return; } } } /** * @param array $arguments */ public static function __callStatic(string $name, array $arguments): PendingTestResponse|TestResponse { $pendingTestResponse = new PendingTestResponse( Container::getInstance(), static::class, ); return $pendingTestResponse->$name(...$arguments); } }