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