fastmcp 4.21.1 → 4.22.0

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.
Files changed (43) hide show
  1. package/README.md +23 -6
  2. package/dist/FastMCP.cjs +4 -3
  3. package/dist/FastMCP.cjs.map +1 -1
  4. package/dist/FastMCP.d.cts +9 -3
  5. package/dist/FastMCP.d.ts +9 -3
  6. package/dist/FastMCP.js +3 -2
  7. package/dist/{OAuthProvider-C4idKF29.d.cts → OAuthProvider-BcDb5bOz.d.cts} +8 -1
  8. package/dist/{OAuthProvider-C4idKF29.d.ts → OAuthProvider-BcDb5bOz.d.ts} +8 -1
  9. package/dist/auth/index.cjs +2 -2
  10. package/dist/auth/index.d.cts +2 -2
  11. package/dist/auth/index.d.ts +2 -2
  12. package/dist/auth/index.js +1 -1
  13. package/dist/{chunk-JNP6BG5L.js → chunk-43N2Q6ZI.js} +146 -36
  14. package/dist/chunk-43N2Q6ZI.js.map +1 -0
  15. package/dist/chunk-O22SYZMC.cjs +22 -0
  16. package/dist/chunk-O22SYZMC.cjs.map +1 -0
  17. package/dist/chunk-UMDUIBFW.js +22 -0
  18. package/dist/chunk-UMDUIBFW.js.map +1 -0
  19. package/dist/{chunk-IKH7YBYP.cjs → chunk-V4JBWBT4.cjs} +150 -40
  20. package/dist/chunk-V4JBWBT4.cjs.map +1 -0
  21. package/dist/{chunk-AR3LVWUF.js → chunk-XPKMA7DH.js} +22 -1
  22. package/dist/chunk-XPKMA7DH.js.map +1 -0
  23. package/dist/{chunk-GTPK7A57.cjs → chunk-YOBCXNFP.cjs} +22 -1
  24. package/dist/chunk-YOBCXNFP.cjs.map +1 -0
  25. package/dist/edge/index.cjs +7 -4
  26. package/dist/edge/index.cjs.map +1 -1
  27. package/dist/edge/index.js +6 -3
  28. package/dist/edge/index.js.map +1 -1
  29. package/dist/examples/custom-routes.cjs +4 -3
  30. package/dist/examples/custom-routes.cjs.map +1 -1
  31. package/dist/examples/custom-routes.js +3 -2
  32. package/dist/examples/custom-routes.js.map +1 -1
  33. package/dist/openapi/index.cjs +25 -9
  34. package/dist/openapi/index.cjs.map +1 -1
  35. package/dist/openapi/index.d.cts +5 -3
  36. package/dist/openapi/index.d.ts +5 -3
  37. package/dist/openapi/index.js +21 -5
  38. package/dist/openapi/index.js.map +1 -1
  39. package/package.json +2 -2
  40. package/dist/chunk-AR3LVWUF.js.map +0 -1
  41. package/dist/chunk-GTPK7A57.cjs.map +0 -1
  42. package/dist/chunk-IKH7YBYP.cjs.map +0 -1
  43. package/dist/chunk-JNP6BG5L.js.map +0 -1
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  cancelResponseBody
3
- } from "./chunk-AR3LVWUF.js";
3
+ } from "./chunk-XPKMA7DH.js";
4
+ import {
5
+ strictInputSchema
6
+ } from "./chunk-UMDUIBFW.js";
4
7
 
5
8
  // src/FastMCP.ts
6
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -376,21 +379,6 @@ function assertToolSchemas(tool) {
376
379
  assertStandardSchema(tool.name, "outputSchema", tool.outputSchema);
377
380
  }
378
381
  }
379
- function strictInputSchema(schema) {
380
- const isDictionary = typeof schema.additionalProperties === "object" && Object.keys(schema.properties ?? {}).length === 0;
381
- return {
382
- ...schema,
383
- additionalProperties: isDictionary ? schema.additionalProperties : false,
384
- ...schema.properties && {
385
- properties: Object.fromEntries(
386
- Object.entries(schema.properties).map(([key, value]) => [
387
- key,
388
- typeof value === "object" && value.type === "object" ? strictInputSchema(value) : value
389
- ])
390
- )
391
- }
392
- };
393
- }
394
382
  var STREAM_KEEPALIVE_LOGGER = "fastmcp-keepalive";
395
383
  var STREAM_KEEPALIVE_DEFAULT_INTERVAL_MS = 2e4;
396
384
  var TextContentZodSchema = z.object({
@@ -476,6 +464,17 @@ var capCompletionValues = (completion) => {
476
464
  values: completion.values.slice(0, COMPLETION_VALUES_LIMIT)
477
465
  };
478
466
  };
467
+ var DEFAULT_LOGGING_LEVEL = "info";
468
+ var LOGGING_LEVEL_SEVERITY = {
469
+ alert: 6,
470
+ critical: 5,
471
+ debug: 0,
472
+ emergency: 7,
473
+ error: 4,
474
+ info: 1,
475
+ notice: 2,
476
+ warning: 3
477
+ };
479
478
  var FastMCPSessionEventEmitterBase = EventEmitter;
480
479
  var ServerState = /* @__PURE__ */ ((ServerState2) => {
481
480
  ServerState2["Error"] = "error";
@@ -493,7 +492,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
493
492
  return this.#connectionState === "ready";
494
493
  }
495
494
  get loggingLevel() {
496
- return this.#loggingLevel;
495
+ return this.#loggingLevel ?? DEFAULT_LOGGING_LEVEL;
497
496
  }
498
497
  get roots() {
499
498
  return this.#roots;
@@ -537,11 +536,16 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
537
536
  */
538
537
  #abortController = new AbortController();
539
538
  #auth;
539
+ #authRequired;
540
540
  #capabilities = {};
541
541
  #clientCapabilities;
542
542
  #connectionState = "connecting";
543
543
  #logger;
544
- #loggingLevel = "info";
544
+ /**
545
+ * The minimum level the client asked for with `logging/setLevel`, or
546
+ * `undefined` while it has not asked. See {@link DEFAULT_LOGGING_LEVEL}.
547
+ */
548
+ #loggingLevel;
545
549
  #needsEventLoopFlush = false;
546
550
  #onToolCall;
547
551
  #pingConfig;
@@ -572,9 +576,16 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
572
576
  */
573
577
  #subscriptions = /* @__PURE__ */ new Set();
574
578
  #transportType;
579
+ /**
580
+ * Capabilities this session has already warned about in `#canRefresh`, so a
581
+ * server that registers a hundred resources at runtime logs one line rather
582
+ * than a hundred.
583
+ */
584
+ #unnegotiatedWarnings = /* @__PURE__ */ new Set();
575
585
  #utils;
576
586
  constructor({
577
587
  auth,
588
+ authRequired = false,
578
589
  hasTools,
579
590
  icons,
580
591
  instructions,
@@ -598,6 +609,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
598
609
  }) {
599
610
  super();
600
611
  this.#auth = auth;
612
+ this.#authRequired = authRequired;
601
613
  this.#logger = logger;
602
614
  this.#onToolCall = onToolCall;
603
615
  this.#pingConfig = ping;
@@ -609,7 +621,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
609
621
  this.#needsEventLoopFlush = transportType === "httpStream";
610
622
  const supportsTools = hasTools ?? tools.length > 0;
611
623
  if (supportsTools) {
612
- this.#capabilities.tools = {};
624
+ this.#capabilities.tools = { listChanged: true };
613
625
  }
614
626
  if (resources.length || resourcesTemplates.length) {
615
627
  this.#capabilities.resources = { listChanged: true, subscribe: true };
@@ -753,6 +765,9 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
753
765
  }
754
766
  }
755
767
  promptsListChanged(prompts) {
768
+ if (!this.#canRefresh("prompts")) {
769
+ return;
770
+ }
756
771
  this.#prompts.clear();
757
772
  for (const prompt of prompts) {
758
773
  this.addPrompt(prompt);
@@ -767,6 +782,9 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
767
782
  return this.#server.createMessage(message, options);
768
783
  }
769
784
  resourcesListChanged(resources) {
785
+ if (!this.#canRefresh("resources")) {
786
+ return;
787
+ }
770
788
  this.#resources.clear();
771
789
  for (const resource of resources) {
772
790
  this.addResource(resource);
@@ -775,6 +793,9 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
775
793
  this.triggerListChangedNotification("notifications/resources/list_changed");
776
794
  }
777
795
  resourceTemplatesListChanged(resourceTemplates) {
796
+ if (!this.#canRefresh("resources")) {
797
+ return;
798
+ }
778
799
  this.#resourceTemplates.clear();
779
800
  for (const resourceTemplate of resourceTemplates) {
780
801
  this.addResourceTemplate(resourceTemplate);
@@ -804,7 +825,12 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
804
825
  }
805
826
  }
806
827
  toolsListChanged(tools) {
807
- this.setupToolHandlers(toolsVisibleTo(tools, this.#auth));
828
+ if (!this.#canRefresh("tools")) {
829
+ return;
830
+ }
831
+ this.setupToolHandlers(
832
+ toolsVisibleTo(tools, this.#auth, this.#authRequired)
833
+ );
808
834
  this.triggerListChangedNotification("notifications/tools/list_changed");
809
835
  }
810
836
  async triggerListChangedNotification(method) {
@@ -864,6 +890,30 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
864
890
  this.#abortController.abort(new SessionError("Session closed"));
865
891
  }
866
892
  }
893
+ /**
894
+ * Whether this session can be told about a primitive added at runtime.
895
+ *
896
+ * A session that connected before the server had any tool, resource or prompt
897
+ * of its own never advertised that capability, and MCP fixes capabilities
898
+ * during `initialize` for the life of the session. Registering the handler
899
+ * anyway makes the SDK throw `Server does not support <capability>` — out of
900
+ * `addTool()`, `addResource()`, `addResourceTemplate()` or `addPrompt()`,
901
+ * which are the public builder methods, and out of the middle of the loop
902
+ * over sessions, so the sessions after this one never heard about the change
903
+ * either. Warn once instead and let the rest of the loop run.
904
+ */
905
+ #canRefresh(capability) {
906
+ if (this.#capabilities[capability]) {
907
+ return true;
908
+ }
909
+ if (!this.#unnegotiatedWarnings.has(capability)) {
910
+ this.#unnegotiatedWarnings.add(capability);
911
+ this.#logger.warn(
912
+ `[FastMCP warning] This session advertised no '${capability}' capability, because the server had none when the client connected, so it cannot be shown the ones registered since. Register one before start(), or reconnect the client; sessions that connect from now on do see them.`
913
+ );
914
+ }
915
+ return false;
916
+ }
867
917
  /**
868
918
  * Builds the context object passed as the third argument to
869
919
  * `resource.load` / `resourceTemplate.load` / `prompt.load`.
@@ -924,6 +974,9 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
924
974
  * disconnects mid-tool takes the server process down with it.
925
975
  */
926
976
  #sendLog(level, message, context) {
977
+ if (this.#loggingLevel && LOGGING_LEVEL_SEVERITY[level] < LOGGING_LEVEL_SEVERITY[this.#loggingLevel]) {
978
+ return;
979
+ }
927
980
  this.#server.sendLoggingMessage({
928
981
  data: {
929
982
  context,
@@ -947,6 +1000,12 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
947
1000
  * request's own stream, which is the only server-to-client route that exists
948
1001
  * when running stateless.
949
1002
  *
1003
+ * It is deliberately not filtered against the level the client set with
1004
+ * `logging/setLevel`, unlike the `log` object a tool is handed: its job is to
1005
+ * put bytes on the stream, and a client that asked for `error` only would
1006
+ * otherwise switch off a feature the server opted into. Raise
1007
+ * `streamKeepalive.logLevel` if the level it uses matters to you.
1008
+ *
950
1009
  * @returns a function that stops the keepalive.
951
1010
  */
952
1011
  #startStreamKeepalive(extra, toolName) {
@@ -1067,9 +1126,10 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1067
1126
  const ref = request.params.ref;
1068
1127
  const prompt = "name" in ref && this.#prompts.get(ref.name);
1069
1128
  if (!prompt) {
1070
- throw new UnexpectedStateError("Unknown prompt", {
1071
- request
1072
- });
1129
+ throw new McpError(
1130
+ ErrorCode.InvalidParams,
1131
+ `Unknown prompt: ${ref.name}`
1132
+ );
1073
1133
  }
1074
1134
  if (!prompt.complete) {
1075
1135
  throw new UnexpectedStateError("Prompt does not support completion", {
@@ -1095,9 +1155,10 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1095
1155
  (resource2) => resource2.uriTemplate === ref.uri
1096
1156
  );
1097
1157
  if (!resource) {
1098
- throw new UnexpectedStateError("Unknown resource", {
1099
- request
1100
- });
1158
+ throw new McpError(
1159
+ ErrorCode.InvalidParams,
1160
+ `Unknown resource template: ${ref.uri}`
1161
+ );
1101
1162
  }
1102
1163
  if (!("uriTemplate" in resource)) {
1103
1164
  throw new UnexpectedStateError("Unexpected resource");
@@ -1627,6 +1688,17 @@ function parseBasicAuthHeader(authHeader) {
1627
1688
  return null;
1628
1689
  }
1629
1690
  }
1691
+ function protectedResourceMetadataUrl(resource) {
1692
+ let url;
1693
+ try {
1694
+ url = new URL(resource);
1695
+ } catch {
1696
+ return `${resource}/.well-known/oauth-protected-resource`;
1697
+ }
1698
+ const path = url.pathname === "/" ? "" : url.pathname;
1699
+ url.pathname = `/.well-known/oauth-protected-resource${path}`;
1700
+ return url.toString();
1701
+ }
1630
1702
  var OAUTH_PROXY_MAX_BODY_SIZE = 1024 * 1024;
1631
1703
  var OAUTH_CREDENTIAL_RESPONSE_HEADERS = {
1632
1704
  "Cache-Control": "no-store",
@@ -1645,8 +1717,13 @@ function stripBasePath(path, basePath) {
1645
1717
  }
1646
1718
  return null;
1647
1719
  }
1648
- function toolsVisibleTo(tools, auth) {
1649
- return auth ? tools.filter((tool) => tool.canAccess ? tool.canAccess(auth) : true) : tools;
1720
+ function toolsVisibleTo(tools, auth, authRequired) {
1721
+ if (auth) {
1722
+ return tools.filter(
1723
+ (tool) => tool.canAccess ? tool.canAccess(auth) : true
1724
+ );
1725
+ }
1726
+ return authRequired ? tools.filter((tool) => !tool.canAccess) : tools;
1650
1727
  }
1651
1728
  var FastMCPEventEmitterBase = EventEmitter;
1652
1729
  var FastMCPEventEmitter = class extends FastMCPEventEmitterBase {
@@ -1664,10 +1741,9 @@ var FastMCP = class extends FastMCPEventEmitter {
1664
1741
  this.#authenticate = options.authenticate;
1665
1742
  }
1666
1743
  if (!options.oauth) {
1667
- this.#options = {
1668
- ...options,
1669
- oauth: options.auth.getOAuthConfig()
1670
- };
1744
+ const oauth = options.auth.getOAuthConfig();
1745
+ this.#providerResourceBase = oauth.protectedResource.resource;
1746
+ this.#options = { ...options, oauth };
1671
1747
  }
1672
1748
  } else {
1673
1749
  this.#authenticate = options.authenticate;
@@ -1685,6 +1761,8 @@ var FastMCP = class extends FastMCPEventEmitter {
1685
1761
  #logger;
1686
1762
  #options;
1687
1763
  #prompts = [];
1764
+ /** The auth provider's base URL, when it supplied the OAuth config. */
1765
+ #providerResourceBase;
1688
1766
  #resources = [];
1689
1767
  #resourcesTemplates = [];
1690
1768
  #serverState = "stopped" /* Stopped */;
@@ -2027,6 +2105,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2027
2105
  if (config.transportType === "stdio") {
2028
2106
  const transport = new StdioServerTransport();
2029
2107
  let auth;
2108
+ const authRequired = this.#authenticate !== void 0;
2030
2109
  if (this.#authenticate) {
2031
2110
  try {
2032
2111
  auth = await this.#authenticate(
@@ -2038,9 +2117,15 @@ var FastMCP = class extends FastMCPEventEmitter {
2038
2117
  error instanceof Error ? error.message : String(error)
2039
2118
  );
2040
2119
  }
2120
+ if (!auth && this.#tools.some((tool) => tool.canAccess)) {
2121
+ this.#logger.warn(
2122
+ "[FastMCP warning] The stdio session has no auth, so tools gated by canAccess are hidden from it."
2123
+ );
2124
+ }
2041
2125
  }
2042
2126
  const session = new FastMCPSession({
2043
2127
  auth,
2128
+ authRequired,
2044
2129
  hasTools: this.#tools.length > 0,
2045
2130
  icons: this.#options.icons,
2046
2131
  instructions: this.#options.instructions,
@@ -2054,7 +2139,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2054
2139
  roots: this.#options.roots,
2055
2140
  streamKeepalive: this.#options.streamKeepalive,
2056
2141
  title: this.#options.title,
2057
- tools: toolsVisibleTo(this.#tools, auth),
2142
+ tools: toolsVisibleTo(this.#tools, auth, authRequired),
2058
2143
  transportType: "stdio",
2059
2144
  utils: this.#options.utils,
2060
2145
  version: this.#options.version,
@@ -2104,6 +2189,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2104
2189
  httpConfig.basePath,
2105
2190
  httpConfig.endpoint
2106
2191
  );
2192
+ this.#useEndpointAsResource(httpConfig.endpoint);
2107
2193
  if (httpConfig.stateless) {
2108
2194
  this.#logger.info(
2109
2195
  `[FastMCP info] Starting server in stateless mode on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${streamEndpoint}`
@@ -2224,8 +2310,10 @@ var FastMCP = class extends FastMCPEventEmitter {
2224
2310
  const errorMessage = "error" in auth && typeof auth.error === "string" ? auth.error : "Authentication failed";
2225
2311
  throw this.#createUnauthorizedResponse(errorMessage);
2226
2312
  }
2313
+ const authRequired = this.#authenticate !== void 0;
2227
2314
  return new FastMCPSession({
2228
2315
  auth,
2316
+ authRequired,
2229
2317
  hasTools: this.#tools.length > 0,
2230
2318
  icons: this.#options.icons,
2231
2319
  instructions: this.#options.instructions,
@@ -2241,7 +2329,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2241
2329
  stateless,
2242
2330
  streamKeepalive: this.#options.streamKeepalive,
2243
2331
  title: this.#options.title,
2244
- tools: toolsVisibleTo(this.#tools, auth),
2332
+ tools: toolsVisibleTo(this.#tools, auth, authRequired),
2245
2333
  transportType: "httpStream",
2246
2334
  utils: this.#options.utils,
2247
2335
  version: this.#options.version,
@@ -2270,7 +2358,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2270
2358
  ];
2271
2359
  if (resource) {
2272
2360
  wwwAuthenticateParts.push(
2273
- `resource_metadata="${resource}/.well-known/oauth-protected-resource"`
2361
+ `resource_metadata="${protectedResourceMetadataUrl(resource)}"`
2274
2362
  );
2275
2363
  }
2276
2364
  return new Response(
@@ -2921,6 +3009,28 @@ var FastMCP = class extends FastMCPEventEmitter {
2921
3009
  session.toolsListChanged(tools);
2922
3010
  }
2923
3011
  }
3012
+ /**
3013
+ * RFC 9728: the protected resource is the MCP endpoint clients connect to,
3014
+ * and the metadata served at `/.well-known/oauth-protected-resource<endpoint>`
3015
+ * must name it. An auth provider only knows its base URL, so the endpoint is
3016
+ * appended once the server knows it. An explicit `oauth` config is left as
3017
+ * it is.
3018
+ */
3019
+ #useEndpointAsResource(endpoint) {
3020
+ const oauth = this.#options.oauth;
3021
+ if (this.#providerResourceBase === void 0 || !oauth?.protectedResource)
3022
+ return;
3023
+ this.#options = {
3024
+ ...this.#options,
3025
+ oauth: {
3026
+ ...oauth,
3027
+ protectedResource: {
3028
+ ...oauth.protectedResource,
3029
+ resource: `${this.#providerResourceBase.replace(/\/+$/, "")}${normalizePath(endpoint)}`
3030
+ }
3031
+ }
3032
+ };
3033
+ }
2924
3034
  };
2925
3035
 
2926
3036
  export {
@@ -2937,4 +3047,4 @@ export {
2937
3047
  FastMCPSession,
2938
3048
  FastMCP
2939
3049
  };
2940
- //# sourceMappingURL=chunk-JNP6BG5L.js.map
3050
+ //# sourceMappingURL=chunk-43N2Q6ZI.js.map