fastmcp 4.23.13 → 4.24.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
@@ -757,6 +757,38 @@ session sees every tool. With one, a session that still ends up without auth —
757
757
  on stdio, where the server carries on when `authenticate` throws or returns
758
758
  nothing — sees none of the tools that have `canAccess`.
759
759
 
760
+ ##### Scope challenges
761
+
762
+ To keep a tool listed but require OAuth scopes to call it, list them in
763
+ `scopes`. FastMCP reads the scopes a session was granted from `auth.scopes`, an
764
+ array or a `Set`, the same way `requireScopes()` does.
765
+
766
+ ```typescript
767
+ server.addTool({
768
+ name: "write-file",
769
+ description: "Writes a file",
770
+ scopes: ["files:write"],
771
+ execute: async () => "Written",
772
+ });
773
+ ```
774
+
775
+ A call without those scopes never runs. Over HTTP it gets MCP's
776
+ [scope challenge](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#scope-challenge-handling),
777
+ a `403` with:
778
+
779
+ ```http
780
+ WWW-Authenticate: Bearer error="insufficient_scope", scope="files:read files:write", resource_metadata="…"
781
+ ```
782
+
783
+ `scope` lists the scopes the session already has plus the ones the tool needs,
784
+ so the client can get a token with all of them and retry. `resource_metadata`
785
+ is included when `oauth.enabled` is set. On stdio and other transports without
786
+ HTTP, the call fails with a JSON-RPC error (code `-32001`) instead.
787
+
788
+ To hide the tool from sessions without the scopes, use
789
+ `canAccess: requireScopes(...)` instead. Like `canAccess`, `scopes` is not
790
+ enforced on a server without an `authenticate` function.
791
+
760
792
  #### Returning a string
761
793
 
762
794
  `execute` can return a string:
@@ -2003,7 +2035,8 @@ server.addTool({
2003
2035
  // ...
2004
2036
  });
2005
2037
 
2006
- // Require specific OAuth scopes
2038
+ // Require specific OAuth scopes (hides the tool; see "Scope challenges" to
2039
+ // keep it listed and answer calls with a 403 instead)
2007
2040
  server.addTool({
2008
2041
  canAccess: requireScopes("read:user", "write:data"),
2009
2042
  name: "scoped-tool",
package/dist/FastMCP.cjs CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
 
13
13
 
14
- var _chunk4P2GDM7Wcjs = require('./chunk-4P2GDM7W.cjs');
14
+ var _chunkJMH7WDO2cjs = require('./chunk-JMH7WDO2.cjs');
15
15
 
16
16
 
17
17
 
@@ -50,5 +50,5 @@ require('./chunk-6TDYSPLC.cjs');
50
50
 
51
51
 
52
52
 
53
- exports.AuthProvider = _chunkBV4PWTT6cjs.AuthProvider; exports.AzureProvider = _chunkBV4PWTT6cjs.AzureProvider; exports.DiscoveryDocumentCache = _chunk4P2GDM7Wcjs.DiscoveryDocumentCache; exports.FastMCP = _chunk4P2GDM7Wcjs.FastMCP; exports.FastMCPError = _chunk4P2GDM7Wcjs.FastMCPError; exports.FastMCPSession = _chunk4P2GDM7Wcjs.FastMCPSession; exports.GitHubProvider = _chunkBV4PWTT6cjs.GitHubProvider; exports.GoogleProvider = _chunkBV4PWTT6cjs.GoogleProvider; exports.MEDIA_FETCH_TIMEOUT_MS = _chunk4P2GDM7Wcjs.MEDIA_FETCH_TIMEOUT_MS; exports.OAuthProvider = _chunkBV4PWTT6cjs.OAuthProvider; exports.ServerState = _chunk4P2GDM7Wcjs.ServerState; exports.SessionError = _chunk4P2GDM7Wcjs.SessionError; exports.UnexpectedStateError = _chunk4P2GDM7Wcjs.UnexpectedStateError; exports.UserError = _chunk4P2GDM7Wcjs.UserError; exports.audioContent = _chunk4P2GDM7Wcjs.audioContent; exports.getAuthSession = _chunkBV4PWTT6cjs.getAuthSession; exports.imageContent = _chunk4P2GDM7Wcjs.imageContent; exports.jsonSchemaAdapter = _chunk4P2GDM7Wcjs.jsonSchemaAdapter; exports.requireAll = _chunkBV4PWTT6cjs.requireAll; exports.requireAny = _chunkBV4PWTT6cjs.requireAny; exports.requireAuth = _chunkBV4PWTT6cjs.requireAuth; exports.requireRole = _chunkBV4PWTT6cjs.requireRole; exports.requireScopes = _chunkBV4PWTT6cjs.requireScopes;
53
+ exports.AuthProvider = _chunkBV4PWTT6cjs.AuthProvider; exports.AzureProvider = _chunkBV4PWTT6cjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkJMH7WDO2cjs.DiscoveryDocumentCache; exports.FastMCP = _chunkJMH7WDO2cjs.FastMCP; exports.FastMCPError = _chunkJMH7WDO2cjs.FastMCPError; exports.FastMCPSession = _chunkJMH7WDO2cjs.FastMCPSession; exports.GitHubProvider = _chunkBV4PWTT6cjs.GitHubProvider; exports.GoogleProvider = _chunkBV4PWTT6cjs.GoogleProvider; exports.MEDIA_FETCH_TIMEOUT_MS = _chunkJMH7WDO2cjs.MEDIA_FETCH_TIMEOUT_MS; exports.OAuthProvider = _chunkBV4PWTT6cjs.OAuthProvider; exports.ServerState = _chunkJMH7WDO2cjs.ServerState; exports.SessionError = _chunkJMH7WDO2cjs.SessionError; exports.UnexpectedStateError = _chunkJMH7WDO2cjs.UnexpectedStateError; exports.UserError = _chunkJMH7WDO2cjs.UserError; exports.audioContent = _chunkJMH7WDO2cjs.audioContent; exports.getAuthSession = _chunkBV4PWTT6cjs.getAuthSession; exports.imageContent = _chunkJMH7WDO2cjs.imageContent; exports.jsonSchemaAdapter = _chunkJMH7WDO2cjs.jsonSchemaAdapter; exports.requireAll = _chunkBV4PWTT6cjs.requireAll; exports.requireAny = _chunkBV4PWTT6cjs.requireAny; exports.requireAuth = _chunkBV4PWTT6cjs.requireAuth; exports.requireRole = _chunkBV4PWTT6cjs.requireRole; exports.requireScopes = _chunkBV4PWTT6cjs.requireScopes;
54
54
  //# sourceMappingURL=FastMCP.cjs.map
@@ -354,6 +354,16 @@ type InputResourceTemplateArgument<T extends FastMCPSessionAuth = FastMCPSession
354
354
  required?: boolean;
355
355
  }>;
356
356
  type LoggingLevel = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning";
357
+ /**
358
+ * Why a tool call may not run with the session's token, and the scopes the
359
+ * client should re-authorize for: the ones the session already has plus the
360
+ * ones the tool needs, so that the new token does not drop any (the approach
361
+ * MCP recommends for scope challenges).
362
+ */
363
+ type ScopeChallenge = {
364
+ description: string;
365
+ scopes: string[];
366
+ };
357
367
  type Prompt<T extends FastMCPSessionAuth = FastMCPSessionAuth, Arguments extends PromptArgument<T>[] = PromptArgument<T>[], Args = PromptArgumentsToObject<Arguments>> = {
358
368
  arguments?: PromptArgument<T>[];
359
369
  complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
@@ -808,6 +818,19 @@ type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolPara
808
818
  name: string;
809
819
  outputSchema?: OutputParams;
810
820
  parameters?: Params;
821
+ /**
822
+ * OAuth scopes a caller needs to call this tool, checked on every call
823
+ * against `auth.scopes` (an array or a `Set`, read as `requireScopes()`
824
+ * reads it). Unlike `canAccess`, this does not hide the tool. A call without
825
+ * them never runs: over HTTP it is answered with MCP's 403
826
+ * `insufficient_scope` challenge, which tells the client which scopes to
827
+ * re-authorize for, and on other transports with a JSON-RPC error.
828
+ *
829
+ * Enforced on the same terms as `canAccess`: not at all on a server without
830
+ * `authenticate`, and as if nothing were granted for a session that ends up
831
+ * without auth on one that has it.
832
+ */
833
+ scopes?: string[];
811
834
  timeoutMs?: number;
812
835
  };
813
836
  /**
@@ -967,6 +990,13 @@ declare class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth>
967
990
  requestSampling(message: z.infer<typeof CreateMessageRequestSchema>["params"], options?: RequestOptions): Promise<SamplingResponse>;
968
991
  resourcesListChanged(resources: Resource<T>[]): void;
969
992
  resourceTemplatesListChanged(resourceTemplates: ResourceTemplate<T>[]): void;
993
+ /**
994
+ * The scope challenge one message a client sent would get, or `undefined` to
995
+ * let it through. FastMCP's HTTP transports ask before mcp-proxy dispatches
996
+ * each message, the last point at which the answer can still be a 403; the
997
+ * `tools/call` handler applies the same check on every transport.
998
+ */
999
+ scopeChallengeFor(message: unknown): ScopeChallenge | undefined;
970
1000
  /**
971
1001
  * Notifies the connected client that the contents of a resource have changed.
972
1002
  *
@@ -1166,4 +1196,4 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
1166
1196
  stop(): Promise<void>;
1167
1197
  }
1168
1198
 
1169
- export { type AudioContent, AuthProvider, type Content, type ContentResult, type Context, DiscoveryDocumentCache, FastMCP, FastMCPError, type FastMCPEvents, type FastMCPRequest, type FastMCPResponse, FastMCPSession, type FastMCPSessionAuth, type FastMCPSessionEvents, type HTTPMethod, type ImageContent, type InputPrompt, type InputPromptArgument, type JsonSchemaObject, type JsonSchemaStandardSchema, type LoadContext, type Logger, type LoggingLevel, MEDIA_FETCH_TIMEOUT_MS, OAuthSession, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type RouteHandler, type RouteOptions, type SSEServer, type SerializableValue, type ServerOptions, ServerState, SessionError, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent, jsonSchemaAdapter };
1199
+ export { type AudioContent, AuthProvider, type Content, type ContentResult, type Context, DiscoveryDocumentCache, FastMCP, FastMCPError, type FastMCPEvents, type FastMCPRequest, type FastMCPResponse, FastMCPSession, type FastMCPSessionAuth, type FastMCPSessionEvents, type HTTPMethod, type ImageContent, type InputPrompt, type InputPromptArgument, type JsonSchemaObject, type JsonSchemaStandardSchema, type LoadContext, type Logger, type LoggingLevel, MEDIA_FETCH_TIMEOUT_MS, OAuthSession, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type RouteHandler, type RouteOptions, type SSEServer, type ScopeChallenge, type SerializableValue, type ServerOptions, ServerState, SessionError, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent, jsonSchemaAdapter };
package/dist/FastMCP.d.ts CHANGED
@@ -354,6 +354,16 @@ type InputResourceTemplateArgument<T extends FastMCPSessionAuth = FastMCPSession
354
354
  required?: boolean;
355
355
  }>;
356
356
  type LoggingLevel = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning";
357
+ /**
358
+ * Why a tool call may not run with the session's token, and the scopes the
359
+ * client should re-authorize for: the ones the session already has plus the
360
+ * ones the tool needs, so that the new token does not drop any (the approach
361
+ * MCP recommends for scope challenges).
362
+ */
363
+ type ScopeChallenge = {
364
+ description: string;
365
+ scopes: string[];
366
+ };
357
367
  type Prompt<T extends FastMCPSessionAuth = FastMCPSessionAuth, Arguments extends PromptArgument<T>[] = PromptArgument<T>[], Args = PromptArgumentsToObject<Arguments>> = {
358
368
  arguments?: PromptArgument<T>[];
359
369
  complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
@@ -808,6 +818,19 @@ type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolPara
808
818
  name: string;
809
819
  outputSchema?: OutputParams;
810
820
  parameters?: Params;
821
+ /**
822
+ * OAuth scopes a caller needs to call this tool, checked on every call
823
+ * against `auth.scopes` (an array or a `Set`, read as `requireScopes()`
824
+ * reads it). Unlike `canAccess`, this does not hide the tool. A call without
825
+ * them never runs: over HTTP it is answered with MCP's 403
826
+ * `insufficient_scope` challenge, which tells the client which scopes to
827
+ * re-authorize for, and on other transports with a JSON-RPC error.
828
+ *
829
+ * Enforced on the same terms as `canAccess`: not at all on a server without
830
+ * `authenticate`, and as if nothing were granted for a session that ends up
831
+ * without auth on one that has it.
832
+ */
833
+ scopes?: string[];
811
834
  timeoutMs?: number;
812
835
  };
813
836
  /**
@@ -967,6 +990,13 @@ declare class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth>
967
990
  requestSampling(message: z.infer<typeof CreateMessageRequestSchema>["params"], options?: RequestOptions): Promise<SamplingResponse>;
968
991
  resourcesListChanged(resources: Resource<T>[]): void;
969
992
  resourceTemplatesListChanged(resourceTemplates: ResourceTemplate<T>[]): void;
993
+ /**
994
+ * The scope challenge one message a client sent would get, or `undefined` to
995
+ * let it through. FastMCP's HTTP transports ask before mcp-proxy dispatches
996
+ * each message, the last point at which the answer can still be a 403; the
997
+ * `tools/call` handler applies the same check on every transport.
998
+ */
999
+ scopeChallengeFor(message: unknown): ScopeChallenge | undefined;
970
1000
  /**
971
1001
  * Notifies the connected client that the contents of a resource have changed.
972
1002
  *
@@ -1166,4 +1196,4 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
1166
1196
  stop(): Promise<void>;
1167
1197
  }
1168
1198
 
1169
- export { type AudioContent, AuthProvider, type Content, type ContentResult, type Context, DiscoveryDocumentCache, FastMCP, FastMCPError, type FastMCPEvents, type FastMCPRequest, type FastMCPResponse, FastMCPSession, type FastMCPSessionAuth, type FastMCPSessionEvents, type HTTPMethod, type ImageContent, type InputPrompt, type InputPromptArgument, type JsonSchemaObject, type JsonSchemaStandardSchema, type LoadContext, type Logger, type LoggingLevel, MEDIA_FETCH_TIMEOUT_MS, OAuthSession, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type RouteHandler, type RouteOptions, type SSEServer, type SerializableValue, type ServerOptions, ServerState, SessionError, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent, jsonSchemaAdapter };
1199
+ export { type AudioContent, AuthProvider, type Content, type ContentResult, type Context, DiscoveryDocumentCache, FastMCP, FastMCPError, type FastMCPEvents, type FastMCPRequest, type FastMCPResponse, FastMCPSession, type FastMCPSessionAuth, type FastMCPSessionEvents, type HTTPMethod, type ImageContent, type InputPrompt, type InputPromptArgument, type JsonSchemaObject, type JsonSchemaStandardSchema, type LoadContext, type Logger, type LoggingLevel, MEDIA_FETCH_TIMEOUT_MS, OAuthSession, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type RouteHandler, type RouteOptions, type SSEServer, type ScopeChallenge, type SerializableValue, type ServerOptions, ServerState, SessionError, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent, jsonSchemaAdapter };
package/dist/FastMCP.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  audioContent,
12
12
  imageContent,
13
13
  jsonSchemaAdapter
14
- } from "./chunk-ULNQF5KG.js";
14
+ } from "./chunk-5JRRZ7MN.js";
15
15
  import {
16
16
  AuthProvider,
17
17
  AzureProvider,
@@ -490,6 +490,8 @@ var LOGGING_LEVEL_SEVERITY = {
490
490
  notice: 2,
491
491
  warning: 3
492
492
  };
493
+ var INSUFFICIENT_SCOPE_ERROR_CODE = -32001;
494
+ var SCOPE_TOKEN = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
493
495
  var FastMCPSessionEventEmitterBase = EventEmitter;
494
496
  var ServerState = /* @__PURE__ */ ((ServerState2) => {
495
497
  ServerState2["Error"] = "error";
@@ -792,6 +794,24 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
792
794
  this.setupResourceTemplateHandlers();
793
795
  this.triggerListChangedNotification("notifications/resources/list_changed");
794
796
  }
797
+ /**
798
+ * The scope challenge one message a client sent would get, or `undefined` to
799
+ * let it through. FastMCP's HTTP transports ask before mcp-proxy dispatches
800
+ * each message, the last point at which the answer can still be a 403; the
801
+ * `tools/call` handler applies the same check on every transport.
802
+ */
803
+ scopeChallengeFor(message) {
804
+ if (typeof message !== "object" || message === null) {
805
+ return void 0;
806
+ }
807
+ const { method, params } = message;
808
+ if (method !== "tools/call" || typeof params !== "object" || !params) {
809
+ return void 0;
810
+ }
811
+ const { name } = params;
812
+ const tool = this.#visibleTools.find((visible) => visible.name === name);
813
+ return tool ? this.#scopeChallenge(tool) : void 0;
814
+ }
795
815
  /**
796
816
  * Notifies the connected client that the contents of a resource have changed.
797
817
  *
@@ -1030,6 +1050,27 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
1030
1050
  }
1031
1051
  }
1032
1052
  }
1053
+ /**
1054
+ * The challenge a call to `tool` gets while the session's token lacks the
1055
+ * tool's `scopes`, or `undefined` when it may run. Applied on the same terms
1056
+ * as `canAccess` (see `toolsVisibleTo`): never without auth on a server that
1057
+ * does not authenticate, and with nothing granted when a server that does
1058
+ * has no auth for this session.
1059
+ */
1060
+ #scopeChallenge(tool) {
1061
+ if (!tool.scopes?.length || !this.#auth && !this.#authRequired) {
1062
+ return void 0;
1063
+ }
1064
+ const granted = grantedScopes(this.#auth);
1065
+ const missing = tool.scopes.filter((scope) => !granted.includes(scope));
1066
+ if (missing.length === 0) {
1067
+ return void 0;
1068
+ }
1069
+ return {
1070
+ description: `Insufficient scope for tool '${tool.name}': requires ${missing.join(" ")}`,
1071
+ scopes: [.../* @__PURE__ */ new Set([...granted, ...tool.scopes])]
1072
+ };
1073
+ }
1033
1074
  /**
1034
1075
  * Sending rejects once the client has hung up, and these four
1035
1076
  * callers are synchronous, so the rejection has nowhere to go. Swallow it the
@@ -1636,6 +1677,14 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
1636
1677
  `Unknown tool: ${request.params.name}`
1637
1678
  );
1638
1679
  }
1680
+ const challenge = this.#scopeChallenge(tool);
1681
+ if (challenge) {
1682
+ throw new McpError(
1683
+ INSUFFICIENT_SCOPE_ERROR_CODE,
1684
+ challenge.description,
1685
+ { error: "insufficient_scope", required_scopes: challenge.scopes }
1686
+ );
1687
+ }
1639
1688
  let args = void 0;
1640
1689
  if (tool.parameters) {
1641
1690
  const parsed = await tool.parameters["~standard"].validate(
@@ -1980,6 +2029,11 @@ var OAUTH_INVALID_CLIENT_RESPONSE_HEADERS = {
1980
2029
  ...OAUTH_CREDENTIAL_RESPONSE_HEADERS,
1981
2030
  "WWW-Authenticate": 'Basic realm="oauth", charset="UTF-8"'
1982
2031
  };
2032
+ function grantedScopes(auth) {
2033
+ const scopes = auth?.scopes;
2034
+ const values = Array.isArray(scopes) ? scopes : scopes instanceof Set ? [...scopes] : [];
2035
+ return values.filter((scope) => typeof scope === "string");
2036
+ }
1983
2037
  function stripBasePath(path, basePath) {
1984
2038
  if (!basePath) {
1985
2039
  return path;
@@ -2464,6 +2518,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2464
2518
  const authenticate = this.#authenticate ? this.#memoizedAuthenticate() : void 0;
2465
2519
  this.#httpStreamServer = await startHTTPServer({
2466
2520
  ...authenticate ? { authenticate } : {},
2521
+ beforeDispatch: ({ message, server }) => this.#answerScopeChallenge(message, server),
2467
2522
  cors: httpConfig.cors,
2468
2523
  createServer: async (request) => {
2469
2524
  let auth;
@@ -2511,6 +2566,7 @@ var FastMCP = class extends FastMCPEventEmitter {
2511
2566
  const authenticate = this.#authenticate ? this.#memoizedAuthenticate() : void 0;
2512
2567
  this.#httpStreamServer = await startHTTPServer({
2513
2568
  ...authenticate ? { authenticate } : {},
2569
+ beforeDispatch: ({ message, server }) => this.#answerScopeChallenge(message, server),
2514
2570
  cors: httpConfig.cors,
2515
2571
  createServer: async (request) => {
2516
2572
  let auth;
@@ -2583,6 +2639,23 @@ var FastMCP = class extends FastMCPEventEmitter {
2583
2639
  }
2584
2640
  this.#serverState = "stopped" /* Stopped */;
2585
2641
  }
2642
+ /**
2643
+ * mcp-proxy's `beforeDispatch`: answers a `tools/call` the session's token
2644
+ * lacks scopes for with MCP's 403 scope challenge. It runs for every message
2645
+ * POSTed over HTTP, after `authenticate` and `updateAuth`, because once a
2646
+ * message is dispatched the SDK answers it on a 200.
2647
+ */
2648
+ #answerScopeChallenge(message, session) {
2649
+ const challenge = session.scopeChallengeFor(message);
2650
+ if (!challenge) {
2651
+ return void 0;
2652
+ }
2653
+ const { id } = message;
2654
+ return this.#createInsufficientScopeResponse(
2655
+ challenge,
2656
+ typeof id === "number" || typeof id === "string" ? id : null
2657
+ );
2658
+ }
2586
2659
  /**
2587
2660
  * Connects `session` to `transport` and lists it in `sessions` until the
2588
2661
  * transport closes. The close is watched from before the session connects,
@@ -2609,6 +2682,54 @@ var FastMCP = class extends FastMCPEventEmitter {
2609
2682
  session
2610
2683
  });
2611
2684
  }
2685
+ /**
2686
+ * Builds MCP's 403 `insufficient_scope` challenge (2025-11-25, Scope
2687
+ * Challenge Handling). `scope` lists what the client should re-authorize
2688
+ * for, and `resource_metadata` is added on the same terms as on the 401 (see
2689
+ * `#createUnauthorizedResponse`).
2690
+ *
2691
+ * Clients such as the MCP SDK pick each parameter out with a simple pattern:
2692
+ * the first match wins and a quoted value ends at the first quote. So the
2693
+ * parameters come first, the description comes last, and it keeps only the
2694
+ * characters RFC 6750 allows in it.
2695
+ */
2696
+ #createInsufficientScopeResponse(challenge, id) {
2697
+ const oauth = this.#options.oauth;
2698
+ const resource = oauth?.enabled ? oauth.protectedResource?.resource : void 0;
2699
+ const wwwAuthenticateParts = [
2700
+ 'error="insufficient_scope"',
2701
+ `scope="${challenge.scopes.filter((scope) => SCOPE_TOKEN.test(scope)).join(" ")}"`
2702
+ ];
2703
+ if (resource) {
2704
+ wwwAuthenticateParts.push(
2705
+ `resource_metadata="${protectedResourceMetadataUrl(resource)}"`
2706
+ );
2707
+ }
2708
+ wwwAuthenticateParts.push(
2709
+ `error_description="${challenge.description.replace(/[^\x20\x21\x23-\x5B\x5D-\x7E]/g, "")}"`
2710
+ );
2711
+ return new Response(
2712
+ JSON.stringify({
2713
+ error: {
2714
+ code: INSUFFICIENT_SCOPE_ERROR_CODE,
2715
+ data: {
2716
+ error: "insufficient_scope",
2717
+ required_scopes: challenge.scopes
2718
+ },
2719
+ message: challenge.description
2720
+ },
2721
+ id,
2722
+ jsonrpc: "2.0"
2723
+ }),
2724
+ {
2725
+ headers: {
2726
+ "Content-Type": "application/json",
2727
+ "WWW-Authenticate": `Bearer ${wwwAuthenticateParts.join(", ")}`
2728
+ },
2729
+ status: 403
2730
+ }
2731
+ );
2732
+ }
2612
2733
  /**
2613
2734
  * Creates a new FastMCPSession instance with the current configuration.
2614
2735
  * Used both for regular sessions and stateless requests.
@@ -3314,12 +3435,11 @@ var FastMCP = class extends FastMCPEventEmitter {
3314
3435
  * Rejects a failed authentication result before it can become a session.
3315
3436
  *
3316
3437
  * Authentication is REQUIRED whenever an `authenticate` function is
3317
- * configured. mcp-proxy gates the HTTP Stream endpoint before `createServer`
3318
- * runs, but it does not gate the SSE endpoint it serves at `/sse` by default:
3319
- * `handleSSERequest` never receives `authenticate`. Throwing here is what
3320
- * stops `/sse` from handing out a session — with access to every tool, since
3321
- * `#createSession` skips `canAccess` filtering when `auth` is falsy — to a
3322
- * client that `/mcp` would have answered with a 401.
3438
+ * configured. mcp-proxy answers a failed `authenticate` with a 401 before
3439
+ * `createServer` runs: on the HTTP Stream endpoint, and since 6.8.0 on the
3440
+ * SSE endpoint's `GET /sse` and every `POST /messages` too. This is the
3441
+ * second gate, so a client that `/mcp` would answer with a 401 never gets a
3442
+ * session, whichever endpoint it uses.
3323
3443
  *
3324
3444
  * The falsy test matches mcp-proxy's own check so that both endpoints agree
3325
3445
  * on what counts as a failed authentication. Returning a nullish value is the
@@ -3394,4 +3514,4 @@ export {
3394
3514
  FastMCPSession,
3395
3515
  FastMCP
3396
3516
  };
3397
- //# sourceMappingURL=chunk-ULNQF5KG.js.map
3517
+ //# sourceMappingURL=chunk-5JRRZ7MN.js.map