@loop_ouroboros/mcp-hub-lite 1.2.4 → 1.2.6

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/server/src/api/web/hub-tools.d.ts.map +1 -1
  3. package/dist/server/src/api/web/hub-tools.js +15 -0
  4. package/dist/server/src/api/web/mcp-status.d.ts.map +1 -1
  5. package/dist/server/src/api/web/mcp-status.js +4 -0
  6. package/dist/server/src/cli/commands/status.js +13 -3
  7. package/dist/server/src/cli/commands/tool-use.d.ts +5 -1
  8. package/dist/server/src/cli/commands/tool-use.d.ts.map +1 -1
  9. package/dist/server/src/cli/commands/tool-use.js +27 -4
  10. package/dist/server/src/cli/server.d.ts +2 -0
  11. package/dist/server/src/cli/server.d.ts.map +1 -1
  12. package/dist/server/src/cli/server.js +2 -0
  13. package/dist/server/src/models/system-tools.constants.d.ts +6 -2
  14. package/dist/server/src/models/system-tools.constants.d.ts.map +1 -1
  15. package/dist/server/src/models/system-tools.constants.js +3 -1
  16. package/dist/server/src/services/connection/connection-manager.d.ts +6 -0
  17. package/dist/server/src/services/connection/connection-manager.d.ts.map +1 -1
  18. package/dist/server/src/services/connection/connection-manager.js +23 -0
  19. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.d.ts.map +1 -1
  20. package/dist/server/src/services/gateway/request-handlers/system-tools-handler.js +28 -1
  21. package/dist/server/src/services/gateway/tool-list-generator.js +1 -1
  22. package/dist/server/src/services/hub-tools/instance-selector.d.ts +8 -1
  23. package/dist/server/src/services/hub-tools/instance-selector.d.ts.map +1 -1
  24. package/dist/server/src/services/hub-tools/instance-selector.js +24 -10
  25. package/dist/server/src/services/hub-tools/resource-generator.d.ts.map +1 -1
  26. package/dist/server/src/services/hub-tools/resource-generator.js +17 -13
  27. package/dist/server/src/services/hub-tools/server-selector.d.ts +2 -2
  28. package/dist/server/src/services/hub-tools/server-selector.d.ts.map +1 -1
  29. package/dist/server/src/services/hub-tools/server-selector.js +4 -3
  30. package/dist/server/src/services/hub-tools/system-tool-definitions.d.ts.map +1 -1
  31. package/dist/server/src/services/hub-tools/system-tool-definitions.js +23 -3
  32. package/dist/server/src/services/hub-tools.service.d.ts +29 -7
  33. package/dist/server/src/services/hub-tools.service.d.ts.map +1 -1
  34. package/dist/server/src/services/hub-tools.service.js +96 -36
  35. package/dist/server/src/services/system-tool-handler.d.ts.map +1 -1
  36. package/dist/server/src/services/system-tool-handler.js +21 -8
  37. package/dist/server/src/utils/name-converter.d.ts.map +1 -1
  38. package/dist/server/src/utils/name-converter.js +2 -0
  39. package/dist/server/tests/unit/services/hub-tools/instance-selector.test.js +21 -16
  40. package/dist/server/tests/unit/services/hub-tools.service.test.js +42 -40
  41. package/package.json +1 -1
@@ -3,14 +3,15 @@ import { InstanceSelector, TagMatchUniqueError } from './instance-selector.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { LOG_MODULES } from '../../utils/logger/log-modules.js';
5
5
  /**
6
- * Gets the description for a server, using a default if none is provided.
6
+ * Gets the description for a server, using the server name as default if none is provided.
7
7
  *
8
8
  * @param serverConfig - Server configuration object (may contain description in template)
9
9
  * @param serverName - Name of the server
10
- * @returns The server description or a default one
10
+ * @returns The server description or the server name with usage note if no description is configured
11
11
  */
12
12
  export function getServerDescription(serverConfig, serverName) {
13
- return serverConfig?.template?.description || `Connected MCP server: ${serverName}`;
13
+ return (serverConfig?.template?.description ||
14
+ `${serverName} (You can check the tool list to understand its capabilities and update the description.)`);
14
15
  }
