Skip to content

Commit ca5e805

Browse files
authored
Merge branch 'main' into pcarleton/token-endpoint-auth-tests
2 parents 005f1a8 + e4cb55e commit ca5e805

File tree

14 files changed

+1272
-585
lines changed

14 files changed

+1272
-585
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
runs-on: ubuntu-latest
1919

2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v6
2222
- uses: actions/setup-node@v4
2323
with:
2424
node-version: 24
@@ -40,7 +40,7 @@ jobs:
4040
id-token: write
4141

4242
steps:
43-
- uses: actions/checkout@v4
43+
- uses: actions/checkout@v6
4444
- uses: actions/setup-node@v4
4545
with:
4646
node-version: 24

.github/workflows/pr-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515

1616
steps:
17-
- uses: actions/checkout@v4
17+
- uses: actions/checkout@v6
1818
- uses: actions/setup-node@v4
1919
with:
2020
node-version: 24

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ A framework for testing MCP (Model Context Protocol) client and server implement
99
### Testing Clients
1010

1111
```bash
12-
npx @modelcontextprotocol/conformance client --command "tsx examples/clients/typescript/test1.ts" --scenario initialize
12+
# Using the everything-client (recommended)
13+
npx @modelcontextprotocol/conformance client --command "tsx examples/clients/typescript/everything-client.ts" --scenario initialize
14+
15+
# Run an entire suite of tests
16+
npx @modelcontextprotocol/conformance client --command "tsx examples/clients/typescript/everything-client.ts" --suite auth
1317
```
1418

1519
### Testing Servers
@@ -59,10 +63,11 @@ npx @modelcontextprotocol/conformance client --command "<client-command>" --scen
5963

6064
- `--command` - The command to run your MCP client (can include flags)
6165
- `--scenario` - The test scenario to run (e.g., "initialize")
66+
- `--suite` - Run a suite of tests in parallel (e.g., "auth")
6267
- `--timeout` - Timeout in milliseconds (default: 30000)
6368
- `--verbose` - Show verbose output
6469

65-
The framework appends the server URL as the final argument to your command.
70+
The framework appends `<server-url>` as an argument to your command and sets the `MCP_CONFORMANCE_SCENARIO` environment variable to the scenario name. For scenarios that require additional context (e.g., client credentials), the `MCP_CONFORMANCE_CONTEXT` environment variable contains a JSON object with scenario-specific data.
6671

6772
### Server Testing
6873

@@ -89,8 +94,9 @@ npx @modelcontextprotocol/conformance server --url <url> [--scenario <scenario>]
8994

9095
## Example Clients
9196

92-
- `examples/clients/typescript/test1.ts` - Valid MCP client (passes all checks)
93-
- `examples/clients/typescript/test-broken.ts` - Invalid client missing required fields (fails checks)
97+
- `examples/clients/typescript/everything-client.ts` - Single client that handles all scenarios based on scenario name (recommended)
98+
- `examples/clients/typescript/test1.ts` - Simple MCP client (for reference)
99+
- `examples/clients/typescript/auth-test.ts` - Well-behaved OAuth client (for reference)
94100

