fastmcp 4.15.3 → 4.16.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/README.md CHANGED
@@ -949,6 +949,50 @@ Limits worth knowing:
949
949
  - On `stdio` there is no proxy to keep alive, so enabling it there only adds
950
950
  notification traffic.
951
951
 
952
+ #### Cancelling Long Tool Calls (`context.signal`)
953
+
954
+ Every `execute` receives an `AbortSignal` that fires once its result can no
955
+ longer reach anyone. Forward it to whatever does the real work so the work stops
956
+ with the call instead of outliving it:
957
+
958
+ ```ts
959
+ server.addTool({
960
+ name: "fetch_report",
961
+ parameters: z.object({ url: z.string() }),
962
+ timeoutMs: 30000,
963
+ execute: async (args, { signal }) => {
964
+ const response = await fetch(args.url, { signal });
965
+ return await response.text();
966
+ },
967
+ });
968
+ ```
969
+
970
+ It aborts on any of three events:
971
+
972
+ - **The client cancelled the call** — it sent `notifications/cancelled`, which is
973
+ what the MCP SDK emits when a caller aborts its own request.
974
+ - **The session ended** — the transport closed, or the session was closed
975
+ explicitly. No cancellation notification is involved here.
976
+ - **`timeoutMs` elapsed** — `signal.reason` is the same `UserError` the caller
977
+ receives, so a tool can tell a timeout apart from a cancellation.
978
+
979
+ The signal is never aborted after a call completes normally, so attaching
980
+ cleanup to it is safe.
981
+
982
+ Limits worth knowing:
983
+
984
+ - **Nothing is killed for you.** FastMCP stops waiting for the tool, but the
985
+ promise `execute` returned keeps running until it settles. A tool that ignores
986
+ the signal still runs to completion — it just does so with nowhere to report.
987
+ - **An HTTP client that vanishes mid-request is not detected.** The MCP SDK only
988
+ aborts a request's own signal for an explicit `notifications/cancelled`, and
989
+ `StreamableHTTPServerTransport` does not treat an abandoned response stream as
990
+ a session close. A caller that hangs up without terminating its session
991
+ (`DELETE`) leaves the tool running until it finishes or times out. Set
992
+ `timeoutMs` on anything expensive rather than relying on disconnect detection.
993
+ - **`load` does not get one.** Resources, resource templates, and prompts
994
+ receive a smaller context without `signal`.
995
+
952
996
  ### Health-check Endpoint
953
997
 
954
998
  When you run FastMCP with the `httpStream` transport you can optionally expose a
package/dist/FastMCP.cjs CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
 
13
13
 
14
- var _chunkIXYHFFR6cjs = require('./chunk-IXYHFFR6.cjs');
14
+ var _chunkGTUJ3UXAcjs = require('./chunk-GTUJ3UXA.cjs');
15
15
 
16
16
 
17
17
 
@@ -49,5 +49,5 @@ var _chunk5FVNY65Mcjs = require('./chunk-5FVNY65M.cjs');
49
49
 
50
50
 
51
51
 
