Skip to content

Commit 389a7fa

Browse files
committed
Align auth-test with MCP_CONFORMANCE_SCENARIO env var pattern
Update auth-test.ts and testClient.ts to use MCP_CONFORMANCE_SCENARIO env var instead of scenario in context object. This aligns with PR #54.
1 parent e4cb55e commit 389a7fa

File tree

2 files changed

+164
-23
lines changed

2 files changed

+164
-23
lines changed

examples/clients/typescript/auth-test.ts

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
44
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5+
import {
6+
ClientCredentialsProvider,
7+
PrivateKeyJwtProvider
8+
} from '@modelcontextprotocol/sdk/client/auth-extensions.js';
9+
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
510
import { withOAuthRetry, handle401 } from './helpers/withOAuthRetry';
611
import { runAsCli } from './helpers/cliRunner';
712
import { logger } from './helpers/logger';
@@ -15,36 +20,140 @@ const CIMD_CLIENT_METADATA_URL =
1520
'https://conformance-test.local/client-metadata.json';
1621

1722
/**
18-
* Well-behaved auth client that follows all OAuth protocols correctly.
23+
* Context passed from the conformance test framework via MCP_CONFORMANCE_CONTEXT env var.
24+
*
25+
* WARNING: This schema is unstable and subject to change.
26+
* Currently only used for client credentials scenarios.
27+
* See: https://github.com/modelcontextprotocol/conformance/issues/51
1928
*/
20-
export async function runClient(serverUrl: string): Promise<void> {
29+
interface ConformanceContext {
30+
client_id?: string;
31+
// For JWT auth (private_key_jwt)
32+
private_key_pem?: string;
33+
signing_algorithm?: string;
34+
// For basic auth (client_secret_basic)
35+
client_secret?: string;
36+
}
37+
38+
function getContext(
39+
passedContext?: Record<string, unknown>
40+
): ConformanceContext {
41+
if (passedContext) {
42+
return passedContext as ConformanceContext;
43+
}
44+
const contextJson = process.env.MCP_CONFORMANCE_CONTEXT;
45+
if (!contextJson) {
46+
// Context is optional - only needed for client credentials scenarios
47+
return {};
48+
}
49+
return JSON.parse(contextJson);
50+
}
51+
52+
/**
53+
* Create an OAuth provider based on the scenario type.
54+
*/
55+
function createProviderForScenario(
56+
scenario: string,
57+
context: ConformanceContext
58+
): OAuthClientProvider | undefined {
59+
// Client credentials scenarios use the dedicated provider classes
60+
if (scenario === 'auth/client-credentials-jwt') {
61+
if (
62+
!context.client_id ||
63+
!context.private_key_pem ||
64+
!context.signing_algorithm
65+
) {
66+
throw new Error(
67+
'auth/client-credentials-jwt requires client_id, private_key_pem, and signing_algorithm in context'
68+
);
69+
}
70+
return new PrivateKeyJwtProvider({
71+
clientId: context.client_id,
72+
privateKey: context.private_key_pem,
73+
algorithm: context.signing_algorithm,
74+
clientName: 'conformance-client-credentials'
75+
});
76+
}
77+
78+
if (scenario === 'auth/client-credentials-basic') {
79+
if (!context.client_id || !context.client_secret) {
80+
throw new Error(
81+
'auth/client-credentials-basic requires client_id and client_secret in context'
82+
);
83+
}
84+
return new ClientCredentialsProvider({
85+
clientId: context.client_id,
86+
clientSecret: context.client_secret,
87+
clientName: 'conformance-client-credentials'
88+
});
89+
}
90+
91+
// For authorization code flow scenarios, return undefined to use withOAuthRetry
92+
return undefined;
93+
}
94+
95+
/**
96+
* Auth client that handles both authorization code flow and client credentials flow
97+
* based on the scenario name in the conformance context.
98+
*/
99+
export async function runClient(
100+
serverUrl: string,
101+
passedContext?: Record<string, unknown>
102+
): Promise<void> {
103+
const scenario = process.env.MCP_CONFORMANCE_SCENARIO;
104+
if (!scenario) {
105+
throw new Error(
106+
'MCP_CONFORMANCE_SCENARIO environment variable is required'
107+
);
108+
}
109+
110+
const context = getContext(passedContext);
111+
logger.debug('Scenario:', scenario);
112+
logger.debug('Parsed context:', JSON.stringify(context, null, 2));
113+
21114
const client = new Client(
22115
{ name: 'test-auth-client', version: '1.0.0' },
23116
{ capabilities: {} }
24117
);
25118

26-
const oauthFetch = withOAuthRetry(
27-
'test-auth-client',
28-
new URL(serverUrl),
29-
handle401,
30-
CIMD_CLIENT_METADATA_URL
31-
)(fetch);
119+
// Check if this is a client credentials scenario
120+
const clientCredentialsProvider = createProviderForScenario(
121+
scenario,
122+
context
123+
);
124+
125+
let transport: StreamableHTTPClientTransport;
126+
127+
if (clientCredentialsProvider) {
128+
// Client credentials flow - use the provider directly
129+
transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
130+
authProvider: clientCredentialsProvider
131+
});
132+
} else {
133+
// Authorization code flow - use withOAuthRetry middleware
134+
const oauthFetch = withOAuthRetry(
135+
'test-auth-client',
136+
new URL(serverUrl),
137+
handle401,
138+
CIMD_CLIENT_METADATA_URL
139+
)(fetch);
32140

33-
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
34-
fetch: oauthFetch
35-
});
141+
transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
142+
fetch: oauthFetch
143+
});
144+
}
36145

