@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.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