52
- exports.AuthProvider = _chunk5FVNY65Mcjs.AuthProvider; exports.AzureProvider = _chunk5FVNY65Mcjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkIXYHFFR6cjs.DiscoveryDocumentCache; exports.FastMCP = _chunkIXYHFFR6cjs.FastMCP; exports.FastMCPError = _chunkIXYHFFR6cjs.FastMCPError; exports.FastMCPSession = _chunkIXYHFFR6cjs.FastMCPSession; exports.GitHubProvider = _chunk5FVNY65Mcjs.GitHubProvider; exports.GoogleProvider = _chunk5FVNY65Mcjs.GoogleProvider; exports.MEDIA_FETCH_TIMEOUT_MS = _chunkIXYHFFR6cjs.MEDIA_FETCH_TIMEOUT_MS; exports.OAuthProvider = _chunk5FVNY65Mcjs.OAuthProvider; exports.ServerState = _chunkIXYHFFR6cjs.ServerState; exports.SessionError = _chunkIXYHFFR6cjs.SessionError; exports.UnexpectedStateError = _chunkIXYHFFR6cjs.UnexpectedStateError; exports.UserError = _chunkIXYHFFR6cjs.UserError; exports.audioContent = _chunkIXYHFFR6cjs.audioContent; exports.getAuthSession = _chunk5FVNY65Mcjs.getAuthSession; exports.imageContent = _chunkIXYHFFR6cjs.imageContent; exports.jsonSchemaAdapter = _chunkIXYHFFR6cjs.jsonSchemaAdapter; exports.requireAll = _chunk5FVNY65Mcjs.requireAll; exports.requireAny = _chunk5FVNY65Mcjs.requireAny; exports.requireAuth = _chunk5FVNY65Mcjs.requireAuth; exports.requireRole = _chunk5FVNY65Mcjs.requireRole; exports.requireScopes = _chunk5FVNY65Mcjs.requireScopes;
52
+ exports.AuthProvider = _chunk5FVNY65Mcjs.AuthProvider; exports.AzureProvider = _chunk5FVNY65Mcjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkGTUJ3UXAcjs.DiscoveryDocumentCache; exports.FastMCP = _chunkGTUJ3UXAcjs.FastMCP; exports.FastMCPError = _chunkGTUJ3UXAcjs.FastMCPError; exports.FastMCPSession = _chunkGTUJ3UXAcjs.FastMCPSession; exports.GitHubProvider = _chunk5FVNY65Mcjs.GitHubProvider; exports.GoogleProvider = _chunk5FVNY65Mcjs.GoogleProvider; exports.MEDIA_FETCH_TIMEOUT_MS = _chunkGTUJ3UXAcjs.MEDIA_FETCH_TIMEOUT_MS; exports.OAuthProvider = _chunk5FVNY65Mcjs.OAuthProvider; exports.ServerState = _chunkGTUJ3UXAcjs.ServerState; exports.SessionError = _chunkGTUJ3UXAcjs.SessionError; exports.UnexpectedStateError = _chunkGTUJ3UXAcjs.UnexpectedStateError; exports.UserError = _chunkGTUJ3UXAcjs.UserError; exports.audioContent = _chunkGTUJ3UXAcjs.audioContent; exports.getAuthSession = _chunk5FVNY65Mcjs.getAuthSession; exports.imageContent = _chunkGTUJ3UXAcjs.imageContent; exports.jsonSchemaAdapter = _chunkGTUJ3UXAcjs.jsonSchemaAdapter; exports.requireAll = _chunk5FVNY65Mcjs.requireAll; exports.requireAny = _chunk5FVNY65Mcjs.requireAny; exports.requireAuth = _chunk5FVNY65Mcjs.requireAuth; exports.requireRole = _chunk5FVNY65Mcjs.requireRole; exports.requireScopes = _chunk5FVNY65Mcjs.requireScopes;
53
53
  //# sourceMappingURL=FastMCP.cjs.map
