Skip to main content
Glama
McpRequestHandler.php9.19 kB
<?php declare(strict_types=1); namespace GoldenPathDigital\LaravelAscend\Server\Mcp; use GoldenPathDigital\LaravelAscend\Server\AscendServer; final class McpRequestHandler { /** @var AscendServer */ private $ascendServer; private bool $initialized = false; public function __construct( AscendServer $ascendServer ) { $this->ascendServer = $ascendServer; } public function handleRaw(string $payload): string { $decoded = json_decode($payload, true); if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) { return $this->encodeError(null, -32700, 'Parse error: ' . json_last_error_msg()); } if (is_array($decoded) && $this->isArrayList($decoded)) { $responses = array_values(array_filter( array_map(fn ($request) => $this->handleRequest($request), $decoded), static fn ($response) => $response !== null, )); return $responses === [] ? '' : json_encode($responses, JSON_THROW_ON_ERROR); } $response = $this->handleRequest($decoded); if ($response === null) { return ''; } return json_encode($response, JSON_THROW_ON_ERROR); } /** * @param array<string, mixed>|null $request * * @return array<string, mixed>|null */ public function handleRequest(?array $request): ?array { $id = $request['id'] ?? null; if ($request === null || ($request['jsonrpc'] ?? null) !== '2.0') { return $this->errorResponse($id, -32600, 'Invalid Request'); } $method = $request['method'] ?? null; $params = $request['params'] ?? []; if (!is_string($method)) { return $this->errorResponse($id, -32601, 'Method not found'); } if (!array_key_exists('id', $request)) { // Notifications are intentionally ignored. $this->dispatchNotification($method, $params); return null; } try { if ($method === 'initialize') { $result = $this->initialize($params); } elseif ($method === 'ping') { $result = ['status' => 'ok']; } elseif ($method === 'tools/list') { $result = $this->listTools($params); } elseif ($method === 'tools/call') { $result = $this->callTool($params); } elseif ($method === 'resources/list') { $result = $this->listResources($params); } elseif ($method === 'resources/read') { $result = $this->readResource($params); } elseif ($method === 'prompts/list') { $result = $this->listPrompts($params); } else { $result = null; } } catch (\Throwable $exception) { return $this->errorResponse($id, -32603, $exception->getMessage()); } if ($result === null) { return $this->errorResponse($id, -32601, 'Method not found'); } return [ 'jsonrpc' => '2.0', 'id' => $id, 'result' => $result, ]; } /** * @param mixed $id * @return array<string, mixed> */ private function errorResponse($id, int $code, string $message): array { return [ 'jsonrpc' => '2.0', 'id' => $id, 'error' => [ 'code' => $code, 'message' => $message, ], ]; } /** * @param mixed $id */ private function encodeError($id, int $code, string $message): string { return json_encode($this->errorResponse($id, $code, $message), JSON_THROW_ON_ERROR); } /** * @param mixed $params */ private function dispatchNotification(string $method, $params): void { if ($method === 'ping') { return; } if ($method === 'tools/listChanged') { return; } } /** * Polyfill for array_is_list (PHP 8.1+) * * @param array<mixed> $array */ private function isArrayList(array $array): bool { if (function_exists('array_is_list')) { return array_is_list($array); } $i = 0; foreach ($array as $k => $v) { if ($k !== $i++) { return false; } } return true; } /** * @param array<string, mixed> $params * @return array<string, mixed> */ private function initialize(array $params): array { $requestedVersion = $params['protocolVersion'] ?? null; $supported = $this->ascendServer->getSupportedProtocolVersions(); // If a version is requested, validate it's in a reasonable format (YYYY-MM-DD) // and accept it even if not explicitly in our list for forward compatibility if ($requestedVersion !== null) { // Check if it matches the MCP date-based version format if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $requestedVersion)) { throw new \InvalidArgumentException('Invalid protocol version format. Expected YYYY-MM-DD format.'); } // Use the requested version for forward/backward compatibility $protocolVersion = $requestedVersion; } else { // If no version requested, use our most recent supported version $protocolVersion = $supported[0]; } $this->initialized = true; return [ 'protocolVersion' => $protocolVersion, 'serverInfo' => [ 'name' => $this->ascendServer->getServerName(), 'version' => $this->ascendServer->getServerVersion(), ], 'capabilities' => $this->ascendServer->getCapabilities(), 'instructions' => $this->ascendServer->getInstructions(), ]; } private function ensureInitialized(): void { if (!$this->initialized) { throw new \RuntimeException('Server has not been initialized. Call initialize first.'); } } /** * @param array<string, mixed> $params * @return array<string, mixed> */ private function listTools(array $params): array { $this->ensureInitialized(); return [ 'tools' => $this->ascendServer->describeTools(), ]; } /** * @param array<string, mixed> $params * @return array<string, mixed> */ private function callTool(array $params): array { $this->ensureInitialized(); $toolName = $params['name'] ?? null; $arguments = $params['arguments'] ?? []; if (!is_string($toolName)) { throw new \InvalidArgumentException('Parameter "name" is required.'); } if (!is_array($arguments)) { throw new \InvalidArgumentException('Parameter "arguments" must be an object.'); } $result = $this->ascendServer->callTool($toolName, $arguments); $isError = isset($result['ok']) ? $result['ok'] === false : false; // Convert result to text format as required by MCP spec $content = [ [ 'type' => 'text', 'text' => json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), ], ]; if (isset($result['stream']) && is_array($result['stream'])) { foreach ($result['stream'] as $chunk) { $content[] = $chunk; } } return [ 'content' => $content, 'isError' => $isError, ]; } /** * @param array<string, mixed> $params * @return array<string, mixed> */ private function listResources(array $params): array { $this->ensureInitialized(); return [ 'resources' => $this->ascendServer->describeResources(), ]; } /** * @param array<string, mixed> $params * @return array<string, mixed> */ private function readResource(array $params): array { $this->ensureInitialized(); $uri = $params['uri'] ?? null; if (!is_string($uri)) { throw new \InvalidArgumentException('Parameter "uri" is required.'); } $resource = $this->ascendServer->readResource($uri); if ($resource === null) { throw new \RuntimeException("Resource not found: {$uri}"); } return [ 'contents' => [ [ 'uri' => $resource['uri'] ?? $uri, 'mimeType' => $resource['mimeType'] ?? 'text/plain', 'text' => $resource['content'] ?? '', ], ], ]; } /** * @param array<string, mixed> $params * @return array<string, mixed> */ private function listPrompts(array $params): array { $this->ensureInitialized(); return [ 'prompts' => $this->ascendServer->describePrompts(), ]; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aarongrtech/laravel-ascend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server