fastmcp 4.13.1 → 4.14.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.
@@ -355,6 +355,8 @@ function assertToolSchemas(tool) {
355
355
  assertStandardSchema(tool.name, "outputSchema", tool.outputSchema);
356
356
  }
357
357
  }
358
+ var STREAM_KEEPALIVE_LOGGER = "fastmcp-keepalive";
359
+ var STREAM_KEEPALIVE_DEFAULT_INTERVAL_MS = 2e4;
358
360
  var TextContentZodSchema = z.object({
359
361
  /**
360
362
  * The text content of the message.
@@ -421,10 +423,23 @@ var CompletionZodSchema = z.object({
421
423
  */
422
424
  total: z.optional(z.number().int()),
423
425
  /**
424
- * An array of completion values. Must not exceed 100 items.
426
+ * An array of completion values. The MCP spec caps this at 100 items; values
427
+ * beyond the cap are trimmed by `capCompletionValues` (which sets `hasMore`)
428
+ * rather than rejected, so the schema itself does not enforce the limit.
425
429
  */
426
- values: z.array(z.string()).max(100)
430
+ values: z.array(z.string())
427
431
  });
432
+ var COMPLETION_VALUES_LIMIT = 100;
433
+ var capCompletionValues = (completion) => {
434
+ if (completion.values.length <= COMPLETION_VALUES_LIMIT) {
435
+ return completion;
436
+ }
437
+ return {
438
+ ...completion,
439
+ hasMore: true,
440
+ values: completion.values.slice(0, COMPLETION_VALUES_LIMIT)
441
+ };
442
+ };
428
443
  var FastMCPSessionEventEmitterBase = EventEmitter;
429
444
  var ServerState = /* @__PURE__ */ ((ServerState2) => {
430
445
  ServerState2["Error"] = "error";
@@ -483,6 +498,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
483
498
  * instance — so capabilities can never be inferred here.
484
499
  */
485
500
  #stateless;
501
+ #streamKeepaliveConfig;
486
502
  /**
487
503
  * Resource URIs the connected client has subscribed to via
488
504
  * `resources/subscribe`. Used to scope `notifications/resources/updated`
@@ -503,6 +519,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
503
519
  roots,
504
520
  sessionId,
505
521
  stateless = false,
522
+ streamKeepalive,
506
523
  tools,
507
524
  transportType,
508
525
  utils,
@@ -516,6 +533,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
516
533
  this.#rootsConfig = roots;
517
534
  this.#sessionId = sessionId;
518
535
  this.#stateless = stateless;
536
+ this.#streamKeepaliveConfig = streamKeepalive;
519
537
  this.#needsEventLoopFlush = transportType === "httpStream";
520
538
  if (tools.length) {
521
539
  this.#capabilities.tools = {};
@@ -843,6 +861,44 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
843
861
  logLevel: pingConfig.logLevel || "debug"
844
862
  };
845
863
  }
864
+ /**
865
+ * Periodically writes to the response stream of an in-flight tool call, so an
866
+ * idle-connection timeout (proxy, load balancer) does not close it while a
867
+ * long-running tool produces no output of its own.
868
+ *
869
+ * The notification is related to the tool call, so it travels on that
870
+ * request's own stream, which is the only server-to-client route that exists
871
+ * when running stateless.
872
+ *
873
+ * @returns a function that stops the keepalive.
874
+ */
875
+ #startStreamKeepalive(extra, toolName) {
876
+ const config = this.#streamKeepaliveConfig;
877
+ if (!config?.enabled) {
878
+ return () => {
879
+ };
880
+ }
881
+ const intervalMs = config.intervalMs && config.intervalMs > 0 ? config.intervalMs : STREAM_KEEPALIVE_DEFAULT_INTERVAL_MS;
882
+ const timer = setInterval(() => {
883
+ extra.sendNotification({
884
+ method: "notifications/message",
885
+ params: {
886
+ data: { message: `keepalive while '${toolName}' is running` },
887
+ level: config.logLevel ?? "debug",
888
+ logger: STREAM_KEEPALIVE_LOGGER
889
+ }
890
+ }).catch((error) => {
891
+ this.#logger.debug(
892
+ `[FastMCP debug] stream keepalive for '${toolName}' failed:`,
893
+ error instanceof Error ? error.message : String(error)
894
+ );
895
+ });
896
+ }, intervalMs);
897
+ timer.unref?.();
898
+ const stop = () => clearInterval(timer);
899
+ extra.signal?.addEventListener("abort", stop, { once: true });
900
+ return stop;
901
+ }
846
902
  async #validateStructuredContent(tool, value, toolName) {
