forked from core/php-mcp
Compare commits
1 commit
main
...
feat/test-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c908fff193 |
1 changed files with 793 additions and 0 deletions
793
tests/Unit/ToolRegistryTest.php
Normal file
793
tests/Unit/ToolRegistryTest.php
Normal file
|
|
@ -0,0 +1,793 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Core MCP Package
|
||||||
|
*
|
||||||
|
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||||
|
* See LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Core\Mcp\Services\ToolRegistry;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Helper: build a ToolRegistry partial mock
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The registry loads YAML from disk via protected methods. We mock those
|
||||||
|
| methods so we can inject deterministic data without touching the
|
||||||
|
| filesystem. Cache::shouldReceive is used to bypass the cache layer.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
function makeRegistry(array $registry = [], array $servers = []): ToolRegistry
|
||||||
|
{
|
||||||
|
$mock = Mockery::mock(ToolRegistry::class)->makePartial()->shouldAllowMockingProtectedMethods();
|
||||||
|
|
||||||
|
$mock->shouldReceive('loadRegistry')->andReturn($registry);
|
||||||
|
|
||||||
|
foreach ($servers as $id => $config) {
|
||||||
|
$mock->shouldReceive('loadServerFull')->with($id)->andReturn($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: unknown server IDs return null
|
||||||
|
$mock->shouldReceive('loadServerFull')->andReturn(null)->byDefault();
|
||||||
|
|
||||||
|
return $mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fixture data
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
function sampleRegistry(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'servers' => [
|
||||||
|
['id' => 'workspace-tools'],
|
||||||
|
['id' => 'billing-tools'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sampleServer(string $id = 'workspace-tools'): array
|
||||||
|
{
|
||||||
|
return match ($id) {
|
||||||
|
'workspace-tools' => [
|
||||||
|
'id' => 'workspace-tools',
|
||||||
|
'name' => 'Workspace Tools',
|
||||||
|
'tagline' => 'Database and system utilities',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'query_database',
|
||||||
|
'description' => 'Execute a read-only SQL SELECT query',
|
||||||
|
'category' => 'database',
|
||||||
|
'inputSchema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'query' => ['type' => 'string'],
|
||||||
|
'explain' => ['type' => 'boolean', 'default' => false],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'list_tables',
|
||||||
|
'description' => 'List all database tables',
|
||||||
|
'category' => 'database',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'list_routes',
|
||||||
|
'description' => 'List application routes',
|
||||||
|
'category' => 'system',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'billing-tools' => [
|
||||||
|
'id' => 'billing-tools',
|
||||||
|
'name' => 'Billing Tools',
|
||||||
|
'tagline' => 'Invoice and subscription management',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'list_invoices',
|
||||||
|
'description' => 'List invoices by status',
|
||||||
|
'category' => 'commerce',
|
||||||
|
'inputSchema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'status' => ['type' => 'string', 'enum' => ['paid', 'unpaid', 'overdue']],
|
||||||
|
'limit' => ['type' => 'integer', 'default' => 25],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'get_billing_status',
|
||||||
|
'description' => 'Get current billing status',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('ToolRegistry', function () {
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// Ensure cache is bypassed for every test — return null so the
|
||||||
|
// callback inside Cache::remember always executes.
|
||||||
|
Cache::shouldReceive('remember')->andReturnUsing(
|
||||||
|
fn (string $key, int $ttl, Closure $callback) => $callback()
|
||||||
|
);
|
||||||
|
Cache::shouldReceive('forget')->andReturn(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// getServers()
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('getServers', function () {
|
||||||
|
it('returns a collection of server summaries', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
[
|
||||||
|
'workspace-tools' => sampleServer('workspace-tools'),
|
||||||
|
'billing-tools' => sampleServer('billing-tools'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$servers = $registry->getServers();
|
||||||
|
|
||||||
|
expect($servers)->toHaveCount(2);
|
||||||
|
expect($servers[0]['id'])->toBe('workspace-tools');
|
||||||
|
expect($servers[0]['name'])->toBe('Workspace Tools');
|
||||||
|
expect($servers[0]['tagline'])->toBe('Database and system utilities');
|
||||||
|
expect($servers[0]['tool_count'])->toBe(3);
|
||||||
|
|
||||||
|
expect($servers[1]['id'])->toBe('billing-tools');
|
||||||
|
expect($servers[1]['tool_count'])->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty collection when no servers are registered', function () {
|
||||||
|
$registry = makeRegistry(['servers' => []], []);
|
||||||
|
|
||||||
|
$servers = $registry->getServers();
|
||||||
|
|
||||||
|
expect($servers)->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty collection when registry has no servers key', function () {
|
||||||
|
$registry = makeRegistry([], []);
|
||||||
|
|
||||||
|
$servers = $registry->getServers();
|
||||||
|
|
||||||
|
expect($servers)->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters out servers whose YAML file is missing', function () {
|
||||||
|
// billing-tools has no corresponding server config
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
// loadServerFull for billing-tools will return null (default)
|
||||||
|
$servers = $registry->getServers();
|
||||||
|
|
||||||
|
expect($servers)->toHaveCount(1);
|
||||||
|
expect($servers[0]['id'])->toBe('workspace-tools');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// getToolsForServer()
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('getToolsForServer', function () {
|
||||||
|
it('returns tools with expected shape', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('workspace-tools');
|
||||||
|
|
||||||
|
expect($tools)->toHaveCount(3);
|
||||||
|
|
||||||
|
$first = $tools[0];
|
||||||
|
expect($first)->toHaveKeys(['name', 'description', 'category', 'inputSchema', 'examples', 'version']);
|
||||||
|
expect($first['name'])->toBe('query_database');
|
||||||
|
expect($first['description'])->toBe('Execute a read-only SQL SELECT query');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns built-in examples for known tools', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('workspace-tools');
|
||||||
|
$queryDb = $tools->firstWhere('name', 'query_database');
|
||||||
|
|
||||||
|
// query_database has predefined examples in the registry
|
||||||
|
expect($queryDb['examples'])->toBe([
|
||||||
|
'query' => 'SELECT id, name FROM users LIMIT 10',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates examples from schema when no built-in examples exist', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'custom-server',
|
||||||
|
'name' => 'Custom',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'custom_tool',
|
||||||
|
'description' => 'A custom tool',
|
||||||
|
'inputSchema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'name' => ['type' => 'string', 'default' => 'hello'],
|
||||||
|
'count' => ['type' => 'integer', 'minimum' => 5],
|
||||||
|
'active' => ['type' => 'boolean'],
|
||||||
|
'tags' => ['type' => 'array'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'custom-server']]],
|
||||||
|
['custom-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('custom-server');
|
||||||
|
$examples = $tools[0]['examples'];
|
||||||
|
|
||||||
|
expect($examples['name'])->toBe('hello'); // default
|
||||||
|
expect($examples['count'])->toBe(5); // minimum
|
||||||
|
expect($examples['active'])->toBeFalse(); // boolean default
|
||||||
|
expect($examples['tags'])->toBe([]); // array default
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty collection for unknown server', function () {
|
||||||
|
$registry = makeRegistry(sampleRegistry(), []);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('nonexistent');
|
||||||
|
|
||||||
|
expect($tools)->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts category from tool definition', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('workspace-tools');
|
||||||
|
|
||||||
|
expect($tools[0]['category'])->toBe('Database');
|
||||||
|
expect($tools[2]['category'])->toBe('System');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses purpose field as description fallback', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'test-server',
|
||||||
|
'name' => 'Test',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'my_tool',
|
||||||
|
'purpose' => 'A fallback description',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'test-server']]],
|
||||||
|
['test-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('test-server');
|
||||||
|
|
||||||
|
expect($tools[0]['description'])->toBe('A fallback description');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds inputSchema from parameters when inputSchema is absent', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'test-server',
|
||||||
|
'name' => 'Test',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'legacy_tool',
|
||||||
|
'description' => 'Uses legacy parameters key',
|
||||||
|
'parameters' => [
|
||||||
|
'query' => ['type' => 'string'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'test-server']]],
|
||||||
|
['test-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('test-server');
|
||||||
|
|
||||||
|
expect($tools[0]['inputSchema'])->toBe([
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => ['query' => ['type' => 'string']],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// getTool() — single tool lookup
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('getTool', function () {
|
||||||
|
it('returns a single tool by name', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tool = $registry->getTool('workspace-tools', 'list_tables');
|
||||||
|
|
||||||
|
expect($tool)->not->toBeNull();
|
||||||
|
expect($tool['name'])->toBe('list_tables');
|
||||||
|
expect($tool['description'])->toBe('List all database tables');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for unknown tool name', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tool = $registry->getTool('workspace-tools', 'does_not_exist');
|
||||||
|
|
||||||
|
expect($tool)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when server does not exist', function () {
|
||||||
|
$registry = makeRegistry(sampleRegistry(), []);
|
||||||
|
|
||||||
|
$tool = $registry->getTool('nonexistent', 'query_database');
|
||||||
|
|
||||||
|
expect($tool)->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// getToolsByCategory()
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('getToolsByCategory', function () {
|
||||||
|
it('groups tools by their category', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$categories = $registry->getToolsByCategory('workspace-tools');
|
||||||
|
|
||||||
|
expect($categories)->toHaveKey('Database');
|
||||||
|
expect($categories)->toHaveKey('System');
|
||||||
|
expect($categories['Database'])->toHaveCount(2);
|
||||||
|
expect($categories['System'])->toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty collection for unknown server', function () {
|
||||||
|
$registry = makeRegistry(sampleRegistry(), []);
|
||||||
|
|
||||||
|
$categories = $registry->getToolsByCategory('nonexistent');
|
||||||
|
|
||||||
|
expect($categories)->toBeEmpty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// searchTools()
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('searchTools', function () {
|
||||||
|
it('finds tools by name substring', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', 'list');
|
||||||
|
|
||||||
|
expect($results)->toHaveCount(2);
|
||||||
|
expect($results->pluck('name')->toArray())->toContain('list_tables', 'list_routes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds tools by description substring', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', 'SQL');
|
||||||
|
|
||||||
|
expect($results)->toHaveCount(1);
|
||||||
|
expect($results[0]['name'])->toBe('query_database');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds tools by category substring', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', 'system');
|
||||||
|
|
||||||
|
expect($results)->toHaveCount(1);
|
||||||
|
expect($results[0]['name'])->toBe('list_routes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is case insensitive', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', 'QUERY');
|
||||||
|
|
||||||
|
expect($results)->toHaveCount(1);
|
||||||
|
expect($results[0]['name'])->toBe('query_database');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all tools when query is empty', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', '');
|
||||||
|
|
||||||
|
expect($results)->toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns all tools when query is whitespace', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', ' ');
|
||||||
|
|
||||||
|
expect($results)->toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty collection for no matches', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $registry->searchTools('workspace-tools', 'zzz_nonexistent');
|
||||||
|
|
||||||
|
expect($results)->toBeEmpty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Example input management
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('example inputs', function () {
|
||||||
|
it('returns predefined examples for known tools', function () {
|
||||||
|
$registry = new ToolRegistry();
|
||||||
|
|
||||||
|
expect($registry->getExampleInputs('query_database'))->toBe([
|
||||||
|
'query' => 'SELECT id, name FROM users LIMIT 10',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($registry->getExampleInputs('create_coupon'))->toBe([
|
||||||
|
'code' => 'SUMMER25',
|
||||||
|
'discount_type' => 'percentage',
|
||||||
|
'discount_value' => 25,
|
||||||
|
'expires_at' => '2025-12-31',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array for unknown tools', function () {
|
||||||
|
$registry = new ToolRegistry();
|
||||||
|
|
||||||
|
expect($registry->getExampleInputs('totally_unknown'))->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows setting custom examples', function () {
|
||||||
|
$registry = new ToolRegistry();
|
||||||
|
|
||||||
|
$registry->setExampleInputs('my_tool', ['foo' => 'bar', 'count' => 42]);
|
||||||
|
|
||||||
|
expect($registry->getExampleInputs('my_tool'))->toBe(['foo' => 'bar', 'count' => 42]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrites existing examples', function () {
|
||||||
|
$registry = new ToolRegistry();
|
||||||
|
|
||||||
|
$registry->setExampleInputs('query_database', ['query' => 'SELECT 1']);
|
||||||
|
|
||||||
|
expect($registry->getExampleInputs('query_database'))->toBe(['query' => 'SELECT 1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// getServerFull()
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('getServerFull', function () {
|
||||||
|
it('returns full server configuration', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
['workspace-tools' => sampleServer('workspace-tools')]
|
||||||
|
);
|
||||||
|
|
||||||
|
$server = $registry->getServerFull('workspace-tools');
|
||||||
|
|
||||||
|
expect($server)->not->toBeNull();
|
||||||
|
expect($server['id'])->toBe('workspace-tools');
|
||||||
|
expect($server['tools'])->toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for unknown server', function () {
|
||||||
|
$registry = makeRegistry(sampleRegistry(), []);
|
||||||
|
|
||||||
|
$server = $registry->getServerFull('nonexistent');
|
||||||
|
|
||||||
|
expect($server)->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Category inference
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('category extraction', function () {
|
||||||
|
it('uses explicit category when provided', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'cat-server',
|
||||||
|
'name' => 'Cat Server',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'some_tool',
|
||||||
|
'description' => 'A tool',
|
||||||
|
'category' => 'analytics',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'cat-server']]],
|
||||||
|
['cat-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('cat-server');
|
||||||
|
|
||||||
|
expect($tools[0]['category'])->toBe('Analytics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('infers commerce category from tool name', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'cat-server',
|
||||||
|
'name' => 'Cat Server',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'create_invoice',
|
||||||
|
'description' => 'Create an invoice',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'cat-server']]],
|
||||||
|
['cat-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('cat-server');
|
||||||
|
|
||||||
|
expect($tools[0]['category'])->toBe('Commerce');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('infers query category from tool name', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'cat-server',
|
||||||
|
'name' => 'Cat Server',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'search_users',
|
||||||
|
'description' => 'Search users',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'cat-server']]],
|
||||||
|
['cat-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('cat-server');
|
||||||
|
|
||||||
|
expect($tools[0]['category'])->toBe('Query');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to General for unrecognised tool names', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'cat-server',
|
||||||
|
'name' => 'Cat Server',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'do_something_unique',
|
||||||
|
'description' => 'A unique tool',
|
||||||
|
'inputSchema' => ['type' => 'object', 'properties' => []],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'cat-server']]],
|
||||||
|
['cat-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('cat-server');
|
||||||
|
|
||||||
|
expect($tools[0]['category'])->toBe('General');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Schema-based example generation
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('generateExampleFromSchema', function () {
|
||||||
|
it('uses enum first value', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'gen-server',
|
||||||
|
'name' => 'Gen',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'enum_tool',
|
||||||
|
'description' => 'Tool with enum',
|
||||||
|
'inputSchema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'status' => ['type' => 'string', 'enum' => ['active', 'inactive']],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'gen-server']]],
|
||||||
|
['gen-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('gen-server');
|
||||||
|
|
||||||
|
expect($tools[0]['examples']['status'])->toBe('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses example property from schema', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'gen-server',
|
||||||
|
'name' => 'Gen',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'example_tool',
|
||||||
|
'description' => 'Tool with example values',
|
||||||
|
'inputSchema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'email' => ['type' => 'string', 'example' => 'user@example.com'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'gen-server']]],
|
||||||
|
['gen-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('gen-server');
|
||||||
|
|
||||||
|
expect($tools[0]['examples']['email'])->toBe('user@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates type-appropriate defaults for properties without hints', function () {
|
||||||
|
$serverConfig = [
|
||||||
|
'id' => 'gen-server',
|
||||||
|
'name' => 'Gen',
|
||||||
|
'tagline' => '',
|
||||||
|
'tools' => [
|
||||||
|
[
|
||||||
|
'name' => 'typed_tool',
|
||||||
|
'description' => 'Tool with various types',
|
||||||
|
'inputSchema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'label' => ['type' => 'string'],
|
||||||
|
'count' => ['type' => 'integer'],
|
||||||
|
'amount' => ['type' => 'number'],
|
||||||
|
'active' => ['type' => 'boolean'],
|
||||||
|
'items' => ['type' => 'array'],
|
||||||
|
'meta' => ['type' => 'object'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$registry = makeRegistry(
|
||||||
|
['servers' => [['id' => 'gen-server']]],
|
||||||
|
['gen-server' => $serverConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
$tools = $registry->getToolsForServer('gen-server');
|
||||||
|
$examples = $tools[0]['examples'];
|
||||||
|
|
||||||
|
expect($examples['label'])->toBe('');
|
||||||
|
expect($examples['count'])->toBe(0);
|
||||||
|
expect($examples['amount'])->toBe(0);
|
||||||
|
expect($examples['active'])->toBeFalse();
|
||||||
|
expect($examples['items'])->toBe([]);
|
||||||
|
expect($examples['meta'])->toBeInstanceOf(stdClass::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// clearCache()
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('clearCache', function () {
|
||||||
|
it('calls Cache::forget for server keys', function () {
|
||||||
|
$registry = makeRegistry(
|
||||||
|
sampleRegistry(),
|
||||||
|
[
|
||||||
|
'workspace-tools' => sampleServer('workspace-tools'),
|
||||||
|
'billing-tools' => sampleServer('billing-tools'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pre-load servers so clearCache knows which keys to forget
|
||||||
|
$registry->getServers();
|
||||||
|
|
||||||
|
// clearCache should call forget on the main key and each server key
|
||||||
|
Cache::shouldReceive('forget')->with('mcp:playground:servers')->once()->andReturn(true);
|
||||||
|
Cache::shouldReceive('forget')->with('mcp:playground:tools:workspace-tools')->once()->andReturn(true);
|
||||||
|
Cache::shouldReceive('forget')->with('mcp:playground:tools:billing-tools')->once()->andReturn(true);
|
||||||
|
|
||||||
|
$registry->clearCache();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue