Skip to content

Commit 3bb5f3e

Browse files
committed
chore: rebased
2 parents be90ac5 + 0d54517 commit 3bb5f3e

File tree

13 files changed

+822
-382
lines changed

13 files changed

+822
-382
lines changed

.github/CODEOWNERS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# TypeScript SDK Code Owners
2+
3+
# Default owners for everything in the repo
4+
* @modelcontextprotocol/typescript-sdk-maintainers
5+
6+
# Auth team owns all auth-related code
7+
/src/server/auth/ @modelcontextprotocol/typescript-sdk-auth
8+
/src/client/auth* @modelcontextprotocol/typescript-sdk-auth
9+
/src/shared/auth* @modelcontextprotocol/typescript-sdk-auth
10+
/src/examples/client/simpleOAuthClient.ts @modelcontextprotocol/typescript-sdk-auth
11+
/src/examples/server/demoInMemoryOAuthProvider.ts @modelcontextprotocol/typescript-sdk-auth

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.15.1",
3+
"version": "1.16.0",
44
"description": "Model Context Protocol implementation for TypeScript",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",

src/client/auth.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,35 @@ describe("OAuth Authorization", () => {
347347
const [url] = calls[0];
348348
expect(url.toString()).toBe("https://custom.example.com/metadata");
349349
});
350+
351+
it("supports overriding the fetch function used for requests", async () => {
352+
const validMetadata = {
353+
resource: "https://resource.example.com",
354+
authorization_servers: ["https://auth.example.com"],
355+
};
356+
357+
const customFetch = jest.fn().mockResolvedValue({
358+
ok: true,
359+
status: 200,
360+
json: async () => validMetadata,
361+
});
362+
363+
const metadata = await discoverOAuthProtectedResourceMetadata(
364+
"https://resource.example.com",
365+
undefined,
366+
customFetch
367+
);
368+
369+
expect(metadata).toEqual(validMetadata);
370+
expect(customFetch).toHaveBeenCalledTimes(1);
371+
expect(mockFetch).not.toHaveBeenCalled();
372+
373+
const [url, options] = customFetch.mock.calls[0];
374+
expect(url.toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
375+
expect(options.headers).toEqual({
376+
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
377+
});
378+
});
350379
});
351380

352381
describe("discoverOAuthMetadata", () => {
@@ -695,6 +724,39 @@ describe("OAuth Authorization", () => {
695724
discoverOAuthMetadata("https://auth.example.com")
696725
).rejects.toThrow();
697726
});
727+
728+
it("supports overriding the fetch function used for requests", async () => {
729+
const validMetadata = {
730+
issuer: "https://auth.example.com",
731+
authorization_endpoint: "https://auth.example.com/authorize",
732+
token_endpoint: "https://auth.example.com/token",
733+
registration_endpoint: "https://auth.example.com/register",
734+
response_types_supported: ["code"],
735+
code_challenge_methods_supported: ["S256"],
736+
};
737+
738+
const customFetch = jest.fn().mockResolvedValue({
739+
ok: true,
740+
status: 200,
741+
json: async () => validMetadata,
742+
});
743+
744+
const metadata = await discoverOAuthMetadata(
745+
"https://auth.example.com",
746+
{},
747+
customFetch
748+
);
749+
750+
expect(metadata).toEqual(validMetadata);
751+
expect(customFetch).toHaveBeenCalledTimes(1);
752+
expect(mockFetch).not.toHaveBeenCalled();
753+
754+
const [url, options] = customFetch.mock.calls[0];
755+
expect(url.toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
756+
expect(options.headers).toEqual({
757+
"MCP-Protocol-Version": LATEST_PROTOCOL_VERSION
758+
});
759+
});
698760
});
699761