847
903
  if (!tool.outputSchema) {
848
904
  return value;
@@ -936,11 +992,13 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
936
992
  request
937
993
  });
938
994
  }
939
- const completion = CompletionZodSchema.parse(
940
- await prompt.complete(
941
- request.params.argument.name,
942
- request.params.argument.value,
943
- this.#auth
995
+ const completion = capCompletionValues(
996
+ CompletionZodSchema.parse(
997
+ await prompt.complete(
998
+ request.params.argument.name,
999
+ request.params.argument.value,
1000
+ this.#auth
1001
+ )
944
1002
  )
945
1003
  );
946
1004
  return {
@@ -968,11 +1026,13 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
968
1026
  }
969
1027
  );
970
1028
  }
971
- const completion = CompletionZodSchema.parse(
972
- await resource.complete(
973
- request.params.argument.name,
974
- request.params.argument.value,
975
- this.#auth
1029
+ const completion = capCompletionValues(
1030
+ CompletionZodSchema.parse(
1031
+ await resource.complete(
1032
+ request.params.argument.name,
1033
+ request.params.argument.value,
1034
+ this.#auth
1035
+ )
976
1036
  )
977
1037
  );
978
1038
  return {
@@ -1260,169 +1320,178 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1260
1320
  tools: cachedToolsList
1261
1321
  };
1262
1322
  });
