From 1a8fafeec5268807aafe6b91230cada491d389ce Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 22:09:22 +0000 Subject: [PATCH] feat(api): enrich MCP server details on demand Co-Authored-By: Virgil --- .../src/Api/Controllers/McpApiController.php | 87 +++++++++++++ .../Api/Tests/Feature/McpServerDetailTest.php | 114 ++++++++++++++++++ .../OpenApiDocumentationComprehensiveTest.php | 12 ++ 3 files changed, 213 insertions(+) create mode 100644 src/php/src/Api/Tests/Feature/McpServerDetailTest.php diff --git a/src/php/src/Api/Controllers/McpApiController.php b/src/php/src/Api/Controllers/McpApiController.php index d50fd5f..4ccf6af 100644 --- a/src/php/src/Api/Controllers/McpApiController.php +++ b/src/php/src/Api/Controllers/McpApiController.php @@ -51,7 +51,29 @@ class McpApiController extends Controller * Get server details with tools and resources. * * GET /api/v1/mcp/servers/{id} + * + * Query params: + * - include_versions: bool - include version info for each tool + * - include_content: bool - include resource content when the definition already contains it */ + #[ApiParameter( + name: 'include_versions', + in: 'query', + type: 'boolean', + description: 'Include version information for each tool', + required: false, + example: false, + default: false + )] + #[ApiParameter( + name: 'include_content', + in: 'query', + type: 'boolean', + description: 'Include resource content when the definition already contains it', + required: false, + example: false, + default: false + )] public function server(Request $request, string $id): JsonResponse { $server = $this->loadServerFull($id); @@ -60,6 +82,14 @@ class McpApiController extends Controller return $this->notFoundResponse('Server'); } + if ($request->boolean('include_versions', false)) { + $server['tools'] = $this->enrichToolsWithVersioning($id, $server['tools'] ?? []); + } + + if ($request->boolean('include_content', false)) { + $server['resources'] = $this->enrichResourcesWithContent($server['resources'] ?? []); + } + return response()->json($server); } @@ -173,6 +203,63 @@ class McpApiController extends Controller ]); } + /** + * Enrich a tool collection with version metadata. + * + * @param array> $tools + * @return array> + */ + protected function enrichToolsWithVersioning(string $serverId, array $tools): array + { + $versionService = app(ToolVersionService::class); + + return collect($tools)->map(function (array $tool) use ($serverId, $versionService) { + $toolName = $tool['name'] ?? ''; + $latestVersion = $versionService->getLatestVersion($serverId, $toolName); + + $tool['versioning'] = [ + 'latest_version' => $latestVersion?->version ?? ToolVersionService::DEFAULT_VERSION, + 'is_versioned' => $latestVersion !== null, + 'deprecated' => $latestVersion?->is_deprecated ?? false, + ]; + + if ($latestVersion?->input_schema) { + $tool['inputSchema'] = $latestVersion->input_schema; + } + + return $tool; + })->all(); + } + + /** + * Enrich a resource collection with inline content when available. + * + * @param array> $resources + * @return array> + */ + protected function enrichResourcesWithContent(array $resources): array + { + return collect($resources) + ->filter(fn ($resource) => is_array($resource)) + ->map(function (array $resource) { + $payload = array_filter([ + 'uri' => $resource['uri'] ?? null, + 'path' => $resource['path'] ?? null, + 'name' => $resource['name'] ?? null, + 'description' => $resource['description'] ?? null, + 'mime_type' => $resource['mime_type'] ?? ($resource['mimeType'] ?? null), + ], static fn ($value) => $value !== null); + + if ($this->resourceDefinitionHasContent($resource)) { + $payload['content'] = $this->normaliseResourceContent($resource); + } + + return $payload; + }) + ->values() + ->all(); + } + /** * Execute a tool on an MCP server. * diff --git a/src/php/src/Api/Tests/Feature/McpServerDetailTest.php b/src/php/src/Api/Tests/Feature/McpServerDetailTest.php new file mode 100644 index 0000000..58b3990 --- /dev/null +++ b/src/php/src/Api/Tests/Feature/McpServerDetailTest.php @@ -0,0 +1,114 @@ +user = User::factory()->create(); + $this->workspace = Workspace::factory()->create(); + $this->workspace->users()->attach($this->user->id, [ + 'role' => 'owner', + 'is_default' => true, + ]); + + $result = ApiKey::generate( + $this->workspace->id, + $this->user->id, + 'MCP Server Detail Key', + [ApiKey::SCOPE_READ, ApiKey::SCOPE_WRITE] + ); + + $this->plainKey = $result['plain_key']; + + $this->serverId = 'test-detail-server'; + $this->serverDir = resource_path('mcp/servers'); + $this->serverFile = $this->serverDir.'/'.$this->serverId.'.yaml'; + + if (! is_dir($this->serverDir)) { + mkdir($this->serverDir, 0777, true); + } + + file_put_contents($this->serverFile, <<instance(ToolVersionService::class, new class + { + public function getLatestVersion(string $serverId, string $toolName): object + { + return (object) [ + 'version' => '2.1.0', + 'is_deprecated' => false, + 'input_schema' => [ + 'type' => 'object', + 'properties' => [ + 'query' => [ + 'type' => 'string', + ], + ], + 'required' => ['query'], + ], + ]; + } + }); +}); + +afterEach(function () { + Cache::flush(); + + if (isset($this->serverFile) && is_file($this->serverFile)) { + unlink($this->serverFile); + } + + if (isset($this->serverDir) && is_dir($this->serverDir)) { + @rmdir($this->serverDir); + } + + $mcpDir = dirname($this->serverDir ?? ''); + if (is_dir($mcpDir)) { + @rmdir($mcpDir); + } +}); + +it('includes tool versions and resource content on server detail requests when requested', function () { + $response = $this->getJson('/api/mcp/servers/test-detail-server?include_versions=1&include_content=1', [ + 'Authorization' => "Bearer {$this->plainKey}", + ]); + + $response->assertOk(); + $response->assertJsonPath('id', 'test-detail-server'); + $response->assertJsonPath('tools.0.name', 'search'); + $response->assertJsonPath('tools.0.versioning.latest_version', '2.1.0'); + $response->assertJsonPath('tools.0.inputSchema.required.0', 'query'); + $response->assertJsonPath('resources.0.uri', 'test-detail-server://documents/welcome'); + $response->assertJsonPath('resources.0.content.message', 'Hello from the server detail endpoint'); + $response->assertJsonPath('resources.0.content.version', 2); +}); diff --git a/src/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php b/src/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php index 1713550..85518b9 100644 --- a/src/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php +++ b/src/php/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.php @@ -231,6 +231,18 @@ describe('Application Endpoint Parameter Docs', function () { expect($includeContent)->not->toBeNull(); expect($includeContent['in'])->toBe('query'); expect($includeContent['schema']['type'])->toBe('boolean'); + + $serverOperation = $spec['paths']['/api/mcp/servers/{id}']['get']; + $serverIncludeVersions = collect($serverOperation['parameters'] ?? [])->firstWhere('name', 'include_versions'); + $serverIncludeContent = collect($serverOperation['parameters'] ?? [])->firstWhere('name', 'include_content'); + + expect($serverIncludeVersions)->not->toBeNull(); + expect($serverIncludeVersions['in'])->toBe('query'); + expect($serverIncludeVersions['schema']['type'])->toBe('boolean'); + + expect($serverIncludeContent)->not->toBeNull(); + expect($serverIncludeContent['in'])->toBe('query'); + expect($serverIncludeContent['schema']['type'])->toBe('boolean'); }); it('documents the MCP tool call request body shape', function () {