@juspay/neurolink 7.12.0 → 7.14.0
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 +12 -0
- package/README.md +90 -25
- package/dist/config/conversationMemoryConfig.js +2 -1
- package/dist/context/ContextManager.d.ts +28 -0
- package/dist/context/ContextManager.js +113 -0
- package/dist/context/config.d.ts +5 -0
- package/dist/context/config.js +42 -0
- package/dist/context/types.d.ts +20 -0
- package/dist/context/types.js +1 -0
- package/dist/context/utils.d.ts +7 -0
- package/dist/context/utils.js +8 -0
- package/dist/core/baseProvider.d.ts +16 -1
- package/dist/core/baseProvider.js +208 -9
- package/dist/core/conversationMemoryManager.js +3 -2
- package/dist/core/factory.js +13 -2
- package/dist/factories/providerFactory.js +5 -11
- package/dist/factories/providerRegistry.js +2 -2
- package/dist/lib/config/conversationMemoryConfig.js +2 -1
- package/dist/lib/context/ContextManager.d.ts +28 -0
- package/dist/lib/context/ContextManager.js +113 -0
- package/dist/lib/context/config.d.ts +5 -0
- package/dist/lib/context/config.js +42 -0
- package/dist/lib/context/types.d.ts +20 -0
- package/dist/lib/context/types.js +1 -0
- package/dist/lib/context/utils.d.ts +7 -0
- package/dist/lib/context/utils.js +8 -0
- package/dist/lib/core/baseProvider.d.ts +16 -1
- package/dist/lib/core/baseProvider.js +208 -9
- package/dist/lib/core/conversationMemoryManager.js +3 -2
- package/dist/lib/core/factory.js +13 -2
- package/dist/lib/factories/providerFactory.js +5 -11
- package/dist/lib/factories/providerRegistry.js +2 -2
- package/dist/lib/mcp/externalServerManager.d.ts +115 -0
- package/dist/lib/mcp/externalServerManager.js +677 -0
- package/dist/lib/mcp/mcpCircuitBreaker.d.ts +184 -0
- package/dist/lib/mcp/mcpCircuitBreaker.js +338 -0
- package/dist/lib/mcp/mcpClientFactory.d.ts +104 -0
- package/dist/lib/mcp/mcpClientFactory.js +416 -0
- package/dist/lib/mcp/toolDiscoveryService.d.ts +192 -0
- package/dist/lib/mcp/toolDiscoveryService.js +578 -0
- package/dist/lib/neurolink.d.ts +128 -16
- package/dist/lib/neurolink.js +555 -49
- package/dist/lib/providers/googleVertex.d.ts +1 -1
- package/dist/lib/providers/googleVertex.js +23 -7
- package/dist/lib/types/externalMcp.d.ts +282 -0
- package/dist/lib/types/externalMcp.js +6 -0
- package/dist/lib/types/generateTypes.d.ts +0 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/mcp/externalServerManager.d.ts +115 -0
- package/dist/mcp/externalServerManager.js +677 -0
- package/dist/mcp/mcpCircuitBreaker.d.ts +184 -0
- package/dist/mcp/mcpCircuitBreaker.js +338 -0
- package/dist/mcp/mcpClientFactory.d.ts +104 -0
- package/dist/mcp/mcpClientFactory.js +416 -0
- package/dist/mcp/toolDiscoveryService.d.ts +192 -0
- package/dist/mcp/toolDiscoveryService.js +578 -0
- package/dist/neurolink.d.ts +128 -16
- package/dist/neurolink.js +555 -49
- package/dist/providers/googleVertex.d.ts +1 -1
- package/dist/providers/googleVertex.js +23 -7
- package/dist/types/externalMcp.d.ts +282 -0
- package/dist/types/externalMcp.js +6 -0
- package/dist/types/generateTypes.d.ts +0 -1
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Discovery Service
|
|
3
|
+
* Automatically discovers and registers tools from external MCP servers
|
|
4
|
+
* Handles tool validation, transformation, and lifecycle management
|
|
5
|
+
*/
|
|
6
|
+
import { EventEmitter } from "events";
|
|
7
|
+
import { mcpLogger } from "../utils/logger.js";
|
|
8
|
+
import { globalCircuitBreakerManager, } from "./mcpCircuitBreaker.js";
|
|
9
|
+
/**
|
|
10
|
+
* ToolDiscoveryService
|
|
11
|
+
* Handles automatic tool discovery and registration from external MCP servers
|
|
12
|
+
*/
|
|
13
|
+
export class ToolDiscoveryService extends EventEmitter {
|
|
14
|
+
toolRegistry = new Map();
|
|
15
|
+
serverTools = new Map();
|
|
16
|
+
discoveryInProgress = new Set();
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Discover tools from an external MCP server
|
|
22
|
+
*/
|
|
23
|
+
async discoverTools(serverId, client, timeout = 10000) {
|
|
24
|
+
const startTime = Date.now();
|
|
25
|
+
try {
|
|
26
|
+
// Prevent concurrent discovery for same server
|
|
27
|
+
if (this.discoveryInProgress.has(serverId)) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: `Discovery already in progress for server: ${serverId}`,
|
|
31
|
+
toolCount: 0,
|
|
32
|
+
tools: [],
|
|
33
|
+
duration: Date.now() - startTime,
|
|
34
|
+
serverId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
this.discoveryInProgress.add(serverId);
|
|
38
|
+
mcpLogger.info(`[ToolDiscoveryService] Starting tool discovery for server: ${serverId}`);
|
|
39
|
+
// Create circuit breaker for tool discovery
|
|
40
|
+
const circuitBreaker = globalCircuitBreakerManager.getBreaker(`tool-discovery-${serverId}`, {
|
|
41
|
+
failureThreshold: 2,
|
|
42
|
+
resetTimeout: 60000,
|
|
43
|
+
operationTimeout: timeout,
|
|
44
|
+
});
|
|
45
|
+
// Discover tools with circuit breaker protection
|
|
46
|
+
const tools = await circuitBreaker.execute(async () => {
|
|
47
|
+
return await this.performToolDiscovery(serverId, client, timeout);
|
|
48
|
+
});
|
|
49
|
+
// Register discovered tools
|
|
50
|
+
const registeredTools = await this.registerDiscoveredTools(serverId, tools);
|
|
51
|
+
const result = {
|
|
52
|
+
success: true,
|
|
53
|
+
toolCount: registeredTools.length,
|
|
54
|
+
tools: registeredTools,
|
|
55
|
+
duration: Date.now() - startTime,
|
|
56
|
+
serverId,
|
|
57
|
+
};
|
|
58
|
+
// Emit discovery completed event
|
|
59
|
+
this.emit("discoveryCompleted", {
|
|
60
|
+
serverId,
|
|
61
|
+
toolCount: registeredTools.length,
|
|
62
|
+
duration: result.duration,
|
|
63
|
+
timestamp: new Date(),
|
|
64
|
+
});
|
|
65
|
+
mcpLogger.info(`[ToolDiscoveryService] Discovery completed for ${serverId}: ${registeredTools.length} tools`);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
+
mcpLogger.error(`[ToolDiscoveryService] Discovery failed for ${serverId}:`, error);
|
|
71
|
+
// Emit discovery failed event
|
|
72
|
+
this.emit("discoveryFailed", {
|
|
73
|
+
serverId,
|
|
74
|
+
error: errorMessage,
|
|
75
|
+
timestamp: new Date(),
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: errorMessage,
|
|
80
|
+
toolCount: 0,
|
|
81
|
+
tools: [],
|
|
82
|
+
duration: Date.now() - startTime,
|
|
83
|
+
serverId,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.discoveryInProgress.delete(serverId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Perform the actual tool discovery
|
|
92
|
+
*/
|
|
93
|
+
async performToolDiscovery(serverId, client, timeout) {
|
|
94
|
+
// List tools from the MCP server
|
|
95
|
+
const listToolsPromise = client.listTools();
|
|
96
|
+
const timeoutPromise = this.createTimeoutPromise(timeout, "Tool discovery timeout");
|
|
97
|
+
const result = await Promise.race([listToolsPromise, timeoutPromise]);
|
|
98
|
+
if (!result || !result.tools) {
|
|
99
|
+
throw new Error("No tools returned from server");
|
|
100
|
+
}
|
|
101
|
+
mcpLogger.debug(`[ToolDiscoveryService] Discovered ${result.tools.length} tools from ${serverId}`);
|
|
102
|
+
return result.tools;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Register discovered tools
|
|
106
|
+
*/
|
|
107
|
+
async registerDiscoveredTools(serverId, tools) {
|
|
108
|
+
const registeredTools = [];
|
|
109
|
+
// Clear existing tools for this server
|
|
110
|
+
this.clearServerTools(serverId);
|
|
111
|
+
for (const tool of tools) {
|
|
112
|
+
try {
|
|
113
|
+
const toolInfo = await this.createToolInfo(serverId, tool);
|
|
114
|
+
const validation = this.validateTool(toolInfo);
|
|
115
|
+
if (!validation.isValid) {
|
|
116
|
+
mcpLogger.warn(`[ToolDiscoveryService] Skipping invalid tool ${tool.name} from ${serverId}:`, validation.errors);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Apply validation metadata
|
|
120
|
+
if (validation.metadata) {
|
|
121
|
+
toolInfo.metadata = {
|
|
122
|
+
...toolInfo.metadata,
|
|
123
|
+
...validation.metadata,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Register the tool
|
|
127
|
+
const toolKey = this.createToolKey(serverId, tool.name);
|
|
128
|
+
this.toolRegistry.set(toolKey, toolInfo);
|
|
129
|
+
// Track server tools
|
|
130
|
+
if (!this.serverTools.has(serverId)) {
|
|
131
|
+
this.serverTools.set(serverId, new Set());
|
|
132
|
+
}
|
|
133
|
+
this.serverTools.get(serverId).add(tool.name);
|
|
134
|
+
registeredTools.push(toolInfo);
|
|
135
|
+
// Emit tool registered event
|
|
136
|
+
this.emit("toolRegistered", {
|
|
137
|
+
serverId,
|
|
138
|
+
toolName: tool.name,
|
|
139
|
+
toolInfo,
|
|
140
|
+
timestamp: new Date(),
|
|
141
|
+
});
|
|
142
|
+
mcpLogger.debug(`[ToolDiscoveryService] Registered tool: ${tool.name} from ${serverId}`);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
mcpLogger.error(`[ToolDiscoveryService] Failed to register tool ${tool.name} from ${serverId}:`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return registeredTools;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create tool info from MCP tool definition
|
|
152
|
+
*/
|
|
153
|
+
async createToolInfo(serverId, tool) {
|
|
154
|
+
return {
|
|
155
|
+
name: tool.name,
|
|
156
|
+
description: tool.description || "No description provided",
|
|
157
|
+
serverId,
|
|
158
|
+
inputSchema: tool.inputSchema,
|
|
159
|
+
isAvailable: true,
|
|
160
|
+
stats: {
|
|
161
|
+
totalCalls: 0,
|
|
162
|
+
successfulCalls: 0,
|
|
163
|
+
failedCalls: 0,
|
|
164
|
+
averageExecutionTime: 0,
|
|
165
|
+
lastExecutionTime: 0,
|
|
166
|
+
},
|
|
167
|
+
metadata: {
|
|
168
|
+
category: this.inferToolCategory(tool),
|
|
169
|
+
version: "1.0.0",
|
|
170
|
+
deprecated: false,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Infer tool category from tool definition
|
|
176
|
+
*/
|
|
177
|
+
inferToolCategory(tool) {
|
|
178
|
+
const name = tool.name.toLowerCase();
|
|
179
|
+
const description = (tool.description || "").toLowerCase();
|
|
180
|
+
// Common patterns for categorization
|
|
181
|
+
if (name.includes("git") || description.includes("git")) {
|
|
182
|
+
return "version-control";
|
|
183
|
+
}
|
|
184
|
+
if (name.includes("file") ||
|
|
185
|
+
name.includes("read") ||
|
|
186
|
+
name.includes("write")) {
|
|
187
|
+
return "file-system";
|
|
188
|
+
}
|
|
189
|
+
if (name.includes("api") ||
|
|
190
|
+
name.includes("http") ||
|
|
191
|
+
name.includes("request")) {
|
|
192
|
+
return "api";
|
|
193
|
+
}
|
|
194
|
+
if (name.includes("data") ||
|
|
195
|
+
name.includes("query") ||
|
|
196
|
+
name.includes("search")) {
|
|
197
|
+
return "data";
|
|
198
|
+
}
|
|
199
|
+
if (name.includes("auth") ||
|
|
200
|
+
name.includes("login") ||
|
|
201
|
+
name.includes("token")) {
|
|
202
|
+
return "authentication";
|
|
203
|
+
}
|
|
204
|
+
if (name.includes("deploy") ||
|
|
205
|
+
name.includes("build") ||
|
|
206
|
+
name.includes("ci")) {
|
|
207
|
+
return "deployment";
|
|
208
|
+
}
|
|
209
|
+
return "general";
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Validate a tool
|
|
213
|
+
*/
|
|
214
|
+
validateTool(toolInfo) {
|
|
215
|
+
const errors = [];
|
|
216
|
+
const warnings = [];
|
|
217
|
+
// Basic validation
|
|
218
|
+
if (!toolInfo.name || toolInfo.name.trim().length === 0) {
|
|
219
|
+
errors.push("Tool name is required");
|
|
220
|
+
}
|
|
221
|
+
if (toolInfo.name && !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(toolInfo.name)) {
|
|
222
|
+
errors.push("Tool name must start with a letter and contain only letters, numbers, underscores, and hyphens");
|
|
223
|
+
}
|
|
224
|
+
if (!toolInfo.description || toolInfo.description.trim().length === 0) {
|
|
225
|
+
warnings.push("Tool description is empty");
|
|
226
|
+
}
|
|
227
|
+
if (!toolInfo.serverId) {
|
|
228
|
+
errors.push("Server ID is required");
|
|
229
|
+
}
|
|
230
|
+
// Schema validation
|
|
231
|
+
if (toolInfo.inputSchema) {
|
|
232
|
+
try {
|
|
233
|
+
JSON.stringify(toolInfo.inputSchema);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
errors.push("Input schema is not valid JSON");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Infer metadata
|
|
240
|
+
const metadata = {
|
|
241
|
+
category: typeof toolInfo.metadata?.category === "string"
|
|
242
|
+
? toolInfo.metadata.category
|
|
243
|
+
: "general",
|
|
244
|
+
complexity: this.inferComplexity(toolInfo),
|
|
245
|
+
requiresAuth: this.inferAuthRequirement(toolInfo),
|
|
246
|
+
isDeprecated: typeof toolInfo.metadata?.deprecated === "boolean"
|
|
247
|
+
? toolInfo.metadata.deprecated
|
|
248
|
+
: false,
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
isValid: errors.length === 0,
|
|
252
|
+
errors,
|
|
253
|
+
warnings,
|
|
254
|
+
metadata,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Infer tool complexity
|
|
259
|
+
*/
|
|
260
|
+
inferComplexity(toolInfo) {
|
|
261
|
+
const schema = toolInfo.inputSchema;
|
|
262
|
+
if (!schema || !schema.properties) {
|
|
263
|
+
return "simple";
|
|
264
|
+
}
|
|
265
|
+
const propertyCount = Object.keys(schema.properties).length;
|
|
266
|
+
if (propertyCount <= 2) {
|
|
267
|
+
return "simple";
|
|
268
|
+
}
|
|
269
|
+
else if (propertyCount <= 5) {
|
|
270
|
+
return "moderate";
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
return "complex";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Infer if tool requires authentication
|
|
278
|
+
*/
|
|
279
|
+
inferAuthRequirement(toolInfo) {
|
|
280
|
+
const name = toolInfo.name.toLowerCase();
|
|
281
|
+
const description = toolInfo.description.toLowerCase();
|
|
282
|
+
return (name.includes("auth") ||
|
|
283
|
+
name.includes("login") ||
|
|
284
|
+
name.includes("token") ||
|
|
285
|
+
description.includes("authentication") ||
|
|
286
|
+
description.includes("credentials") ||
|
|
287
|
+
description.includes("permission"));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Execute a tool
|
|
291
|
+
*/
|
|
292
|
+
async executeTool(toolName, serverId, client, parameters, options = {}) {
|
|
293
|
+
const startTime = Date.now();
|
|
294
|
+
try {
|
|
295
|
+
const toolKey = this.createToolKey(serverId, toolName);
|
|
296
|
+
const toolInfo = this.toolRegistry.get(toolKey);
|
|
297
|
+
if (!toolInfo) {
|
|
298
|
+
throw new Error(`Tool '${toolName}' not found for server '${serverId}'`);
|
|
299
|
+
}
|
|
300
|
+
if (!toolInfo.isAvailable) {
|
|
301
|
+
throw new Error(`Tool '${toolName}' is not available`);
|
|
302
|
+
}
|
|
303
|
+
// Validate input parameters if requested
|
|
304
|
+
if (options.validateInput !== false) {
|
|
305
|
+
this.validateToolParameters(toolInfo, parameters);
|
|
306
|
+
}
|
|
307
|
+
mcpLogger.debug(`[ToolDiscoveryService] Executing tool: ${toolName} on ${serverId}`, {
|
|
308
|
+
parameters,
|
|
309
|
+
});
|
|
310
|
+
// Create circuit breaker for tool execution
|
|
311
|
+
const circuitBreaker = globalCircuitBreakerManager.getBreaker(`tool-execution-${serverId}-${toolName}`, {
|
|
312
|
+
failureThreshold: 3,
|
|
313
|
+
resetTimeout: 30000,
|
|
314
|
+
operationTimeout: options.timeout || 30000,
|
|
315
|
+
});
|
|
316
|
+
// Execute tool with circuit breaker protection
|
|
317
|
+
const result = await circuitBreaker.execute(async () => {
|
|
318
|
+
const timeout = options.timeout || 30000;
|
|
319
|
+
const executePromise = client.callTool({
|
|
320
|
+
name: toolName,
|
|
321
|
+
arguments: parameters,
|
|
322
|
+
});
|
|
323
|
+
const timeoutPromise = this.createTimeoutPromise(timeout, `Tool execution timeout: ${toolName}`);
|
|
324
|
+
return await Promise.race([executePromise, timeoutPromise]);
|
|
325
|
+
});
|
|
326
|
+
const duration = Date.now() - startTime;
|
|
327
|
+
// Update tool statistics
|
|
328
|
+
this.updateToolStats(toolKey, true, duration);
|
|
329
|
+
// Validate output if requested
|
|
330
|
+
if (options.validateOutput !== false && result) {
|
|
331
|
+
this.validateToolOutput(result);
|
|
332
|
+
}
|
|
333
|
+
mcpLogger.debug(`[ToolDiscoveryService] Tool execution completed: ${toolName}`, {
|
|
334
|
+
duration,
|
|
335
|
+
hasContent: !!result.content,
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
data: result,
|
|
340
|
+
duration,
|
|
341
|
+
metadata: {
|
|
342
|
+
toolName,
|
|
343
|
+
serverId,
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const duration = Date.now() - startTime;
|
|
350
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
351
|
+
// Update tool statistics
|
|
352
|
+
const toolKey = this.createToolKey(serverId, toolName);
|
|
353
|
+
this.updateToolStats(toolKey, false, duration);
|
|
354
|
+
mcpLogger.error(`[ToolDiscoveryService] Tool execution failed: ${toolName}`, error);
|
|
355
|
+
return {
|
|
356
|
+
success: false,
|
|
357
|
+
error: errorMessage,
|
|
358
|
+
duration,
|
|
359
|
+
metadata: {
|
|
360
|
+
toolName,
|
|
361
|
+
serverId,
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Validate tool parameters
|
|
369
|
+
*/
|
|
370
|
+
validateToolParameters(toolInfo, parameters) {
|
|
371
|
+
if (!toolInfo.inputSchema) {
|
|
372
|
+
return; // No schema to validate against
|
|
373
|
+
}
|
|
374
|
+
// Basic validation - check required properties
|
|
375
|
+
const schema = toolInfo.inputSchema;
|
|
376
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
377
|
+
for (const requiredProp of schema.required) {
|
|
378
|
+
if (typeof requiredProp === "string" && !(requiredProp in parameters)) {
|
|
379
|
+
throw new Error(`Missing required parameter: ${requiredProp}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Type validation for properties
|
|
384
|
+
if (schema.properties) {
|
|
385
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
386
|
+
if (propName in parameters) {
|
|
387
|
+
this.validateParameterType(propName, parameters[propName], propSchema);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Validate parameter type
|
|
394
|
+
*/
|
|
395
|
+
validateParameterType(name, value, schema) {
|
|
396
|
+
if (!schema.type) {
|
|
397
|
+
return; // No type constraint
|
|
398
|
+
}
|
|
399
|
+
const expectedType = schema.type;
|
|
400
|
+
const actualType = typeof value;
|
|
401
|
+
switch (expectedType) {
|
|
402
|
+
case "string":
|
|
403
|
+
if (actualType !== "string") {
|
|
404
|
+
throw new Error(`Parameter '${name}' must be a string, got ${actualType}`);
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
case "number":
|
|
408
|
+
if (actualType !== "number") {
|
|
409
|
+
throw new Error(`Parameter '${name}' must be a number, got ${actualType}`);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
case "boolean":
|
|
413
|
+
if (actualType !== "boolean") {
|
|
414
|
+
throw new Error(`Parameter '${name}' must be a boolean, got ${actualType}`);
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
case "array":
|
|
418
|
+
if (!Array.isArray(value)) {
|
|
419
|
+
throw new Error(`Parameter '${name}' must be an array, got ${actualType}`);
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case "object":
|
|
423
|
+
if (actualType !== "object" || value === null || Array.isArray(value)) {
|
|
424
|
+
throw new Error(`Parameter '${name}' must be an object, got ${actualType}`);
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Validate tool output
|
|
431
|
+
*/
|
|
432
|
+
validateToolOutput(result) {
|
|
433
|
+
// Basic output validation
|
|
434
|
+
if (!result) {
|
|
435
|
+
throw new Error("Tool returned no result");
|
|
436
|
+
}
|
|
437
|
+
// Check for error indicators
|
|
438
|
+
if (result.error) {
|
|
439
|
+
throw new Error(`Tool execution error: ${result.error}`);
|
|
440
|
+
}
|
|
441
|
+
if (result.isError === true) {
|
|
442
|
+
throw new Error("Tool execution failed");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Update tool statistics
|
|
447
|
+
*/
|
|
448
|
+
updateToolStats(toolKey, success, duration) {
|
|
449
|
+
const toolInfo = this.toolRegistry.get(toolKey);
|
|
450
|
+
if (!toolInfo) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
toolInfo.stats.totalCalls++;
|
|
454
|
+
toolInfo.lastCalled = new Date();
|
|
455
|
+
toolInfo.stats.lastExecutionTime = duration;
|
|
456
|
+
if (success) {
|
|
457
|
+
toolInfo.stats.successfulCalls++;
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
toolInfo.stats.failedCalls++;
|
|
461
|
+
}
|
|
462
|
+
// Update average execution time
|
|
463
|
+
const totalTime = toolInfo.stats.averageExecutionTime * (toolInfo.stats.totalCalls - 1) +
|
|
464
|
+
duration;
|
|
465
|
+
toolInfo.stats.averageExecutionTime = totalTime / toolInfo.stats.totalCalls;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get tool by name and server
|
|
469
|
+
*/
|
|
470
|
+
getTool(toolName, serverId) {
|
|
471
|
+
const toolKey = this.createToolKey(serverId, toolName);
|
|
472
|
+
return this.toolRegistry.get(toolKey);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get all tools for a server
|
|
476
|
+
*/
|
|
477
|
+
getServerTools(serverId) {
|
|
478
|
+
const tools = [];
|
|
479
|
+
const serverToolNames = this.serverTools.get(serverId);
|
|
480
|
+
if (serverToolNames) {
|
|
481
|
+
for (const toolName of serverToolNames) {
|
|
482
|
+
const toolKey = this.createToolKey(serverId, toolName);
|
|
483
|
+
const toolInfo = this.toolRegistry.get(toolKey);
|
|
484
|
+
if (toolInfo) {
|
|
485
|
+
tools.push(toolInfo);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return tools;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Get all registered tools
|
|
493
|
+
*/
|
|
494
|
+
getAllTools() {
|
|
495
|
+
return Array.from(this.toolRegistry.values());
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Clear tools for a server
|
|
499
|
+
*/
|
|
500
|
+
clearServerTools(serverId) {
|
|
501
|
+
const serverToolNames = this.serverTools.get(serverId);
|
|
502
|
+
if (serverToolNames) {
|
|
503
|
+
for (const toolName of serverToolNames) {
|
|
504
|
+
const toolKey = this.createToolKey(serverId, toolName);
|
|
505
|
+
this.toolRegistry.delete(toolKey);
|
|
506
|
+
// Emit tool unregistered event
|
|
507
|
+
this.emit("toolUnregistered", {
|
|
508
|
+
serverId,
|
|
509
|
+
toolName,
|
|
510
|
+
timestamp: new Date(),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
this.serverTools.delete(serverId);
|
|
514
|
+
}
|
|
515
|
+
mcpLogger.debug(`[ToolDiscoveryService] Cleared tools for server: ${serverId}`);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Update tool availability
|
|
519
|
+
*/
|
|
520
|
+
updateToolAvailability(toolName, serverId, isAvailable) {
|
|
521
|
+
const toolKey = this.createToolKey(serverId, toolName);
|
|
522
|
+
const toolInfo = this.toolRegistry.get(toolKey);
|
|
523
|
+
if (toolInfo) {
|
|
524
|
+
toolInfo.isAvailable = isAvailable;
|
|
525
|
+
mcpLogger.debug(`[ToolDiscoveryService] Updated availability for ${toolName}: ${isAvailable}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Create tool key for registry
|
|
530
|
+
*/
|
|
531
|
+
createToolKey(serverId, toolName) {
|
|
532
|
+
return `${serverId}:${toolName}`;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Create timeout promise
|
|
536
|
+
*/
|
|
537
|
+
createTimeoutPromise(timeout, message) {
|
|
538
|
+
return new Promise((_, reject) => {
|
|
539
|
+
setTimeout(() => {
|
|
540
|
+
reject(new Error(message));
|
|
541
|
+
}, timeout);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get discovery statistics
|
|
546
|
+
*/
|
|
547
|
+
getStatistics() {
|
|
548
|
+
const toolsByServer = {};
|
|
549
|
+
const toolsByCategory = {};
|
|
550
|
+
let availableTools = 0;
|
|
551
|
+
let unavailableTools = 0;
|
|
552
|
+
for (const toolInfo of this.toolRegistry.values()) {
|
|
553
|
+
// Count by server
|
|
554
|
+
toolsByServer[toolInfo.serverId] =
|
|
555
|
+
(toolsByServer[toolInfo.serverId] || 0) + 1;
|
|
556
|
+
// Count by category
|
|
557
|
+
const category = typeof toolInfo.metadata?.category === "string"
|
|
558
|
+
? toolInfo.metadata.category
|
|
559
|
+
: "unknown";
|
|
560
|
+
toolsByCategory[category] = (toolsByCategory[category] || 0) + 1;
|
|
561
|
+
// Count availability
|
|
562
|
+
if (toolInfo.isAvailable) {
|
|
563
|
+
availableTools++;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
unavailableTools++;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
totalTools: this.toolRegistry.size,
|
|
571
|
+
availableTools,
|
|
572
|
+
unavailableTools,
|
|
573
|
+
totalServers: this.serverTools.size,
|
|
574
|
+
toolsByServer,
|
|
575
|
+
toolsByCategory,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|