700762
describe("startAuthorization", () => {
@@ -993,6 +1055,46 @@ describe("OAuth Authorization", () => {
9931055
})
9941056
).rejects.toThrow("Token exchange failed");
9951057
});
1058+
1059+
it("supports overriding the fetch function used for requests", async () => {
1060+
const customFetch = jest.fn().mockResolvedValue({
1061+
ok: true,
1062+
status: 200,
1063+
json: async () => validTokens,
1064+
});
1065+
1066+
const tokens = await exchangeAuthorization("https://auth.example.com", {
1067+
clientInformation: validClientInfo,
1068+
authorizationCode: "code123",
1069+
codeVerifier: "verifier123",
1070+
redirectUri: "http://localhost:3000/callback",
1071+
resource: new URL("https://api.example.com/mcp-server"),
1072+
fetchFn: customFetch,
1073+
});
1074+
1075+
expect(tokens).toEqual(validTokens);
1076+
expect(customFetch).toHaveBeenCalledTimes(1);
1077+
expect(mockFetch).not.toHaveBeenCalled();
1078+
1079+
const [url, options] = customFetch.mock.calls[0];
1080+
expect(url.toString()).toBe("https://auth.example.com/token");
1081+
expect(options).toEqual(
1082+
expect.objectContaining({
1083+
method: "POST",
1084+
headers: expect.any(Headers),
1085+
body: expect.any(URLSearchParams),
1086+
})
1087+
);
1088+
1089+
const body = options.body as URLSearchParams;
1090+
expect(body.get("grant_type")).toBe("authorization_code");
1091+
expect(body.get("code")).toBe("code123");
1092+
expect(body.get("code_verifier")).toBe("verifier123");
1093+
expect(body.get("client_id")).toBe("client123");
1094+
expect(body.get("client_secret")).toBe("secret123");
1095+
expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback");
1096+
expect(body.get("resource")).toBe("https://api.example.com/mcp-server");
1097+
});
9961098
});
9971099

9981100
describe("refreshAuthorization", () => {
@@ -1900,6 +2002,68 @@ describe("OAuth Authorization", () => {
19002002
// Second call should be to AS metadata with the path from authorization server
19012003
expect(calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server/oauth");
19022004
});
2005+
2006+
it("supports overriding the fetch function used for requests", async () => {
2007+
const customFetch = jest.fn();
2008+
2009+
// Mock PRM discovery
2010+
customFetch.mockResolvedValueOnce({
2011+
ok: true,
2012+
status: 200,
2013+
json: async () => ({
2014+
resource: "https://resource.example.com",
2015+
authorization_servers: ["https://auth.example.com"],
2016+
}),
2017+
});
2018+
2019+
// Mock AS metadata discovery
2020+
customFetch.mockResolvedValueOnce({
2021+
ok: true,
2022+
status: 200,
2023+
json: async () => ({
2024+
issuer: "https://auth.example.com",
2025+
authorization_endpoint: "https://auth.example.com/authorize",
2026+
token_endpoint: "https://auth.example.com/token",
2027+
registration_endpoint: "https://auth.example.com/register",
2028+
response_types_supported: ["code"],
2029+
code_challenge_methods_supported: ["S256"],
2030+
}),
2031+
});
2032+
2033+
const mockProvider: OAuthClientProvider = {
2034+
get redirectUrl() { return "http://localhost:3000/callback"; },
2035+
get clientMetadata() {
2036+
return {
2037+
client_name: "Test Client",
2038+
redirect_uris: ["http://localhost:3000/callback"],
2039+
};
2040+
},
2041+
clientInformation: jest.fn().mockResolvedValue({
2042+
client_id: "client123",
2043+
client_secret: "secret123",
2044+
}),
2045+
tokens: jest.fn().mockResolvedValue(undefined),
2046+
saveTokens: jest.fn(),
2047+
redirectToAuthorization: jest.fn(),
2048+
saveCodeVerifier: jest.fn(),
2049+
codeVerifier: jest.fn().mockResolvedValue("verifier123"),
2050+
};
2051+
2052+
const result = await auth(mockProvider, {
2053+
serverUrl: "https://resource.example.com",
2054+
fetchFn: customFetch,
2055+
});
2056+
2057+
expect(result).toBe("REDIRECT");
2058+
expect(customFetch).toHaveBeenCalledTimes(2);
2059+
expect(mockFetch).not.toHaveBeenCalled();
2060+
2061+
// Verify custom fetch was called for PRM discovery
2062+
expect(customFetch.mock.calls[0][0].toString()).toBe("https://resource.example.com/.well-known/oauth-protected-resource");
2063+
2064+
// Verify custom fetch was called for AS metadata discovery
2065+
expect(customFetch.mock.calls[1][0].toString()).toBe("https://auth.example.com/.well-known/oauth-authorization-server");
2066+
});
19032067
});
19042068

19052069
describe("exchangeAuthorization with multiple client authentication methods", () => {

0 commit comments

Comments
 (0)