@mozilla-ai/mcpd 0.0.2 → 0.0.4

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 CHANGED
@@ -941,6 +941,14 @@ class FunctionBuilder {
941
941
  getCacheSize() {
942
942
  return this.#functionCache.size;
943
943
  }
944
+ /**
945
+ * Get all cached functions.
946
+ *
947
+ * @returns Array of all cached agent functions, or empty array if cache is empty
948
+ */
949
+ getCachedFunctions() {
950
+ return Array.from(this.#functionCache.values());
951
+ }
944
952
  }
945
953
  const API_BASE = "/api/v1";
946
954
  const SERVERS_BASE = `${API_BASE}/servers`;
@@ -971,13 +979,76 @@ const API_PATHS = {
971
979
  HEALTH_ALL: HEALTH_SERVERS_BASE,
972
980
  HEALTH_SERVER: (serverName) => `${HEALTH_SERVERS_BASE}/${encodeURIComponent(serverName)}`
973
981
  };
982
+ const LogLevels = {
983
+ OFF: "off"
984
+ };
985
+ const ranks = {
986
+ trace: 5,
987
+ debug: 10,
988
+ info: 20,
989
+ warn: 30,
990
+ error: 40,
991
+ off: 1e3
992
+ };
993
+ function resolve(raw) {
994
+ const candidate = raw?.toLowerCase();
995
+ return candidate && candidate in ranks ? candidate : LogLevels.OFF;
996
+ }
997
+ function getLevel() {
998
+ return resolve(
999
+ typeof process !== "undefined" ? process.env.MCPD_LOG_LEVEL : void 0
1000
+ );
1001
+ }
1002
+ function defaultLogger() {
1003
+ return {
1004
+ trace: (...args) => {
1005
+ const lvl = getLevel();
1006
+ if (lvl !== LogLevels.OFF && ranks[lvl] <= ranks.trace)
1007
+ console.trace(...args);
1008
+ },
1009
+ debug: (...args) => {
1010
+ const lvl = getLevel();
1011
+ if (lvl !== LogLevels.OFF && ranks[lvl] <= ranks.debug)
1012
+ console.debug(...args);
1013
+ },
1014
+ info: (...args) => {
1015
+ const lvl = getLevel();
1016
+ if (lvl !== LogLevels.OFF && ranks[lvl] <= ranks.info)
1017
+ console.info(...args);
1018
+ },
1019
+ warn: (...args) => {
1020
+ const lvl = getLevel();
1021
+ if (lvl !== LogLevels.OFF && ranks[lvl] <= ranks.warn)
1022
+ console.warn(...args);
1023
+ },
1024
+ error: (...args) => {
1025
+ const lvl = getLevel();
1026
+ if (lvl !== LogLevels.OFF && ranks[lvl] <= ranks.error)
1027
+ console.error(...args);
1028
+ }
1029
+ };
1030
+ }
1031
+ function createLogger(impl) {
1032
+ const base = defaultLogger();
1033
+ return {
1034
+ trace: impl?.trace ?? base.trace,
1035
+ debug: impl?.debug ?? base.debug,
1036
+ info: impl?.info ?? base.info,
1037
+ warn: impl?.warn ?? base.warn,
1038
+ error: impl?.error ?? base.error
1039
+ };
1040
+ }
1041
+ const REQUEST_TIMEOUT_SECONDS = 30;
1042
+ const SERVER_HEALTH_CACHE_TTL_SECONDS = 10;
974
1043
  const SERVER_HEALTH_CACHE_MAXSIZE = 100;
1044
+ const TOOL_SEPARATOR = "__";
975
1045
  class McpdClient {
976
1046
  #endpoint;
977
1047
  #apiKey;
978
1048
  #timeout;
979
1049
  #serverHealthCache;
980
1050
  #functionBuilder;
1051
+ #logger;
981
1052
  #cacheableExceptions = /* @__PURE__ */ new Set([
982
1053
  ServerNotFoundError,
983
1054
  ServerUnhealthyError,
@@ -993,14 +1064,18 @@ class McpdClient {
993
1064
  * @param options - Configuration options for the client
994
1065
  */
995
1066
  constructor(options) {
1067
+ const toMs = (s) => s * 1e3;
996
1068
  this.#endpoint = options.apiEndpoint.replace(/\/$/, "");
997
1069
  this.#apiKey = options.apiKey;
998
- this.#timeout = options.timeout ?? 3e4;
999
- const healthCacheTtlMs = (options.healthCacheTtl ?? 10) * 1e3;
1070
+ this.#timeout = options.timeout ?? toMs(REQUEST_TIMEOUT_SECONDS);
1071
+ const healthCacheTtlMs = toMs(
1072
+ options.healthCacheTtl ?? SERVER_HEALTH_CACHE_TTL_SECONDS
1073
+ );
1000
1074
  this.#serverHealthCache = createCache({
1001
1075
  max: SERVER_HEALTH_CACHE_MAXSIZE,
1002
1076
  ttl: healthCacheTtlMs
1003
1077
  });
1078
+ this.#logger = createLogger(options.logger);
1004
1079
  this.servers = new ServersNamespace({
1005
1080
  performCall: this.#performCall.bind(this),
1006
1081
  getTools: this.#getToolsByServer.bind(this),
@@ -1017,8 +1092,15 @@ class McpdClient {
1017
1092
  *
1018
1093
  * @param path - The API path (e.g., '/servers', '/servers/{server_name}/tools')
1019
1094
  * @param options - Request options
1095
+ *
1020
1096
  * @returns The JSON response from the daemon
1097
+ *
1098
+ * @throws {AuthenticationError} If API key was present and authentication fails
1099
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1100
+ * @throws {TimeoutError} If the request times out
1021
1101
  * @throws {McpdError} If the request fails
1102
+ *
1103
+ * @internal
1022
1104
  */
1023
1105
  async #request(path, options = {}) {
1024
1106
  const url = `${this.#endpoint}${path}`;
@@ -1098,6 +1180,10 @@ class McpdClient {
1098
1180
  * Get a list of all configured MCP servers.
1099
1181
  *
1100
1182
  * @returns Array of server names
1183
+ *
1184
+ * @throws {AuthenticationError} If API key was present and authentication fails
1185
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1186
+ * @throws {TimeoutError} If the request times out
1101
1187
  * @throws {McpdError} If the request fails
1102
1188
  *
1103
1189
  * @example
@@ -1110,16 +1196,23 @@ class McpdClient {
1110
1196
  return await this.#request(API_PATHS.SERVERS);
1111
1197
  }
1112
1198
  /**
1113
- * Internal method to get tool schemas for a server.
1199
+ * Get tool schemas for a server.
1200
+ *
1201
+ * @privateRemarks
1114
1202
  * Used by dependency injection for ServersNamespace and internally for getAgentTools.
1115
1203
  *
1116
1204
  * @param serverName - Server name to get tools for
1205
+ *
1117
1206
  * @returns Tool schemas for the specified server
1207
+ *
1118
1208
  * @throws {ServerNotFoundError} If the specified server doesn't exist
1119
1209
  * @throws {ServerUnhealthyError} If the server is not healthy
1210
+ *
1211
+ * @throws {AuthenticationError} If API key was present and authentication fails
1120
1212
  * @throws {ConnectionError} If unable to connect to the mcpd daemon
1121
1213
  * @throws {TimeoutError} If the request times out
1122
1214
  * @throws {McpdError} If the request fails
1215
+ *
1123
1216
  * @internal
1124
1217
  */
1125
1218
  async #getToolsByServer(serverName) {
@@ -1135,17 +1228,24 @@ class McpdClient {
1135
1228
  return response.tools;
1136
1229
  }
1137
1230
  /**
1138
- * Internal method to get prompt schemas for a server.
1231
+ * Get prompt schemas for a server.
1232
+ *
1233
+ * @privateRemarks
1139
1234
  * Used internally for getPromptSchemas.
1140
1235
  *
1141
1236
  * @param serverName - Server name to get prompts for
1142
- * @param cursor - Optional cursor for pagination
1237
+ * @param cursor - Cursor for pagination
1238
+ *
1143
1239
  * @returns Prompt schemas for the specified server
1240
+ *
1144
1241
  * @throws {ServerNotFoundError} If the specified server doesn't exist
1145
1242
  * @throws {ServerUnhealthyError} If the server is not healthy
1243
+ *
1244
+ * @throws {AuthenticationError} If API key was present and authentication fails
1146
1245
  * @throws {ConnectionError} If unable to connect to the mcpd daemon
1147
1246
  * @throws {TimeoutError} If the request times out
1148
1247
  * @throws {McpdError} If the request fails
1248
+ *
1149
1249
  * @internal
1150
1250
  */
1151
1251
  async #getPromptsByServer(serverName, cursor) {
@@ -1162,16 +1262,24 @@ class McpdClient {
1162
1262
  }
1163
1263
  }
1164
1264
  /**
1165
- * Internal method to generate a prompt on a server.
1265
+ * Generate a prompt on a server.
1166
1266
  *
1167
- * This method is used internally by:
1267
+ * @privateRemarks
1268
+ * Used internally by:
1168
1269
  * - PromptsNamespace (via dependency injection)
1169
1270
  * - Server.generatePrompt() (via dependency injection)
1170
1271
  *
1171
1272
  * @param serverName - The name of the server
1172
1273
  * @param promptName - The exact name of the prompt
1173
1274
  * @param args - The prompt arguments
1275
+ *
1174
1276
  * @returns The generated prompt response
1277
+ *
1278
+ * @throws {AuthenticationError} If API key was present and authentication fails
1279
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1280
+ * @throws {TimeoutError} If the request times out
1281
+ * @throws {McpdError} If the request fails
1282
+ *
1175
1283
  * @internal
1176
1284
  */
1177
1285
  async #generatePromptInternal(serverName, promptName, args) {
@@ -1187,17 +1295,24 @@ class McpdClient {
1187
1295
  return response;
1188
1296
  }
1189
1297
  /**
1190
- * Internal method to get resource schemas for a server.
1298
+ * Get resource schemas for a server.
1299
+ *
1300
+ * @privateRemarks
1191
1301
  * Used internally for getResources and by dependency injection for ServersNamespace.
1192
1302
  *
1193
1303
  * @param serverName - Server name to get resources for
1194
- * @param cursor - Optional cursor for pagination
1304
+ * @param cursor - Cursor for pagination
1305
+ *
1195
1306
  * @returns Resource schemas for the specified server
1307
+ *
1196
1308
  * @throws {ServerNotFoundError} If the specified server doesn't exist
1197
1309
  * @throws {ServerUnhealthyError} If the server is not healthy
1310
+ *
1311
+ * @throws {AuthenticationError} If API key was present and authentication fails
1198
1312
  * @throws {ConnectionError} If unable to connect to the mcpd daemon
1199
1313
  * @throws {TimeoutError} If the request times out
1200
1314
  * @throws {McpdError} If the request fails
1315
+ *
1201
1316
  * @internal
1202
1317
  */
1203
1318
  async #getResourcesByServer(serverName, cursor) {
@@ -1214,17 +1329,24 @@ class McpdClient {
1214
1329
  }
1215
1330
  }
1216
1331
  /**
1217
- * Internal method to get resource template schemas for a server.
1332
+ * Get resource template schemas for a server.
1333
+ *
1334
+ * @privateRemarks
1218
1335
  * Used internally for getResourceTemplates and by dependency injection for ServersNamespace.
1219
1336
  *
1220
1337
  * @param serverName - Server name to get resource templates for
1221
- * @param cursor - Optional cursor for pagination
1338
+ * @param cursor - Cursor for pagination
1339
+ *
1222
1340
  * @returns Resource template schemas for the specified server
1341
+ *
1223
1342
  * @throws {ServerNotFoundError} If the specified server doesn't exist
1224
1343
  * @throws {ServerUnhealthyError} If the server is not healthy
1344
+ *
1345
+ * @throws {AuthenticationError} If API key was present and authentication fails
1225
1346
  * @throws {ConnectionError} If unable to connect to the mcpd daemon
1226
1347
  * @throws {TimeoutError} If the request times out
1227
1348
  * @throws {McpdError} If the request fails
1349
+ *
1228
1350
  * @internal
1229
1351
  */
1230
1352
  async #getResourceTemplatesByServer(serverName, cursor) {
@@ -1241,17 +1363,24 @@ class McpdClient {
1241
1363
  }
1242
1364
  }
1243
1365
  /**
1244
- * Internal method to read resource content from a server.
1366
+ * Read resource content from a server.
1367
+ *
1368
+ * @privateRemarks
1245
1369
  * Used by dependency injection for ServersNamespace.
1246
1370
  *
1247
1371
  * @param serverName - Server name to read resource from
1248
1372
  * @param uri - The resource URI
1373
+ *
1249
1374
  * @returns Array of resource contents (text or blob)
1375
+ *
1250
1376
  * @throws {ServerNotFoundError} If the specified server doesn't exist
1251
1377
  * @throws {ServerUnhealthyError} If the server is not healthy
1378
+ *
1379
+ * @throws {AuthenticationError} If API key was present and authentication fails
1252
1380
  * @throws {ConnectionError} If unable to connect to the mcpd daemon
1253
1381
  * @throws {TimeoutError} If the request times out
1254
1382
  * @throws {McpdError} If the request fails
1383
+ *
1255
1384
  * @internal
1256
1385
  */
1257
1386
  async #readResourceByServer(serverName, uri) {
@@ -1303,8 +1432,14 @@ class McpdClient {
1303
1432
  * Check if a specific server is healthy.
1304
1433
  *
1305
1434
  * @param serverName - The name of the server to check
1435
+ *
1306
1436
  * @returns True if the server is healthy, false otherwise
1307
1437
  *
1438
+ * @throws {AuthenticationError} If API key was present and authentication fails
1439
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1440
+ * @throws {TimeoutError} If the request times out
1441
+ * @throws {McpdError} If the request fails
1442
+ *
1308
1443
  * @example
1309
1444
  * ```typescript
1310
1445
  * if (await client.isServerHealthy('time')) {
@@ -1327,8 +1462,14 @@ class McpdClient {
1327
1462
  * Ensure a server is healthy before performing an operation.
1328
1463
  *
1329
1464
  * @param serverName - The name of the server to check
1465
+ *
1330
1466
  * @throws {ServerNotFoundError} If the server doesn't exist
1331
1467
  * @throws {ServerUnhealthyError} If the server is not healthy
1468
+ *
1469
+ * @throws {AuthenticationError} If API key was present and authentication fails
1470
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1471
+ * @throws {TimeoutError} If the request times out
1472
+ * @throws {McpdError} If the request fails
1332
1473
  */
1333
1474
  async #ensureServerHealthy(serverName) {
1334
1475
  const health = await this.getServerHealth(serverName);
@@ -1347,35 +1488,60 @@ class McpdClient {
1347
1488
  }
1348
1489
  }
1349
1490
  /**
1350
- * Get list of healthy servers from optional server names.
1491
+ * Get list of healthy servers.
1351
1492
  *
1352
- * This helper fetches server names (if not provided) and filters to only healthy servers.
1353
- * Used by getToolSchemas(), getPrompts(), and agentTools() to avoid timeouts on failed servers.
1493
+ * @remarks
1494
+ * If logging is enabled, warnings are logged for servers that do not exist or are unhealthy.
1354
1495
  *
1355
- * @param servers - Optional array of server names. If not provided, fetches all servers.
1356
- * @returns Array of healthy server names
1357
- * @internal
1496
+ * @param servers - List of server names to use for health checking.
1497
+ * If not provided, or empty, checks health for all servers.
1498
+ *
1499
+ * @returns List of server names with 'ok' health status.
1500
+ *
1501
+ * @throws {AuthenticationError} If API key was present and authentication fails
1502
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1503
+ * @throws {TimeoutError} If the request times out
1504
+ * @throws {McpdError} If the request fails
1358
1505
  */
1359
1506
  async #getHealthyServers(servers) {
1360
- const serverNames = servers && servers.length > 0 ? servers : await this.listServers();
1507
+ const serverNames = servers?.length ? servers : await this.listServers();
1361
1508
  const healthMap = await this.getServerHealth();
1362
1509
  return serverNames.filter((name) => {
1363
1510
  const health = healthMap[name];
1364
- return health && HealthStatusHelpers.isHealthy(health.status);
1511
+ if (!health) {
1512
+ this.#logger.warn(`Skipping non-existent server '${name}'`);
1513
+ return false;
1514
+ }
1515
+ if (!HealthStatusHelpers.isHealthy(health.status)) {
1516
+ this.#logger.warn(
1517
+ `Skipping unhealthy server '${name}' with status '${health.status}'`
1518
+ );
1519
+ return false;
1520
+ }
1521
+ return true;
1365
1522
  });
1366
1523
  }
1367
1524
  /**
1368
- * Internal method to perform a tool call on a server.
1525
+ * Perform a tool call on a server.
1369
1526
  *
1370
- * This method is used internally by:
1527
+ * @privateRemarks
1528
+ * Used internally by:
1371
1529
  * - ToolsNamespace (via dependency injection)
1372
1530
  * - FunctionBuilder (via dependency injection)
1373
1531
  *
1374
1532
  * @param serverName - The name of the server
1375
1533
  * @param toolName - The exact name of the tool
1376
1534
  * @param args - The tool arguments
1535
+ *
1377
1536
  * @returns The tool's response
1537
+ *
1378
1538
  * @throws {ToolExecutionError} If the tool execution fails
1539
+ *
1540
+ * @throws {AuthenticationError} If API key was present and authentication fails
1541
+ * @throws {ConnectionError} If unable to connect to the mcpd daemon
1542
+ * @throws {TimeoutError} If the request times out
1543
+ * @throws {McpdError} If the request fails
1544
+ *
1379
1545
  * @internal
1380
1546
  */
1381
1547
  async #performCall(serverName, toolName, args) {
@@ -1421,29 +1587,33 @@ class McpdClient {
1421
1587
  this.#serverHealthCache.clear();
1422
1588
  }
1423
1589
  /**
1424
- * Generate callable functions for use with AI agent frameworks (internal).
1590
+ * Fetch and cache callable functions from all healthy servers.
1425
1591
  *
1426
- * This method queries servers and creates self-contained, callable functions
1592
+ * This method queries all healthy servers and creates self-contained, callable functions
1427
1593
  * that can be passed to AI agent frameworks. Each function includes its schema
1428
1594
  * as metadata and handles the MCP communication internally.
1429
1595
  *
1430
- * This method automatically filters out unhealthy servers by checking their health
1431
- * status before fetching tools. Unhealthy servers are silently skipped to ensure
1432
- * the method returns quickly without waiting for timeouts on failed servers.
1596
+ * Unhealthy servers are automatically filtered out and skipped (with optional warnings
1597
+ * when logging is enabled) to ensure the method returns quickly without waiting for timeouts.
1433
1598
  *
1434
1599
  * Tool fetches from multiple servers are executed concurrently for optimal performance.
1600
+ * Functions are cached indefinitely until explicitly cleared.
1435
1601
  *
1436
- * @param servers - Optional list of server names to include. If not specified, includes all servers.
1437
- * @returns Array of callable functions with metadata. Only includes tools from healthy servers.
1602
+ * @returns Array of callable functions with metadata from all healthy servers.
1438
1603
  *
1604
+ * @throws {AuthenticationError} If API key was present and authentication fails
1439
1605
  * @throws {ConnectionError} If unable to connect to the mcpd daemon
1440
- * @throws {TimeoutError} If requests to the daemon time out
1441
- * @throws {AuthenticationError} If API key authentication fails
1442
- * @throws {McpdError} If unable to retrieve health status, server list, or generate functions
1606
+ * @throws {TimeoutError} If the request times out
1607
+ * @throws {McpdError} If the request fails
1608
+ *
1443
1609
  * @internal
1444
1610
  */
1445
- async agentTools(servers) {
1446
- const healthyServers = await this.#getHealthyServers(servers);
1611
+ async #agentTools() {
1612
+ const cachedFunctions = this.#functionBuilder.getCachedFunctions();
1613
+ if (cachedFunctions.length > 0) {
1614
+ return cachedFunctions;
1615
+ }
1616
+ const healthyServers = await this.#getHealthyServers();
1447
1617
  const results = await Promise.allSettled(
1448
1618
  healthyServers.map(async (serverName) => ({
1449
1619
  serverName,
@@ -1462,17 +1632,43 @@ class McpdClient {
1462
1632
  return agentTools;
1463
1633
  }
1464
1634
  async getAgentTools(options = {}) {
1465
- const { servers, format = "array" } = options;
1466
- const tools = await this.agentTools(servers);
1467
- switch (format) {
1468
- case "object":
1469
- return Object.fromEntries(tools.map((tool) => [tool.name, tool]));
1470
- case "map":
1471
- return new Map(tools.map((tool) => [tool.name, tool]));
1472
- case "array":
1473
- default:
1474
- return tools;
1475
- }
1635
+ const { servers, tools, format = "array", refreshCache = false } = options;
1636
+ if (refreshCache) this.#functionBuilder.clearCache();
1637
+ const allTools = await this.#agentTools();
1638
+ const filteredTools = allTools.filter((tool) => !servers || servers.includes(tool._serverName)).filter((tool) => !tools || this.#matchesToolFilter(tool, tools));
1639
+ const formatters = {
1640
+ array: (t) => t,
1641
+ object: (t) => Object.fromEntries(t.map((tool) => [tool.name, tool])),
1642
+ map: (t) => new Map(t.map((tool) => [tool.name, tool]))
1643
+ };
1644
+ return formatters[format](filteredTools);
1645
+ }
1646
+ /**
1647
+ * Check if a tool matches the tool filter.
1648
+ *
1649
+ * Supports two formats:
1650
+ * - Raw tool name: "get_current_time" (matches across all servers)
1651
+ * - Server-prefixed: "time__get_current_time" (matches specific server + tool)
1652
+ *
1653
+ * @remarks
1654
+ * When a filter contains "__", it's first checked as server-prefixed (exact match).
1655
+ * If that fails, it's checked as a raw tool name. This handles tools whose names
1656
+ * contain "__" (e.g., "my__special__tool").
1657
+ *
1658
+ * @param tool The tool to match.
1659
+ * @param tools List of tool names to match against.
1660
+ *
1661
+ * @returns True if a match is found in tools, based on the predicate.
1662
+ *
1663
+ * @internal
1664
+ */
1665
+ #matchesToolFilter(tool, tools) {
1666
+ return tools.some((filterItem) => {
1667
+ if (filterItem.indexOf(TOOL_SEPARATOR) === -1) {
1668
+ return filterItem === tool._toolName;
1669
+ }
1670
+ return filterItem === tool.name || filterItem === tool._toolName;
1671
+ });
1476
1672
  }
1477
1673
  }
1478
1674
  export {