1263
- this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
1264
- const tool = toolsMap.get(request.params.name);
1265
- if (!tool) {
1266
- throw new McpError(
1267
- ErrorCode.MethodNotFound,
1268
- `Unknown tool: ${request.params.name}`
1269
- );
1270
- }
1271
- let args = void 0;
1272
- if (tool.parameters) {
1273
- const parsed = await tool.parameters["~standard"].validate(
1274
- request.params.arguments
1275
- );
1276
- if (parsed.issues) {
1277
- const friendlyErrors = this.#formatSchemaIssues(parsed.issues);
1323
+ this.#server.setRequestHandler(
1324
+ CallToolRequestSchema,
1325
+ async (request, extra) => {
1326
+ const tool = toolsMap.get(request.params.name);
1327
+ if (!tool) {
1278
1328
  throw new McpError(
1279
- ErrorCode.InvalidParams,
1280
- `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`
1329
+ ErrorCode.MethodNotFound,
1330
+ `Unknown tool: ${request.params.name}`
1281
1331
  );
1282
1332
  }
1283
- args = parsed.value;
1284
- }
1285
- const progressToken = request.params?._meta?.progressToken;
1286
- let result;
1287
- try {
1288
- const reportProgress = async (progress) => {
1289
- if (progressToken === void 0) {
1290
- return;
1333
+ let args = void 0;
1334
+ if (tool.parameters) {
1335
+ const parsed = await tool.parameters["~standard"].validate(
1336
+ request.params.arguments
1337
+ );
1338
+ if (parsed.issues) {
1339
+ const friendlyErrors = this.#formatSchemaIssues(parsed.issues);
1340
+ throw new McpError(
1341
+ ErrorCode.InvalidParams,
1342
+ `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`
1343
+ );
1291
1344
  }
1292
- try {
1293
- await this.#server.notification({
1294
- method: "notifications/progress",
1295
- params: {
1296
- ...progress,
1297
- progressToken
1345
+ args = parsed.value;
1346
+ }
1347
+ const progressToken = request.params?._meta?.progressToken;
1348
+ let result;
1349
+ try {
1350
+ const reportProgress = async (progress) => {
1351
+ if (progressToken === void 0) {
1352
+ return;
1353
+ }
1354
+ try {
1355
+ await this.#server.notification({
1356
+ method: "notifications/progress",
1357
+ params: {
1358
+ ...progress,
1359
+ progressToken
1360
+ }
1361
+ });
1362
+ if (this.#needsEventLoopFlush) {
1363
+ await new Promise((resolve) => setImmediate(resolve));
1298
1364
  }
1299
- });
1300
- if (this.#needsEventLoopFlush) {
1301
- await new Promise((resolve) => setImmediate(resolve));
1365
+ } catch (progressError) {
1366
+ this.#logger.warn(
1367
+ `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
1368
+ progressError instanceof Error ? progressError.message : String(progressError)
1369
+ );
1302
1370
  }
1303
- } catch (progressError) {
1304
- this.#logger.warn(
1305
- `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
1306
- progressError instanceof Error ? progressError.message : String(progressError)
1307
- );
1308
- }
1309
- };
1310
- const log = this.#createLog();
1311
- const streamContent = async (content) => {
1312
- const contentArray = Array.isArray(content) ? content : [content];
1313
- try {
1314
- await this.#server.notification({
1315
- method: "notifications/tool/streamContent",
1316
- params: {
1317
- content: contentArray,
1318
- toolName: request.params.name
1371
+ };
1372
+ const log = this.#createLog();
1373
+ const streamContent = async (content) => {
1374
+ const contentArray = Array.isArray(content) ? content : [content];
1375
+ try {
1376
+ await this.#server.notification({
1377
+ method: "notifications/tool/streamContent",
1378
+ params: {
1379
+ content: contentArray,
1380
+ toolName: request.params.name
1381
+ }
1382
+ });
1383
+ if (this.#needsEventLoopFlush) {
1384
+ await new Promise((resolve) => setImmediate(resolve));
1319
1385
  }
1320
- });
1321
- if (this.#needsEventLoopFlush) {
1322
- await new Promise((resolve) => setImmediate(resolve));
1386
+ } catch (streamError) {
1387
+ this.#logger.warn(
1388
+ `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
1389
+ streamError instanceof Error ? streamError.message : String(streamError)
1390
+ );
1323
1391
  }
1324
- } catch (streamError) {
1325
- this.#logger.warn(
1326
- `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
1327
- streamError instanceof Error ? streamError.message : String(streamError)
1328
- );
1392
+ };
1393
+ if (this.#onToolCall) {
1394
+ await this.#onToolCall({
1395
+ arguments: args ?? {},
1396
+ toolName: request.params.name
1397
+ });
1329
1398
  }
1330
- };
1331
- if (this.#onToolCall) {
1332
- await this.#onToolCall({
1333
- arguments: args ?? {},
1334
- toolName: request.params.name
1335
- });
1336
- }
1337
- const executeToolPromise = tool.execute(args, {
1338
- client: {
1339
- version: this.#server.getClientVersion()
1340
- },
1341
- elicit: (params, options) => this.#server.elicitInput(params, options),
1342
- log,
1343
- reportProgress,
1344
- requestId: typeof request.params?._meta?.requestId === "string" ? request.params._meta.requestId : void 0,
1345
- session: this.#auth,
1346
- sessionId: this.#sessionId,
1347
- streamContent
1348
- });
1349
- const maybeStringResult = await (tool.timeoutMs ? Promise.race([
1350
- executeToolPromise,
1351
- new Promise((_, reject) => {
1352
- const timeoutId = setTimeout(() => {
1353
- reject(
1354
- new UserError(
1355
- `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
1356
- )
1399
+ const executeToolPromise = Promise.resolve(
1400
+ tool.execute(args, {
1401
+ client: {
1402
+ version: this.#server.getClientVersion()
1403
+ },
1404
+ elicit: (params, options) => this.#server.elicitInput(params, options),
1405
+ log,
1406
+ reportProgress,
1407
+ requestId: typeof request.params?._meta?.requestId === "string" ? request.params._meta.requestId : void 0,
1408
+ session: this.#auth,
1409
+ sessionId: this.#sessionId,
1410
+ streamContent
1411
+ })
1412
+ );
1413
+ const stopStreamKeepalive = this.#startStreamKeepalive(
1414
+ extra,
1415
+ request.params.name
1416
+ );
1417
+ const maybeStringResult = await (tool.timeoutMs ? Promise.race([
1418
+ executeToolPromise,
1419
+ new Promise((_, reject) => {
1420
+ const timeoutId = setTimeout(() => {
1421
+ reject(
1422
+ new UserError(
1423
+ `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
1424
+ )
1425
+ );
1426
+ }, tool.timeoutMs);
1427
+ executeToolPromise.then(
1428
+ () => clearTimeout(timeoutId),
1429
+ () => clearTimeout(timeoutId)
1357
1430
  );
1358
- }, tool.timeoutMs);
1359
- executeToolPromise.then(
1360
- () => clearTimeout(timeoutId),
1361
- () => clearTimeout(timeoutId)
1362
- );
1363
- })
1364
- ]) : executeToolPromise);
1365
- await delay(1);
1366
- if (maybeStringResult === void 0 || maybeStringResult === null) {
1367
- result = ContentResultZodSchema.parse({
1368
- content: []
1369
- });
1370
- } else if (typeof maybeStringResult === "string") {
1371
- result = ContentResultZodSchema.parse({
1372
- content: [{ text: maybeStringResult, type: "text" }]
1373
- });
1374
- } else if ("type" in maybeStringResult) {
1375
- result = ContentResultZodSchema.parse({
1376
- content: [maybeStringResult]
1377
- });
1378
- } else if ("content" in maybeStringResult) {
1379
- result = ContentResultZodSchema.parse(maybeStringResult);
1380
- if (result.structuredContent !== void 0 && tool.outputSchema) {
1381
- result.structuredContent = await this.#validateStructuredContent(
1431
+ })
1432
+ ]) : executeToolPromise).finally(stopStreamKeepalive);
1433
+ await delay(1);
1434
+ if (maybeStringResult === void 0 || maybeStringResult === null) {
1435
+ result = ContentResultZodSchema.parse({
1436
+ content: []
1437
+ });
1438
+ } else if (typeof maybeStringResult === "string") {
1439
+ result = ContentResultZodSchema.parse({
1440
+ content: [{ text: maybeStringResult, type: "text" }]
1441
+ });
1442
+ } else if ("type" in maybeStringResult) {
1443
+ result = ContentResultZodSchema.parse({
1444
+ content: [maybeStringResult]
1445
+ });
1446
+ } else if ("content" in maybeStringResult) {
1447
+ result = ContentResultZodSchema.parse(maybeStringResult);
1448
+ if (result.structuredContent !== void 0 && tool.outputSchema) {
1449
+ result.structuredContent = await this.#validateStructuredContent(
1450
+ tool,
1451
+ result.structuredContent,
1452
+ request.params.name
1453
+ );
1454
+ }
1455
+ } else if (tool.outputSchema) {
1456
+ const structuredContent = await this.#validateStructuredContent(
1382
1457
  tool,
1383
- result.structuredContent,
1458
+ maybeStringResult,
1384
1459
  request.params.name
1385
1460
  );