95101
## Available Scenarios
96102

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Everything client - a single conformance test client that handles all scenarios.
5+
*
6+
* Usage: everything-client <server-url>
7+
*
8+
* The scenario name is read from the MCP_CONFORMANCE_SCENARIO environment variable,
9+
* which is set by the conformance test runner.
10+
*
11+
* This client routes to the appropriate behavior based on the scenario name,
12+
* consolidating all the individual test clients into one.
13+
*/
14+
15+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
16+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
17+
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
18+
import { withOAuthRetry } from './helpers/withOAuthRetry.js';
19+
import { logger } from './helpers/logger.js';
20+
21+
// Scenario handler type
22+
type ScenarioHandler = (serverUrl: string) => Promise<void>;
23+
24+
// Registry of scenario handlers
25+
const scenarioHandlers: Record<string, ScenarioHandler> = {};
26+
27+
// Helper to register a scenario handler
28+
function registerScenario(name: string, handler: ScenarioHandler): void {
29+
scenarioHandlers[name] = handler;
30+
}
31+
32+
// Helper to register multiple scenarios with the same handler
33+
function registerScenarios(names: string[], handler: ScenarioHandler): void {
34+
for (const name of names) {
35+
scenarioHandlers[name] = handler;
36+
}
37+
}
38+
39+
// ============================================================================
40+
// Basic scenarios (initialize, tools-call)
41+
// ============================================================================
42+
43+
async function runBasicClient(serverUrl: string): Promise<void> {
44+
const client = new Client(
45+
{ name: 'test-client', version: '1.0.0' },
46+
{ capabilities: {} }
47+
);
48+
49+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
50+
51+
await client.connect(transport);
52+
logger.debug('Successfully connected to MCP server');
53+
54+
await client.listTools();
55+
logger.debug('Successfully listed tools');
56+
57+
await transport.close();
58+
logger.debug('Connection closed successfully');
59+
}
60+
61+
registerScenarios(['initialize', 'tools-call'], runBasicClient);
62+
63+
// ============================================================================
64+
// Auth scenarios - well-behaved client
65+
// ============================================================================
66+
67+
async function runAuthClient(serverUrl: string): Promise<void> {
68+
const client = new Client(
69+
{ name: 'test-auth-client', version: '1.0.0' },
70+
{ capabilities: {} }
71+
);
72+
73+
const oauthFetch = withOAuthRetry(
74+
'test-auth-client',
75+
new URL(serverUrl)
76+
)(fetch);
77+
78+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
79+
fetch: oauthFetch
80+
});
81+
82+
await client.connect(transport);
83+
logger.debug('Successfully connected to MCP server');
84+
85+
await client.listTools();
86+
logger.debug('Successfully listed tools');
87+
88+
await client.callTool({ name: 'test-tool', arguments: {} });
89+
logger.debug('Successfully called tool');
90+
91+
await transport.close();
92+
logger.debug('Connection closed successfully');
93+
}
94+
95+
// Register all auth scenarios that should use the well-behaved auth client
96+
registerScenarios(
97+
[
98+
'auth/basic-dcr',
99+
'auth/basic-metadata-var1',
100+
'auth/basic-metadata-var2',
101+
'auth/basic-metadata-var3',
102+
'auth/2025-03-26-oauth-metadata-backcompat',
103+
'auth/2025-03-26-oauth-endpoint-fallback',
104+
'auth/scope-from-www-authenticate',
105+
'auth/scope-from-scopes-supported',
106+
'auth/scope-omitted-when-undefined',
107+
'auth/scope-step-up'
108+
],
109+
runAuthClient
110+
);
111+
112+
// ============================================================================
113+
// Elicitation defaults scenario
114+
// ============================================================================
115+
116+
async function runElicitationDefaultsClient(serverUrl: string): Promise<void> {
117+
const client = new Client(
118+
{ name: 'elicitation-defaults-test-client', version: '1.0.0' },
119+
{
120+
capabilities: {
121+
elicitation: {
122+
applyDefaults: true
123+
}
124+
}
125+
}
126+
);
127+
128+
// Register elicitation handler that returns empty content
129+
// The SDK should fill in defaults for all omitted fields
130+
client.setRequestHandler(ElicitRequestSchema, async (request) => {
131+
logger.debug(
132+
'Received elicitation request:',
133+
JSON.stringify(request.params, null, 2)
134+
);
135+
logger.debug('Accepting with empty content - SDK should apply defaults');
136+
137+
// Return empty content - SDK should merge in defaults
138+
return {
139+
action: 'accept' as const,
140+
content: {}
141+
};
142+
});
143+
144+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
145+
146+
await client.connect(transport);
147+
logger.debug('Successfully connected to MCP server');
148+
149+
// List available tools
150+
const tools = await client.listTools();
151+
logger.debug(
152+
'Available tools:',
153+
tools.tools.map((t) => t.name)
154+
);
155+
156+
// Call the test tool which will trigger elicitation
157+
const testTool = tools.tools.find(
158+
(t) => t.name === 'test_client_elicitation_defaults'
159+
);
160+
if (!testTool) {
161+
throw new Error('Test tool not found: test_client_elicitation_defaults');
162+
}
163+
164+
logger.debug('Calling test_client_elicitation_defaults tool...');
165+
const result = await client.callTool({
166+
name: 'test_client_elicitation_defaults',
167+
arguments: {}
168+
});
169+
170+
logger.debug('Tool result:', JSON.stringify(result, null, 2));
171+
172+
await transport.close();
173+
logger.debug('Connection closed successfully');
174+
}
175+
176+
registerScenario('elicitation-defaults', runElicitationDefaultsClient);
177+
178+
// ============================================================================
179+
// Main entry point
180+
// ============================================================================
181+
182+
async function main(): Promise<void> {
183+
const scenarioName = process.env.MCP_CONFORMANCE_SCENARIO;
184+
const serverUrl = process.argv[2];
185+
186+
if (!scenarioName || !serverUrl) {
187+
console.error(
188+
'Usage: MCP_CONFORMANCE_SCENARIO=<scenario> everything-client <server-url>'
189+
);
190+
console.error(
191+
'\nThe MCP_CONFORMANCE_SCENARIO env var is set automatically by the conformance runner.'
192+
);
193+
console.error('\nAvailable scenarios:');
194+
for (const name of Object.keys(scenarioHandlers).sort()) {
195+
console.error(` - ${name}`);
196+
}
197+
process.exit(1);
198+
}
199+
200+
const handler = scenarioHandlers[scenarioName];
201+
if (!handler) {
202+
console.error(`Unknown scenario: ${scenarioName}`);
203+
console.error('\nAvailable scenarios:');
204+
for (const name of Object.keys(scenarioHandlers).sort()) {
205+
console.error(` - ${name}`);
206+
}
207+
process.exit(1);
208+
}
209+
210+
try {
211+
await handler(serverUrl);
212+
process.exit(0);
213+
} catch (error) {
214+
console.error('Error:', error);
215+
process.exit(1);
216+
}
217+
}
218+
219+
main().catch((error) => {
220+
console.error('Unhandled error:', error);
221+
process.exit(1);
222+
});

0 commit comments

Comments
 (0)