@mozilla-ai/mcpd 0.0.1
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/LICENSE +201 -0
- package/README.md +429 -0
- package/dist/apiPaths.d.ts +11 -0
- package/dist/apiPaths.d.ts.map +1 -0
- package/dist/client.d.ts +231 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/dynamicCaller.d.ts +146 -0
- package/dist/dynamicCaller.d.ts.map +1 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/functionBuilder.d.ts +110 -0
- package/dist/functionBuilder.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1130 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1130 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types.d.ts +317 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cache.d.ts +22 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/typeConverter.d.ts +34 -0
- package/dist/utils/typeConverter.d.ts.map +1 -0
- package/package.json +80 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
import { LRUCache } from "lru-cache";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
class McpdError extends Error {
|
|
4
|
+
constructor(message, cause) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "McpdError";
|
|
7
|
+
this.cause = cause;
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class ConnectionError extends McpdError {
|
|
12
|
+
constructor(message, cause) {
|
|
13
|
+
super(message, cause);
|
|
14
|
+
this.name = "ConnectionError";
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class AuthenticationError extends McpdError {
|
|
19
|
+
constructor(message, cause) {
|
|
20
|
+
super(message, cause);
|
|
21
|
+
this.name = "AuthenticationError";
|
|
22
|
+
Error.captureStackTrace(this, this.constructor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
class ServerNotFoundError extends McpdError {
|
|
26
|
+
serverName;
|
|
27
|
+
constructor(message, serverName, cause) {
|
|
28
|
+
super(message, cause);
|
|
29
|
+
this.name = "ServerNotFoundError";
|
|
30
|
+
this.serverName = serverName;
|
|
31
|
+
Error.captureStackTrace(this, this.constructor);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class ServerUnhealthyError extends McpdError {
|
|
35
|
+
serverName;
|
|
36
|
+
healthStatus;
|
|
37
|
+
constructor(message, serverName, healthStatus, cause) {
|
|
38
|
+
super(message, cause);
|
|
39
|
+
this.name = "ServerUnhealthyError";
|
|
40
|
+
this.serverName = serverName;
|
|
41
|
+
this.healthStatus = healthStatus;
|
|
42
|
+
Error.captureStackTrace(this, this.constructor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
class ToolNotFoundError extends McpdError {
|
|
46
|
+
serverName;
|
|
47
|
+
toolName;
|
|
48
|
+
constructor(message, serverName, toolName, cause) {
|
|
49
|
+
super(message, cause);
|
|
50
|
+
this.name = "ToolNotFoundError";
|
|
51
|
+
this.serverName = serverName;
|
|
52
|
+
this.toolName = toolName;
|
|
53
|
+
Error.captureStackTrace(this, this.constructor);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
class ToolExecutionError extends McpdError {
|
|
57
|
+
serverName;
|
|
58
|
+
toolName;
|
|
59
|
+
errorModel;
|
|
60
|
+
constructor(message, serverName, toolName, errorModel, cause) {
|
|
61
|
+
super(message, cause);
|
|
62
|
+
this.name = "ToolExecutionError";
|
|
63
|
+
this.serverName = serverName;
|
|
64
|
+
this.toolName = toolName;
|
|
65
|
+
this.errorModel = errorModel;
|
|
66
|
+
Error.captureStackTrace(this, this.constructor);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class ValidationError extends McpdError {
|
|
70
|
+
validationErrors;
|
|
71
|
+
constructor(message, validationErrors, cause) {
|
|
72
|
+
super(message, cause);
|
|
73
|
+
this.name = "ValidationError";
|
|
74
|
+
this.validationErrors = validationErrors || [];
|
|
75
|
+
Error.captureStackTrace(this, this.constructor);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class TimeoutError extends McpdError {
|
|
79
|
+
operation;
|
|
80
|
+
timeout;
|
|
81
|
+
constructor(message, operation, timeout, cause) {
|
|
82
|
+
super(message, cause);
|
|
83
|
+
this.name = "TimeoutError";
|
|
84
|
+
this.operation = operation;
|
|
85
|
+
this.timeout = timeout;
|
|
86
|
+
Error.captureStackTrace(this, this.constructor);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var HealthStatus = /* @__PURE__ */ ((HealthStatus2) => {
|
|
90
|
+
HealthStatus2["OK"] = "ok";
|
|
91
|
+
HealthStatus2["TIMEOUT"] = "timeout";
|
|
92
|
+
HealthStatus2["UNREACHABLE"] = "unreachable";
|
|
93
|
+
HealthStatus2["UNKNOWN"] = "unknown";
|
|
94
|
+
return HealthStatus2;
|
|
95
|
+
})(HealthStatus || {});
|
|
96
|
+
const HealthStatusHelpers = {
|
|
97
|
+
/**
|
|
98
|
+
* Check if the given health status is a transient error state.
|
|
99
|
+
*/
|
|
100
|
+
isTransient(status) {
|
|
101
|
+
return status === "timeout" || status === "unknown";
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Check if the given status string represents a healthy state.
|
|
105
|
+
*/
|
|
106
|
+
isHealthy(status) {
|
|
107
|
+
return status === "ok";
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
function createCache(options = {}) {
|
|
111
|
+
return new LRUCache({
|
|
112
|
+
max: options.max ?? 100,
|
|
113
|
+
ttl: options.ttl ?? 1e4,
|
|
114
|
+
// Default 10 seconds
|
|
115
|
+
updateAgeOnGet: false,
|
|
116
|
+
updateAgeOnHas: false
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
class ServersNamespace {
|
|
120
|
+
#performCall;
|
|
121
|
+
#getTools;
|
|
122
|
+
/**
|
|
123
|
+
* Initialize the ServersNamespace with injected functions.
|
|
124
|
+
*
|
|
125
|
+
* @param performCall - Function to execute tool calls
|
|
126
|
+
* @param getTools - Function to get tool schemas
|
|
127
|
+
*/
|
|
128
|
+
constructor(performCall, getTools) {
|
|
129
|
+
this.#performCall = performCall;
|
|
130
|
+
this.#getTools = getTools;
|
|
131
|
+
return new Proxy(this, {
|
|
132
|
+
get: (target, serverName) => {
|
|
133
|
+
if (typeof serverName !== "string") {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
return new Server(target.#performCall, target.#getTools, serverName);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
class Server {
|
|
142
|
+
tools;
|
|
143
|
+
#performCall;
|
|
144
|
+
#getTools;
|
|
145
|
+
#serverName;
|
|
146
|
+
/**
|
|
147
|
+
* Initialize a Server for a specific server.
|
|
148
|
+
*
|
|
149
|
+
* @param performCall - Function to execute tool calls
|
|
150
|
+
* @param getTools - Function to get tool schemas
|
|
151
|
+
* @param serverName - The name of the MCP server
|
|
152
|
+
*/
|
|
153
|
+
constructor(performCall, getTools, serverName) {
|
|
154
|
+
this.#performCall = performCall;
|
|
155
|
+
this.#getTools = getTools;
|
|
156
|
+
this.#serverName = serverName;
|
|
157
|
+
this.tools = new ToolsNamespace(
|
|
158
|
+
this.#performCall,
|
|
159
|
+
this.#getTools,
|
|
160
|
+
this.#serverName
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* List all tools available on this server.
|
|
165
|
+
*
|
|
166
|
+
* @returns Array of tool schemas
|
|
167
|
+
* @throws {ServerNotFoundError} If the server doesn't exist
|
|
168
|
+
* @throws {ServerUnhealthyError} If the server is unhealthy
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* const tools = await client.servers.time.listTools();
|
|
173
|
+
* for (const tool of tools) {
|
|
174
|
+
* console.log(`${tool.name}: ${tool.description}`);
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
async listTools() {
|
|
179
|
+
return this.#getTools(this.#serverName);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if a tool exists on this server.
|
|
183
|
+
*
|
|
184
|
+
* The tool name must match exactly as returned by the server.
|
|
185
|
+
*
|
|
186
|
+
* @param toolName - The exact name of the tool to check
|
|
187
|
+
* @returns True if the tool exists, false otherwise
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* if (await client.servers.time.hasTool('get_current_time')) {
|
|
192
|
+
* const result = await client.servers.time.callTool('get_current_time', { timezone: 'UTC' });
|
|
193
|
+
* }
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
async hasTool(toolName) {
|
|
197
|
+
try {
|
|
198
|
+
const tools = await this.#getTools(this.#serverName);
|
|
199
|
+
return tools.some((t) => t.name === toolName);
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Call a tool by name with the given arguments.
|
|
206
|
+
*
|
|
207
|
+
* This method is useful for programmatic tool invocation when the tool name
|
|
208
|
+
* is in a variable. The tool name must match exactly as returned by the server.
|
|
209
|
+
*
|
|
210
|
+
* @param toolName - The exact name of the tool to call
|
|
211
|
+
* @param args - The arguments to pass to the tool
|
|
212
|
+
* @returns The tool's response
|
|
213
|
+
* @throws {ToolNotFoundError} If the tool doesn't exist on the server
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* // Call with explicit method (useful for dynamic tool names):
|
|
218
|
+
* const toolName = 'get_current_time';
|
|
219
|
+
* await client.servers.time.callTool(toolName, { timezone: 'UTC' });
|
|
220
|
+
*
|
|
221
|
+
* // Or with dynamic server name:
|
|
222
|
+
* const serverName = 'time';
|
|
223
|
+
* await client.servers[serverName].callTool(toolName, { timezone: 'UTC' });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
async callTool(toolName, args) {
|
|
227
|
+
const tools = await this.#getTools(this.#serverName);
|
|
228
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
229
|
+
if (!tool) {
|
|
230
|
+
throw new ToolNotFoundError(
|
|
231
|
+
`Tool '${toolName}' not found on server '${this.#serverName}'. Use client.servers.${this.#serverName}.listTools() to see available tools.`,
|
|
232
|
+
this.#serverName,
|
|
233
|
+
toolName
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return this.#performCall(this.#serverName, toolName, args);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
class ToolsNamespace {
|
|
240
|
+
#performCall;
|
|
241
|
+
#getTools;
|
|
242
|
+
#serverName;
|
|
243
|
+
/**
|
|
244
|
+
* Initialize a ToolsNamespace for a specific server.
|
|
245
|
+
*
|
|
246
|
+
* @param performCall - Function to execute tool calls
|
|
247
|
+
* @param getTools - Function to get tool schemas
|
|
248
|
+
* @param serverName - The name of the MCP server
|
|
249
|
+
*/
|
|
250
|
+
constructor(performCall, getTools, serverName) {
|
|
251
|
+
this.#performCall = performCall;
|
|
252
|
+
this.#getTools = getTools;
|
|
253
|
+
this.#serverName = serverName;
|
|
254
|
+
return new Proxy(this, {
|
|
255
|
+
get: (target, prop) => {
|
|
256
|
+
if (typeof prop !== "string") {
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
return async (args) => {
|
|
260
|
+
const toolName = prop;
|
|
261
|
+
const tools = await target.#getTools(target.#serverName);
|
|
262
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
263
|
+
if (!tool) {
|
|
264
|
+
throw new ToolNotFoundError(
|
|
265
|
+
`Tool '${toolName}' not found on server '${target.#serverName}'. Use client.servers.${target.#serverName}.listTools() to see available tools.`,
|
|
266
|
+
target.#serverName,
|
|
267
|
+
toolName
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return target.#performCall(target.#serverName, toolName, args);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
class TypeConverter {
|
|
277
|
+
/**
|
|
278
|
+
* Convert JSON schema types to JavaScript type names.
|
|
279
|
+
*
|
|
280
|
+
* Maps JSON Schema types to their JavaScript equivalents:
|
|
281
|
+
* - "string" → "string"
|
|
282
|
+
* - "integer" → "number"
|
|
283
|
+
* - "number" → "number"
|
|
284
|
+
* - "boolean" → "boolean"
|
|
285
|
+
* - "array" → "array"
|
|
286
|
+
* - "object" → "object"
|
|
287
|
+
* - "null" → "null"
|
|
288
|
+
* - unknown types → "any"
|
|
289
|
+
*/
|
|
290
|
+
static jsonTypeToJavaScriptType(jsonType, schemaDef) {
|
|
291
|
+
switch (jsonType) {
|
|
292
|
+
case "string":
|
|
293
|
+
if (schemaDef.enum && Array.isArray(schemaDef.enum)) {
|
|
294
|
+
return "string";
|
|
295
|
+
}
|
|
296
|
+
return "string";
|
|
297
|
+
case "number":
|
|
298
|
+
case "integer":
|
|
299
|
+
return "number";
|
|
300
|
+
case "boolean":
|
|
301
|
+
return "boolean";
|
|
302
|
+
case "array":
|
|
303
|
+
return "array";
|
|
304
|
+
case "object":
|
|
305
|
+
return "object";
|
|
306
|
+
case "null":
|
|
307
|
+
return "null";
|
|
308
|
+
default:
|
|
309
|
+
return "any";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse a schema definition and return the appropriate JavaScript type name.
|
|
314
|
+
*/
|
|
315
|
+
static parseSchemaType(schemaDef) {
|
|
316
|
+
if (schemaDef.anyOf && Array.isArray(schemaDef.anyOf)) {
|
|
317
|
+
const firstType = schemaDef.anyOf[0];
|
|
318
|
+
if (firstType && typeof firstType === "object" && firstType.type) {
|
|
319
|
+
return this.jsonTypeToJavaScriptType(firstType.type, firstType);
|
|
320
|
+
}
|
|
321
|
+
return "any";
|
|
322
|
+
}
|
|
323
|
+
if (schemaDef.type) {
|
|
324
|
+
return this.jsonTypeToJavaScriptType(schemaDef.type, schemaDef);
|
|
325
|
+
}
|
|
326
|
+
return "any";
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get a human-readable description of the expected type.
|
|
330
|
+
*/
|
|
331
|
+
static getTypeDescription(schemaDef) {
|
|
332
|
+
const baseType = this.parseSchemaType(schemaDef);
|
|
333
|
+
if (schemaDef.enum && Array.isArray(schemaDef.enum)) {
|
|
334
|
+
return `one of: ${schemaDef.enum.map((v) => JSON.stringify(v)).join(", ")}`;
|
|
335
|
+
}
|
|
336
|
+
if (schemaDef.type === "array" && schemaDef.items) {
|
|
337
|
+
const itemType = this.parseSchemaType(schemaDef.items);
|
|
338
|
+
return `array of ${itemType}`;
|
|
339
|
+
}
|
|
340
|
+
return baseType;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Validate a value against a JSON schema type.
|
|
344
|
+
* Returns true if valid, false otherwise.
|
|
345
|
+
*/
|
|
346
|
+
static validateValue(value, schemaDef) {
|
|
347
|
+
if (value === null || value === void 0) {
|
|
348
|
+
return schemaDef.type === "null" || (schemaDef.anyOf?.some((s) => s.type === "null") ?? false);
|
|
349
|
+
}
|
|
350
|
+
if (schemaDef.enum && Array.isArray(schemaDef.enum)) {
|
|
351
|
+
return schemaDef.enum.includes(value);
|
|
352
|
+
}
|
|
353
|
+
if (schemaDef.anyOf && Array.isArray(schemaDef.anyOf)) {
|
|
354
|
+
return schemaDef.anyOf.some(
|
|
355
|
+
(subSchema) => this.validateValue(value, subSchema)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (!schemaDef.type) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
switch (schemaDef.type) {
|
|
362
|
+
case "string":
|
|
363
|
+
return typeof value === "string";
|
|
364
|
+
case "number":
|
|
365
|
+
return typeof value === "number" && isFinite(value);
|
|
366
|
+
case "integer":
|
|
367
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
368
|
+
case "boolean":
|
|
369
|
+
return typeof value === "boolean";
|
|
370
|
+
case "array":
|
|
371
|
+
if (!Array.isArray(value)) return false;
|
|
372
|
+
if (schemaDef.items) {
|
|
373
|
+
return value.every(
|
|
374
|
+
(item) => this.validateValue(item, schemaDef.items)
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
case "object":
|
|
379
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
380
|
+
case "null":
|
|
381
|
+
return value === null;
|
|
382
|
+
default:
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
class FunctionBuilder {
|
|
388
|
+
#performCall;
|
|
389
|
+
#functionCache = /* @__PURE__ */ new Map();
|
|
390
|
+
/**
|
|
391
|
+
* Initialize a FunctionBuilder with an injected function.
|
|
392
|
+
*
|
|
393
|
+
* @param performCall - Function to execute tool calls
|
|
394
|
+
*/
|
|
395
|
+
constructor(performCall) {
|
|
396
|
+
this.#performCall = performCall;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Convert JSON schema to Zod schema.
|
|
400
|
+
*/
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
402
|
+
#jsonSchemaToZod(jsonSchema) {
|
|
403
|
+
if (!jsonSchema || typeof jsonSchema !== "object") {
|
|
404
|
+
return z.object({});
|
|
405
|
+
}
|
|
406
|
+
switch (jsonSchema.type) {
|
|
407
|
+
case "string":
|
|
408
|
+
return z.string();
|
|
409
|
+
case "number":
|
|
410
|
+
return z.number();
|
|
411
|
+
case "integer":
|
|
412
|
+
return z.number().int();
|
|
413
|
+
case "boolean":
|
|
414
|
+
return z.boolean();
|
|
415
|
+
case "array":
|
|
416
|
+
return z.array(
|
|
417
|
+
jsonSchema.items ? this.#jsonSchemaToZod(jsonSchema.items) : z.any()
|
|
418
|
+
);
|
|
419
|
+
case "object":
|
|
420
|
+
if (jsonSchema.properties) {
|
|
421
|
+
const propertyKeys = Object.keys(jsonSchema.properties);
|
|
422
|
+
if (propertyKeys.length > 0) {
|
|
423
|
+
const shape = {};
|
|
424
|
+
const required = new Set(jsonSchema.required || []);
|
|
425
|
+
for (const [key, value] of Object.entries(jsonSchema.properties)) {
|
|
426
|
+
let schema = this.#jsonSchemaToZod(value);
|
|
427
|
+
if (!required.has(key)) {
|
|
428
|
+
schema = schema.nullable().optional();
|
|
429
|
+
}
|
|
430
|
+
shape[key] = schema;
|
|
431
|
+
}
|
|
432
|
+
return z.object(shape);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return z.object({});
|
|
436
|
+
default:
|
|
437
|
+
if (jsonSchema.properties) {
|
|
438
|
+
const propertyKeys = Object.keys(jsonSchema.properties);
|
|
439
|
+
if (propertyKeys.length > 0) {
|
|
440
|
+
const shape = {};
|
|
441
|
+
const required = new Set(jsonSchema.required || []);
|
|
442
|
+
for (const [key, value] of Object.entries(jsonSchema.properties)) {
|
|
443
|
+
let schema = this.#jsonSchemaToZod(value);
|
|
444
|
+
if (!required.has(key)) {
|
|
445
|
+
schema = schema.nullable().optional();
|
|
446
|
+
}
|
|
447
|
+
shape[key] = schema;
|
|
448
|
+
}
|
|
449
|
+
return z.object(shape);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return z.object({});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Convert a string into a safe JavaScript identifier.
|
|
457
|
+
*
|
|
458
|
+
* This method sanitizes arbitrary strings (like server names or tool names) to create
|
|
459
|
+
* valid JavaScript identifiers that can be used as function names.
|
|
460
|
+
* It replaces non-word characters and handles edge cases like leading digits.
|
|
461
|
+
*
|
|
462
|
+
* @param name - The string to convert into a safe identifier
|
|
463
|
+
* @returns A string that is a valid JavaScript identifier
|
|
464
|
+
*/
|
|
465
|
+
safeName(name) {
|
|
466
|
+
return name.replace(/\W|^(?=\d)/g, "_");
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Generate a unique function name from server and tool names.
|
|
470
|
+
*
|
|
471
|
+
* This method creates a qualified function name by combining the server name
|
|
472
|
+
* and tool name with a double underscore separator. Both names are sanitized
|
|
473
|
+
* using safeName() to ensure the result is a valid JavaScript identifier.
|
|
474
|
+
*
|
|
475
|
+
* @param serverName - The name of the MCP server hosting the tool
|
|
476
|
+
* @param schemaName - The name of the tool from the schema definition
|
|
477
|
+
* @returns A qualified function name in the format "{safe_server}__{safe_tool}"
|
|
478
|
+
*/
|
|
479
|
+
functionName(serverName, schemaName) {
|
|
480
|
+
return `${this.safeName(serverName)}__${this.safeName(schemaName)}`;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Create a callable JavaScript function from an MCP tool's JSON Schema definition.
|
|
484
|
+
*
|
|
485
|
+
* This method generates a self-contained, callable function that validates parameters
|
|
486
|
+
* and executes the corresponding MCP tool. The function includes proper parameter
|
|
487
|
+
* validation and comprehensive metadata based on the tool's JSON Schema.
|
|
488
|
+
*
|
|
489
|
+
* Generated functions are cached for performance. If a function for the same
|
|
490
|
+
* server/tool combination already exists in the cache, it returns the cached function.
|
|
491
|
+
*
|
|
492
|
+
* @param schema - The MCP tool's JSON Schema definition
|
|
493
|
+
* @param serverName - The name of the MCP server hosting this tool
|
|
494
|
+
* @returns A callable JavaScript function with metadata
|
|
495
|
+
*/
|
|
496
|
+
createFunctionFromSchema(schema, serverName) {
|
|
497
|
+
const cacheKey = `${serverName}__${schema.name}`;
|
|
498
|
+
if (this.#functionCache.has(cacheKey)) {
|
|
499
|
+
return this.#functionCache.get(cacheKey);
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const generatedFunction = this.buildFunction(schema, serverName);
|
|
503
|
+
this.#functionCache.set(cacheKey, generatedFunction);
|
|
504
|
+
return generatedFunction;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
throw new McpdError(
|
|
507
|
+
`Error creating function ${cacheKey}: ${error.message}`,
|
|
508
|
+
error
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Build the actual function from the schema.
|
|
514
|
+
*
|
|
515
|
+
* @param schema - The tool schema
|
|
516
|
+
* @param serverName - The server name
|
|
517
|
+
* @returns The generated function with metadata
|
|
518
|
+
*/
|
|
519
|
+
buildFunction(schema, serverName) {
|
|
520
|
+
const inputSchema = schema.inputSchema || {};
|
|
521
|
+
const properties = inputSchema.properties || {};
|
|
522
|
+
const required = new Set(inputSchema.required || []);
|
|
523
|
+
const implementation = async (...args) => {
|
|
524
|
+
let params = {};
|
|
525
|
+
if (args.length === 1 && typeof args[0] === "object" && args[0] !== null && !Array.isArray(args[0])) {
|
|
526
|
+
params = args[0];
|
|
527
|
+
} else {
|
|
528
|
+
const propertyNames = Object.keys(properties);
|
|
529
|
+
for (let i = 0; i < args.length && i < propertyNames.length; i++) {
|
|
530
|
+
const propertyName = propertyNames[i];
|
|
531
|
+
if (propertyName) {
|
|
532
|
+
params[propertyName] = args[i];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const missingParams = [];
|
|
537
|
+
for (const paramName of required) {
|
|
538
|
+
if (!(paramName in params) || params[paramName] === null || params[paramName] === void 0) {
|
|
539
|
+
missingParams.push(paramName);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (missingParams.length > 0) {
|
|
543
|
+
throw new ValidationError(
|
|
544
|
+
`Missing required parameters: ${missingParams.join(", ")}`,
|
|
545
|
+
missingParams
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const validationErrors = [];
|
|
549
|
+
for (const [paramName, paramValue] of Object.entries(params)) {
|
|
550
|
+
const paramSchema = properties[paramName];
|
|
551
|
+
if (paramValue !== null && paramValue !== void 0 && paramSchema) {
|
|
552
|
+
if (!TypeConverter.validateValue(paramValue, paramSchema)) {
|
|
553
|
+
const expectedType = TypeConverter.getTypeDescription(paramSchema);
|
|
554
|
+
validationErrors.push(
|
|
555
|
+
`Parameter '${paramName}' should be ${expectedType}, got ${typeof paramValue}`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (validationErrors.length > 0) {
|
|
561
|
+
throw new ValidationError(
|
|
562
|
+
`Parameter validation failed: ${validationErrors.join("; ")}`,
|
|
563
|
+
validationErrors
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
const cleanParams = {};
|
|
567
|
+
for (const [key, value] of Object.entries(params)) {
|
|
568
|
+
if (value !== null && value !== void 0) {
|
|
569
|
+
cleanParams[key] = value;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return this.#performCall(serverName, schema.name, cleanParams);
|
|
573
|
+
};
|
|
574
|
+
const invoke = async (args) => {
|
|
575
|
+
return implementation(args);
|
|
576
|
+
};
|
|
577
|
+
const execute = async (args) => {
|
|
578
|
+
return implementation(args);
|
|
579
|
+
};
|
|
580
|
+
const zodSchema = this.#jsonSchemaToZod(inputSchema);
|
|
581
|
+
const qualifiedName = this.functionName(serverName, schema.name);
|
|
582
|
+
const docstring = this.createDocstring(schema);
|
|
583
|
+
const agentFunction = implementation;
|
|
584
|
+
Object.defineProperty(agentFunction, "name", {
|
|
585
|
+
value: qualifiedName,
|
|
586
|
+
writable: false,
|
|
587
|
+
enumerable: true,
|
|
588
|
+
configurable: true
|
|
589
|
+
});
|
|
590
|
+
agentFunction.description = schema.description || docstring.split("\n")[0] || "No description provided";
|
|
591
|
+
agentFunction.schema = zodSchema;
|
|
592
|
+
agentFunction.invoke = invoke;
|
|
593
|
+
agentFunction.lc_namespace = ["mcpd", "tools"];
|
|
594
|
+
agentFunction.returnDirect = false;
|
|
595
|
+
agentFunction.inputSchema = zodSchema;
|
|
596
|
+
agentFunction.execute = execute;
|
|
597
|
+
agentFunction._schema = schema;
|
|
598
|
+
agentFunction._serverName = serverName;
|
|
599
|
+
agentFunction._toolName = schema.name;
|
|
600
|
+
return agentFunction;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Generate a comprehensive docstring for the dynamically created function.
|
|
604
|
+
*
|
|
605
|
+
* This method builds a properly formatted docstring that includes the
|
|
606
|
+
* tool's description, parameter documentation with optional/required status,
|
|
607
|
+
* return value information, and exception documentation.
|
|
608
|
+
*
|
|
609
|
+
* @param schema - The MCP tool's JSON Schema definition
|
|
610
|
+
* @returns A multi-line string containing the complete docstring text
|
|
611
|
+
*/
|
|
612
|
+
createDocstring(schema) {
|
|
613
|
+
const description = schema.description || "No description provided";
|
|
614
|
+
const inputSchema = schema.inputSchema || {};
|
|
615
|
+
const properties = inputSchema.properties || {};
|
|
616
|
+
const required = new Set(inputSchema.required || []);
|
|
617
|
+
const docstringParts = [description];
|
|
618
|
+
if (Object.keys(properties).length > 0) {
|
|
619
|
+
docstringParts.push("");
|
|
620
|
+
docstringParts.push("Parameters:");
|
|
621
|
+
for (const [paramName, paramInfo] of Object.entries(properties)) {
|
|
622
|
+
const isRequired = required.has(paramName);
|
|
623
|
+
const paramDesc = paramInfo.description || "No description provided";
|
|
624
|
+
const paramType = TypeConverter.getTypeDescription(paramInfo);
|
|
625
|
+
const requiredText = isRequired ? "" : " (optional)";
|
|
626
|
+
docstringParts.push(
|
|
627
|
+
` ${paramName} (${paramType}): ${paramDesc}${requiredText}`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
docstringParts.push("");
|
|
632
|
+
docstringParts.push("Returns:");
|
|
633
|
+
docstringParts.push(" Promise<any>: Function execution result");
|
|
634
|
+
docstringParts.push("");
|
|
635
|
+
docstringParts.push("Throws:");
|
|
636
|
+
docstringParts.push(
|
|
637
|
+
" ValidationError: If required parameters are missing or invalid"
|
|
638
|
+
);
|
|
639
|
+
docstringParts.push(" McpdError: If the API call fails");
|
|
640
|
+
return docstringParts.join("\n");
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Clear the function cache.
|
|
644
|
+
*
|
|
645
|
+
* This method clears all cached generated functions, forcing them to be
|
|
646
|
+
* regenerated on the next call to createFunctionFromSchema().
|
|
647
|
+
*/
|
|
648
|
+
clearCache() {
|
|
649
|
+
this.#functionCache.clear();
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get the current cache size.
|
|
653
|
+
*
|
|
654
|
+
* @returns The number of functions currently cached
|
|
655
|
+
*/
|
|
656
|
+
getCacheSize() {
|
|
657
|
+
return this.#functionCache.size;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const API_BASE = "/api/v1";
|
|
661
|
+
const SERVERS_BASE = `${API_BASE}/servers`;
|
|
662
|
+
const HEALTH_SERVERS_BASE = `${API_BASE}/health/servers`;
|
|
663
|
+
const API_PATHS = {
|
|
664
|
+
// Server management
|
|
665
|
+
SERVERS: SERVERS_BASE,
|
|
666
|
+
// Tools
|
|
667
|
+
SERVER_TOOLS: (serverName) => `${SERVERS_BASE}/${encodeURIComponent(serverName)}/tools`,
|
|
668
|
+
TOOL_CALL: (serverName, toolName) => `${SERVERS_BASE}/${encodeURIComponent(serverName)}/tools/${encodeURIComponent(toolName)}`,
|
|
669
|
+
// Health
|
|
670
|
+
HEALTH_ALL: HEALTH_SERVERS_BASE,
|
|
671
|
+
HEALTH_SERVER: (serverName) => `${HEALTH_SERVERS_BASE}/${encodeURIComponent(serverName)}`
|
|
672
|
+
};
|
|
673
|
+
class McpdClient {
|
|
674
|
+
#endpoint;
|
|
675
|
+
#apiKey;
|
|
676
|
+
#timeout;
|
|
677
|
+
#serverHealthCache;
|
|
678
|
+
#functionBuilder;
|
|
679
|
+
#cacheableExceptions = /* @__PURE__ */ new Set([
|
|
680
|
+
ServerNotFoundError,
|
|
681
|
+
ServerUnhealthyError,
|
|
682
|
+
AuthenticationError
|
|
683
|
+
]);
|
|
684
|
+
/**
|
|
685
|
+
* Namespace for accessing MCP servers and their tools.
|
|
686
|
+
*/
|
|
687
|
+
servers;
|
|
688
|
+
/**
|
|
689
|
+
* Initialize a new McpdClient instance.
|
|
690
|
+
*
|
|
691
|
+
* @param options - Configuration options for the client
|
|
692
|
+
*/
|
|
693
|
+
constructor(options) {
|
|
694
|
+
this.#endpoint = options.apiEndpoint.replace(/\/$/, "");
|
|
695
|
+
this.#apiKey = options.apiKey;
|
|
696
|
+
this.#timeout = options.timeout ?? 3e4;
|
|
697
|
+
const healthCacheTtlMs = (options.healthCacheTtl ?? 10) * 1e3;
|
|
698
|
+
this.#serverHealthCache = createCache({
|
|
699
|
+
max: 100,
|
|
700
|
+
// TODO: Extract to const like Python SDK see:
|
|
701
|
+
// _SERVER_HEALTH_CACHE_MAXSIZE: int = 100
|
|
702
|
+
// """Maximum number of server health entries to cache.
|
|
703
|
+
// Prevents unbounded memory growth while allowing legitimate large-scale monitoring."""
|
|
704
|
+
ttl: healthCacheTtlMs
|
|
705
|
+
});
|
|
706
|
+
this.servers = new ServersNamespace(
|
|
707
|
+
this.#performCall.bind(this),
|
|
708
|
+
this.#getToolsByServer.bind(this)
|
|
709
|
+
);
|
|
710
|
+
this.#functionBuilder = new FunctionBuilder(this.#performCall.bind(this));
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Make an HTTP request to the mcpd daemon.
|
|
714
|
+
*
|
|
715
|
+
* @param path - The API path (e.g., '/servers', '/servers/{server_name}/tools')
|
|
716
|
+
* @param options - Request options
|
|
717
|
+
* @returns The JSON response from the daemon
|
|
718
|
+
* @throws {McpdError} If the request fails
|
|
719
|
+
*/
|
|
720
|
+
async #request(path, options = {}) {
|
|
721
|
+
const url = `${this.#endpoint}${path}`;
|
|
722
|
+
const headers = {
|
|
723
|
+
"Content-Type": "application/json",
|
|
724
|
+
...options.headers || {}
|
|
725
|
+
};
|
|
726
|
+
if (this.#apiKey) {
|
|
727
|
+
headers["Authorization"] = `Bearer ${this.#apiKey}`;
|
|
728
|
+
}
|
|
729
|
+
const controller = new AbortController();
|
|
730
|
+
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
731
|
+
try {
|
|
732
|
+
const response = await fetch(url, {
|
|
733
|
+
...options,
|
|
734
|
+
headers,
|
|
735
|
+
signal: controller.signal
|
|
736
|
+
});
|
|
737
|
+
clearTimeout(timeoutId);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
const body = await response.text();
|
|
740
|
+
let errorModel = null;
|
|
741
|
+
try {
|
|
742
|
+
errorModel = JSON.parse(body);
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
if (errorModel && errorModel.detail) {
|
|
746
|
+
const errorDetails = errorModel.errors?.map((e) => `${e.location}: ${e.message}`).join("; ");
|
|
747
|
+
const fullMessage = errorDetails ? `${errorModel.detail} - ${errorDetails}` : errorModel.detail;
|
|
748
|
+
if (response.status === 401 || response.status === 403) {
|
|
749
|
+
throw new AuthenticationError(fullMessage);
|
|
750
|
+
}
|
|
751
|
+
throw new McpdError(
|
|
752
|
+
`${errorModel.title || "Request failed"}: ${fullMessage}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (response.status === 401 || response.status === 403) {
|
|
756
|
+
throw new AuthenticationError(
|
|
757
|
+
`Authentication failed: ${response.status} ${response.statusText}`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
throw new McpdError(
|
|
761
|
+
`Request failed: ${response.status} ${response.statusText} - ${body}`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
return await response.json();
|
|
766
|
+
} catch (error) {
|
|
767
|
+
throw new McpdError("Failed to parse JSON response", error);
|
|
768
|
+
}
|
|
769
|
+
} catch (error) {
|
|
770
|
+
clearTimeout(timeoutId);
|
|
771
|
+
if (error.name === "AbortError") {
|
|
772
|
+
throw new TimeoutError(
|
|
773
|
+
`Request timed out after ${this.#timeout}ms`,
|
|
774
|
+
path,
|
|
775
|
+
this.#timeout
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
779
|
+
throw new ConnectionError(
|
|
780
|
+
`Cannot connect to mcpd daemon at ${this.#endpoint}. Is it running?`,
|
|
781
|
+
error
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
if (error instanceof McpdError) {
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
throw new McpdError(
|
|
788
|
+
`Request failed: ${error.message}`,
|
|
789
|
+
error
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Get a list of all configured MCP servers.
|
|
795
|
+
*
|
|
796
|
+
* @returns Array of server names
|
|
797
|
+
* @throws {McpdError} If the request fails
|
|
798
|
+
*
|
|
799
|
+
* @example
|
|
800
|
+
* ```typescript
|
|
801
|
+
* const servers = await client.listServers();
|
|
802
|
+
* console.log(servers); // ['time', 'fetch', 'git']
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
async listServers() {
|
|
806
|
+
return await this.#request(API_PATHS.SERVERS);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get tool schemas from all (or specific) MCP servers with transformed names.
|
|
810
|
+
*
|
|
811
|
+
* IMPORTANT: Tool names are transformed to `serverName__toolName` format to:
|
|
812
|
+
* 1. Prevent naming clashes when aggregating tools from multiple servers
|
|
813
|
+
* 2. Identify which server each tool belongs to
|
|
814
|
+
*
|
|
815
|
+
* This method automatically filters out unhealthy servers by checking their health
|
|
816
|
+
* status before fetching tools. Unhealthy servers are silently skipped to ensure
|
|
817
|
+
* the method returns quickly without waiting for timeouts on failed servers.
|
|
818
|
+
*
|
|
819
|
+
* Tool fetches from multiple servers are executed concurrently for optimal performance.
|
|
820
|
+
*
|
|
821
|
+
* This is useful for:
|
|
822
|
+
* - MCP servers that aggregate and re-expose tools from multiple upstream servers
|
|
823
|
+
* - Tool inspection and discovery across all servers
|
|
824
|
+
* - Custom tooling that needs raw MCP tool schemas
|
|
825
|
+
*
|
|
826
|
+
* @param options - Optional configuration
|
|
827
|
+
* @param options.servers - Array of server names to include. If not specified, includes all servers.
|
|
828
|
+
* @returns Array of tool schemas with transformed names (serverName__toolName). Only includes tools from healthy servers.
|
|
829
|
+
* @throws {ConnectionError} If unable to connect to the mcpd daemon
|
|
830
|
+
* @throws {TimeoutError} If requests to the daemon time out
|
|
831
|
+
* @throws {AuthenticationError} If API key authentication fails
|
|
832
|
+
* @throws {McpdError} If health check or initial server listing fails
|
|
833
|
+
*
|
|
834
|
+
* @example
|
|
835
|
+
* ```typescript
|
|
836
|
+
* // Get all tools from all servers
|
|
837
|
+
* const allTools = await client.getToolSchemas();
|
|
838
|
+
* // Returns: [
|
|
839
|
+
* // { name: "time__get_current_time", description: "...", ... },
|
|
840
|
+
* // { name: "fetch__fetch_url", description: "...", ... }
|
|
841
|
+
* // ]
|
|
842
|
+
*
|
|
843
|
+
* // Get tools from specific servers only
|
|
844
|
+
* const someTools = await client.getToolSchemas({ servers: ['time', 'fetch'] });
|
|
845
|
+
*
|
|
846
|
+
* // Original tool name "get_current_time" becomes "time__get_current_time"
|
|
847
|
+
* // This prevents clashes if multiple servers have tools with the same name
|
|
848
|
+
* ```
|
|
849
|
+
*/
|
|
850
|
+
async getToolSchemas(options) {
|
|
851
|
+
const { servers } = options || {};
|
|
852
|
+
const serverNames = servers && servers.length > 0 ? servers : await this.listServers();
|
|
853
|
+
const healthMap = await this.getServerHealth();
|
|
854
|
+
const healthyServers = serverNames.filter((name) => {
|
|
855
|
+
const health = healthMap[name];
|
|
856
|
+
return health && HealthStatusHelpers.isHealthy(health.status);
|
|
857
|
+
});
|
|
858
|
+
const results = await Promise.allSettled(
|
|
859
|
+
healthyServers.map(async (serverName) => ({
|
|
860
|
+
serverName,
|
|
861
|
+
tools: await this.#getToolsByServer(serverName)
|
|
862
|
+
}))
|
|
863
|
+
);
|
|
864
|
+
const allTools = [];
|
|
865
|
+
for (const result of results) {
|
|
866
|
+
if (result.status === "fulfilled") {
|
|
867
|
+
const { serverName, tools } = result.value;
|
|
868
|
+
for (const tool of tools) {
|
|
869
|
+
allTools.push({
|
|
870
|
+
...tool,
|
|
871
|
+
name: `${serverName}__${tool.name}`
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
console.warn(`Failed to get tools for server:`, result.reason);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return allTools;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Internal method to get tool schemas for a server.
|
|
882
|
+
* Used by dependency injection for ServersNamespace and internally for getAgentTools.
|
|
883
|
+
*
|
|
884
|
+
* @param serverName - Server name to get tools for
|
|
885
|
+
* @returns Tool schemas for the specified server
|
|
886
|
+
* @throws {ServerNotFoundError} If the specified server doesn't exist
|
|
887
|
+
* @throws {ServerUnhealthyError} If the server is not healthy
|
|
888
|
+
* @throws {ConnectionError} If unable to connect to the mcpd daemon
|
|
889
|
+
* @throws {TimeoutError} If the request times out
|
|
890
|
+
* @throws {McpdError} If the request fails
|
|
891
|
+
* @internal
|
|
892
|
+
*/
|
|
893
|
+
async #getToolsByServer(serverName) {
|
|
894
|
+
await this.#ensureServerHealthy(serverName);
|
|
895
|
+
const path = API_PATHS.SERVER_TOOLS(serverName);
|
|
896
|
+
const response = await this.#request(path);
|
|
897
|
+
if (!response.tools) {
|
|
898
|
+
throw new ServerNotFoundError(
|
|
899
|
+
`Server '${serverName}' not found`,
|
|
900
|
+
serverName
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
return response.tools;
|
|
904
|
+
}
|
|
905
|
+
async getServerHealth(serverName) {
|
|
906
|
+
if (serverName) {
|
|
907
|
+
const cacheKey = `health:${serverName}`;
|
|
908
|
+
const cached = this.#serverHealthCache.get(cacheKey);
|
|
909
|
+
if (cached !== void 0) {
|
|
910
|
+
if (cached instanceof Error) {
|
|
911
|
+
throw cached;
|
|
912
|
+
}
|
|
913
|
+
return cached;
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const path = API_PATHS.HEALTH_SERVER(serverName);
|
|
917
|
+
const health = await this.#request(path);
|
|
918
|
+
this.#serverHealthCache.set(cacheKey, health);
|
|
919
|
+
return health;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (error instanceof Error) {
|
|
922
|
+
for (const errorType of this.#cacheableExceptions) {
|
|
923
|
+
if (error instanceof errorType) {
|
|
924
|
+
this.#serverHealthCache.set(cacheKey, error);
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
const response = await this.#request(
|
|
933
|
+
API_PATHS.HEALTH_ALL
|
|
934
|
+
);
|
|
935
|
+
const healthMap = {};
|
|
936
|
+
for (const server of response.servers) {
|
|
937
|
+
healthMap[server.name] = server;
|
|
938
|
+
const cacheKey = `health:${server.name}`;
|
|
939
|
+
this.#serverHealthCache.set(cacheKey, server);
|
|
940
|
+
}
|
|
941
|
+
return healthMap;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Check if a specific server is healthy.
|
|
946
|
+
*
|
|
947
|
+
* @param serverName - The name of the server to check
|
|
948
|
+
* @returns True if the server is healthy, false otherwise
|
|
949
|
+
*
|
|
950
|
+
* @example
|
|
951
|
+
* ```typescript
|
|
952
|
+
* if (await client.isServerHealthy('time')) {
|
|
953
|
+
* const time = await client.servers.time.get_current_time();
|
|
954
|
+
* }
|
|
955
|
+
* ```
|
|
956
|
+
*/
|
|
957
|
+
async isServerHealthy(serverName) {
|
|
958
|
+
try {
|
|
959
|
+
const health = await this.getServerHealth(serverName);
|
|
960
|
+
return HealthStatusHelpers.isHealthy(health.status);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
if (error instanceof ServerNotFoundError) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Ensure a server is healthy before performing an operation.
|
|
970
|
+
*
|
|
971
|
+
* @param serverName - The name of the server to check
|
|
972
|
+
* @throws {ServerNotFoundError} If the server doesn't exist
|
|
973
|
+
* @throws {ServerUnhealthyError} If the server is not healthy
|
|
974
|
+
*/
|
|
975
|
+
async #ensureServerHealthy(serverName) {
|
|
976
|
+
const health = await this.getServerHealth(serverName);
|
|
977
|
+
if (!health) {
|
|
978
|
+
throw new ServerNotFoundError(
|
|
979
|
+
`Server '${serverName}' not found`,
|
|
980
|
+
serverName
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (!HealthStatusHelpers.isHealthy(health.status)) {
|
|
984
|
+
throw new ServerUnhealthyError(
|
|
985
|
+
`Server '${serverName}' is not healthy: ${health.status}`,
|
|
986
|
+
serverName,
|
|
987
|
+
health.status
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Internal method to perform a tool call on a server.
|
|
993
|
+
*
|
|
994
|
+
* ⚠️ This method is truly private and cannot be accessed by SDK consumers.
|
|
995
|
+
* Use the fluent API instead: `client.servers.foo.tools.bar(args)`
|
|
996
|
+
*
|
|
997
|
+
* This method is used internally by:
|
|
998
|
+
* - ToolsProxy (via dependency injection)
|
|
999
|
+
* - FunctionBuilder (via dependency injection)
|
|
1000
|
+
*
|
|
1001
|
+
* @param serverName - The name of the server
|
|
1002
|
+
* @param toolName - The exact name of the tool
|
|
1003
|
+
* @param args - The tool arguments
|
|
1004
|
+
* @returns The tool's response
|
|
1005
|
+
* @throws {ToolExecutionError} If the tool execution fails
|
|
1006
|
+
* @internal
|
|
1007
|
+
*/
|
|
1008
|
+
async #performCall(serverName, toolName, args) {
|
|
1009
|
+
const path = API_PATHS.TOOL_CALL(serverName, toolName);
|
|
1010
|
+
try {
|
|
1011
|
+
const response = await this.#request(path, {
|
|
1012
|
+
method: "POST",
|
|
1013
|
+
body: JSON.stringify(args || {})
|
|
1014
|
+
});
|
|
1015
|
+
if (typeof response === "string") {
|
|
1016
|
+
try {
|
|
1017
|
+
return JSON.parse(response);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return response;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return response;
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
if (error instanceof McpdError) {
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
throw new ToolExecutionError(
|
|
1028
|
+
`Failed to execute tool '${toolName}' on server '${serverName}': ${error.message}`,
|
|
1029
|
+
serverName,
|
|
1030
|
+
toolName,
|
|
1031
|
+
void 0,
|
|
1032
|
+
error
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Clear the cached agent tools functions.
|
|
1038
|
+
* This should be called when the tool schemas might have changed.
|
|
1039
|
+
*/
|
|
1040
|
+
clearAgentToolsCache() {
|
|
1041
|
+
this.#functionBuilder.clearCache();
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Clear the server health cache.
|
|
1045
|
+
* This forces fresh health checks on the next getServerHealth() or isServerHealthy() call.
|
|
1046
|
+
*/
|
|
1047
|
+
clearServerHealthCache() {
|
|
1048
|
+
this.#serverHealthCache.clear();
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Generate callable functions for use with AI agent frameworks (internal).
|
|
1052
|
+
*
|
|
1053
|
+
* This method queries servers and creates self-contained, callable functions
|
|
1054
|
+
* that can be passed to AI agent frameworks. Each function includes its schema
|
|
1055
|
+
* as metadata and handles the MCP communication internally.
|
|
1056
|
+
*
|
|
1057
|
+
* This method automatically filters out unhealthy servers by checking their health
|
|
1058
|
+
* status before fetching tools. Unhealthy servers are silently skipped to ensure
|
|
1059
|
+
* the method returns quickly without waiting for timeouts on failed servers.
|
|
1060
|
+
*
|
|
1061
|
+
* Tool fetches from multiple servers are executed concurrently for optimal performance.
|
|
1062
|
+
*
|
|
1063
|
+
* @param servers - Optional list of server names to include. If not specified, includes all servers.
|
|
1064
|
+
* @returns Array of callable functions with metadata. Only includes tools from healthy servers.
|
|
1065
|
+
*
|
|
1066
|
+
* @throws {ConnectionError} If unable to connect to the mcpd daemon
|
|
1067
|
+
* @throws {TimeoutError} If requests to the daemon time out
|
|
1068
|
+
* @throws {AuthenticationError} If API key authentication fails
|
|
1069
|
+
* @throws {McpdError} If unable to retrieve health status, server list, or generate functions
|
|
1070
|
+
* @internal
|
|
1071
|
+
*/
|
|
1072
|
+
async agentTools(servers) {
|
|
1073
|
+
const serverNames = servers && servers.length > 0 ? servers : await this.listServers();
|
|
1074
|
+
const healthMap = await this.getServerHealth();
|
|
1075
|
+
const healthyServers = serverNames.filter((name) => {
|
|
1076
|
+
const health = healthMap[name];
|
|
1077
|
+
return health && HealthStatusHelpers.isHealthy(health.status);
|
|
1078
|
+
});
|
|
1079
|
+
const results = await Promise.allSettled(
|
|
1080
|
+
healthyServers.map(async (serverName) => ({
|
|
1081
|
+
serverName,
|
|
1082
|
+
tools: await this.#getToolsByServer(serverName)
|
|
1083
|
+
}))
|
|
1084
|
+
);
|
|
1085
|
+
const agentTools = [];
|
|
1086
|
+
for (const result of results) {
|
|
1087
|
+
if (result.status === "fulfilled") {
|
|
1088
|
+
const { serverName, tools } = result.value;
|
|
1089
|
+
for (const toolSchema of tools) {
|
|
1090
|
+
const func = this.#functionBuilder.createFunctionFromSchema(
|
|
1091
|
+
toolSchema,
|
|
1092
|
+
serverName
|
|
1093
|
+
);
|
|
1094
|
+
agentTools.push(func);
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
console.warn(`Failed to get tools for server:`, result.reason);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return agentTools;
|
|
1101
|
+
}
|
|
1102
|
+
async getAgentTools(options = {}) {
|
|
1103
|
+
const { servers, format = "array" } = options;
|
|
1104
|
+
const tools = await this.agentTools(servers);
|
|
1105
|
+
switch (format) {
|
|
1106
|
+
case "object":
|
|
1107
|
+
return Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
1108
|
+
case "map":
|
|
1109
|
+
return new Map(tools.map((tool) => [tool.name, tool]));
|
|
1110
|
+
case "array":
|
|
1111
|
+
default:
|
|
1112
|
+
return tools;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
export {
|
|
1117
|
+
AuthenticationError,
|
|
1118
|
+
ConnectionError,
|
|
1119
|
+
HealthStatus,
|
|
1120
|
+
HealthStatusHelpers,
|
|
1121
|
+
McpdClient,
|
|
1122
|
+
McpdError,
|
|
1123
|
+
ServerNotFoundError,
|
|
1124
|
+
ServerUnhealthyError,
|
|
1125
|
+
TimeoutError,
|
|
1126
|
+
ToolExecutionError,
|
|
1127
|
+
ToolNotFoundError,
|
|
1128
|
+
ValidationError
|
|
1129
|
+
};
|
|
1130
|
+
//# sourceMappingURL=index.mjs.map
|