fastmcp 3.11.0 → 3.13.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 +47 -0
- package/dist/FastMCP.d.ts +132 -4
- package/dist/FastMCP.js +167 -103
- package/dist/FastMCP.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/FastMCP.oauth.test.ts +48 -0
- package/src/FastMCP.test.ts +112 -0
- package/src/FastMCP.ts +373 -141
- package/src/examples/oauth-server.ts +12 -0
package/src/FastMCP.ts
CHANGED
|
@@ -624,20 +624,162 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
|
|
|
624
624
|
enabled: boolean;
|
|
625
625
|
|
|
626
626
|
/**
|
|
627
|
-
* OAuth Protected Resource metadata for
|
|
627
|
+
* OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`
|
|
628
628
|
*
|
|
629
|
-
* This endpoint follows
|
|
630
|
-
* and provides metadata
|
|
629
|
+
* This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}
|
|
630
|
+
* and provides metadata describing how an OAuth 2.0 protected resource (in this case,
|
|
631
|
+
* an MCP server) expects to be accessed.
|
|
631
632
|
*
|
|
632
|
-
*
|
|
633
|
+
* When configured, FastMCP will automatically serve this metadata at the
|
|
634
|
+
* `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`
|
|
635
|
+
* fields are required. All others are optional and will be omitted from the published
|
|
636
|
+
* metadata if not specified.
|
|
637
|
+
*
|
|
638
|
+
* This satisfies the requirements of the MCP Authorization specification's
|
|
639
|
+
* {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.
|
|
640
|
+
*
|
|
641
|
+
* Clients consuming this metadata MUST validate that any presented values comply with
|
|
642
|
+
* RFC 9728, including strict validation of the `resource` identifier and intended audience
|
|
643
|
+
* when access tokens are issued and presented (per RFC 8707 §2).
|
|
644
|
+
*
|
|
645
|
+
* @remarks Required by MCP Specification version 2025-06-18
|
|
633
646
|
*/
|
|
634
647
|
protectedResource?: {
|
|
648
|
+
/**
|
|
649
|
+
* Allows for additional metadata fields beyond those defined in RFC 9728.
|
|
650
|
+
*
|
|
651
|
+
* @remarks This supports vendor-specific or experimental extensions.
|
|
652
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}
|
|
653
|
+
*/
|
|
654
|
+
[key: string]: unknown;
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Supported values for the `authorization_details` parameter (RFC 9396).
|
|
658
|
+
*
|
|
659
|
+
* @remarks Used when fine-grained access control is in play.
|
|
660
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}
|
|
661
|
+
*/
|
|
662
|
+
authorizationDetailsTypesSupported?: string[];
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* List of OAuth 2.0 authorization server issuer identifiers.
|
|
666
|
+
*
|
|
667
|
+
* These correspond to ASes that can issue access tokens for this protected resource.
|
|
668
|
+
* MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`
|
|
669
|
+
* metadata for initiating the OAuth flow.
|
|
670
|
+
*
|
|
671
|
+
* @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.
|
|
672
|
+
* Clients are responsible for choosing among them (see RFC 9728 §7.6).
|
|
673
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}
|
|
674
|
+
*/
|
|
635
675
|
authorizationServers: string[];
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* List of supported methods for presenting OAuth 2.0 bearer tokens.
|
|
679
|
+
*
|
|
680
|
+
* @remarks Valid values are `header`, `body`, and `query`.
|
|
681
|
+
* If omitted, clients MAY assume only `header` is supported, per RFC 6750.
|
|
682
|
+
* This is a client-side interpretation and not a serialization default.
|
|
683
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}
|
|
684
|
+
*/
|
|
636
685
|
bearerMethodsSupported?: string[];
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Whether this resource requires all access tokens to be DPoP-bound.
|
|
689
|
+
*
|
|
690
|
+
* @remarks If omitted, clients SHOULD assume this is `false`.
|
|
691
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}
|
|
692
|
+
*/
|
|
693
|
+
dpopBoundAccessTokensRequired?: boolean;
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Supported algorithms for verifying DPoP proofs (RFC 9449).
|
|
697
|
+
*
|
|
698
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}
|
|
699
|
+
*/
|
|
700
|
+
dpopSigningAlgValuesSupported?: string[];
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* JWKS URI of this resource. Used to validate access tokens or sign responses.
|
|
704
|
+
*
|
|
705
|
+
* @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).
|
|
706
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}
|
|
707
|
+
*/
|
|
637
708
|
jwksUri?: string;
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Canonical OAuth resource identifier for this protected resource (the MCP server).
|
|
712
|
+
*
|
|
713
|
+
* @remarks Typically the base URL of the MCP server. Clients MUST use this as the
|
|
714
|
+
* `resource` parameter in authorization and token requests (per RFC 8707).
|
|
715
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}
|
|
716
|
+
*/
|
|
638
717
|
resource: string;
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* URL to developer-accessible documentation for this resource.
|
|
721
|
+
*
|
|
722
|
+
* @remarks This field MAY be localized.
|
|
723
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}
|
|
724
|
+
*/
|
|
639
725
|
resourceDocumentation?: string;
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Human-readable name for display purposes (e.g., in UIs).
|
|
729
|
+
*
|
|
730
|
+
* @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).
|
|
731
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}
|
|
732
|
+
*/
|
|
733
|
+
resourceName?: string;
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* URL to a human-readable policy page describing acceptable use.
|
|
737
|
+
*
|
|
738
|
+
* @remarks This field MAY be localized.
|
|
739
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}
|
|
740
|
+
*/
|
|
640
741
|
resourcePolicyUri?: string;
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Supported JWS algorithms for signed responses from this resource (e.g., response signing).
|
|
745
|
+
*
|
|
746
|
+
* @remarks MUST NOT include `none`.
|
|
747
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}
|
|
748
|
+
*/
|
|
749
|
+
resourceSigningAlgValuesSupported?: string[];
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* URL to the protected resource’s Terms of Service.
|
|
753
|
+
*
|
|
754
|
+
* @remarks This field MAY be localized.
|
|
755
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}
|
|
756
|
+
*/
|
|
757
|
+
resourceTosUri?: string;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Supported OAuth scopes for requesting access to this resource.
|
|
761
|
+
*
|
|
762
|
+
* @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.
|
|
763
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}
|
|
764
|
+
*/
|
|
765
|
+
scopesSupported?: string[];
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Developer-accessible documentation for how to use the service (not end-user docs).
|
|
769
|
+
*
|
|
770
|
+
* @remarks Semantically equivalent to `resourceDocumentation`, but included under its
|
|
771
|
+
* alternate name for compatibility with tools or schemas expecting either.
|
|
772
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}
|
|
773
|
+
*/
|
|
774
|
+
serviceDocumentation?: string;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Whether mutual-TLS-bound access tokens are required.
|
|
778
|
+
*
|
|
779
|
+
* @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).
|
|
780
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}
|
|
781
|
+
*/
|
|
782
|
+
tlsClientCertificateBoundAccessTokens?: boolean;
|
|
641
783
|
};
|
|
642
784
|
};
|
|
643
785
|
|
|
@@ -1451,7 +1593,11 @@ export class FastMCPSession<
|
|
|
1451
1593
|
"[FastMCP debug] listRoots method not supported by client",
|
|
1452
1594
|
);
|
|
1453
1595
|
} else {
|
|
1454
|
-
console.error(
|
|
1596
|
+
console.error(
|
|
1597
|
+
`[FastMCP error] received error listing roots.\n\n${
|
|
1598
|
+
error instanceof Error ? error.stack : JSON.stringify(error)
|
|
1599
|
+
}`,
|
|
1600
|
+
);
|
|
1455
1601
|
}
|
|
1456
1602
|
});
|
|
1457
1603
|
},
|
|
@@ -1872,6 +2018,7 @@ export class FastMCP<
|
|
|
1872
2018
|
endpoint?: `/${string}`;
|
|
1873
2019
|
eventStore?: EventStore;
|
|
1874
2020
|
port: number;
|
|
2021
|
+
stateless?: boolean;
|
|
1875
2022
|
};
|
|
1876
2023
|
transportType: "httpStream" | "stdio";
|
|
1877
2024
|
}>,
|
|
@@ -1904,151 +2051,86 @@ export class FastMCP<
|
|
|
1904
2051
|
} else if (config.transportType === "httpStream") {
|
|
1905
2052
|
const httpConfig = config.httpStream;
|
|
1906
2053
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
auth = await this.#authenticate(request);
|
|
1913
|
-
}
|
|
1914
|
-
const allowedTools = auth
|
|
1915
|
-
? this.#tools.filter((tool) =>
|
|
1916
|
-
tool.canAccess ? tool.canAccess(auth) : true,
|
|
1917
|
-
)
|
|
1918
|
-
: this.#tools;
|
|
1919
|
-
return new FastMCPSession<T>({
|
|
1920
|
-
auth,
|
|
1921
|
-
name: this.#options.name,
|
|
1922
|
-
ping: this.#options.ping,
|
|
1923
|
-
prompts: this.#prompts,
|
|
1924
|
-
resources: this.#resources,
|
|
1925
|
-
resourcesTemplates: this.#resourcesTemplates,
|
|
1926
|
-
roots: this.#options.roots,
|
|
1927
|
-
tools: allowedTools,
|
|
1928
|
-
transportType: "httpStream",
|
|
1929
|
-
utils: this.#options.utils,
|
|
1930
|
-
version: this.#options.version,
|
|
1931
|
-
});
|
|
1932
|
-
},
|
|
1933
|
-
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
1934
|
-
eventStore: httpConfig.eventStore,
|
|
1935
|
-
onClose: async (session) => {
|
|
1936
|
-
this.emit("disconnect", {
|
|
1937
|
-
session: session as FastMCPSession<FastMCPSessionAuth>,
|
|
1938
|
-
});
|
|
1939
|
-
},
|
|
1940
|
-
onConnect: async (session) => {
|
|
1941
|
-
this.#sessions.push(session);
|
|
1942
|
-
|
|
1943
|
-
console.info(`[FastMCP info] HTTP Stream session established`);
|
|
1944
|
-
|
|
1945
|
-
this.emit("connect", {
|
|
1946
|
-
session: session as FastMCPSession<FastMCPSessionAuth>,
|
|
1947
|
-
});
|
|
1948
|
-
},
|
|
1949
|
-
|
|
1950
|
-
onUnhandledRequest: async (req, res) => {
|
|
1951
|
-
const healthConfig = this.#options.health ?? {};
|
|
2054
|
+
if (httpConfig.stateless) {
|
|
2055
|
+
// Stateless mode - create new server instance for each request
|
|
2056
|
+
console.info(
|
|
2057
|
+
`[FastMCP info] Starting server in stateless mode on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
|
|
2058
|
+
);
|
|
1952
2059
|
|
|
1953
|
-
|
|
1954
|
-
|
|
2060
|
+
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2061
|
+
createServer: async (request) => {
|
|
2062
|
+
let auth: T | undefined;
|
|
1955
2063
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
2064
|
+
if (this.#authenticate) {
|
|
2065
|
+
auth = await this.#authenticate(request);
|
|
2066
|
+
}
|
|
1959
2067
|
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2068
|
+
// In stateless mode, create a new session for each request
|
|
2069
|
+
// without persisting it in the sessions array
|
|
2070
|
+
return this.#createSession(auth);
|
|
2071
|
+
},
|
|
2072
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
2073
|
+
eventStore: httpConfig.eventStore,
|
|
2074
|
+
// In stateless mode, we don't track sessions
|
|
2075
|
+
onClose: async () => {
|
|
2076
|
+
// No session tracking in stateless mode
|
|
2077
|
+
},
|
|
2078
|
+
onConnect: async () => {
|
|
2079
|
+
// No persistent session tracking in stateless mode
|
|
2080
|
+
console.debug(
|
|
2081
|
+
`[FastMCP debug] Stateless HTTP Stream request handled`,
|
|
2082
|
+
);
|
|
2083
|
+
},
|
|
2084
|
+
onUnhandledRequest: async (req, res) => {
|
|
2085
|
+
await this.#handleUnhandledRequest(req, res, true);
|
|
2086
|
+
},
|
|
2087
|
+
port: httpConfig.port,
|
|
2088
|
+
stateless: true,
|
|
2089
|
+
streamEndpoint: httpConfig.endpoint,
|
|
2090
|
+
});
|
|
2091
|
+
} else {
|
|
2092
|
+
// Regular mode with session management
|
|
2093
|
+
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2094
|
+
createServer: async (request) => {
|
|
2095
|
+
let auth: T | undefined;
|
|
1970
2096
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
const readySessions = this.#sessions.filter(
|
|
1974
|
-
(s) => s.isReady,
|
|
1975
|
-
).length;
|
|
1976
|
-
const totalSessions = this.#sessions.length;
|
|
1977
|
-
const allReady =
|
|
1978
|
-
readySessions === totalSessions && totalSessions > 0;
|
|
1979
|
-
|
|
1980
|
-
const response = {
|
|
1981
|
-
ready: readySessions,
|
|
1982
|
-
status: allReady
|
|
1983
|
-
? "ready"
|
|
1984
|
-
: totalSessions === 0
|
|
1985
|
-
? "no_sessions"
|
|
1986
|
-
: "initializing",
|
|
1987
|
-
total: totalSessions,
|
|
1988
|
-
};
|
|
1989
|
-
|
|
1990
|
-
res
|
|
1991
|
-
.writeHead(allReady ? 200 : 503, {
|
|
1992
|
-
"Content-Type": "application/json",
|
|
1993
|
-
})
|
|
1994
|
-
.end(JSON.stringify(response));
|
|
1995
|
-
|
|
1996
|
-
return;
|
|
1997
|
-
}
|
|
1998
|
-
} catch (error) {
|
|
1999
|
-
console.error("[FastMCP error] health endpoint error", error);
|
|
2097
|
+
if (this.#authenticate) {
|
|
2098
|
+
auth = await this.#authenticate(request);
|
|
2000
2099
|
}
|
|
2001
|
-
}
|
|
2002
2100
|
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
);
|
|
2015
|
-
res
|
|
2016
|
-
.writeHead(200, {
|
|
2017
|
-
"Content-Type": "application/json",
|
|
2018
|
-
})
|
|
2019
|
-
.end(JSON.stringify(metadata));
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2101
|
+
return this.#createSession(auth);
|
|
2102
|
+
},
|
|
2103
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
2104
|
+
eventStore: httpConfig.eventStore,
|
|
2105
|
+
onClose: async (session) => {
|
|
2106
|
+
this.emit("disconnect", {
|
|
2107
|
+
session: session as FastMCPSession<FastMCPSessionAuth>,
|
|
2108
|
+
});
|
|
2109
|
+
},
|
|
2110
|
+
onConnect: async (session) => {
|
|
2111
|
+
this.#sessions.push(session);
|
|
2022
2112
|
|
|
2023
|
-
|
|
2024
|
-
url.pathname === "/.well-known/oauth-protected-resource" &&
|
|
2025
|
-
oauthConfig.protectedResource
|
|
2026
|
-
) {
|
|
2027
|
-
const metadata = convertObjectToSnakeCase(
|
|
2028
|
-
oauthConfig.protectedResource,
|
|
2029
|
-
);
|
|
2030
|
-
res
|
|
2031
|
-
.writeHead(200, {
|
|
2032
|
-
"Content-Type": "application/json",
|
|
2033
|
-
})
|
|
2034
|
-
.end(JSON.stringify(metadata));
|
|
2035
|
-
return;
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2113
|
+
console.info(`[FastMCP info] HTTP Stream session established`);
|
|
2038
2114
|
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
streamEndpoint: httpConfig.endpoint,
|
|
2044
|
-
});
|
|
2115
|
+
this.emit("connect", {
|
|
2116
|
+
session: session as FastMCPSession<FastMCPSessionAuth>,
|
|
2117
|
+
});
|
|
2118
|
+
},
|
|
2045
2119
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2120
|
+
onUnhandledRequest: async (req, res) => {
|
|
2121
|
+
await this.#handleUnhandledRequest(req, res, false);
|
|
2122
|
+
},
|
|
2123
|
+
port: httpConfig.port,
|
|
2124
|
+
streamEndpoint: httpConfig.endpoint,
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
console.info(
|
|
2128
|
+
`[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
|
|
2129
|
+
);
|
|
2130
|
+
console.info(
|
|
2131
|
+
`[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`,
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2052
2134
|
} else {
|
|
2053
2135
|
throw new Error("Invalid transport type");
|
|
2054
2136
|
}
|
|
@@ -2063,12 +2145,153 @@ export class FastMCP<
|
|
|
2063
2145
|
}
|
|
2064
2146
|
}
|
|
2065
2147
|
|
|
2148
|
+
/**
|
|
2149
|
+
* Creates a new FastMCPSession instance with the current configuration.
|
|
2150
|
+
* Used both for regular sessions and stateless requests.
|
|
2151
|
+
*/
|
|
2152
|
+
#createSession(auth?: T): FastMCPSession<T> {
|
|
2153
|
+
const allowedTools = auth
|
|
2154
|
+
? this.#tools.filter((tool) =>
|
|
2155
|
+
tool.canAccess ? tool.canAccess(auth) : true,
|
|
2156
|
+
)
|
|
2157
|
+
: this.#tools;
|
|
2158
|
+
return new FastMCPSession<T>({
|
|
2159
|
+
auth,
|
|
2160
|
+
name: this.#options.name,
|
|
2161
|
+
ping: this.#options.ping,
|
|
2162
|
+
prompts: this.#prompts,
|
|
2163
|
+
resources: this.#resources,
|
|
2164
|
+
resourcesTemplates: this.#resourcesTemplates,
|
|
2165
|
+
roots: this.#options.roots,
|
|
2166
|
+
tools: allowedTools,
|
|
2167
|
+
transportType: "httpStream",
|
|
2168
|
+
utils: this.#options.utils,
|
|
2169
|
+
version: this.#options.version,
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
|
|
2175
|
+
*/
|
|
2176
|
+
#handleUnhandledRequest = async (
|
|
2177
|
+
req: http.IncomingMessage,
|
|
2178
|
+
res: http.ServerResponse,
|
|
2179
|
+
isStateless = false,
|
|
2180
|
+
) => {
|
|
2181
|
+
const healthConfig = this.#options.health ?? {};
|
|
2182
|
+
|
|
2183
|
+
const enabled =
|
|
2184
|
+
healthConfig.enabled === undefined ? true : healthConfig.enabled;
|
|
2185
|
+
|
|
2186
|
+
if (enabled) {
|
|
2187
|
+
const path = healthConfig.path ?? "/health";
|
|
2188
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
2189
|
+
|
|
2190
|
+
try {
|
|
2191
|
+
if (req.method === "GET" && url.pathname === path) {
|
|
2192
|
+
res
|
|
2193
|
+
.writeHead(healthConfig.status ?? 200, {
|
|
2194
|
+
"Content-Type": "text/plain",
|
|
2195
|
+
})
|
|
2196
|
+
.end(healthConfig.message ?? "✓ Ok");
|
|
2197
|
+
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// Enhanced readiness check endpoint
|
|
2202
|
+
if (req.method === "GET" && url.pathname === "/ready") {
|
|
2203
|
+
if (isStateless) {
|
|
2204
|
+
// In stateless mode, we're always ready if the server is running
|
|
2205
|
+
const response = {
|
|
2206
|
+
mode: "stateless",
|
|
2207
|
+
ready: 1,
|
|
2208
|
+
status: "ready",
|
|
2209
|
+
total: 1,
|
|
2210
|
+
};
|
|
2211
|
+
|
|
2212
|
+
res
|
|
2213
|
+
.writeHead(200, {
|
|
2214
|
+
"Content-Type": "application/json",
|
|
2215
|
+
})
|
|
2216
|
+
.end(JSON.stringify(response));
|
|
2217
|
+
} else {
|
|
2218
|
+
const readySessions = this.#sessions.filter(
|
|
2219
|
+
(s) => s.isReady,
|
|
2220
|
+
).length;
|
|
2221
|
+
const totalSessions = this.#sessions.length;
|
|
2222
|
+
const allReady =
|
|
2223
|
+
readySessions === totalSessions && totalSessions > 0;
|
|
2224
|
+
|
|
2225
|
+
const response = {
|
|
2226
|
+
ready: readySessions,
|
|
2227
|
+
status: allReady
|
|
2228
|
+
? "ready"
|
|
2229
|
+
: totalSessions === 0
|
|
2230
|
+
? "no_sessions"
|
|
2231
|
+
: "initializing",
|
|
2232
|
+
total: totalSessions,
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
res
|
|
2236
|
+
.writeHead(allReady ? 200 : 503, {
|
|
2237
|
+
"Content-Type": "application/json",
|
|
2238
|
+
})
|
|
2239
|
+
.end(JSON.stringify(response));
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
console.error("[FastMCP error] health endpoint error", error);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// Handle OAuth well-known endpoints
|
|
2250
|
+
const oauthConfig = this.#options.oauth;
|
|
2251
|
+
if (oauthConfig?.enabled && req.method === "GET") {
|
|
2252
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
2253
|
+
|
|
2254
|
+
if (
|
|
2255
|
+
url.pathname === "/.well-known/oauth-authorization-server" &&
|
|
2256
|
+
oauthConfig.authorizationServer
|
|
2257
|
+
) {
|
|
2258
|
+
const metadata = convertObjectToSnakeCase(
|
|
2259
|
+
oauthConfig.authorizationServer,
|
|
2260
|
+
);
|
|
2261
|
+
res
|
|
2262
|
+
.writeHead(200, {
|
|
2263
|
+
"Content-Type": "application/json",
|
|
2264
|
+
})
|
|
2265
|
+
.end(JSON.stringify(metadata));
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
if (
|
|
2270
|
+
url.pathname === "/.well-known/oauth-protected-resource" &&
|
|
2271
|
+
oauthConfig.protectedResource
|
|
2272
|
+
) {
|
|
2273
|
+
const metadata = convertObjectToSnakeCase(
|
|
2274
|
+
oauthConfig.protectedResource,
|
|
2275
|
+
);
|
|
2276
|
+
res
|
|
2277
|
+
.writeHead(200, {
|
|
2278
|
+
"Content-Type": "application/json",
|
|
2279
|
+
})
|
|
2280
|
+
.end(JSON.stringify(metadata));
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// If the request was not handled above, return 404
|
|
2286
|
+
res.writeHead(404).end();
|
|
2287
|
+
};
|
|
2066
2288
|
#parseRuntimeConfig(
|
|
2067
2289
|
overrides?: Partial<{
|
|
2068
2290
|
httpStream: {
|
|
2069
2291
|
enableJsonResponse?: boolean;
|
|
2070
2292
|
endpoint?: `/${string}`;
|
|
2071
2293
|
port: number;
|
|
2294
|
+
stateless?: boolean;
|
|
2072
2295
|
};
|
|
2073
2296
|
transportType: "httpStream" | "stdio";
|
|
2074
2297
|
}>,
|
|
@@ -2079,6 +2302,7 @@ export class FastMCP<
|
|
|
2079
2302
|
endpoint: `/${string}`;
|
|
2080
2303
|
eventStore?: EventStore;
|
|
2081
2304
|
port: number;
|
|
2305
|
+
stateless?: boolean;
|
|
2082
2306
|
};
|
|
2083
2307
|
transportType: "httpStream";
|
|
2084
2308
|
}
|
|
@@ -2095,10 +2319,12 @@ export class FastMCP<
|
|
|
2095
2319
|
const transportArg = getArg("transport");
|
|
2096
2320
|
const portArg = getArg("port");
|
|
2097
2321
|
const endpointArg = getArg("endpoint");
|
|
2322
|
+
const statelessArg = getArg("stateless");
|
|
2098
2323
|
|
|
2099
2324
|
const envTransport = process.env.FASTMCP_TRANSPORT;
|
|
2100
2325
|
const envPort = process.env.FASTMCP_PORT;
|
|
2101
2326
|
const envEndpoint = process.env.FASTMCP_ENDPOINT;
|
|
2327
|
+
const envStateless = process.env.FASTMCP_STATELESS;
|
|
2102
2328
|
|
|
2103
2329
|
// Overrides > CLI > env > defaults
|
|
2104
2330
|
const transportType =
|
|
@@ -2115,12 +2341,18 @@ export class FastMCP<
|
|
|
2115
2341
|
overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
|
|
2116
2342
|
const enableJsonResponse =
|
|
2117
2343
|
overrides?.httpStream?.enableJsonResponse || false;
|
|
2344
|
+
const stateless =
|
|
2345
|
+
overrides?.httpStream?.stateless ||
|
|
2346
|
+
statelessArg === "true" ||
|
|
2347
|
+
envStateless === "true" ||
|
|
2348
|
+
false;
|
|
2118
2349
|
|
|
2119
2350
|
return {
|
|
2120
2351
|
httpStream: {
|
|
2121
2352
|
enableJsonResponse,
|
|
2122
2353
|
endpoint: endpoint as `/${string}`,
|
|
2123
2354
|
port,
|
|
2355
|
+
stateless,
|
|
2124
2356
|
},
|
|
2125
2357
|
transportType: "httpStream" as const,
|
|
2126
2358
|
};
|
|
@@ -46,12 +46,24 @@ const server = new FastMCP({
|
|
|
46
46
|
},
|
|
47
47
|
enabled: true,
|
|
48
48
|
protectedResource: {
|
|
49
|
+
authorizationDetailsTypesSupported: [
|
|
50
|
+
"payment_initiation",
|
|
51
|
+
"account_access",
|
|
52
|
+
],
|
|
49
53
|
authorizationServers: ["https://auth.example.com"],
|
|
50
54
|
bearerMethodsSupported: ["header"],
|
|
55
|
+
dpopBoundAccessTokensRequired: false,
|
|
56
|
+
dpopSigningAlgValuesSupported: ["ES256", "RS256"],
|
|
51
57
|
jwksUri: "https://oauth-example-server.example.com/.well-known/jwks.json",
|
|
52
58
|
resource: "mcp://oauth-example-server",
|
|
53
59
|
resourceDocumentation: "https://docs.example.com/mcp-api",
|
|
60
|
+
resourceName: "OAuth Example API",
|
|
54
61
|
resourcePolicyUri: "https://example.com/resource-policy",
|
|
62
|
+
resourceSigningAlgValuesSupported: ["RS256", "ES256"],
|
|
63
|
+
resourceTosUri: "https://example.com/terms-of-service",
|
|
64
|
+
scopesSupported: ["read", "write", "admin"],
|
|
65
|
+
serviceDocumentation: "https://developer.example.com/api-docs",
|
|
66
|
+
tlsClientCertificateBoundAccessTokens: false,
|
|
55
67
|
},
|
|
56
68
|
},
|
|
57
69
|
version: "1.0.0",
|