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,130 @@ 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+ scenario: string ;
31+ client_id ? : string ;
32+ // For JWT auth (private_key_jwt)
33+ private_key_pem ? : string ;
34+ signing_algorithm ? : string ;
35+ // For basic auth (client_secret_basic)
36+ client_secret ? : string ;
37+ }
38+
39+ function getContext (
40+ passedContext ?: Record < string , unknown >
41+ ) : ConformanceContext {
42+ if ( passedContext ) {
43+ return passedContext as ConformanceContext ;
44+ }
45+ const contextJson = process . env . MCP_CONFORMANCE_CONTEXT ;
46+ if ( ! contextJson ) {
47+ throw new Error ( 'MCP_CONFORMANCE_CONTEXT environment variable is required' ) ;
48+ }
49+ return JSON . parse ( contextJson ) ;
50+ }
51+
52+ /**
53+ * Create an OAuth provider based on the scenario type.
54+ */
55+ function createProviderForScenario (
56+ context : ConformanceContext
57+ ) : OAuthClientProvider | undefined {
58+ const { scenario } = context ;
59+
60+ // Client credentials scenarios use the dedicated provider classes
61+ if ( scenario === 'auth/client-credentials-jwt' ) {
62+ if (
63+ ! context . client_id ||
64+ ! context . private_key_pem ||
65+ ! context . signing_algorithm
66+ ) {
67+ throw new Error (
68+ 'auth/client-credentials-jwt requires client_id, private_key_pem, and signing_algorithm in context'
69+ ) ;
70+ }
71+ return new PrivateKeyJwtProvider ( {
72+ clientId : context . client_id ,
73+ privateKey : context . private_key_pem ,
74+ algorithm : context . signing_algorithm ,
75+ clientName : 'conformance-client-credentials'
76+ } ) ;
77+ }
78+
79+ if ( scenario === 'auth/client-credentials-basic' ) {
80+ if ( ! context . client_id || ! context . client_secret ) {
81+ throw new Error (
82+ 'auth/client-credentials-basic requires client_id and client_secret in context'
83+ ) ;
84+ }
85+ return new ClientCredentialsProvider ( {
86+ clientId : context . client_id ,
87+ clientSecret : context . client_secret ,
88+ clientName : 'conformance-client-credentials'
89+ } ) ;
90+ }
91+
92+ // For authorization code flow scenarios, return undefined to use withOAuthRetry
93+ return undefined ;
94+ }
95+
96+ /**
97+ * Auth client that handles both authorization code flow and client credentials flow
98+ * based on the scenario name in the conformance context.
99+ */
100+ export async function runClient (
101+ serverUrl : string ,
102+ passedContext ? : Record < string , unknown >
103+ ) : Promise < void > {
104+ const context = getContext ( passedContext ) ;
105+ logger . debug ( 'Parsed context:' , JSON . stringify ( context , null , 2 ) ) ;
106+
21107 const client = new Client (
22108 { name : 'test-auth-client' , version : '1.0.0' } ,
23109 { capabilities : { } }
24110 ) ;
25111
26- const oauthFetch = withOAuthRetry (
27- 'test-auth-client' ,
28- new URL ( serverUrl ) ,
29- handle401 ,
30- CIMD_CLIENT_METADATA_URL
31- ) ( fetch ) ;
112+ // Check if this is a client credentials scenario
113+ const clientCredentialsProvider = createProviderForScenario ( context ) ;
114+
115+ let transport : StreamableHTTPClientTransport ;
116+
117+ if ( clientCredentialsProvider ) {
118+ // Client credentials flow - use the provider directly
119+ transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
120+ authProvider : clientCredentialsProvider
121+ } ) ;
122+ } else {
123+ // Authorization code flow - use withOAuthRetry middleware
124+ const oauthFetch = withOAuthRetry (
125+ 'test-auth-client' ,
126+ new URL ( serverUrl ) ,
127+ handle401 ,
128+ CIMD_CLIENT_METADATA_URL
129+ ) ( fetch ) ;
32130
33- const transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
34- fetch : oauthFetch
35- } ) ;
131+ transport = new StreamableHTTPClientTransport ( new URL ( serverUrl ) , {
132+ fetch : oauthFetch
133+ } ) ;
134+ }
36135
37136 await client . connect ( transport ) ;
38- logger . debug ( '✅ Successfully connected to MCP server' ) ;
137+ logger . debug ( 'Successfully connected to MCP server' ) ;
39138
40139 await client . listTools ( ) ;
41- logger . debug ( '✅ Successfully listed tools' ) ;
140+ logger . debug ( 'Successfully listed tools' ) ;
42141
43142 await client . callTool ( { name : 'test-tool' , arguments : { } } ) ;
44- logger . debug ( '✅ Successfully called tool' ) ;
143+ logger . debug ( 'Successfully called tool' ) ;
45144
46145 await transport . close ( ) ;
47- logger . debug ( '✅ Connection closed successfully' ) ;
146+ logger . debug ( 'Connection closed successfully' ) ;
48147}
49148
50149runAsCli ( runClient , import . meta. url , 'auth - test < server - url > ') ;
0 commit comments