@@ -190,6 +190,19 @@ type Context<T extends FastMCPSessionAuth> = {
190
190
  * counters, or maintain user-specific data across multiple requests.
191
191
  */
192
192
  sessionId?: string;
193
+ /**
194
+ * Aborted once the tool's result can no longer reach anyone: the client
195
+ * cancelled the call, the session went away, or `timeoutMs` elapsed.
196
+ *
197
+ * Nothing is killed on your behalf — FastMCP stops waiting, but the promise
198
+ * `execute` returned keeps running until it settles. Forward this signal to
199
+ * whatever does the real work (`fetch`, a database driver, a subprocess) so
200
+ * the work stops with the call instead of outliving it.
201
+ *
202
+ * It is never aborted after a call completes normally, so it is safe to
203
+ * attach cleanup to it.
204
+ */
205
+ signal: AbortSignal;
193
206
  /**
194
207
  * Streams incremental content while the tool is still executing, by emitting
195
208
  * a `notifications/tool/streamContent` notification.
@@ -216,9 +229,11 @@ type Literal = boolean | null | number | string | undefined;
216
229
  *
217
230
  * This is a subset of the tool execution {@link Context}. `reportProgress`
218
231
  * and `streamContent` are tied to a tool call's progress token / streaming
219
- * notification and are not available outside of `tool.execute`.
232
+ * notification and are not available outside of `tool.execute`. `signal` is
233
+ * omitted too: its timeout leg comes from `tool.timeoutMs`, which `load` has
234
+ * no equivalent of.
220
235
  */
221
- type LoadContext<T extends FastMCPSessionAuth> = Omit<Context<T>, "reportProgress" | "streamContent">;
236
+ type LoadContext<T extends FastMCPSessionAuth> = Omit<Context<T>, "reportProgress" | "signal" | "streamContent">;
222
237
  type Progress = {
223
238
  /**
224
239
  * An optional human-readable message describing the current progress.
package/dist/FastMCP.d.ts CHANGED
@@ -190,6 +190,19 @@ type Context<T extends FastMCPSessionAuth> = {
190
190
  * counters, or maintain user-specific data across multiple requests.
191
191
  */
192
192
  sessionId?: string;
193
+ /**
194
+ * Aborted once the tool's result can no longer reach anyone: the client
195
+ * cancelled the call, the session went away, or `timeoutMs` elapsed.
196
+ *
197
+ * Nothing is killed on your behalf — FastMCP stops waiting, but the promise
198
+ * `execute` returned keeps running until it settles. Forward this signal to
199
+ * whatever does the real work (`fetch`, a database driver, a subprocess) so
200
+ * the work stops with the call instead of outliving it.
201
+ *
202
+ * It is never aborted after a call completes normally, so it is safe to
203
+ * attach cleanup to it.
204
+ */
205
+ signal: AbortSignal;
193
206
  /**
194
207
  * Streams incremental content while the tool is still executing, by emitting
195
208
  * a `notifications/tool/streamContent` notification.
@@ -216,9 +229,11 @@ type Literal = boolean | null | number | string | undefined;
216
229
  *
217
230
  * This is a subset of the tool execution {@link Context}. `reportProgress`
218
231
  * and `streamContent` are tied to a tool call's progress token / streaming
219
- * notification and are not available outside of `tool.execute`.
232
+ * notification and are not available outside of `tool.execute`. `signal` is
233
+ * omitted too: its timeout leg comes from `tool.timeoutMs`, which `load` has
234
+ * no equivalent of.
220
235
  */
221
- type LoadContext<T extends FastMCPSessionAuth> = Omit<Context<T>, "reportProgress" | "streamContent">;
236
+ type LoadContext<T extends FastMCPSessionAuth> = Omit<Context<T>, "reportProgress" | "signal" | "streamContent">;
222
237
  type Progress = {
223
238
  /**
224
239
  * An optional human-readable message describing the current progress.
package/dist/FastMCP.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  audioContent,
12
12
  imageContent,
13
13
  jsonSchemaAdapter
14
- } from "./chunk-UEBA2F55.js";
14
+ } from "./chunk-KLA3HXAN.js";
15
15
  import {
16
16
  AuthProvider,
17
17
  AzureProvider,
@@ -490,6 +490,15 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
490
490
  set sessionId(value) {
491
491
  this.#sessionId = value;
492
492
  }
493
+ /**
494
+ * Aborted once the session ends, and folded into the `signal` every tool
495
+ * call receives. The MCP SDK only aborts a request's own signal for an
496
+ * explicit `notifications/cancelled`, and neither `Protocol` nor
497
+ * `StreamableHTTPServerTransport` touches it when the transport simply goes
498
+ * away — so without this a tool keeps running after the client that asked
499
+ * for it has hung up.
500
+ */
501
+ #abortController = new AbortController();
493
502
  #auth;
494
503
  #capabilities = {};
495
504
  #clientCapabilities;
@@ -593,14 +602,12 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
593
602
  for (const resource of resources) {
594
603
  this.addResource(resource);
595
604
  }
605
+ for (const resourceTemplate of resourcesTemplates) {
606
+ this.addResourceTemplate(resourceTemplate);
607
+ }
596
608
  this.setupResourceHandlers();
597
609
  this.setupResourceSubscriptionHandlers();
598
- if (resourcesTemplates.length) {
599
- for (const resourceTemplate of resourcesTemplates) {
600
- this.addResourceTemplate(resourceTemplate);
601
- }
602
- this.setupResourceTemplateHandlers();
603
- }
610
+ this.setupResourceTemplateHandlers();
604
611
  }
605
612
  if (prompts.length) {
606
613
  this.setupPromptHandlers();
@@ -611,6 +618,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
611
618
  if (this.#pingInterval) {
612
619
  clearInterval(this.#pingInterval);
613
620
  }
621
+ this.#abortSession();
614
622
  try {
615
623
  await this.#server.close();
616
624
  } catch (error) {
@@ -801,6 +809,16 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
801
809
  });
802
810
  });
803
811
  }