1461
+ result = ContentResultZodSchema.parse({
1462
+ content: [
1463
+ {
1464
+ text: JSON.stringify(structuredContent),
1465
+ type: "text"
1466
+ }
1467
+ ],
1468
+ structuredContent
1469
+ });
1470
+ } else {
1471
+ result = ContentResultZodSchema.parse(maybeStringResult);
1386
1472
  }
1387
- } else if (tool.outputSchema) {
1388
- const structuredContent = await this.#validateStructuredContent(
1389
- tool,
1390
- maybeStringResult,
1391
- request.params.name
1392
- );
1393
- result = ContentResultZodSchema.parse({
1473
+ } catch (error) {
1474
+ if (error instanceof UserError) {
1475
+ return {
1476
+ content: [{ text: error.message, type: "text" }],
1477
+ isError: true,
1478
+ ...error.extras ? { structuredContent: error.extras } : {}
1479
+ };
1480
+ }
1481
+ const errorMessage = error instanceof Error ? error.message : String(error);
1482
+ return {
1394
1483
  content: [
1395
1484
  {
1396
- text: JSON.stringify(structuredContent),
1485
+ text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,
1397
1486
  type: "text"
1398
1487
  }
1399
1488
  ],
1400
- structuredContent
1401
- });
1402
- } else {
1403
- result = ContentResultZodSchema.parse(maybeStringResult);
1404
- }
1405
- } catch (error) {
1406
- if (error instanceof UserError) {
1407
- return {
1408
- content: [{ text: error.message, type: "text" }],
1409
- isError: true,
1410
- ...error.extras ? { structuredContent: error.extras } : {}
1489
+ isError: true
1411
1490
  };
1412
1491
  }
1413
- const errorMessage = error instanceof Error ? error.message : String(error);
1414
- return {
1415
- content: [
1416
- {
1417
- text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,
1418
- type: "text"
1419
- }
1420
- ],
1421
- isError: true
1422
- };
1492
+ return result;
1423
1493
  }
1424
- return result;
1425
- });
1494
+ );
1426
1495
  }
1427
1496
  };
1428
1497
  function camelToSnakeCase(str) {
@@ -1863,6 +1932,7 @@ var FastMCP = class extends FastMCPEventEmitter {
1863
1932
  resources: this.#resources,
1864
1933
  resourcesTemplates: this.#resourcesTemplates,
1865
1934
  roots: this.#options.roots,
1935
+ streamKeepalive: this.#options.streamKeepalive,
1866
1936
  tools: this.#tools,
1867
1937
  transportType: "stdio",
1868
1938
  utils: this.#options.utils,
@@ -2062,6 +2132,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2062
2132
  roots: this.#options.roots,
2063
2133
  sessionId,
2064
2134
  stateless,
2135
+ streamKeepalive: this.#options.streamKeepalive,
2065
2136
  tools: allowedTools,
2066
2137
  transportType: "httpStream",
2067
2138
  utils: this.#options.utils,
@@ -2676,4 +2747,4 @@ export {
2676
2747
  FastMCPSession,
2677
2748
  FastMCP
2678
2749
  };
2679
- //# sourceMappingURL=chunk-3BMGHXXY.js.map
2750
+ //# sourceMappingURL=chunk-SGCRCQNU.js.map