Skip to content

Commit 3a87236

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/vitest-4.0.13
2 parents 6a17566 + be51f87 commit 3a87236

File tree

3 files changed

+240
-5
lines changed

3 files changed

+240
-5
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 `<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+
});

src/runner/client.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface ClientExecutionResult {
1414

1515
async function executeClient(
1616
command: string,
17+
scenarioName: string,
1718
serverUrl: string,
1819
timeout: number = 30000,
1920
context?: Record<string, unknown>
@@ -26,8 +27,13 @@ async function executeClient(
2627
let stderr = '';
2728
let timedOut = false;
2829

29-
// Build environment with optional context
30+
// Build environment with scenario name and optional context.
31+
// We use separate env vars rather than putting scenario in context because:
32+
// 1. Scenario is always set, context is only set when there's scenario-specific data
33+
// 2. Simpler to read a string vs parsing JSON just to get the scenario name
34+
// 3. Semantic separation: scenario identifies "which test", context provides "test data"
3035
const env = { ...process.env };
36+
env.MCP_CONFORMANCE_SCENARIO = scenarioName;
3137
if (context) {
3238
env.MCP_CONFORMANCE_CONTEXT = JSON.stringify(context);
3339
}
@@ -105,6 +111,7 @@ export async function runConformanceTest(
105111
try {
106112
const clientOutput = await executeClient(
107113
clientCommand,
114+
scenarioName,
108115
urls.serverUrl,
109116
timeout,
110117
urls.context

0 commit comments

Comments
 (0)