812
+ /**
813
+ * Cancels the `signal` held by every tool still executing on this session.
814
+ * Idempotent, so the close path and the transport's own close handler can
815
+ * both call it.
816
+ */
817
+ #abortSession() {
818
+ if (!this.#abortController.signal.aborted) {
819
+ this.#abortController.abort(new SessionError("Session closed"));
820
+ }
821
+ }
804
822
  /**
805
823
  * Builds the context object passed as the third argument to
806
824
  * `resource.load` / `resourceTemplate.load` / `prompt.load`.
@@ -1070,6 +1088,9 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1070
1088
  this.#server.onerror = (error) => {
1071
1089
  this.#logger.error("[FastMCP error]", error);
1072
1090
  };
1091
+ this.#server.onclose = () => {
1092
+ this.#abortSession();
1093
+ };
1073
1094
  }
1074
1095
  setupLoggingHandlers() {
1075
1096
  this.#server.setRequestHandler(_typesjs.SetLevelRequestSchema, (request) => {
@@ -1418,6 +1439,14 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1418
1439
  toolName: request.params.name
1419
1440
  });
1420
1441
  }
1442
+ const timeoutAbort = new AbortController();
1443
+ const signal = AbortSignal.any([
1444
+ timeoutAbort.signal,
1445
+ this.#abortController.signal,
1446
+ // Only ever aborted for an explicit `notifications/cancelled`; the
1447
+ // session signal above is what covers a client that simply left.
1448
+ ...extra.signal ? [extra.signal] : []
1449
+ ]);
1421
1450
  const executeToolPromise = Promise.resolve(
1422
1451
  tool.execute(args, {
1423
1452
  client: {
@@ -1429,6 +1458,7 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1429
1458
  requestId: typeof _optionalChain([request, 'access', _41 => _41.params, 'optionalAccess', _42 => _42._meta, 'optionalAccess', _43 => _43.requestId]) === "string" ? request.params._meta.requestId : void 0,
1430
1459
  session: this.#auth,
1431
1460
  sessionId: this.sessionId,
1461
+ signal,
1432
1462
  streamContent
1433
1463
  })
1434
1464
  );
@@ -1440,11 +1470,11 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1440
1470
  executeToolPromise,
1441
1471
  new Promise((_, reject) => {
1442
1472
  const timeoutId = setTimeout(() => {
1443
- reject(
1444
- new UserError(
1445
- `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
1446
- )
1473
+ const timedOut = new UserError(
1474
+ `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
1447
1475
  );
1476
+ timeoutAbort.abort(timedOut);
1477
+ reject(timedOut);
1448
1478
  }, tool.timeoutMs);
1449
1479
  executeToolPromise.then(
1450
1480
  () => clearTimeout(timeoutId),
@@ -2775,4 +2805,4 @@ var FastMCP = class extends FastMCPEventEmitter {
2775
2805
 
2776
2806
 
2777
2807
  exports.DiscoveryDocumentCache = DiscoveryDocumentCache; exports.jsonSchemaAdapter = jsonSchemaAdapter; exports.MEDIA_FETCH_TIMEOUT_MS = MEDIA_FETCH_TIMEOUT_MS; exports.imageContent = imageContent; exports.audioContent = audioContent; exports.FastMCPError = FastMCPError; exports.SessionError = SessionError; exports.UnexpectedStateError = UnexpectedStateError; exports.UserError = UserError; exports.ServerState = ServerState; exports.FastMCPSession = FastMCPSession; exports.FastMCP = FastMCP;
2778
- //# sourceMappingURL=chunk-IXYHFFR6.cjs.map
2808
+ //# sourceMappingURL=chunk-GTUJ3UXA.cjs.map