37146
await client.connect(transport);
38-
logger.debug('Successfully connected to MCP server');
147+
logger.debug('Successfully connected to MCP server');
39148

40149
await client.listTools();
41-
logger.debug('Successfully listed tools');
150+
logger.debug('Successfully listed tools');
42151

43152
await client.callTool({ name: 'test-tool', arguments: {} });
44-
logger.debug('Successfully called tool');
153+
logger.debug('Successfully called tool');
45154

46155
await transport.close();
47-
logger.debug('Connection closed successfully');
156+
logger.debug('Connection closed successfully');
48157
}
49158

50159
runAsCli(runClient, import.meta.url, 'auth-test <server-url>');

src/scenarios/client/auth/test_helpers/testClient.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ export interface ClientRunner {
1111
/**
1212
* Run the client against the given server URL.
1313
* Should reject if the client fails.
14+
* @param serverUrl The MCP server URL to connect to
15+
* @param context Optional context object passed via MCP_CONFORMANCE_CONTEXT env var
1416
*/
15-
run(serverUrl: string): Promise<void>;
17+
run(serverUrl: string, context?: Record<string, unknown>): Promise<void>;
1618
}
1719

1820
/**
@@ -21,10 +23,19 @@ export interface ClientRunner {
2123
export class SpawnedClientRunner implements ClientRunner {
2224
constructor(private clientPath: string) {}
2325

24-
async run(serverUrl: string): Promise<void> {
26+
async run(
27+
serverUrl: string,
28+
context?: Record<string, unknown>
29+
): Promise<void> {
2530
await new Promise<void>((resolve, reject) => {
31+
const env = { ...process.env };
32+
if (context) {
33+
env.MCP_CONFORMANCE_CONTEXT = JSON.stringify(context);
34+
}
35+
2636
const clientProcess = spawn('npx', ['tsx', this.clientPath, serverUrl], {
27-
stdio: ['ignore', 'pipe', 'pipe']
37+
stdio: ['ignore', 'pipe', 'pipe'],
38+
env
2839
});
2940

3041
let stdout = '';
@@ -76,10 +87,18 @@ export class SpawnedClientRunner implements ClientRunner {
7687
* Client runner that executes a client function inline without spawning a shell.
7788
*/
7889
export class InlineClientRunner implements ClientRunner {
79-
constructor(private clientFn: (serverUrl: string) => Promise<void>) {}
80-
81-
async run(serverUrl: string): Promise<void> {
82-
await this.clientFn(serverUrl);
90+
constructor(
91+
private clientFn: (
92+
serverUrl: string,
93+
context?: Record<string, unknown>
94+
) => Promise<void>
95+
) {}
96+
97+
async run(
98+
serverUrl: string,
99+
context?: Record<string, unknown>
100+
): Promise<void> {
101+
await this.clientFn(serverUrl, context);
83102
}
84103
}
85104

@@ -106,10 +125,17 @@ export async function runClientAgainstScenario(
106125
const urls = await scenario.start();
107126
const serverUrl = urls.serverUrl;
108127

128+
// Context contains scenario-specific data (credentials, etc.)
129+
const context = urls.context;
130+
131+
// Set scenario env var for inline runners
132+
const previousScenario = process.env.MCP_CONFORMANCE_SCENARIO;
133+
process.env.MCP_CONFORMANCE_SCENARIO = scenarioName;
134+
109135
try {
110136
// Run the client
111137
try {
112-
await runner.run(serverUrl);
138+
await runner.run(serverUrl, context);
113139
} catch (err) {
114140
if (expectedFailureSlugs.length === 0 && !allowClientError) {
115141
throw err; // Unexpected failure
@@ -166,6 +192,12 @@ export async function runClientAgainstScenario(
166192
}
167193
}
168194
} finally {
195+
// Restore previous env var
196+
if (previousScenario !== undefined) {
197+
process.env.MCP_CONFORMANCE_SCENARIO = previousScenario;
198+
} else {
199+
delete process.env.MCP_CONFORMANCE_SCENARIO;
200+
}
169201
// Stop the scenario server
170202
await scenario.stop();
171203
}

0 commit comments

Comments
 (0)