@prefecthq/fastmcp-ts 1.7.2 → 1.8.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/dist/server.d.ts CHANGED
@@ -903,8 +903,9 @@ interface FastMCPOptions {
903
903
  * Bearer-token verifier, or a RequestVerifier for header-based
904
904
  * (trusted-proxy) deployments. See each type's docs. A RequestVerifier
905
905
  * applies to HTTP transports only; stdio's FASTMCP_CLI_AUTH_TOKEN needs a
906
- * TokenVerifier. When both `oauth` and `auth` are set, `oauth` serves the
907
- * endpoint and `auth` is ignored.
906
+ * TokenVerifier. Wrap either in `remoteAuth(...)` to also serve RFC 9728
907
+ * protected resource metadata over HTTP. When both `oauth` and `auth` are
908
+ * set, `oauth` serves the endpoint and `auth` is ignored.
908
909
  */
909
910
  auth?: TokenVerifier | RequestVerifier;
910
911
  /** Full OAuth 2.1 server with Dynamic Client Registration support. */
@@ -1156,6 +1157,7 @@ declare class FastMCP {
1156
1157
  private _templateResources;
1157
1158
  private _prompts;
1158
1159
  private _customRoutes;
1160
+ private _protectedResourceRoutePath;
1159
1161
  private _promptsPageSize;
1160
1162
  private _middleware;
1161
1163
  private _transforms;
@@ -1507,6 +1509,7 @@ declare class FastMCP {
1507
1509
  private _runHttpOAuth;
1508
1510
  private _runHttpSimple;
1509
1511
  close(): Promise<void>;
1512
+ private _unregisterProtectedResourceRoute;
1510
1513
  }
1511
1514
 
1512
1515
  declare class Image {
@@ -1527,6 +1530,71 @@ declare class ToolResult {
1527
1530
 
1528
1531
  declare function multiAuth(...verifiers: TokenVerifier[]): TokenVerifier;
1529
1532
 
1533
+ /** Options for {@link remoteAuth}. */
1534
+ interface RemoteAuthOptions<V extends TokenVerifier | RequestVerifier> {
1535
+ /** Verifier that checks each request's credentials, such as `jwtVerifier(...)`. */
1536
+ verifier: V;
1537
+ /** Issuer URLs of the authorization servers that mint tokens for this server. */
1538
+ authorizationServers: Array<string | URL>;
1539
+ /** Public URL clients use to reach this server, without the MCP path. */
1540
+ baseUrl: string | URL;
1541
+ /** Scopes to advertise in the metadata document and in 401 challenges. */
1542
+ scopesSupported?: string[];
1543
+ /** Human-readable name of this server, shown by some clients. */
1544
+ resourceName?: string;
1545
+ /** URL of documentation for this server. */
1546
+ resourceDocumentation?: string | URL;
1547
+ }
1548
+ /** Validated protected-resource settings that `run()` serves as metadata. */
1549
+ interface ProtectedResourceConfig {
1550
+ readonly baseUrl: URL;
1551
+ readonly authorizationServers: readonly string[];
1552
+ readonly scopesSupported?: readonly string[];
1553
+ readonly resourceName?: string;
1554
+ readonly resourceDocumentation?: string;
1555
+ }
1556
+ /** A verifier that also carries {@link ProtectedResourceConfig}. */
1557
+ type RemoteAuthProvider<V extends TokenVerifier | RequestVerifier> = V & {
1558
+ readonly protectedResource: ProtectedResourceConfig;
1559
+ };
1560
+ /**
1561
+ * Make this server an OAuth resource server for an external authorization
1562
+ * server. Pass the result to `FastMCPOptions.auth`.
1563
+ *
1564
+ * Use it when another service issues the tokens, such as Auth0, Keycloak or
1565
+ * Okta, and this server only verifies them. `verifier` checks each request, as
1566
+ * with `jwtVerifier` or `introspectionVerifier`. `remoteAuth` adds the
1567
+ * discovery information that lets MCP clients find the authorization server on
1568
+ * their own.
1569
+ *
1570
+ * The resource URL is `baseUrl` joined with the path given to `run()`. For a
1571
+ * `baseUrl` of `https://mcp.example.com` and the default path `/mcp`, the
1572
+ * resource is `https://mcp.example.com/mcp`. Set `baseUrl` to the public URL
1573
+ * that clients use, such as the address of your reverse proxy. A wrong value
1574
+ * makes the advertised resource differ from the URL clients call.
1575
+ *
1576
+ * Over HTTP the server then serves the RFC 9728 metadata document at
1577
+ * `/.well-known/oauth-protected-resource` followed by the MCP path. Each 401
1578
+ * response also carries a `WWW-Authenticate: Bearer` challenge whose
1579
+ * `resource_metadata` parameter points to that document.
1580
+ *
1581
+ * If you set both this and `oauth`, `oauth` takes precedence.
1582
+ *
1583
+ * @example
1584
+ * ```ts
1585
+ * const mcp = new FastMCP({
1586
+ * name: 'my-server',
1587
+ * auth: remoteAuth({
1588
+ * verifier: jwtVerifier({ jwksUri: 'https://auth.example.com/.well-known/jwks.json' }),
1589
+ * authorizationServers: ['https://auth.example.com'],
1590
+ * baseUrl: 'https://mcp.example.com',
1591
+ * }),
1592
+ * })
1593
+ * ```
1594
+ */
1595
+ declare function remoteAuth(options: RemoteAuthOptions<TokenVerifier>): RemoteAuthProvider<TokenVerifier>;
1596
+ declare function remoteAuth(options: RemoteAuthOptions<RequestVerifier>): RemoteAuthProvider<RequestVerifier>;
1597
+
1530
1598
  interface JwtVerifierOptions {
1531
1599
  jwksUri: string;
1532
1600
  issuer?: string;
@@ -1970,4 +2038,4 @@ declare class FormInput {
1970
2038
  constructor(options: FormInputOptions);
1971
2039
  }
1972
2040
 
1973
- export { type AccessToken, Approval, Area, type AuthCheck, AuthorizationError, type BackendToolConfig, Badge, Bar, type BrowserPermissions, Button, type CacheKeyFn, CachingMiddleware, CancellationMiddleware, type CatalogEntry, Choice, Column, type CompleteCallback, type CompletionContext, type CompletionResult, type Component, type ComponentFn, type CorsOptions, type CspPolicy, type CustomRouteConfig, type CustomRouteHandler, type ElicitationResult, type ElicitationSchema, type EntrypointConfig, ErrorNormalizationMiddleware, FastMCP, FastMCPApp, type FastMCPAppOptions, type FastMCPOptions, File, type FileHandle, type FileStorageAdapter, FileUpload, type FileUploadOptions, FilterTransform, ForEach, FormInput, type FormInputOptions, type FrameworkLogLevel, GenerativeUI, Grid, type HTTPRoute, type HealthOptions, type HttpMethod, type HttpRequestContext, If, type IfNode, Image, Input, Line, type LogLevel, type Logger, LoggingMiddleware, type MCPType, type McpContext, type Middleware, type MiddlewareContext, NamespaceTransform, type Next, type OAuthConfig, type OAuthProviderOptions, type OAuthProxyOptions, type OpenAPIClientOptions, type OpenAPIComponent, type OpenAPIResourceComponent, type OpenAPIResourceTemplateComponent, type OpenAPIServerOptions, type OpenAPIToolComponent, type ParameterInfo, Pie, type PromptArgument, type PromptConfig, type PromptContent, type PromptMessage, PromptResult, type PromptView, PromptsAsTools, type ProxyTransport, RateLimitingMiddleware, type RequestBodyInfo, type RequestVerifier, type ResourceAnnotations, type ResourceConfig, ResourceResult, type ResourceUiMeta, type ResourceView, ResourcesAsTools, type ResponseInfo, type Root, type RouteMap, type RouteMapFn, Row, type RunOptions, Rx, type SamplingMessage, type SamplingParams, type SamplingResult, Select, type ServerAddress, SizeLimitingMiddleware, type SynthesizedTool, Table, Text, type TokenVerifier, type ToolAnnotations, type ToolConfig, ToolResult, type ToolView, type Transform, type UiToolMeta, VersionFilter, type Visibility, actionRef, createOpenAPIServer, createProxy, debugTokenVerifier, forwardableHeaders, introspectionVerifier, jwtVerifier, multiAuth, oauthProvider, oauthProxy, redescribeTool, renameTool, requireScopes, staticTokenVerifier };
2041
+ export { type AccessToken, Approval, Area, type AuthCheck, AuthorizationError, type BackendToolConfig, Badge, Bar, type BrowserPermissions, Button, type CacheKeyFn, CachingMiddleware, CancellationMiddleware, type CatalogEntry, Choice, Column, type CompleteCallback, type CompletionContext, type CompletionResult, type Component, type ComponentFn, type CorsOptions, type CspPolicy, type CustomRouteConfig, type CustomRouteHandler, type ElicitationResult, type ElicitationSchema, type EntrypointConfig, ErrorNormalizationMiddleware, FastMCP, FastMCPApp, type FastMCPAppOptions, type FastMCPOptions, File, type FileHandle, type FileStorageAdapter, FileUpload, type FileUploadOptions, FilterTransform, ForEach, FormInput, type FormInputOptions, type FrameworkLogLevel, GenerativeUI, Grid, type HTTPRoute, type HealthOptions, type HttpMethod, type HttpRequestContext, If, type IfNode, Image, Input, Line, type LogLevel, type Logger, LoggingMiddleware, type MCPType, type McpContext, type Middleware, type MiddlewareContext, NamespaceTransform, type Next, type OAuthConfig, type OAuthProviderOptions, type OAuthProxyOptions, type OpenAPIClientOptions, type OpenAPIComponent, type OpenAPIResourceComponent, type OpenAPIResourceTemplateComponent, type OpenAPIServerOptions, type OpenAPIToolComponent, type ParameterInfo, Pie, type PromptArgument, type PromptConfig, type PromptContent, type PromptMessage, PromptResult, type PromptView, PromptsAsTools, type ProtectedResourceConfig, type ProxyTransport, RateLimitingMiddleware, type RemoteAuthOptions, type RemoteAuthProvider, type RequestBodyInfo, type RequestVerifier, type ResourceAnnotations, type ResourceConfig, ResourceResult, type ResourceUiMeta, type ResourceView, ResourcesAsTools, type ResponseInfo, type Root, type RouteMap, type RouteMapFn, Row, type RunOptions, Rx, type SamplingMessage, type SamplingParams, type SamplingResult, Select, type ServerAddress, SizeLimitingMiddleware, type SynthesizedTool, Table, Text, type TokenVerifier, type ToolAnnotations, type ToolConfig, ToolResult, type ToolView, type Transform, type UiToolMeta, VersionFilter, type Visibility, actionRef, createOpenAPIServer, createProxy, debugTokenVerifier, forwardableHeaders, introspectionVerifier, jwtVerifier, multiAuth, oauthProvider, oauthProxy, redescribeTool, remoteAuth, renameTool, requireScopes, staticTokenVerifier };
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
 
5
5
  // src/server/FastMCP.ts
6
6
  import { hostHeaderValidation, originValidation } from "@modelcontextprotocol/node";
7
- import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode3, ResourceNotFoundError, Server as Server2, createMcpHandler, legacyStatelessFallback, isLegacyRequest, isJsonContentType, createRequestStateCodec, localhostAllowedHostnames, localhostAllowedOrigins, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate } from "@modelcontextprotocol/server";
7
+ import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode3, ResourceNotFoundError, Server as Server2, createMcpHandler, legacyStatelessFallback, isLegacyRequest, isJsonContentType, createRequestStateCodec, localhostAllowedHostnames, localhostAllowedOrigins, getOAuthProtectedResourceMetadataUrl as getOAuthProtectedResourceMetadataUrl2, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate } from "@modelcontextprotocol/server";
8
8
  import { randomUUID as randomUUID2 } from "crypto";
9
9
 
10
10
  // src/server/auth/types.ts
@@ -18,6 +18,98 @@ var AuthorizationError = class extends Error {
18
18
  }
19
19
  };
20
20
 
21
+ // src/server/auth/remote.ts
22
+ import {
23
+ getOAuthProtectedResourceMetadataUrl
24
+ } from "@modelcontextprotocol/server";
25
+ var SCOPE_TOKEN = /^[\x21\x23-\x5B\x5D-\x7E]+$/;
26
+ function parseHttpUrl(value, option) {
27
+ let url;
28
+ try {
29
+ url = new URL(value);
30
+ } catch {
31
+ throw new Error(`remoteAuth: ${option} must be a valid URL, got "${String(value)}"`);
32
+ }
33
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
34
+ throw new Error(`remoteAuth: ${option} must use http or https, got "${String(value)}"`);
35
+ }
36
+ return url;
37
+ }
38
+ function remoteAuth(options) {
39
+ const { verifier } = options;
40
+ const baseUrl = parseHttpUrl(options.baseUrl, "baseUrl");
41
+ if (baseUrl.search !== "" || baseUrl.hash !== "") {
42
+ throw new Error("remoteAuth: baseUrl must not have a query string or fragment");
43
+ }
44
+ if (options.authorizationServers.length === 0) {
45
+ throw new Error("remoteAuth: authorizationServers must contain at least one entry");
46
+ }
47
+ const authorizationServers = options.authorizationServers.map((entry) => {
48
+ const url = parseHttpUrl(entry, "authorizationServers entry");
49
+ return typeof entry === "string" ? entry : url.href;
50
+ });
51
+ for (const scope of options.scopesSupported ?? []) {
52
+ if (!SCOPE_TOKEN.test(scope)) {
53
+ throw new Error(
54
+ `remoteAuth: scopesSupported entry "${scope}" must not be empty or contain spaces, quotes or backslashes`
55
+ );
56
+ }
57
+ }
58
+ let resourceDocumentation;
59
+ if (options.resourceDocumentation !== void 0) {
60
+ try {
61
+ resourceDocumentation = new URL(options.resourceDocumentation).href;
62
+ } catch {
63
+ throw new Error(
64
+ `remoteAuth: resourceDocumentation must be a valid URL, got "${String(options.resourceDocumentation)}"`
65
+ );
66
+ }
67
+ }
68
+ const protectedResource = {
69
+ baseUrl,
70
+ authorizationServers,
71
+ ...options.scopesSupported && { scopesSupported: [...options.scopesSupported] },
72
+ ...options.resourceName !== void 0 && { resourceName: options.resourceName },
73
+ ...resourceDocumentation !== void 0 && { resourceDocumentation }
74
+ };
75
+ if (isRequestVerifier(verifier)) {
76
+ return { verifyRequest: (request) => verifier.verifyRequest(request), protectedResource };
77
+ }
78
+ return { verify: (token) => verifier.verify(token), protectedResource };
79
+ }
80
+ function getProtectedResource(auth) {
81
+ return auth?.protectedResource;
82
+ }
83
+ function resolveResourceUrl(baseUrl, mcpPath) {
84
+ const base = baseUrl.href.replace(/\/+$/, "");
85
+ return new URL(`${base}/${mcpPath.replace(/^\/+/, "")}`);
86
+ }
87
+ function protectedResourceRoute(config, mcpPath) {
88
+ const resourceUrl = resolveResourceUrl(config.baseUrl, mcpPath);
89
+ const metadataUrl = getOAuthProtectedResourceMetadataUrl(resourceUrl);
90
+ return {
91
+ path: new URL(metadataUrl).pathname,
92
+ metadataUrl,
93
+ metadata: {
94
+ resource: resourceUrl.href,
95
+ authorization_servers: [...config.authorizationServers],
96
+ ...config.scopesSupported && { scopes_supported: [...config.scopesSupported] },
97
+ bearer_methods_supported: ["header"],
98
+ ...config.resourceName !== void 0 && { resource_name: config.resourceName },
99
+ ...config.resourceDocumentation !== void 0 && {
100
+ resource_documentation: config.resourceDocumentation
101
+ }
102
+ }
103
+ };
104
+ }
105
+ function bearerChallenge(config, error) {
106
+ const parts = ['realm="mcp"'];
107
+ if (error) parts.push(`error="${error}"`);
108
+ if (config.scopes && config.scopes.length > 0) parts.push(`scope="${config.scopes.join(" ")}"`);
109
+ parts.push(`resource_metadata="${config.resourceMetadataUrl}"`);
110
+ return `Bearer ${parts.join(", ")}`;
111
+ }
112
+
21
113
  // src/server/legacyEventStore.ts
22
114
  import { randomUUID } from "crypto";
23
115
  var DEFAULT_MAX_EVENTS = 256;
@@ -312,6 +404,10 @@ var CustomRouteRegistry = class {
312
404
  }
313
405
  this._routes.set(path, { methods: new Set(methods), handler });
314
406
  }
