Skip to content

Commit b83e039

Browse files
Merge branch 'main' into migrate-to-vitest
2 parents e36d141 + 324d471 commit b83e039

File tree

4 files changed

+78
-6
lines changed

4 files changed

+78
-6
lines changed

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.21.1",
3+
"version": "1.22.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: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1596,10 +1596,81 @@ describe('OAuth Authorization', () => {
15961596
// First call should be to protected resource metadata
15971597
expect(mockFetch.mock.calls[0][0].toString()).toBe('https://resource.example.com/.well-known/oauth-protected-resource');
15981598

1599-
// Second call should be to oauth metadata
1599+
// Second call should be to oauth metadata at the root path
16001600
expect(mockFetch.mock.calls[1][0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server');
16011601
});
16021602

1603+
it('uses base URL (with root path) as authorization server when protected-resource-metadata discovery fails', async () => {
1604+
// Setup: First call to protected resource metadata fails (404)
1605+
// When no authorization_servers are found in protected resource metadata,
1606+
// the auth server URL should be set to the base URL with "/" path
1607+
let callCount = 0;
1608+
mockFetch.mockImplementation(url => {
1609+
callCount++;
1610+
1611+
const urlString = url.toString();
1612+
1613+
if (urlString.includes('/.well-known/oauth-protected-resource')) {
1614+
// Protected resource metadata discovery attempts (both path-aware and root) fail with 404
1615+
return Promise.resolve({
1616+
ok: false,
1617+
status: 404
1618+
});
1619+
} else if (urlString === 'https://resource.example.com/.well-known/oauth-authorization-server') {
1620+
// Should fetch from base URL with root path, not the full serverUrl path
1621+
return Promise.resolve({
1622+
ok: true,
1623+
status: 200,
1624+
json: async () => ({
1625+
issuer: 'https://resource.example.com/',
1626+
authorization_endpoint: 'https://resource.example.com/authorize',
1627+
token_endpoint: 'https://resource.example.com/token',
1628+
registration_endpoint: 'https://resource.example.com/register',
1629+
response_types_supported: ['code'],
1630+
code_challenge_methods_supported: ['S256']
1631+
})
1632+
});
1633+
} else if (urlString.includes('/register')) {
1634+
// Client registration succeeds
1635+
return Promise.resolve({
1636+
ok: true,
1637+
status: 200,
1638+
json: async () => ({
1639+
client_id: 'test-client-id',
1640+
client_secret: 'test-client-secret',
1641+
client_id_issued_at: 1612137600,
1642+
client_secret_expires_at: 1612224000,
1643+
redirect_uris: ['http://localhost:3000/callback'],
1644+
client_name: 'Test Client'
1645+
})
1646+
});
1647+
}
1648+
1649+
return Promise.reject(new Error(`Unexpected fetch call #${callCount}: ${urlString}`));
1650+
});
1651+
1652+
// Mock provider methods
1653+
(mockProvider.clientInformation as jest.Mock).mockResolvedValue(undefined);
1654+
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
1655+
mockProvider.saveClientInformation = jest.fn();
1656+
1657+
// Call the auth function with a server URL that has a path
1658+
const result = await auth(mockProvider, {
1659+
serverUrl: 'https://resource.example.com/path/to/server'
1660+
});
1661+
1662+
// Verify the result
1663+
expect(result).toBe('REDIRECT');
1664+
1665+
// Verify that the oauth-authorization-server call uses the base URL
1666+
// This proves the fix: using new URL("/", serverUrl) instead of serverUrl
1667+
const authServerCall = mockFetch.mock.calls.find(call =>
1668+
call[0].toString().includes('/.well-known/oauth-authorization-server')
1669+
);
1670+
expect(authServerCall).toBeDefined();
1671+
expect(authServerCall[0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server');
1672+
});
1673+
16031674
it('passes resource parameter through authorization flow', async () => {
16041675
// Mock successful metadata discovery - need to include protected resource metadata
16051676
mockFetch.mockImplementation(url => {

src/client/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ async function authInternal(
348348
): Promise<AuthResult> {
349349
let resourceMetadata: OAuthProtectedResourceMetadata | undefined;
350350
let authorizationServerUrl: string | URL | undefined;
351+
351352
try {
352353
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
353354
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
@@ -359,10 +360,10 @@ async function authInternal(
359360

360361
/**
361362
* If we don't get a valid authorization server metadata from protected resource metadata,
362-
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
363+
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server base URL acts as the Authorization server.
363364
*/
364365
if (!authorizationServerUrl) {
365-
authorizationServerUrl = serverUrl;
366+
authorizationServerUrl = new URL('/', serverUrl);
366367
}
367368

368369
const resource: URL | undefined = await selectResourceURL(serverUrl, provider, resourceMetadata);

0 commit comments

Comments
 (0)