feat(api): enrich MCP server details on demand

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 22:09:22 +00:00
parent 2cfa970993
commit 1a8fafeec5
3 changed files with 213 additions and 0 deletions

View file

@ -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<int, array<string, mixed>> $tools
* @return array<int, array<string, mixed>>
*/
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<int, array<string, mixed>> $resources
* @return array<int, array<string, mixed>>
*/
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.
*

View file

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
use Core\Mod\Mcp\Services\ToolVersionService;
use Illuminate\Support\Facades\Cache;
use Mod\Api\Models\ApiKey;
use Mod\Tenant\Models\User;
use Mod\Tenant\Models\Workspace;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
$this->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, <<<YAML
id: test-detail-server
name: Test Detail Server
status: available
tools:
- name: search
description: Search records
inputSchema:
type: object
properties:
query:
type: string
required:
- query
resources:
- uri: test-detail-server://documents/welcome
path: documents/welcome
name: welcome
content:
message: Hello from the server detail endpoint
version: 2
YAML);
app()->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);
});

View file

@ -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 () {