fastmcp 3.23.1 → 3.25.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
@@ -1311,9 +1311,50 @@ server.addTool({
1311
1311
 
1312
1312
  In this example, only clients authenticating with the `admin` role will be able to list or call the `admin-dashboard` tool. The `public-info` tool will be available to all authenticated users.
1313
1313
 
1314
- #### OAuth Support
1314
+ #### OAuth Proxy
1315
1315
 
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):
1316
+ FastMCP includes a built-in **OAuth Proxy** that acts as a secure intermediary between MCP clients and upstream OAuth providers. The proxy handles the complete OAuth 2.1 authorization flow, including Dynamic Client Registration (DCR), PKCE, consent management, and token management with encryption and token swap patterns enabled by default.
1317
+
1318
+ **Key Features:**
1319
+
1320
+ - 🔐 **Secure by Default**: Automatic encryption (AES-256-GCM) and token swap pattern
1321
+ - 🚀 **Zero Configuration**: Auto-generates keys and handles OAuth flows automatically
1322
+ - 🔌 **Pre-configured Providers**: Built-in support for Google, GitHub, and Azure
1323
+ - 🎯 **RFC Compliant**: Implements DCR (RFC 7591), PKCE, and OAuth 2.1
1324
+ - 🔑 **Optional JWKS**: Support for RS256/ES256 token verification (via optional `jose` dependency)
1325
+
1326
+ **Quick Start:**
1327
+
1328
+ ```ts
1329
+ import { FastMCP } from "fastmcp";
1330
+ import { GoogleProvider } from "fastmcp/auth";
1331
+
1332
+ const authProxy = new GoogleProvider({
1333
+ clientId: process.env.GOOGLE_CLIENT_ID,
1334
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
1335
+ baseUrl: "https://your-server.com",
1336
+ scopes: ["openid", "profile", "email"],
1337
+ });
1338
+
1339
+ const server = new FastMCP({
1340
+ name: "My Server",
1341
+ oauth: {
1342
+ enabled: true,
1343
+ authorizationServer: authProxy.getAuthorizationServerMetadata(),
1344
+ proxy: authProxy, // Routes automatically registered!
1345
+ },
1346
+ });
1347
+ ```
1348
+
1349
+ **Documentation:**
1350
+
1351
+ - [OAuth Proxy Features](docs/oauth-proxy-features.md) - Complete feature list and capabilities
1352
+ - [OAuth Proxy Implementation Guide](docs/oauth-proxy-guide.md) - Setup and configuration
1353
+ - [Python vs TypeScript Comparison](docs/oauth-python-typescript.md) - Feature comparison
1354
+
1355
+ #### OAuth Discovery Endpoints
1356
+
1357
+ FastMCP also supports OAuth discovery endpoints for direct integration with OAuth providers, supporting both **MCP Specification 2025-03-26** and **MCP Specification 2025-06-18**. This provides standard discovery endpoints that comply with RFC 8414 (OAuth 2.0 Authorization Server Metadata) and RFC 9470 (OAuth 2.0 Protected Resource Metadata):
1317
1358
 
1318
1359
  ```ts
1319
1360
  import { FastMCP, DiscoveryDocumentCache } from "fastmcp";
@@ -1413,7 +1454,18 @@ const server = new FastMCP({
1413
1454
  This configuration automatically exposes OAuth discovery endpoints:
1414
1455
 
1415
1456
  - `/.well-known/oauth-authorization-server` - Authorization server metadata (RFC 8414)
1416
- - `/.well-known/oauth-protected-resource` - Protected resource metadata (RFC 9470)
1457
+ - `/.well-known/oauth-protected-resource` - Protected resource metadata (RFC 9728)
1458
+ - `/.well-known/oauth-protected-resource<endpoint>` - Protected resource metadata at sub-path (MCP 2025-11-25)
1459
+
1460
+ **Discovery Mechanism (MCP Specification 2025-11-25):**
1461
+
1462
+ Clients discover protected resource metadata using the following search order:
1463
+
1464
+ 1. **WWW-Authenticate header** - Primary method (handled automatically by mcp-proxy)
1465
+ 2. **Sub-path well-known** - `/.well-known/oauth-protected-resource<endpoint>` (e.g., `/.well-known/oauth-protected-resource/mcp`)
1466
+ 3. **Root well-known** - `/.well-known/oauth-protected-resource` (fallback)
1467
+
1468
+ Both the sub-path and root endpoints return identical metadata, ensuring compatibility with all MCP client implementations.
1417
1469
 
1418
1470
  For JWT token validation, you can use libraries like [`get-jwks`](https://github.com/nearform/get-jwks) and [`@fastify/jwt`](https://github.com/fastify/fastify-jwt) for OAuth JWT tokens.
1419
1471
 
package/dist/FastMCP.d.ts CHANGED
@@ -9,6 +9,7 @@ import { EventEmitter } from 'events';
9
9
  import http from 'http';
10
10
  import { StrictEventEmitter } from 'strict-event-emitter-types';
11
11
  import { z } from 'zod';
12
+ import { O as OAuthProxy } from './OAuthProxy-BOCkkAhO.js';
12
13
 
13
14
  declare class DiscoveryDocumentCache {
14
15
  #private;
@@ -493,6 +494,16 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
493
494
  */
494
495
  tlsClientCertificateBoundAccessTokens?: boolean;
495
496
  };
497
+ /**
498
+ * OAuth Proxy instance for automatic OAuth flow handling.
499
+ * When provided, FastMCP will automatically register OAuth endpoints:
500
+ * - /oauth/register (DCR)
501
+ * - /oauth/authorize
502
+ * - /oauth/token
503
+ * - /oauth/callback
504
+ * - /oauth/consent
505
+ */
506
+ proxy?: OAuthProxy;
496
507
  };
497
508
  ping?: {
498
509
  /**
package/dist/FastMCP.js CHANGED
@@ -1475,7 +1475,13 @@ var FastMCP = class extends FastMCPEventEmitter {
1475
1475
  );
1476
1476
  },
1477
1477
  onUnhandledRequest: async (req, res) => {
1478
- await this.#handleUnhandledRequest(req, res, true, httpConfig.host);
1478
+ await this.#handleUnhandledRequest(
1479
+ req,
1480
+ res,
1481
+ true,
1482
+ httpConfig.host,
1483
+ httpConfig.endpoint
1484
+ );
1479
1485
  },
1480
1486
  port: httpConfig.port,
1481
1487
  stateless: true,
@@ -1521,7 +1527,8 @@ var FastMCP = class extends FastMCPEventEmitter {
1521
1527
  req,
1522
1528
  res,
1523
1529
  false,
1524
- httpConfig.host
1530
+ httpConfig.host,
1531
+ httpConfig.endpoint
1525
1532
  );
1526
1533
  },
1527
1534
  port: httpConfig.port,
@@ -1578,7 +1585,7 @@ var FastMCP = class extends FastMCPEventEmitter {
1578
1585
  /**
1579
1586
  * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
1580
1587
  */
1581
- #handleUnhandledRequest = async (req, res, isStateless = false, host) => {
1588
+ #handleUnhandledRequest = async (req, res, isStateless = false, host, streamEndpoint) => {
1582
1589
  const healthConfig = this.#options.health ?? {};
1583
1590
  const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
1584
1591
  if (enabled) {
@@ -1635,13 +1642,176 @@ var FastMCP = class extends FastMCPEventEmitter {
1635
1642
  }).end(JSON.stringify(metadata));
1636
1643
  return;
1637
1644
  }
1638
- if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) {
1639
- const metadata = convertObjectToSnakeCase(
1640
- oauthConfig.protectedResource
1641
- );
1642
- res.writeHead(200, {
1643
- "Content-Type": "application/json"
1644
- }).end(JSON.stringify(metadata));
1645
+ if (oauthConfig.protectedResource) {
1646
+ const wellKnownBase = "/.well-known/oauth-protected-resource";
1647
+ let shouldServeMetadata = false;
1648
+ if (streamEndpoint && url.pathname === `${wellKnownBase}${streamEndpoint}`) {
1649
+ shouldServeMetadata = true;
1650
+ } else if (url.pathname === wellKnownBase) {
1651
+ shouldServeMetadata = true;
1652
+ }
1653
+ if (shouldServeMetadata) {
1654
+ const metadata = convertObjectToSnakeCase(
1655
+ oauthConfig.protectedResource
1656
+ );
1657
+ res.writeHead(200, {
1658
+ "Content-Type": "application/json"
1659
+ }).end(JSON.stringify(metadata));
1660
+ return;
1661
+ }
1662
+ }
1663
+ }
1664
+ const oauthProxy = oauthConfig?.proxy;
1665
+ if (oauthProxy && oauthConfig?.enabled) {
1666
+ const url = new URL(req.url || "", `http://${host}`);
1667
+ try {
1668
+ if (req.method === "POST" && url.pathname === "/oauth/register") {
1669
+ let body = "";
1670
+ req.on("data", (chunk) => body += chunk);
1671
+ req.on("end", async () => {
1672
+ try {
1673
+ const request = JSON.parse(body);
1674
+ const response = await oauthProxy.registerClient(request);
1675
+ res.writeHead(201, { "Content-Type": "application/json" }).end(JSON.stringify(response));
1676
+ } catch (error) {
1677
+ const statusCode = error.statusCode || 400;
1678
+ res.writeHead(statusCode, { "Content-Type": "application/json" }).end(
1679
+ JSON.stringify(
1680
+ error.toJSON?.() || {
1681
+ error: "invalid_request"
1682
+ }
1683
+ )
1684
+ );
1685
+ }
1686
+ });
1687
+ return;
1688
+ }
1689
+ if (req.method === "GET" && url.pathname === "/oauth/authorize") {
1690
+ try {
1691
+ const params = Object.fromEntries(url.searchParams.entries());
1692
+ const response = await oauthProxy.authorize(
1693
+ params
1694
+ );
1695
+ const location = response.headers.get("Location");
1696
+ if (location) {
1697
+ res.writeHead(response.status, { Location: location }).end();
1698
+ } else {
1699
+ const html = await response.text();
1700
+ res.writeHead(response.status, { "Content-Type": "text/html" }).end(html);
1701
+ }
1702
+ } catch (error) {
1703
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
1704
+ JSON.stringify(
1705
+ error.toJSON?.() || {
1706
+ error: "invalid_request"
1707
+ }
1708
+ )
1709
+ );
1710
+ }
1711
+ return;
1712
+ }
1713
+ if (req.method === "GET" && url.pathname === "/oauth/callback") {
1714
+ try {
1715
+ const mockRequest = new Request(`http://${host}${req.url}`);
1716
+ const response = await oauthProxy.handleCallback(mockRequest);
1717
+ const location = response.headers.get("Location");
1718
+ if (location) {
1719
+ res.writeHead(response.status, { Location: location }).end();
1720
+ } else {
1721
+ const text = await response.text();
1722
+ res.writeHead(response.status).end(text);
1723
+ }
1724
+ } catch (error) {
1725
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
1726
+ JSON.stringify(
1727
+ error.toJSON?.() || {
1728
+ error: "server_error"
1729
+ }
1730
+ )
1731
+ );
1732
+ }
1733
+ return;
1734
+ }
1735
+ if (req.method === "POST" && url.pathname === "/oauth/consent") {
1736
+ let body = "";
1737
+ req.on("data", (chunk) => body += chunk);
1738
+ req.on("end", async () => {
1739
+ try {
1740
+ const mockRequest = new Request(`http://${host}/oauth/consent`, {
1741
+ body,
1742
+ headers: {
1743
+ "Content-Type": "application/x-www-form-urlencoded"
1744
+ },
1745
+ method: "POST"
1746
+ });
1747
+ const response = await oauthProxy.handleConsent(mockRequest);
1748
+ const location = response.headers.get("Location");
1749
+ if (location) {
1750
+ res.writeHead(response.status, { Location: location }).end();
1751
+ } else {
1752
+ const text = await response.text();
1753
+ res.writeHead(response.status).end(text);
1754
+ }
1755
+ } catch (error) {
1756
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
1757
+ JSON.stringify(
1758
+ error.toJSON?.() || {
1759
+ error: "server_error"
1760
+ }
1761
+ )
1762
+ );
1763
+ }
1764
+ });
1765
+ return;
1766
+ }
1767
+ if (req.method === "POST" && url.pathname === "/oauth/token") {
1768
+ let body = "";
1769
+ req.on("data", (chunk) => body += chunk);
1770
+ req.on("end", async () => {
1771
+ try {
1772
+ const params = new URLSearchParams(body);
1773
+ const grantType = params.get("grant_type");
1774
+ let response;
1775
+ if (grantType === "authorization_code") {
1776
+ response = await oauthProxy.exchangeAuthorizationCode({
1777
+ client_id: params.get("client_id") || "",
1778
+ client_secret: params.get("client_secret") || void 0,
1779
+ code: params.get("code") || "",
1780
+ code_verifier: params.get("code_verifier") || void 0,
1781
+ grant_type: "authorization_code",
1782
+ redirect_uri: params.get("redirect_uri") || ""
1783
+ });
1784
+ } else if (grantType === "refresh_token") {
1785
+ response = await oauthProxy.exchangeRefreshToken({
1786
+ client_id: params.get("client_id") || "",
1787
+ client_secret: params.get("client_secret") || void 0,
1788
+ grant_type: "refresh_token",
1789
+ refresh_token: params.get("refresh_token") || "",
1790
+ scope: params.get("scope") || void 0
1791
+ });
1792
+ } else {
1793
+ throw {
1794
+ statusCode: 400,
1795
+ toJSON: () => ({ error: "unsupported_grant_type" })
1796
+ };
1797
+ }
1798
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(response));
1799
+ } catch (error) {
1800
+ const statusCode = error.statusCode || 400;
1801
+ res.writeHead(statusCode, { "Content-Type": "application/json" }).end(
1802
+ JSON.stringify(
1803
+ error.toJSON?.() || {
1804
+ error: "invalid_request"
1805
+ }
1806
+ )
1807
+ );
1808
+ }
1809
+ });
1810
+ return;
1811
+ }
1812
+ } catch (error) {
1813
+ this.#logger.error("[FastMCP error] OAuth Proxy endpoint error", error);
1814
+ res.writeHead(500).end();
1645
1815
  return;
1646
1816
  }
1647
1817
  }