22
33import { Client } from '@modelcontextprotocol/sdk/client/index.js' ;
44import { 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' ;
510import { withOAuthRetry , handle401 } from './helpers/withOAuthRetry' ;
611import { runAsCli } from './helpers/cliRunner' ;
712import { 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
50159runAsCli ( runClient , import . meta. url , 'auth - test < server - url > ') ;
0 commit comments