407
+ /** Remove the route at `path`, if any. */
408
+ unregister(path) {
409
+ this._routes.delete(path);
410
+ }
315
411
  /** Throws when a registered route sits on the MCP endpoint path. Called at HTTP startup. */
316
412
  assertNoMcpCollision(mcpPath) {
317
413
  if (this._routes.has(mcpPath)) {
@@ -996,6 +1092,9 @@ function isUiCapable(clientCapabilities) {
996
1092
  const uiCapability = clientCapabilities?.extensions?.[UI_EXTENSION_KEY];
997
1093
  return !!uiCapability?.mimeTypes?.includes(UI_RESOURCE_MIME_TYPE);
998
1094
  }
1095
+ function shouldServeUi(clientCapabilities) {
1096
+ return clientCapabilities === void 0 || isUiCapable(clientCapabilities);
1097
+ }
999
1098
 
1000
1099
  // src/server/prompt.ts
1001
1100
  var PromptResult = class {
@@ -1483,6 +1582,9 @@ var FastMCP = class {
1483
1582
  _templateResources = /* @__PURE__ */ new Map();
1484
1583
  _prompts = /* @__PURE__ */ new Map();
1485
1584
  _customRoutes = new CustomRouteRegistry();
1585
+ // The metadata route that run() registers for remoteAuth; close() removes it
1586
+ // so that a later run() can register it again.
1587
+ _protectedResourceRoutePath = null;
1486
1588
  _promptsPageSize;
1487
1589
  _middleware;
1488
1590
  _transforms;
@@ -1609,7 +1711,7 @@ var FastMCP = class {
1609
1711
  return contextStore.run(
1610
1712
  ctx,
1611
1713
  () => runMiddlewareChain(this._middleware, "tools/list", req.params, ctx, async () => {
1612
- const clientIsUiCapable = isUiCapable(server.getClientCapabilities());
1714
+ const clientIsUiCapable = shouldServeUi(server.getClientCapabilities());
1613
1715
  const allVisible = (await Promise.all(
1614
1716
  [...this._tools.values()].map(async (t) => {
1615
1717
  if (t.config.disabled) return null;
@@ -1761,11 +1863,10 @@ var FastMCP = class {
1761
1863
  let resultValue = await executePromise;
1762
1864
  if (resolvedTool.config.output) resultValue = await validateInput(resolvedTool.config.output, resultValue);
1763
1865
  const callResult = convertResult(resultValue, opts?.stateless, this._logger);
1764
- if (resolvedTool.config.ui) {
1765
- const clientIsUi = isUiCapable(server.getClientCapabilities());
1766
- if (!clientIsUi && callResult.structuredContent !== void 0) {
1767
- return { content: [{ type: "text", text: "[UI not available in this client]" }] };
1768
- }
1866
+ const uiGateClosed = resolvedTool.config.ui !== void 0 && !shouldServeUi(server.getClientCapabilities()) && !resolvedTool.config.output && !resolvedTool.config.outputSchema;
1867
+ if (uiGateClosed && callResult.structuredContent !== void 0) {
1868
+ const { structuredContent: _dropped, ...textResult } = callResult;
1869
+ return textResult;
1769
1870
  }
1770
1871
  return callResult;
1771
1872
  })
@@ -1808,7 +1909,7 @@ var FastMCP = class {
1808
1909
  }
1809
1910
  const page = transformedResources.slice(startIdx, startIdx + pageSize);
1810
1911
  const nextCursor = startIdx + pageSize < transformedResources.length ? Buffer.from(page[page.length - 1].uri).toString("base64url") : void 0;
1811
- const clientIsUiCapableRes = isUiCapable(server.getClientCapabilities());
1912
+ const clientIsUiCapableRes = shouldServeUi(server.getClientCapabilities());
1812
1913
  return {
1813
1914
  resources: page.map((r) => ({
1814
1915
  uri: r.uri,
@@ -1899,7 +2000,7 @@ var FastMCP = class {
1899
2000
  }
1900
2001
  const result = await executePromise;
1901
2002
  const converted = convertResourceResult(result, requestedUri, resource.config.mimeType, opts?.stateless);
1902
- return attachResourceUiMeta(converted, resource.config.ui, isUiCapable(server.getClientCapabilities()));
2003
+ return attachResourceUiMeta(converted, resource.config.ui, shouldServeUi(server.getClientCapabilities()));
1903
2004
  })
1904
2005
  );
1905
2006
  });
@@ -2743,11 +2844,29 @@ var FastMCP = class {
2743
2844
  )
2744
2845
  );
2745
2846
  }
2746
- this._customRoutes.assertNoMcpCollision(path);
2747
- if (this._oauth) {
2748
- await this._runHttpOAuth(port, host, path);
2749
- } else {
2750
- await this._runHttpSimple(port, host, path);
2847
+ const protectedResource = this._oauth ? void 0 : getProtectedResource(this._auth);
2848
+ let challenge;
2849
+ if (protectedResource) {
2850
+ const route = protectedResourceRoute(protectedResource, path);
2851
+ this._customRoutes.register(
2852
+ { path: route.path },
2853
+ (request) => Response.json(route.metadata, {
2854
+ headers: this._cors ? corsResponseHeaders(this._cors, request.headers.get("origin") ?? void 0, this._logger) : {}
2855
+ })
2856
+ );
2857
+ this._protectedResourceRoutePath = route.path;
2858
+ challenge = { resourceMetadataUrl: route.metadataUrl, scopes: protectedResource.scopesSupported };
2859
+ }
2860
+ try {
2861
+ this._customRoutes.assertNoMcpCollision(path);
2862
+ if (this._oauth) {
2863
+ await this._runHttpOAuth(port, host, path);
2864
+ } else {
2865
+ await this._runHttpSimple(port, host, path, challenge);
2866
+ }
2867
+ } catch (err) {
2868
+ this._unregisterProtectedResourceRoute();
2869
+ throw err;
2751
2870
  }
2752
2871
  }
2753
2872
  }
@@ -2911,16 +3030,19 @@ var FastMCP = class {
2911
3030
  });
2912
3031
  const bound = httpServer.address();
2913
3032
  const issuerUrl = oauth.issuerUrl ?? new URL(`http://${bound.address}:${bound.port}`);
3033
+ const resourceServerUrl = new URL(path, issuerUrl);
3034
+ const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl2(resourceServerUrl);
2914
3035
  app.use(
2915
3036
  mcpAuthRouter({
2916
3037
  provider: oauth.provider,
2917
3038
  issuerUrl,
3039
+ resourceServerUrl,
2918
3040
  scopesSupported: oauth.scopes
2919
3041
  })
2920
3042
  );
2921
3043
  app.all(
2922
3044
  path,
2923
- requireBearerAuth({ verifier: oauth.provider }),
3045
+ requireBearerAuth({ verifier: oauth.provider, resourceMetadataUrl }),
2924
3046
  async (req, res) => {
2925
3047
  await this._dispatchHttp(req, res);
2926
3048
  }
@@ -2931,7 +3053,7 @@ var FastMCP = class {
2931
3053
  this._logger.startupBanner({ name: this.name, version: this.version, transport: "http", url });
2932
3054
  this._logger.listening(url);
2933
3055
  }
2934
- async _runHttpSimple(port, host, path) {
3056
+ async _runHttpSimple(port, host, path, challenge) {
2935
3057
  this._resolveDnsRebindingGuards(host);
2936
3058
  const { createServer } = await import("http");
2937
3059
  const auth = this._auth;
@@ -2964,7 +3086,10 @@ var FastMCP = class {
2964
3086
  if (err instanceof AuthorizationError) {
2965
3087
  res.writeHead(403, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err.message }));
2966
3088
  } else {
2967
- res.writeHead(401, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
3089
+ res.writeHead(401, {
3090
+ "Content-Type": "application/json",
3091
+ ...challenge && { "WWW-Authenticate": bearerChallenge(challenge) }
3092
+ }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
2968
3093
  }
2969
3094
  return;
2970
3095
  }
@@ -2988,7 +3113,10 @@ var FastMCP = class {
2988
3113
  const authHeader = req.headers.authorization;
2989
3114
  const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
2990
3115
  if (!bearer) {
2991
- res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error: "Missing bearer token" }));
3116
+ res.writeHead(401, {
3117
+ "Content-Type": "application/json",
3118
+ "WWW-Authenticate": challenge ? bearerChallenge(challenge) : 'Bearer realm="mcp"'
3119
+ }).end(JSON.stringify({ error: "Missing bearer token" }));
2992
3120
  return;
2993
3121
  }
2994
3122
  try {
@@ -3004,7 +3132,10 @@ var FastMCP = class {
3004
3132
  if (err instanceof AuthorizationError) {
3005
3133
  res.writeHead(403, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err.message }));
3006
3134
  } else {
3007
- res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
3135
+ res.writeHead(401, {
3136
+ "Content-Type": "application/json",
3137
+ "WWW-Authenticate": challenge ? bearerChallenge(challenge, "invalid_token") : 'Bearer realm="mcp"'
3138
+ }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
3008
3139
  }
3009
3140
  return;
3010
3141
  }
@@ -3044,6 +3175,7 @@ var FastMCP = class {
3044
3175
  this._stdioState = null;
3045
3176
  this._stdioModern = false;
3046
3177
  }
3178
+ this._unregisterProtectedResourceRoute();
3047
3179
  if (this._httpServer) {
3048
3180
  await new Promise((resolve, reject) => {
3049
3181
  this._httpServer.close((err) => err != null ? reject(err) : resolve());
@@ -3053,6 +3185,12 @@ var FastMCP = class {
3053
3185
  this._address = null;
3054
3186
  await this._primaryServer.close();
3055
3187
  }
3188
+ _unregisterProtectedResourceRoute() {
3189
+ if (this._protectedResourceRoutePath) {
3190
+ this._customRoutes.unregister(this._protectedResourceRoutePath);
3191
+ this._protectedResourceRoutePath = null;
3192
+ }
3193
+ }
3056
3194
  };
3057
3195
 
3058
3196
  // src/server/auth/authorization.ts
@@ -3189,7 +3327,7 @@ function debugTokenVerifier() {
3189
3327
 
3190
3328
  // src/server/auth/oauth/provider.ts
3191
3329
  import { randomUUID as randomUUID3 } from "crypto";
3192
- import { UnsupportedGrantTypeError } from "@modelcontextprotocol/server-legacy/auth";
3330
+ import { InvalidTokenError, UnsupportedGrantTypeError } from "@modelcontextprotocol/server-legacy/auth";
3193
3331
  function oauthProvider(options = {}) {
3194
3332
  const clients = /* @__PURE__ */ new Map();
3195
3333
  const codes = /* @__PURE__ */ new Map();
@@ -3250,8 +3388,8 @@ function oauthProvider(options = {}) {
3250
3388
  },
3251
3389
  async verifyAccessToken(token) {
3252
3390
  const data = tokens.get(token);
3253
- if (!data) throw new Error("Invalid token");
3254
- if (data.expiresAt < Math.floor(Date.now() / 1e3)) throw new Error("Token has expired");
3391
+ if (!data) throw new InvalidTokenError("Invalid token");
3392
+ if (data.expiresAt < Math.floor(Date.now() / 1e3)) throw new InvalidTokenError("Token has expired");
3255
3393
  return {
3256
3394
  token,
3257
3395
  clientId: data.clientId,
@@ -5646,6 +5784,7 @@ export {
5646
5784
  oauthProvider,
5647
5785
  oauthProxy,
5648
5786
  redescribeTool,
5787
+ remoteAuth,
5649
5788
  renameTool,
5650
5789
  requireScopes,
5651
5790
  staticTokenVerifier