15
16
  /**
16
17
  * Type guard to validate that a server object has valid name and configuration.
@@ -1 +1 @@
1
- {"version":3,"file":"system-tool-definitions.d.ts","sourceRoot":"","sources":["../../../../../src/services/hub-tools/system-tool-definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAU/D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,EAAE,CA6IvD"}
1
+ {"version":3,"file":"system-tool-definitions.d.ts","sourceRoot":"","sources":["../../../../../src/services/hub-tools/system-tool-definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAW/D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,IAAI,oBAAoB,EAAE,CAiKvD"}
@@ -1,4 +1,4 @@
1
- import { SYSTEM_TOOL_NAMES, LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL } from '../../models/system-tools.constants.js';
1
+ import { SYSTEM_TOOL_NAMES, LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, LIST_TAGS_TOOL } from '../../models/system-tools.constants.js';
2
2
  /**
3
3
  * Retrieves the complete list of system tools provided by this service.
4
4
  *
@@ -91,7 +91,7 @@ export function getSystemTools() {
91
91
  required: ['serverName', 'toolName']
92
92
  },
93
93
  annotations: {
94
- title: 'Get Tool Details',
94
+ title: 'Get Tool Schema',
95
95
  readOnlyHint: true,
96
96
  destructiveHint: false,
97
97
  idempotentHint: true,
@@ -103,7 +103,7 @@ export function getSystemTools() {
103
103
  systemTools.push({
104
104
  name: toolName,
105
105
  description: 'Call a specific tool from an external MCP server. ' +
106
- 'System tools (list_servers, list_tools, get_tool, update_server_description) ' +
106
+ 'System tools (list_servers, list_tools, get_tool, list_tags, update_server_description) ' +
107
107
  'must be called directly via tools/call, not through this tool.',
108
108
  inputSchema: {
109
109
  type: 'object',
@@ -154,6 +154,26 @@ export function getSystemTools() {
154
154
  }
155
155
  });
156
156
  break;
157
+ case LIST_TAGS_TOOL:
158
+ systemTools.push({
159
+ name: toolName,
160
+ description: 'List all instance tags for a specific MCP server',
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ serverName: { type: 'string', description: 'Name of the MCP server' }
165
+ },
166
+ required: ['serverName']
167
+ },
168
+ annotations: {
169
+ title: 'List Instance Tags',
170
+ readOnlyHint: true,
171
+ destructiveHint: false,
172
+ idempotentHint: true,
173
+ openWorldHint: false
174
+ }
175
+ });
176
+ break;
157
177
  default:
158
178
  // This should never happen due to TypeScript type checking
159
179
  throw new Error(`Unknown system tool: ${toolName}`);
@@ -1,7 +1,7 @@
1
1
  import type { Tool, ToolSummary } from '../../shared/models/tool.model.js';
2
2
  import type { Resource } from '../../shared/models/resource.model.js';
3
- import { LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL } from '../models/system-tools.constants.js';
4
- import type { SystemToolName, ListServersParams, ListToolsInServerParams, GetToolParams, CallToolParams, UpdateServerDescriptionParams } from '../models/system-tools.constants.js';
3
+ import { LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, LIST_TAGS_TOOL } from '../models/system-tools.constants.js';
4
+ import type { SystemToolName, ListServersParams, ListToolsInServerParams, GetToolParams, CallToolParams, UpdateServerDescriptionParams, ListTagsParams } from '../models/system-tools.constants.js';
5
5
  /**
6
6
  * Central service for managing system tools and MCP server interactions in the MCP Hub Lite gateway.
7
7
  *
@@ -51,10 +51,6 @@ import type { SystemToolName, ListServersParams, ListToolsInServerParams, GetToo
51
51
  * ```
52
52
  */
