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 +34 -1
- package/dist/FastMCP.cjs +2 -2
- package/dist/FastMCP.d.cts +31 -1
- package/dist/FastMCP.d.ts +31 -1
- package/dist/FastMCP.js +1 -1
- package/dist/{chunk-ULNQF5KG.js → chunk-5JRRZ7MN.js} +127 -7
- package/dist/chunk-5JRRZ7MN.js.map +1 -0
- package/dist/{chunk-4P2GDM7W.cjs → chunk-JMH7WDO2.cjs} +163 -43
- package/dist/chunk-JMH7WDO2.cjs.map +1 -0
- package/dist/examples/custom-routes.cjs +2 -2
- package/dist/examples/custom-routes.js +1 -1
- package/dist/openapi/index.cjs +6 -6
- package/dist/openapi/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-4P2GDM7W.cjs.map +0 -1
- package/dist/chunk-ULNQF5KG.js.map +0 -1
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
|
|
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 =
|
|
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
|
package/dist/FastMCP.d.cts
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.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
|
@@ -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
|
|
3318
|
-
* runs
|
|
3319
|
-
* `
|
|
3320
|
-
*
|
|
3321
|
-
*
|
|
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-
|
|
3517
|
+
//# sourceMappingURL=chunk-5JRRZ7MN.js.map
|