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 +55 -3
- package/dist/FastMCP.d.ts +11 -0
- package/dist/FastMCP.js +180 -10
- package/dist/FastMCP.js.map +1 -1
- package/dist/OAuthProxy-BOCkkAhO.d.ts +519 -0
- package/dist/auth/index.d.ts +445 -0
- package/dist/auth/index.js +1844 -0
- package/dist/auth/index.js.map +1 -0
- package/package.json +21 -2
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
|
|
1314
|
+
#### OAuth Proxy
|
|
1315
1315
|
|
|
1316
|
-
FastMCP includes built-in
|
|
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
|
|
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(
|
|
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 (
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
)
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
}
|