53
53
  export declare class HubToolsService {
54
- /**
55
- * Cached dynamic resource list to avoid regenerating on every request
56
- */
57
- private generatedResourcesCache;
58
54
  constructor();
59
55
  /**
60
56
  * Retrieves the complete list of system tools provided by this service.
@@ -122,6 +118,25 @@ export declare class HubToolsService {
122
118
  serverName: string;
123
119
  description: string;
124
120
  }>;
121
+ /**
122
+ * Lists all instance tags for a specific MCP server.
123
+ *
124
+ * This method retrieves all instances of the specified server and returns their tags,
125
+ * useful for understanding which instances are available and how to select them
126
+ * when using tag-match-unique instance selection strategy.
127
+ *
128
+ * @param {ListTagsParams} args - Server name
129
+ * @returns {Promise<{ serverName: string; instances: Array<{ index: number; id: string; tags: Record<string, string> }> }>} Instance tags information
130
+ * @throws {Error} If the specified server is not found
131
+ */
132
+ listTags(args: ListTagsParams): Promise<{
133
+ serverName: string;
134
+ instances: Array<{
135
+ index: number;
136
+ id: string;
137
+ tags: Record<string, string>;
138
+ }>;
139
+ }>;
125
140
  /**
126
141
  * Calls a specific system tool directly with type-safe conditional return types.
127
142
  *
@@ -134,13 +149,20 @@ export declare class HubToolsService {
134
149
  * @returns {Promise<ConditionalReturnType>} Tool execution result with accurate type safety matching actual method return types
135
150
  * @throws {Error} If the system tool is not found or execution fails
136
151
  */
137
- callSystemTool<T extends SystemToolName>(toolName: T, toolArgs: T extends typeof LIST_SERVERS_TOOL ? ListServersParams : T extends typeof LIST_TOOLS_TOOL ? ListToolsInServerParams : T extends typeof GET_TOOL_TOOL ? GetToolParams : T extends typeof CALL_TOOL_TOOL ? CallToolParams : T extends typeof UPDATE_SERVER_DESCRIPTION_TOOL ? UpdateServerDescriptionParams : never): Promise<T extends typeof LIST_SERVERS_TOOL ? Record<string, string> : T extends typeof LIST_TOOLS_TOOL ? {
152
+ callSystemTool<T extends SystemToolName>(toolName: T, toolArgs: T extends typeof LIST_SERVERS_TOOL ? ListServersParams : T extends typeof LIST_TOOLS_TOOL ? ListToolsInServerParams : T extends typeof GET_TOOL_TOOL ? GetToolParams : T extends typeof CALL_TOOL_TOOL ? CallToolParams : T extends typeof UPDATE_SERVER_DESCRIPTION_TOOL ? UpdateServerDescriptionParams : T extends typeof LIST_TAGS_TOOL ? ListTagsParams : never): Promise<T extends typeof LIST_SERVERS_TOOL ? Record<string, string> : T extends typeof LIST_TOOLS_TOOL ? {
138
153
  serverName: string;
139
154
  tools: ToolSummary[];
140
155
  } : T extends typeof GET_TOOL_TOOL ? Tool | undefined : T extends typeof CALL_TOOL_TOOL ? unknown : T extends typeof UPDATE_SERVER_DESCRIPTION_TOOL ? {
141
156
  success: boolean;
142
157
  serverName: string;
143
158
  description: string;
159
+ } : T extends typeof LIST_TAGS_TOOL ? {
160
+ serverName: string;
161
+ instances: Array<{
162
+ index: number;
163
+ id: string;
164
+ tags: Record<string, string>;
165
+ }>;
144
166
  } : never>;
145
167
  /**
146
168
  * Calls a specific tool from a specific MCP server with comprehensive event tracking.
@@ -1 +1 @@
1
- {"version":3,"file":"hub-tools.service.d.ts","sourceRoot":"","sources":["../../../../src/services/hub-tools.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hub-tools.service.d.ts","sourceRoot":"","sources":["../../../../src/services/hub-tools.service.ts"],"names":[],"mappings":"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"}
@@ -6,9 +6,11 @@ import { logger, LOG_MODULES } from '../utils/logger.js';
6
6
  import { stringifyForLogging } from '../utils/json-utils.js';
7
7
  import { normalizeToolName } from '../utils/name-converter.js';
8
8
  import { McpError } from '@modelcontextprotocol/sdk/types.js';
9
- import { MCP_HUB_LITE_SERVER, LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, SYSTEM_TOOL_NAMES } from '../models/system-tools.constants.js';
9
+ import { MCP_HUB_LITE_SERVER, LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, LIST_TAGS_TOOL, SYSTEM_TOOL_NAMES } from '../models/system-tools.constants.js';
10
10
  import { ToolArgsParser } from '../utils/tool-args-parser.js';
11
11
  import { hasValidId, selectBestInstance, getServerDescription, getSystemTools, generateDynamicResources, readResource as readResourceUtil } from './hub-tools/index.js';
12
+ import { InstanceSelector } from './hub-tools/instance-selector.js';
13
+ import { InstanceSelectionStrategy } from '../models/server.model.js';
12
14
  /**
13
15
  * Central service for managing system tools and MCP server interactions in the MCP Hub Lite gateway.
14
16
  *
@@ -58,27 +60,9 @@ import { hasValidId, selectBestInstance, getServerDescription, getSystemTools, g
58
60
  * ```
59
61
  */
60
62
  export class HubToolsService {
61
- /**
62
- * Cached dynamic resource list to avoid regenerating on every request
63
- */
64
- generatedResourcesCache = null;
63
+ // Cache removed - listResources() now calls generateDynamicResources() directly
65
64
  constructor() {
66
- // Listen for server status change events to invalidate resource cache
67
- eventBus.subscribe(EventTypes.SERVER_STATUS_CHANGE, () => {
68
- this.generatedResourcesCache = null;
69
- });
70
- eventBus.subscribe(EventTypes.SERVER_CONNECTED, () => {
71
- this.generatedResourcesCache = null;
72
- });
73
- eventBus.subscribe(EventTypes.SERVER_DISCONNECTED, () => {
74
- this.generatedResourcesCache = null;
75
- });
76
- eventBus.subscribe(EventTypes.RESOURCES_UPDATED, () => {
77
- this.generatedResourcesCache = null;
78
- });
79
- eventBus.subscribe(EventTypes.TOOLS_UPDATED, () => {
80
- this.generatedResourcesCache = null;
81
- });
65
+ // No cache-related initialization needed
82
66
  }
83
67
  /**
84
68
  * Retrieves the complete list of system tools provided by this service.
@@ -107,9 +91,9 @@ export class HubToolsService {
107
91
  const servers = hubManager.getAllServers();
108
92
  const result = {};
109
93
  for (const server of servers.filter(hasValidId)) {
110
- // Check if server is actually connected
111
- const status = mcpConnectionManager.getStatusByName(server.name);
112
- if (!status?.connected) {
94
+ // Use getConnectedIndexes for reliable multi-instance support
95
+ const indexes = mcpConnectionManager.getConnectedIndexes(server.name);
96
+ if (indexes.length === 0) {
113
97
  continue;
114
98
  }
115
99
  // Use non-strict mode for management operations to avoid tag-match-unique errors
@@ -136,6 +120,9 @@ export class HubToolsService {
136
120
  * @throws {Error} If the specified server is not found or not connected
137
121
  */
138
122
  async listToolsInServer(args) {
123
+ if (!args.serverName) {
124
+ throw new Error('serverName is required');
125
+ }
139
126
  // Handle MCP Hub Lite server (return system tools list)
140
127
  if (typeof args.serverName === 'string' && args.serverName === MCP_HUB_LITE_SERVER) {
141
128
  // Generate tool list using the same logic as tools/list
@@ -152,6 +139,11 @@ export class HubToolsService {
152
139
  tools: toolSummaries
153
140
  };
154
141
  }
142
+ // Check if server has any connected instances before trying to get tools
143
+ const indexes = mcpConnectionManager.getConnectedIndexes(args.serverName);
144
+ if (indexes.length === 0) {
145
+ throw new Error(`Server not found: ${args.serverName}`);
146
+ }
155
147
  // Use server name level cache to get tools directly without triggering instance selection
156
148
  // This avoids tag-match-unique errors for multi-instance servers when listing tools
157
149
  const tools = mcpConnectionManager.getToolsByServerName(args.serverName);
@@ -193,6 +185,11 @@ export class HubToolsService {
193
185
  }
194
186
  return undefined;
195
187
  }
188
+ // Check if server has any connected instances before trying to get tools
189
+ const indexes = mcpConnectionManager.getConnectedIndexes(args.serverName);
190
+ if (indexes.length === 0) {
191
+ throw new Error(`Server not found: ${args.serverName}`);
192
+ }
196
193
  // Use server name level cache to get tools directly without triggering instance selection
197
194
  // This avoids tag-match-unique errors for multi-instance servers when getting tool details
198
195
  const tools = mcpConnectionManager.getToolsByServerName(args.serverName);
@@ -237,6 +234,33 @@ export class HubToolsService {
237
234
  description
238
235
  };
239
236
  }
237
+ /**
238
+ * Lists all instance tags for a specific MCP server.
239
+ *
240
+ * This method retrieves all instances of the specified server and returns their tags,
241
+ * useful for understanding which instances are available and how to select them
242
+ * when using tag-match-unique instance selection strategy.
243
+ *
244
+ * @param {ListTagsParams} args - Server name
245
+ * @returns {Promise<{ serverName: string; instances: Array<{ index: number; id: string; tags: Record<string, string> }> }>} Instance tags information
246
+ * @throws {Error} If the specified server is not found
247
+ */
248
+ async listTags(args) {
249
+ const serverConfig = hubManager.getServerByName(args.serverName);
250
+ if (!serverConfig) {
251
+ throw new Error(`Server not found: ${args.serverName}`);
252
+ }
253
+ const instances = hubManager.getServerInstancesByName(args.serverName);
254
+ const instanceTags = instances.map((instance) => ({
255
+ index: instance.index ?? 0,
256
+ id: instance.id || '',
257
+ tags: instance.tags || {}
258
+ }));
259
+ return {
260
+ serverName: args.serverName,
261
+ instances: instanceTags
262
+ };
263
+ }
240
264
  /**
241
265
  * Calls a specific system tool directly with type-safe conditional return types.
242
266
  *
@@ -281,6 +305,10 @@ export class HubToolsService {
281
305
  result = await this.updateServerDescription(toolArgs);
282
306
  break;
283
307
  }
308
+ case LIST_TAGS_TOOL: {
309
+ result = await this.listTags(toolArgs);
310
+ break;
311
+ }
284
312
  default:
285
313
  throw new Error(`System tool "${toolName}" not found`);
286
314
  }
@@ -317,14 +345,17 @@ export class HubToolsService {
317
345
  serverName = parsedTool.serverName;
318
346
  toolName = parsedTool.toolName;
319
347
  }
320
- // Handle MCP Hub Lite server (system tool call or find tool in all servers)
321
- if (!serverName || serverName === 'undefined') {
322
- serverName = MCP_HUB_LITE_SERVER;
348
+ // Validate serverName is required
349
+ if (!serverName) {
350
+ throw new Error('serverName is required');
323
351
  }
352
+ // Handle MCP Hub Lite server (system tool call or find tool in all servers)
324
353
  if (typeof serverName === 'string' && serverName === MCP_HUB_LITE_SERVER) {
325
- // System tools cannot be called via call_tool - they must be called directly
354
+ // System tools (except call_tool) cannot be called via call_tool - they must be called directly
355
+ // call_tool is the gateway tool for calling external tools, so it should be allowed
326
356
  if (Array.isArray(SYSTEM_TOOL_NAMES) &&
327
- SYSTEM_TOOL_NAMES.includes(toolName)) {
357
+ SYSTEM_TOOL_NAMES.includes(toolName) &&
358
+ toolName !== CALL_TOOL_TOOL) {
328
359
  throw new McpError(-32801, `System tools cannot be called via 'call_tool'. Use 'tools/call' with the system tool name directly. ` +
329
360
  `Example: use 'list_servers' directly instead of call_tool(serverName: "mcp-hub-lite", toolName: "list_servers").`);
330
361
  }
@@ -377,7 +408,40 @@ export class HubToolsService {
377
408
  // Server not found in hubManager, try direct call by name through mcpConnectionManager
378
409
  logger.debug(`Server not found in hubManager, trying direct call by name: ${serverName}`, LOG_MODULES.HUB_TOOLS);
379
410
  // Use selectBestInstance with non-strict mode to find an available instance
380
- const fallbackServerInfo = selectBestInstance(serverName, undefined, false);
411
+ let fallbackServerInfo = selectBestInstance(serverName, undefined, false);
412
+ // If selectBestInstance returns undefined (e.g., TAG_MATCH_UNIQUE strategy without tags),
413
+ // fall back to directly selecting an enabled instance with RANDOM strategy
414
+ if (!fallbackServerInfo) {
415
+ logger.debug(`selectBestInstance returned undefined for ${serverName}, trying direct instance selection with RANDOM strategy`, LOG_MODULES.HUB_TOOLS);
416
+ const serverConfig = hubManager.getServerByName(serverName);
417
+ if (serverConfig && serverConfig.instances.length > 0) {
418
+ // Filter: use runtime connected status, NOT config enabled flag
419
+ // enabled=false means "do not auto-start" but user can manually start it
420
+ const connectedInstances = serverConfig.instances.filter((instance) => {
421
+ if (instance.index === undefined)
422
+ return false;
423
+ const status = mcpConnectionManager.getStatus(serverName, instance.index);
424
+ return status?.connected;
425
+ });
426
+ if (connectedInstances.length > 0) {
427
+ // Use RANDOM strategy regardless of server's configured strategy
428
+ const selectedInstance = InstanceSelector.selectInstance(serverName, {
429
+ ...serverConfig,
430
+ template: {
431
+ ...serverConfig.template,
432
+ instanceSelectionStrategy: InstanceSelectionStrategy.RANDOM
433
+ }
434
+ }, undefined);
435
+ if (selectedInstance) {
436
+ fallbackServerInfo = {
437
+ name: serverName,
438
+ config: serverConfig,
439
+ instance: selectedInstance
440
+ };
441
+ }
442
+ }
443
+ }
444
+ }
381
445
  if (!fallbackServerInfo) {
382
446
  logger.error(`Server not found: ${serverName}`, LOG_MODULES.HUB_TOOLS);
383
447
  throw new Error(`Server not found: ${serverName}`);
@@ -525,12 +589,8 @@ export class HubToolsService {
525
589
  * @returns {Promise<Resource[]>} Array of MCP resource objects representing Hub resources
526
590
  */
527
591
  async listResources() {
528
- if (this.generatedResourcesCache) {
529
- return this.generatedResourcesCache;
530
- }
531
- const resources = generateDynamicResources();
532
- this.generatedResourcesCache = resources;
533
- return resources;
592
+ // Always regenerate to ensure fresh data based on runtime status
593
+ return generateDynamicResources();
534
594
  }
535
595
  /**
536
596
  * Reads content from a specific Hub resource URI.
@@ -1 +1 @@
1
- {"version":3,"file":"system-tool-handler.d.ts","sourceRoot":"","sources":["../../../../src/services/system-tool-handler.ts"],"names":[],"mappings":"AAoBA;;GAEG;AACH,qBAAa,iBAAiB;IAC5B;;OAEG;WACU,oBAAoB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,OAAO,CAAC;CA8DpB"}
1
+ {"version":3,"file":"system-tool-handler.d.ts","sourceRoot":"","sources":["../../../../src/services/system-tool-handler.ts"],"names":[],"mappings":"AAqBA;;GAEG;AACH,qBAAa,iBAAiB;IAC5B;;OAEG;WACU,oBAAoB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CAAC,OAAO,CAAC;CA2EpB"}
@@ -1,7 +1,7 @@
1
1
  import { hubToolsService } from './hub-tools.service.js';
2
2
  import { McpError } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { logger, LOG_MODULES } from '../utils/logger.js';
4
- import { LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, MCP_HUB_LITE_SERVER } from '../models/system-tools.constants.js';
4
+ import { LIST_SERVERS_TOOL, LIST_TOOLS_TOOL, GET_TOOL_TOOL, CALL_TOOL_TOOL, UPDATE_SERVER_DESCRIPTION_TOOL, LIST_TAGS_TOOL } from '../models/system-tools.constants.js';
5
5
  import { stringifyForLogging } from '../utils/json-utils.js';
6
6
  /**
7
7
  * Unified system tool call handler
@@ -20,31 +20,44 @@ export class SystemToolHandler {
20
20
  break;
21
21
  case LIST_TOOLS_TOOL: {
22
22
  const listToolsArgs = toolArgs;
23
+ if (!listToolsArgs.serverName) {
24
+ throw new McpError(-32802, 'serverName is required');
25
+ }
23
26
  result = await hubToolsService.listToolsInServer(listToolsArgs);
24
27
  break;
25
28
  }
26
29
  case GET_TOOL_TOOL: {
27
30
  const getToolArgs = toolArgs;
31
+ if (!getToolArgs.serverName) {
32
+ throw new McpError(-32802, 'serverName is required');
33
+ }
28
34
  result = await hubToolsService.getTool(getToolArgs);
29
35
  break;
30
36
  }
31
37
  case CALL_TOOL_TOOL: {
32
38
  const callToolArgs = toolArgs;
33
- let serverName = callToolArgs.serverName;
34
- if (!serverName || serverName === 'undefined') {
35
- serverName = MCP_HUB_LITE_SERVER;
39
+ if (!callToolArgs.serverName) {
40
+ throw new McpError(-32802, 'serverName is required');
36
41
  }
37
- result = await hubToolsService.callTool({
38
- ...callToolArgs,
39
- serverName
40
- });
42
+ result = await hubToolsService.callTool(callToolArgs);
41
43
  break;
42
44
  }
43
45
  case UPDATE_SERVER_DESCRIPTION_TOOL: {
44
46
  const updateDescArgs = toolArgs;
47
+ if (!updateDescArgs.serverName) {
48
+ throw new McpError(-32802, 'serverName is required');
49
+ }
45
50
  result = await hubToolsService.updateServerDescription(updateDescArgs);
46
51
  break;
47
52
  }
53
+ case LIST_TAGS_TOOL: {
54
+ const listTagsArgs = toolArgs;
55
+ if (!listTagsArgs.serverName) {
56
+ throw new McpError(-32802, 'serverName is required');
57
+ }
58
+ result = await hubToolsService.listTags(listTagsArgs);
59
+ break;
60
+ }
48
61
  default:
49
62
  throw new McpError(-32801, `Unknown system tool: ${toolName}`);
50
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"name-converter.d.ts","sourceRoot":"","sources":["../../../../src/utils/name-converter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAa1D"}
1
+ {"version":3,"file":"name-converter.d.ts","sourceRoot":"","sources":["../../../../src/utils/name-converter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAe1D"}
@@ -14,6 +14,8 @@
14
14
  * normalizeToolName('chat_completions') // returns 'chat_completions'
15
15
  */
16
16
  export function normalizeToolName(toolName) {
17
+ if (!toolName)
18
+ return '';
17
19
  return (toolName
18
20
  // Insert underscore before uppercase letters (for camelCase handling)
19
21
  // e.g., 'chatCompletions' -> 'chat_Completions'
@@ -17,8 +17,13 @@ describe('InstanceSelector', () => {
17
17
  headers: {},
18
18
  tags: {}
19
19
  };
20
+ // Mock statusChecker that simulates connected instances
21
+ const mockConnectedStatus = () => ({ connected: true });
22
+ const mockDisconnectedStatus = () => ({ connected: false });
20
23
  describe('random strategy', () => {
21
- it('should select random instance from enabled instances', () => {
24
+ it('should select random instance from connected instances', () => {
25
+ // Local mock that simulates idx=2 being disconnected
26
+ const localMockConnectedStatus = (_name, idx) => idx === 2 ? { connected: false } : { connected: true };
22
27
  const config = {
23
28
  template: {
24
29
  ...baseTemplate,
@@ -31,11 +36,11 @@ describe('InstanceSelector', () => {
31
36
  ],
32
37
  tagDefinitions: []
33
38
  };
34
- const selected = InstanceSelector.selectInstance('test-server', config);
39
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, localMockConnectedStatus);
35
40
  expect(selected).toBeDefined();
36
41
  expect(['1', '2']).toContain(selected.id);
37
42
  });
38
- it('should return undefined when no enabled instances', () => {
43
+ it('should return undefined when no connected instances', () => {
39
44
  const config = {
40
45
  template: {
41
46
  ...baseTemplate,
@@ -47,12 +52,12 @@ describe('InstanceSelector', () => {
47
52
  ],
48
53
  tagDefinitions: []
49
54
  };
50
- const selected = InstanceSelector.selectInstance('test-server', config);
55
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockDisconnectedStatus);
51
56
  expect(selected).toBeUndefined();
52
57
  });
53
58
  });
54
59
  describe('round-robin strategy', () => {
55
- it('should cycle through enabled instances', () => {
60
+ it('should cycle through connected instances', () => {
56
61
  const config = {
57
62
  template: {
58
63
  ...baseTemplate,
@@ -65,10 +70,10 @@ describe('InstanceSelector', () => {
65
70
  ],
66
71
  tagDefinitions: []
67
72
  };
68
- const selected1 = InstanceSelector.selectInstance('test-server-rr', config);
69
- const selected2 = InstanceSelector.selectInstance('test-server-rr', config);
70
- const selected3 = InstanceSelector.selectInstance('test-server-rr', config);
71
- const selected4 = InstanceSelector.selectInstance('test-server-rr', config);
73
+ const selected1 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
74
+ const selected2 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
75
+ const selected3 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
76
+ const selected4 = InstanceSelector.selectInstance('test-server-rr', config, undefined, mockConnectedStatus);
72
77
  expect(selected1.id).toBe('1');
73
78
  expect(selected2.id).toBe('2');
74
79
  expect(selected3.id).toBe('3');
@@ -91,7 +96,7 @@ describe('InstanceSelector', () => {
91
96
  };
92
97
  const selected = InstanceSelector.selectInstance('test-server', config, {
93
98
  tags: { env: 'prod', region: 'us' }
94
- });
99
+ }, mockConnectedStatus);
95
100
  expect(selected).toBeDefined();
96
101
  expect(selected.id).toBe('2');
97
102
  });
@@ -110,7 +115,7 @@ describe('InstanceSelector', () => {
110
115
  expect(() => {
111
116
  InstanceSelector.selectInstance('test-server', config, {
112
117
  tags: { env: 'staging' }
113
- });
118
+ }, mockConnectedStatus);
114
119
  }).toThrow('No instance found matching tags');
115
120
  });
116
121
  it('should throw error when multiple instances match tags', () => {
@@ -128,7 +133,7 @@ describe('InstanceSelector', () => {
128
133
  expect(() => {
129
134
  InstanceSelector.selectInstance('test-server', config, {
130
135
  tags: { env: 'prod' }
131
- });
136
+ }, mockConnectedStatus);
132
137
  }).toThrow('Multiple instances match tags');
133
138
  });
134
139
  it('should return the single instance when no tags provided and only one instance exists', () => {
@@ -140,7 +145,7 @@ describe('InstanceSelector', () => {
140
145
  instances: [{ ...baseInstance, id: '1', index: 0 }],
141
146
  tagDefinitions: []
142
147
  };
143
- const selected = InstanceSelector.selectInstance('test-server', config);
148
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
144
149
  expect(selected).toBeDefined();
145
150
  expect(selected.id).toBe('1');
146
151
  });
@@ -157,7 +162,7 @@ describe('InstanceSelector', () => {
157
162
  tagDefinitions: []
158
163
  };
159
164
  expect(() => {
160
- InstanceSelector.selectInstance('test-server', config);
165
+ InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
161
166
  }).toThrow('No tags provided for tag-match-unique strategy with 2 instances. Available: [0:{}, 1:{}]. Pass matching tags to select.');
162
167
  });
163
168
  });
@@ -171,7 +176,7 @@ describe('InstanceSelector', () => {
171
176
  instances: [{ ...baseInstance, id: '1', index: 0 }],
172
177
  tagDefinitions: []
173
178
  };
174
- const selected = InstanceSelector.selectInstance('test-server', config);
179
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
175
180
  expect(selected).toBeDefined();
176
181
  expect(selected.id).toBe('1');
177
182
  });
@@ -187,7 +192,7 @@ describe('InstanceSelector', () => {
187
192
  tagDefinitions: []
188
193
  // No instanceSelectionStrategy field
189
194
  };
190
- const selected = InstanceSelector.selectInstance('test-server', config);
195
+ const selected = InstanceSelector.selectInstance('test-server', config, undefined, mockConnectedStatus);
191
196
  expect(selected).toBeDefined();
192
197
  expect(['1', '2']).toContain(selected.id);
193
198
  });