Skip to content

Commit a8d1868

Browse files
pcarletonclaude
andcommitted
Refactor client testing to pass scenario name and server URL
The CLI now passes both scenario name and server URL as arguments to the client command (instead of just the server URL). This enables a single "everything" client that can handle all scenarios based on the scenario name it receives. Changes: - Update executeClient() to pass scenarioName as first argument to client - Add everything-client.ts that routes to appropriate behavior based on scenario name - Update README with new usage examples and documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f0e9481 commit a8d1868

File tree

3 files changed

+230
-6
lines changed

3 files changed

+230
-6
lines changed

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

src/runner/client.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ export interface ClientExecutionResult {
1414

1515
async function executeClient(
1616
command: string,
17+
scenarioName: string,
1718
serverUrl: string,
1819
timeout: number = 30000
1920
): Promise<ClientExecutionResult> {
2021
const commandParts = command.split(' ');
2122
const executable = commandParts[0];
22-
const args = [...commandParts.slice(1), serverUrl];
23+
const args = [...commandParts.slice(1), scenarioName, serverUrl];
2324

2425
let stdout = '';
2526
let stderr = '';
@@ -89,11 +90,14 @@ export async function runConformanceTest(
8990
console.error(`Starting scenario: ${scenarioName}`);
9091
const urls = await scenario.start();
9192

92-
console.error(`Executing client: ${clientCommand} ${urls.serverUrl}`);
93+
console.error(
94+
`Executing client: ${clientCommand} ${scenarioName} ${urls.serverUrl}`
95+
);
9396

9497
try {
9598
const clientOutput = await executeClient(
9699
clientCommand,
100+
scenarioName,
97101
urls.serverUrl,
98102
timeout
99103
);

0 commit comments

Comments
 (0)