@loop_ouroboros/mcp-hub-lite 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +8 -3
- package/dist/server/src/api/web/hub-tools.d.ts.map +1 -1
- package/dist/server/src/api/web/hub-tools.js +2 -2
- package/dist/server/src/api/web/search.d.ts +1 -1
- package/dist/server/src/api/web/search.d.ts.map +1 -1
- package/dist/server/src/api/web/search.js +18 -16
- package/dist/server/src/models/system-tools.constants.d.ts +1 -0
- package/dist/server/src/models/system-tools.constants.d.ts.map +1 -1
- package/dist/server/src/services/gateway/request-handlers/system-tools-handler.d.ts.map +1 -1
- package/dist/server/src/services/gateway/request-handlers/system-tools-handler.js +3 -2
- package/dist/server/src/services/hub-tools/index.d.ts +1 -0
- package/dist/server/src/services/hub-tools/index.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools/index.js +1 -0
- package/dist/server/src/services/hub-tools/resource-generator.d.ts +1 -1
- package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools/resource-generator.js +11 -29
- package/dist/server/src/services/hub-tools/server-metadata-cache.d.ts +19 -0
- package/dist/server/src/services/hub-tools/server-metadata-cache.d.ts.map +1 -0
- package/dist/server/src/services/hub-tools/server-metadata-cache.js +117 -0
- package/dist/server/src/services/hub-tools/server-selector.d.ts +1 -1
- package/dist/server/src/services/hub-tools/server-selector.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools/server-selector.js +3 -8
- package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools/system-tool-definitions.js +8 -1
- package/dist/server/src/services/hub-tools.service.d.ts +1 -1
- package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
- package/dist/server/src/services/hub-tools.service.js +62 -88
- package/dist/server/src/services/system-tool-handler.js +1 -1
- package/dist/server/src/utils/search-matcher.d.ts +6 -0
- package/dist/server/src/utils/search-matcher.d.ts.map +1 -0
- package/dist/server/src/utils/search-matcher.js +24 -0
- package/dist/server/tests/unit/services/hub-tools.service.test.js +358 -8
- package/package.json +1 -1
|
@@ -6,10 +6,11 @@ import { generateGatewayToolsList } from './gateway/tool-list-generator.js';
|
|
|
6
6
|
import { logger, LOG_MODULES } from '../utils/logger/index.js';
|
|
7
7
|
import { stringifyForLogging } from '../utils/json-utils.js';
|
|
8
8
|
import { normalizeToolName } from '../utils/name-converter.js';
|
|
9
|
+
import { countMatchingTokens } from '../utils/search-matcher.js';
|
|
9
10
|
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
10
11
|
import { MCP_HUB_LITE_SERVER, LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, LIST_TAGS_TOOL, SEARCH_TOOLS_TOOL, SYSTEM_TOOL_NAMES } from '../models/system-tools.constants.js';
|
|
11
12
|
import { ToolArgsParser } from '../utils/tool-args-parser.js';
|
|
12
|
-
import { hasValidId, selectBestInstance, getServerDescription, getSystemTools, generateDynamicResources, readResource as readResourceUtil } from './hub-tools/index.js';
|
|
13
|
+
import { hasValidId, selectBestInstance, getServerDescription, getSystemTools, generateDynamicResources, readResource as readResourceUtil, serverMetadataCache } from './hub-tools/index.js';
|
|
13
14
|
import { InstanceSelector } from './hub-tools/instance-selector.js';
|
|
14
15
|
import { InstanceSelectionStrategy } from '../models/server.model.js';
|
|
15
16
|
/**
|
|
@@ -63,7 +64,7 @@ import { InstanceSelectionStrategy } from '../models/server.model.js';
|
|
|
63
64
|
export class HubToolsService {
|
|
64
65
|
// Cache removed - listResources() now calls generateDynamicResources() directly
|
|
65
66
|
constructor() {
|
|
66
|
-
|
|
67
|
+
serverMetadataCache.initialize();
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
70
|
* Retrieves the complete list of system tools provided by this service.
|
|
@@ -371,86 +372,54 @@ export class HubToolsService {
|
|
|
371
372
|
throw new McpError(-32801, `System tools cannot be called via 'call_tool'. Use 'tools/call' with the system tool name directly. ` +
|
|
372
373
|
`Example: use 'list_servers' directly instead of call_tool(serverName: "mcp-hub-lite", toolName: "list_servers").`);
|
|
373
374
|
}
|
|
374
|
-
// Not a system tool
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const serverInfo = selectBestInstance(server.name, requestOptions, true);
|
|
384
|
-
if (serverInfo && serverInfo.instance.id) {
|
|
385
|
-
const instanceIndex = serverInfo.instance.index;
|
|
386
|
-
const tools = mcpConnectionManager.getTools(server.name, instanceIndex);
|
|
387
|
-
if (tools.some((tool) => normalizeToolName(tool.name) === normalizeToolName(toolName))) {
|
|
388
|
-
matchingServers.push(server.name);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (matchingServers.length === 0) {
|
|
393
|
-
logger.error(`Tool '${toolName}' not found in any connected server`, LOG_MODULES.HUB_TOOLS);
|
|
394
|
-
throw new Error(`Tool '${toolName}' not found`);
|
|
395
|
-
}
|
|
396
|
-
if (matchingServers.length > 1) {
|
|
397
|
-
logger.warn(`Tool '${toolName}' found in multiple servers: ${matchingServers.join(', ')}. Using first match.`, LOG_MODULES.HUB_TOOLS);
|
|
398
|
-
}
|
|
399
|
-
// Use the first matching server
|
|
400
|
-
serverName = matchingServers[0];
|
|
375
|
+
// Not a system tool — reject with actionable guidance
|
|
376
|
+
// The aggregated gateway tools (wrapped by tool-list-generator) already hardcode
|
|
377
|
+
// the correct serverName, so this path should only be hit when LLM manually fills
|
|
378
|
+
// "mcp-hub-lite" incorrectly. Guide them to use search_tools instead.
|
|
379
|
+
throw new McpError(-32602, `Cannot call external tool '${toolName}' with serverName "mcp-hub-lite". ` +
|
|
380
|
+
`Use 'search_tools' to find which server provides this tool, ` +
|
|
381
|
+
`then call it with the correct serverName.`);
|
|
401
382
|
}
|
|
402
383
|
logger.debug(`Tool call received: serverName=${serverName}, toolName=${toolName}, args=${stringifyForLogging(toolArgs)}`, LOG_MODULES.HUB_TOOLS);
|
|
403
|
-
// Validate tool exists
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const tools = mcpConnectionManager.getTools(serverName, instanceIndex);
|
|
410
|
-
const matchedTool = tools.find((tool) => normalizeToolName(tool.name) === normalizeToolName(toolName));
|
|
411
|
-
if (!matchedTool) {
|
|
412
|
-
throw new Error(`Tool '${toolName}' not found in server '${serverName}'. ` +
|
|
413
|
-
`Use list_tools(serverName: "${serverName}") to see available tools.`);
|
|
414
|
-
}
|
|
415
|
-
actualToolName = matchedTool.name;
|
|
384
|
+
// Validate tool exists using server-name-level aggregation (no instance selection needed)
|
|
385
|
+
const aggregatedTools = mcpConnectionManager.getToolsByServerName(serverName);
|
|
386
|
+
const matchedTool = aggregatedTools.find((tool) => normalizeToolName(tool.name) === normalizeToolName(toolName));
|
|
387
|
+
if (!matchedTool) {
|
|
388
|
+
throw new Error(`Tool '${toolName}' not found in server '${serverName}'. ` +
|
|
389
|
+
`Use list_tools(serverName: "${serverName}") to see available tools.`);
|
|
416
390
|
}
|
|
417
|
-
|
|
391
|
+
let actualToolName = matchedTool.name;
|
|
392
|
+
const serverInfo = selectBestInstance(serverName, requestOptions);
|
|
418
393
|
const requestId = `tool-call-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
419
394
|
if (!serverInfo) {
|
|
420
395
|
// Server not found in hubManager, try direct call by name through mcpConnectionManager
|
|
421
396
|
logger.debug(`Server not found in hubManager, trying direct call by name: ${serverName}`, LOG_MODULES.HUB_TOOLS);
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const selectedInstance = InstanceSelector.selectInstance(serverName, {
|
|
441
|
-
...serverConfig,
|
|
442
|
-
template: {
|
|
443
|
-
...serverConfig.template,
|
|
444
|
-
instanceSelectionStrategy: InstanceSelectionStrategy.RANDOM
|
|
445
|
-
}
|
|
446
|
-
}, undefined);
|
|
447
|
-
if (selectedInstance) {
|
|
448
|
-
fallbackServerInfo = {
|
|
449
|
-
name: serverName,
|
|
450
|
-
config: serverConfig,
|
|
451
|
-
instance: selectedInstance
|
|
452
|
-
};
|
|
397
|
+
// Fallback: force RANDOM strategy on any connected instance
|
|
398
|
+
// (selectBestInstance may fail for TAG_MATCH_UNIQUE without tags)
|
|
399
|
+
let fallbackServerInfo;
|
|
400
|
+
const serverConfig = hubManager.getServerByName(serverName);
|
|
401
|
+
if (serverConfig && serverConfig.instances.length > 0) {
|
|
402
|
+
// Filter: use runtime connected status, NOT config enabled flag
|
|
403
|
+
const connectedInstances = serverConfig.instances.filter((instance) => {
|
|
404
|
+
if (instance.index === undefined)
|
|
405
|
+
return false;
|
|
406
|
+
const status = mcpConnectionManager.getStatus(serverName, instance.index);
|
|
407
|
+
return status?.connected;
|
|
408
|
+
});
|
|
409
|
+
if (connectedInstances.length > 0) {
|
|
410
|
+
const selectedInstance = InstanceSelector.selectInstance(serverName, {
|
|
411
|
+
...serverConfig,
|
|
412
|
+
template: {
|
|
413
|
+
...serverConfig.template,
|
|
414
|
+
instanceSelectionStrategy: InstanceSelectionStrategy.RANDOM
|
|
453
415
|
}
|
|
416
|
+
}, undefined);
|
|
417
|
+
if (selectedInstance) {
|
|
418
|
+
fallbackServerInfo = {
|
|
419
|
+
name: serverName,
|
|
420
|
+
config: serverConfig,
|
|
421
|
+
instance: selectedInstance
|
|
422
|
+
};
|
|
454
423
|
}
|
|
455
424
|
}
|
|
456
425
|
}
|
|
@@ -601,11 +570,11 @@ export class HubToolsService {
|
|
|
601
570
|
* @returns {Promise<Record<string, { description: string; tools: ToolSummary[] }>>}
|
|
602
571
|
* Object mapping server names to their descriptions and matching tools
|
|
603
572
|
*/
|
|
604
|
-
async searchTools(query) {
|
|
573
|
+
async searchTools(query, limit = 5) {
|
|
605
574
|
if (!query || typeof query !== 'string') {
|
|
606
575
|
throw new Error('query is required and must be a non-empty string');
|
|
607
576
|
}
|
|
608
|
-
const
|
|
577
|
+
const effectiveLimit = Math.min(Math.max(1, limit), 10);
|
|
609
578
|
const servers = hubManager.getAllServers();
|
|
610
579
|
const result = {};
|
|
611
580
|
for (const server of servers) {
|
|
@@ -621,21 +590,26 @@ export class HubToolsService {
|
|
|
621
590
|
if (tools.length === 0) {
|
|
622
591
|
continue;
|
|
623
592
|
}
|
|
624
|
-
const
|
|
625
|
-
.
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
593
|
+
const scored = tools
|
|
594
|
+
.map((tool) => {
|
|
595
|
+
const matchCount = countMatchingTokens(query, [tool.name, tool.description || '']);
|
|
596
|
+
return {
|
|
597
|
+
tool,
|
|
598
|
+
matchCount,
|
|
599
|
+
summary: {
|
|
600
|
+
name: tool.name,
|
|
601
|
+
description: tool.description,
|
|
602
|
+
serverName: server.name
|
|
603
|
+
}
|
|
604
|
+
};
|
|
629
605
|
})
|
|
630
|
-
.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}));
|
|
635
|
-
if (matchingTools.length > 0) {
|
|
606
|
+
.filter((item) => item.matchCount > 0)
|
|
607
|
+
.sort((a, b) => b.matchCount - a.matchCount)
|
|
608
|
+
.slice(0, effectiveLimit);
|
|
609
|
+
if (scored.length > 0) {
|
|
636
610
|
result[server.name] = {
|
|
637
611
|
description,
|
|
638
|
-
tools:
|
|
612
|
+
tools: scored.map((item) => item.summary)
|
|
639
613
|
};
|
|
640
614
|
}
|
|
641
615
|
}
|
|
@@ -63,7 +63,7 @@ export class SystemToolHandler {
|
|
|
63
63
|
if (!searchArgs.query) {
|
|
64
64
|
throw new McpError(-32802, 'query is required for search_tools');
|
|
65
65
|
}
|
|
66
|
-
result = await hubToolsService.searchTools(searchArgs.query);
|
|
66
|
+
result = await hubToolsService.searchTools(searchArgs.query, searchArgs.limit);
|
|
67
67
|
break;
|
|
68
68
|
}
|
|
69
69
|
default:
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts how many tokens from a whitespace-delimited query match against any of the given fields.
|
|
3
|
+
* Returns a large value for empty or whitespace-only queries so they match all results.
|
|
4
|
+
*/
|
|
5
|
+
export declare function countMatchingTokens(query: string, fields: string[]): number;
|
|
6
|
+
//# sourceMappingURL=search-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-matcher.d.ts","sourceRoot":"","sources":["../../../../src/utils/search-matcher.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAwB3E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts how many tokens from a whitespace-delimited query match against any of the given fields.
|
|
3
|
+
* Returns a large value for empty or whitespace-only queries so they match all results.
|
|
4
|
+
*/
|
|
5
|
+
export function countMatchingTokens(query, fields) {
|
|
6
|
+
if (!query || typeof query !== 'string') {
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
const tokens = query
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.split(/[,\s;|]+/)
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (tokens.length === 0) {
|
|
14
|
+
return Number.MAX_SAFE_INTEGER;
|
|
15
|
+
}
|
|
16
|
+
const lowerFields = fields.map((f) => (f || '').toLowerCase());
|
|
17
|
+
let matchCount = 0;
|
|
18
|
+
for (const token of tokens) {
|
|
19
|
+
if (lowerFields.some((field) => field.includes(token))) {
|
|
20
|
+
matchCount++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return matchCount;
|
|
24
|
+
}
|