Skip to content

Commit b6ca339

Browse files
committed
Merge main into pcarleton/move-metadata-around
2 parents 44397a2 + e5cdffe commit b6ca339

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3605
-546
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Enforce LF line endings for all text files
2+
* text=auto eol=lf
3+

.github/dependabot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
3+
updates:
4+
- package-ecosystem: npm
5+
directory: /
6+
schedule:
7+
interval: weekly
8+
cooldown:
9+
default-days: 7
10+
11+
- package-ecosystem: github-actions
12+
directory: /
13+
schedule:
14+
interval: weekly
15+
cooldown:
16+
default-days: 7

.prettierrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"endOfLine": "lf",
23
"singleQuote": true,
34
"trailingComma": "none",
45
"overrides": [

examples/clients/typescript/auth-test-bad-prm.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import {
77
UnauthorizedError
88
} from '@modelcontextprotocol/sdk/client/auth.js';
99
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
10-
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
11-
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
12-
import { runAsCli } from './helpers/cliRunner.js';
13-
import { logger } from './helpers/logger.js';
10+
import { withOAuthRetry } from './helpers/withOAuthRetry';
11+
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
12+
import { runAsCli } from './helpers/cliRunner';
13+
import { logger } from './helpers/logger';
1414

1515
/**
1616
* Broken client that always uses root-based PRM discovery.

examples/clients/typescript/auth-test-ignore-403.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
UnauthorizedError
99
} from '@modelcontextprotocol/sdk/client/auth.js';
1010
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
11-
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
12-
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
13-
import { runAsCli } from './helpers/cliRunner.js';
14-
import { logger } from './helpers/logger.js';
11+
import { withOAuthRetry } from './helpers/withOAuthRetry';
12+
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
13+
import { runAsCli } from './helpers/cliRunner';
14+
import { logger } from './helpers/logger';
1515

1616
/**
1717
* Broken client that only responds to 401, not 403.

examples/clients/typescript/auth-test-ignore-scope.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
UnauthorizedError
99
} from '@modelcontextprotocol/sdk/client/auth.js';
1010
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
11-
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
12-
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
13-
import { runAsCli } from './helpers/cliRunner.js';
14-
import { logger } from './helpers/logger.js';
11+
import { withOAuthRetry } from './helpers/withOAuthRetry';
12+
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
13+
import { runAsCli } from './helpers/cliRunner';
14+
import { logger } from './helpers/logger';
1515

1616
/**
1717
* Broken client that ignores the scope from WWW-Authenticate header.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env node
2+
3+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5+
import { withOAuthRetry } from './helpers/withOAuthRetry';
6+
import { runAsCli } from './helpers/cliRunner';
7+
import { logger } from './helpers/logger';
8+
9+
/**
10+
* Non-compliant client that doesn't use CIMD (Client ID Metadata Document).
11+
*
12+
* This client intentionally omits the clientMetadataUrl parameter when the server
13+
* advertises client_id_metadata_document_supported=true. A compliant client should
14+
* use CIMD when the server supports it, but this client falls back to DCR (Dynamic
15+
* Client Registration) instead.
16+
*
17+
* Used to test that conformance checks detect clients that don't properly
18+
* implement CIMD support.
19+
*/
20+
export async function runClient(serverUrl: string): Promise<void> {
21+
const client = new Client(
22+
{ name: 'test-auth-client-no-cimd', version: '1.0.0' },
23+
{ capabilities: {} }
24+
);
25+
26+
// Non-compliant: omitting clientMetadataUrl causes fallback to DCR
27+
// A compliant client would pass a clientMetadataUrl here when the server
28+
// advertises client_id_metadata_document_supported=true
29+
const oauthFetch = withOAuthRetry(
30+
'test-auth-client-no-cimd',
31+
new URL(serverUrl)
32+
)(fetch);
33+
34+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
35+
fetch: oauthFetch
36+
});
37+
38+
await client.connect(transport);
39+
logger.debug('Connected to MCP server (without CIMD)');
40+
41+
await client.listTools();
42+
logger.debug('Successfully listed tools');
43+
44+
await client.callTool({ name: 'test-tool', arguments: {} });
45+
logger.debug('Successfully called tool');
46+
47+
await transport.close();
48+
logger.debug('Connection closed successfully');
49+
}
50+
51+
runAsCli(runClient, import.meta.url, 'auth-test-no-cimd <server-url>');
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env node
2+
3+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5+
import {
6+
auth,
7+
extractWWWAuthenticateParams,
8+
UnauthorizedError
9+
} from '@modelcontextprotocol/sdk/client/auth.js';
10+
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
11+
import type { Middleware } from '@modelcontextprotocol/sdk/client/middleware.js';
12+
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
13+
import { runAsCli } from './helpers/cliRunner';
14+
import { logger } from './helpers/logger';
15+
16+
/**
17+
* Broken client that retries auth infinitely without any retry limit.
18+
* BUG: Does not implement retry limits, causing infinite auth loops.
19+
*/
20+
21+
const withOAuthRetryNoLimit = (
22+
clientName: string,
23+
baseUrl?: string | URL
24+
): Middleware => {
25+
const provider = new ConformanceOAuthProvider(
26+
'http://localhost:3000/callback',
27+
{
28+
client_name: clientName,
29+
redirect_uris: ['http://localhost:3000/callback']
30+
}
31+
);
32+
33+
return (next: FetchLike) => {
34+
return async (
35+
input: string | URL,
36+
init?: RequestInit
37+
): Promise<Response> => {
38+
const makeRequest = async (): Promise<Response> => {
39+
const headers = new Headers(init?.headers);
40+
const tokens = await provider.tokens();
41+
if (tokens) {
42+
headers.set('Authorization', `Bearer ${tokens.access_token}`);
43+
}
44+
return await next(input, { ...init, headers });
45+
};
46+
47+
let response = await makeRequest();
48+
49+
// BUG: No retry limit - keeps retrying on every 401/403
50+
while (response.status === 401 || response.status === 403) {
51+
const serverUrl =
52+
baseUrl ||
53+
(typeof input === 'string' ? new URL(input).origin : input.origin);
54+
55+
const { resourceMetadataUrl, scope } =
56+
extractWWWAuthenticateParams(response);
57+
let result = await auth(provider, {
58+
serverUrl,
59+
resourceMetadataUrl,
60+
scope,
61+
fetchFn: next
62+
});
63+
64+
if (result === 'REDIRECT') {
65+
const authorizationCode = await provider.getAuthCode();
66+
result = await auth(provider, {
67+
serverUrl,
68+
resourceMetadataUrl,
69+
scope,
70+
authorizationCode,
71+
fetchFn: next
72+
});
73+
if (result !== 'AUTHORIZED') {
74+
throw new UnauthorizedError(
75+
`Authentication failed with result: ${result}`
76+
);
77+
}
78+
}
79+
80+
response = await makeRequest();
81+
}
82+
83+
return response;
84+
};
85+
};
86+
};
87+
88+
export async function runClient(serverUrl: string): Promise<void> {
89+
const client = new Client(
90+
{ name: 'test-auth-client-no-retry-limit', version: '1.0.0' },
91+
{ capabilities: {} }
92+
);
93+
94+
const oauthFetch = withOAuthRetryNoLimit(
95+
'test-auth-client-no-retry-limit',
96+
new URL(serverUrl)
97+
)(fetch);
98+
99+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
100+
fetch: oauthFetch
101+
});
102+
103+
await client.connect(transport);
104+
logger.debug('✅ Successfully connected to MCP server');
105+
106+
await client.listTools();
107+
logger.debug('✅ Successfully listed tools');
108+
109+
await transport.close();
110+
logger.debug('✅ Connection closed successfully');
111+
}
112+
113+
runAsCli(runClient, import.meta.url, 'auth-test-no-retry-limit <server-url>');

