fastmcp 3.23.1 → 3.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
@@ -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";
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
@@ -1645,6 +1645,160 @@ var FastMCP = class extends FastMCPEventEmitter {
1645
1645
  return;
1646
1646
  }
1647
1647
  }
1648
+ const oauthProxy = oauthConfig?.proxy;
1649
+ if (oauthProxy && oauthConfig?.enabled) {
1650
+ const url = new URL(req.url || "", `http://${host}`);
1651
+ try {
1652
+ if (req.method === "POST" && url.pathname === "/oauth/register") {
1653
+ let body = "";
1654
+ req.on("data", (chunk) => body += chunk);
1655
+ req.on("end", async () => {
1656
+ try {
1657
+ const request = JSON.parse(body);
1658
+ const response = await oauthProxy.registerClient(request);
1659
+ res.writeHead(201, { "Content-Type": "application/json" }).end(JSON.stringify(response));
1660
+ } catch (error) {
1661
+ const statusCode = error.statusCode || 400;
1662
+ res.writeHead(statusCode, { "Content-Type": "application/json" }).end(
1663
+ JSON.stringify(
1664
+ error.toJSON?.() || {
1665
+ error: "invalid_request"
1666
+ }
1667
+ )
1668
+ );
1669
+ }
1670
+ });
1671
+ return;
1672
+ }
1673
+ if (req.method === "GET" && url.pathname === "/oauth/authorize") {
1674
+ try {
1675
+ const params = Object.fromEntries(url.searchParams.entries());
1676
+ const response = await oauthProxy.authorize(
1677
+ params
1678
+ );
1679
+ const location = response.headers.get("Location");
1680
+ if (location) {
1681
+ res.writeHead(response.status, { Location: location }).end();
1682
+ } else {
1683
+ const html = await response.text();
1684
+ res.writeHead(response.status, { "Content-Type": "text/html" }).end(html);
1685
+ }
1686
+ } catch (error) {
1687
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
1688
+ JSON.stringify(
1689
+ error.toJSON?.() || {
1690
+ error: "invalid_request"
1691
+ }
1692
+ )
1693
+ );
1694
+ }
1695
+ return;
1696
+ }
1697
+ if (req.method === "GET" && url.pathname === "/oauth/callback") {
1698
+ try {
1699
+ const mockRequest = new Request(`http://${host}${req.url}`);
1700
+ const response = await oauthProxy.handleCallback(mockRequest);
1701
+ const location = response.headers.get("Location");
1702
+ if (location) {
1703
+ res.writeHead(response.status, { Location: location }).end();
1704
+ } else {
1705
+ const text = await response.text();
1706
+ res.writeHead(response.status).end(text);
1707
+ }
1708
+ } catch (error) {
1709
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
1710
+ JSON.stringify(
1711
+ error.toJSON?.() || {
1712
+ error: "server_error"
1713
+ }
1714
+ )
1715
+ );
1716
+ }
1717
+ return;
1718
+ }
1719
+ if (req.method === "POST" && url.pathname === "/oauth/consent") {
1720
+ let body = "";
1721
+ req.on("data", (chunk) => body += chunk);
1722
+ req.on("end", async () => {
1723
+ try {
1724
+ const mockRequest = new Request(`http://${host}/oauth/consent`, {
1725
+ body,
1726
+ headers: {
1727
+ "Content-Type": "application/x-www-form-urlencoded"
1728
+ },
1729
+ method: "POST"
1730
+ });
1731
+ const response = await oauthProxy.handleConsent(mockRequest);
1732
+ const location = response.headers.get("Location");
1733
+ if (location) {
1734
+ res.writeHead(response.status, { Location: location }).end();
1735
+ } else {
1736
+ const text = await response.text();
1737
+ res.writeHead(response.status).end(text);
1738
+ }
1739
+ } catch (error) {
1740
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
1741
+ JSON.stringify(
1742
+ error.toJSON?.() || {
1743
+ error: "server_error"
1744
+ }
1745
+ )
1746
+ );
1747
+ }
1748
+ });
1749
+ return;
1750
+ }
1751
+ if (req.method === "POST" && url.pathname === "/oauth/token") {
1752
+ let body = "";
1753
+ req.on("data", (chunk) => body += chunk);
1754
+ req.on("end", async () => {
1755
+ try {
1756
+ const params = new URLSearchParams(body);
1757
+ const grantType = params.get("grant_type");
1758
+ let response;
1759
+ if (grantType === "authorization_code") {
1760
+ response = await oauthProxy.exchangeAuthorizationCode({
1761
+ client_id: params.get("client_id") || "",
1762
+ client_secret: params.get("client_secret") || void 0,
1763
+ code: params.get("code") || "",
1764
+ code_verifier: params.get("code_verifier") || void 0,
1765
+ grant_type: "authorization_code",
1766
+ redirect_uri: params.get("redirect_uri") || ""
1767
+ });
1768
+ } else if (grantType === "refresh_token") {
1769
+ response = await oauthProxy.exchangeRefreshToken({
1770
+ client_id: params.get("client_id") || "",
1771
+ client_secret: params.get("client_secret") || void 0,
1772
+ grant_type: "refresh_token",
1773
+ refresh_token: params.get("refresh_token") || "",
1774
+ scope: params.get("scope") || void 0
1775
+ });
1776
+ } else {
1777
+ throw {
1778
+ statusCode: 400,
1779
+ toJSON: () => ({ error: "unsupported_grant_type" })
1780
+ };
1781
+ }
1782
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(response));
1783
+ } catch (error) {
1784
+ const statusCode = error.statusCode || 400;
1785
+ res.writeHead(statusCode, { "Content-Type": "application/json" }).end(
1786
+ JSON.stringify(
1787
+ error.toJSON?.() || {
1788
+ error: "invalid_request"
1789
+ }
1790
+ )
1791
+ );
1792
+ }
1793
+ });
1794
+ return;
1795
+ }
1796
+ } catch (error) {
1797
+ this.#logger.error("[FastMCP error] OAuth Proxy endpoint error", error);
1798
+ res.writeHead(500).end();
1799
+ return;
1800
+ }
1801
+ }
1648
1802
  res.writeHead(404).end();
1649
1803
  };
1650
1804
  #parseRuntimeConfig(overrides) {