feat(api): enrich MCP server details on demand
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
2cfa970993
commit
1a8fafeec5
3 changed files with 213 additions and 0 deletions
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
114
src/php/src/Api/Tests/Feature/McpServerDetailTest.php
Normal file
114
src/php/src/Api/Tests/Feature/McpServerDetailTest.php
Normal 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);
|
||||
});
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue