fastmcp 3.20.1 → 3.21.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.
package/README.md CHANGED
@@ -1316,10 +1316,15 @@ In this example, only clients authenticating with the `admin` role will be able
1316
1316
  FastMCP includes built-in support for OAuth discovery endpoints, supporting both **MCP Specification 2025-03-26** and **MCP Specification 2025-06-18** for OAuth integration. This makes it easy to integrate with OAuth authorization flows by providing standard discovery endpoints that comply with RFC 8414 (OAuth 2.0 Authorization Server Metadata) and RFC 9470 (OAuth 2.0 Protected Resource Metadata):
1317
1317
 
1318
1318
  ```ts
1319
- import { FastMCP } from "fastmcp";
1319
+ import { FastMCP, DiscoveryDocumentCache } from "fastmcp";
1320
1320
  import { buildGetJwks } from "get-jwks";
1321
1321
  import fastJwt from "fast-jwt";
1322
1322
 
1323
+ // Create a cache for discovery documents (reuse across requests)
1324
+ const discoveryCache = new DiscoveryDocumentCache({
1325
+ ttl: 3600000, // Cache for 1 hour (default)
1326
+ });
1327
+
1323
1328
  const server = new FastMCP({
1324
1329
  name: "My Server",
1325
1330
  version: "1.0.0",
@@ -1351,19 +1356,16 @@ const server = new FastMCP({
1351
1356
 
1352
1357
  // Validate OAuth JWT access token using OpenID Connect discovery
1353
1358
  try {
1354
- // TODO: Cache the discovery document to avoid repeated requests
1355
- // Discover OAuth/OpenID configuration from well-known endpoint
1359
+ // Fetch and cache the discovery document
1356
1360
  const discoveryUrl =
1357
1361
  "https://auth.example.com/.well-known/openid-configuration";
1358
1362
  // Alternative: Use OAuth authorization server metadata endpoint
1359
1363
  // const discoveryUrl = 'https://auth.example.com/.well-known/oauth-authorization-server';
1360
1364
 
1361
- const discoveryResponse = await fetch(discoveryUrl);
1362
- if (!discoveryResponse.ok) {
1363
- throw new Error("Failed to fetch OAuth discovery document");
1364
- }
1365
-
1366
- const config = await discoveryResponse.json();
1365
+ const config = (await discoveryCache.get(discoveryUrl)) as {
1366
+ jwks_uri: string;
1367
+ issuer: string;
1368
+ };
1367
1369
  const jwksUri = config.jwks_uri;
1368
1370
  const issuer = config.issuer;
1369
1371
 
package/dist/FastMCP.d.ts CHANGED
@@ -10,6 +10,37 @@ import http from 'http';
10
10
  import { StrictEventEmitter } from 'strict-event-emitter-types';
11
11
  import { z } from 'zod';
12
12
 
13
+ declare class DiscoveryDocumentCache {
14
+ #private;
15
+ get size(): number;
16
+ /**
17
+ * @param options - configuration options
18
+ * @param options.ttl - time-to-live in miliseconds
19
+ */
20
+ constructor(options?: {
21
+ ttl?: number;
22
+ });
23
+ /**
24
+ * @param url - optional URL to clear. if omitted, clears all cached documents.
25
+ */
26
+ clear(url?: string): void;
27
+ /**
28
+ * fetches a discovery document from the given URL.
29
+ * uses cached value if available and not expired.
30
+ * coalesces concurrent requests for the same URL to prevent duplicate fetches.
31
+ *
32
+ * @param url - the discovery document URL (e.g., /.well-known/openid-configuration)
33
+ * @returns the discovery document as a JSON object
34
+ * @throws Error if the fetch fails or returns non-OK status
35
+ */
36
+ get(url: string): Promise<unknown>;
37
+ /**
38
+ * @param url - the URL to check
39
+ * @returns true if the URL is cached and nott expired
40
+ */
41
+ has(url: string): boolean;
42
+ }
43
+
13
44
  interface Logger {
14
45
  debug(...args: unknown[]): void;
15
46
  error(...args: unknown[]): void;
@@ -648,4 +679,4 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
648
679
  stop(): Promise<void>;
649
680
  }
650
681
 
651
- export { type AudioContent, type Content, type ContentResult, type Context, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent };
682
+ export { type AudioContent, type Content, type ContentResult, type Context, DiscoveryDocumentCache, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent };
package/dist/FastMCP.js CHANGED
@@ -20,16 +20,104 @@ import { readFile } from "fs/promises";
20
20
  import Fuse from "fuse.js";
21
21
  import { startHTTPServer } from "mcp-proxy";
22
22
  import { setTimeout as delay } from "timers/promises";
23
- import { fetch } from "undici";
23
+ import { fetch as fetch2 } from "undici";
24
24
  import parseURITemplate from "uri-templates";
25
25
  import { toJsonSchema } from "xsschema";
26
26
  import { z } from "zod";
27
+
28
+ // src/DiscoveryDocumentCache.ts
29
+ var DiscoveryDocumentCache = class {
30
+ get size() {
31
+ return this.#cache.size;
32
+ }
33
+ #cache = /* @__PURE__ */ new Map();
34
+ #inFlight = /* @__PURE__ */ new Map();
35
+ #ttl;
36
+ /**
37
+ * @param options - configuration options
38
+ * @param options.ttl - time-to-live in miliseconds
39
+ */
40
+ constructor(options = {}) {
41
+ this.#ttl = options.ttl ?? 36e5;
42
+ }
43
+ /**
44
+ * @param url - optional URL to clear. if omitted, clears all cached documents.
45
+ */
46
+ clear(url) {
47
+ if (url) {
48
+ this.#cache.delete(url);
49
+ } else {
50
+ this.#cache.clear();
51
+ }
52
+ }
53
+ /**
54
+ * fetches a discovery document from the given URL.
55
+ * uses cached value if available and not expired.
56
+ * coalesces concurrent requests for the same URL to prevent duplicate fetches.
57
+ *
58
+ * @param url - the discovery document URL (e.g., /.well-known/openid-configuration)
59
+ * @returns the discovery document as a JSON object
60
+ * @throws Error if the fetch fails or returns non-OK status
61
+ */
62
+ async get(url) {
63
+ const now = Date.now();
64
+ const cached = this.#cache.get(url);
65
+ if (cached && cached.expiresAt > now) {
66
+ return cached.data;
67
+ }
68
+ const inFlight = this.#inFlight.get(url);
69
+ if (inFlight) {
70
+ return inFlight;
71
+ }
72
+ const fetchPromise = this.#fetchAndCache(url);
73
+ this.#inFlight.set(url, fetchPromise);
74
+ try {
75
+ const data = await fetchPromise;
76
+ return data;
77
+ } finally {
78
+ this.#inFlight.delete(url);
79
+ }
80
+ }
81
+ /**
82
+ * @param url - the URL to check
83
+ * @returns true if the URL is cached and nott expired
84
+ */
85
+ has(url) {
86
+ const cached = this.#cache.get(url);
87
+ if (!cached) {
88
+ return false;
89
+ }
90
+ const now = Date.now();
91
+ if (cached.expiresAt <= now) {
92
+ this.#cache.delete(url);
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ async #fetchAndCache(url) {
98
+ const res = await fetch(url);
99
+ if (!res.ok) {
100
+ throw new Error(
101
+ `Failed to fetch discovery document from ${url}: ${res.status} ${res.statusText}`
102
+ );
103
+ }
104
+ const data = await res.json();
105
+ const expiresAt = Date.now() + this.#ttl;
106
+ this.#cache.set(url, {
107
+ data,
108
+ expiresAt
109
+ });
110
+ return data;
111
+ }
112
+ };
113
+
114
+ // src/FastMCP.ts
27
115
  var imageContent = async (input) => {
28
116
  let rawData;
29
117
  try {
30
118
  if ("url" in input) {
31
119
  try {
32
- const response = await fetch(input.url);
120
+ const response = await fetch2(input.url);
33
121
  if (!response.ok) {
34
122
  throw new Error(
35
123
  `Server responded with status: ${response.status} - ${response.statusText}`
@@ -82,7 +170,7 @@ var audioContent = async (input) => {
82
170
  try {
83
171
  if ("url" in input) {
84
172
  try {
85
- const response = await fetch(input.url);
173
+ const response = await fetch2(input.url);
86
174
  if (!response.ok) {
87
175
  throw new Error(
88
176
  `Server responded with status: ${response.status} - ${response.statusText}`
@@ -1382,6 +1470,7 @@ var FastMCP = class extends FastMCPEventEmitter {
1382
1470
  }
1383
1471
  };
1384
1472
  export {
1473
+ DiscoveryDocumentCache,
1385
1474
  FastMCP,
1386
1475
  FastMCPSession,
1387
1476
  UnexpectedStateError,