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