examples/clients/typescript/auth-test-partial-scopes.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
UnauthorizedError
99
} from '@modelcontextprotocol/sdk/client/auth.js';
1010
import type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js';
11-
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
12-
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider.js';
13-
import { runAsCli } from './helpers/cliRunner.js';
14-
import { logger } from './helpers/logger.js';
11+
import { withOAuthRetry } from './helpers/withOAuthRetry';
12+
import { ConformanceOAuthProvider } from './helpers/ConformanceOAuthProvider';
13+
import { runAsCli } from './helpers/cliRunner';
14+
import { logger } from './helpers/logger';
1515

1616
/**
1717
* Broken client that only requests a subset of scopes.

examples/clients/typescript/auth-test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
44
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5-
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
6-
import { runAsCli } from './helpers/cliRunner.js';
7-
import { logger } from './helpers/logger.js';
5+
import { withOAuthRetry, handle401 } from './helpers/withOAuthRetry';
6+
import { runAsCli } from './helpers/cliRunner';
7+
import { logger } from './helpers/logger';
8+
9+
/**
10+
* Fixed client metadata URL for CIMD conformance tests.
11+
* When server supports client_id_metadata_document_supported, this URL
12+
* will be used as the client_id instead of doing dynamic registration.
13+
*/
14+
const CIMD_CLIENT_METADATA_URL =
15+
'https://conformance-test.local/client-metadata.json';
816

917
/**
1018
* Well-behaved auth client that follows all OAuth protocols correctly.
@@ -17,7 +25,9 @@ export async function runClient(serverUrl: string): Promise<void> {
1725

1826
const oauthFetch = withOAuthRetry(
1927
'test-auth-client',
20-
new URL(serverUrl)
28+
new URL(serverUrl),
29+
handle401,
30+
CIMD_CLIENT_METADATA_URL
2131
)(fetch);
2232

2333
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {

0 commit comments

Comments
 (0)