77fetching the authorization URL and extracting the auth code from the redirect.
88
99Usage:
10- python -m mcp_conformance_auth_client <server-url>
10+ python -m mcp_conformance_auth_client <scenario> <server-url>
11+
12+ Environment Variables:
13+ MCP_CONFORMANCE_CONTEXT - JSON object containing test credentials:
14+ {
15+ "client_id": "...",
16+ "client_secret": "...", # For client_secret_basic flow
17+ "private_key_pem": "...", # For private_key_jwt flow
18+ "signing_algorithm": "ES256" # Optional, defaults to ES256
19+ }
20+
21+ Scenarios:
22+ auth/* - Authorization code flow scenarios (default behavior)
23+ auth/client-credentials-jwt - Client credentials with JWT authentication (SEP-1046)
24+ auth/client-credentials-basic - Client credentials with client_secret_basic
1125"""
1226
1327import asyncio
28+ import json
1429import logging
30+ import os
1531import sys
1632from datetime import timedelta
1733from urllib .parse import ParseResult , parse_qs , urlparse
1834
1935import httpx
2036from mcp import ClientSession
2137from mcp .client .auth import OAuthClientProvider , TokenStorage
38+ from mcp .client .auth .extensions .client_credentials import (
39+ ClientCredentialsOAuthProvider ,
40+ PrivateKeyJWTOAuthProvider ,
41+ SignedJWTParameters ,
42+ )
2243from mcp .client .streamable_http import streamablehttp_client
2344from mcp .shared .auth import OAuthClientInformationFull , OAuthClientMetadata , OAuthToken
2445from pydantic import AnyUrl
2546
47+
48+ def get_conformance_context () -> dict :
49+ """Load conformance test context from MCP_CONFORMANCE_CONTEXT environment variable."""
50+ context_json = os .environ .get ("MCP_CONFORMANCE_CONTEXT" )
51+ if not context_json :
52+ raise RuntimeError (
53+ "MCP_CONFORMANCE_CONTEXT environment variable not set. "
54+ "Expected JSON with client_id, client_secret, and/or private_key_pem."
55+ )
56+ try :
57+ return json .loads (context_json )
58+ except json .JSONDecodeError as e :
59+ raise RuntimeError (f"Failed to parse MCP_CONFORMANCE_CONTEXT as JSON: { e } " ) from e
60+
2661# Set up logging to stderr (stdout is for conformance test output)
2762logging .basicConfig (
2863 level = logging .DEBUG ,
@@ -111,17 +146,17 @@ async def handle_callback(self) -> tuple[str, str | None]:
111146 return auth_code , state
112147
113148
114- async def run_client (server_url : str ) -> None :
149+ async def run_authorization_code_client (server_url : str ) -> None :
115150 """
116- Run the conformance test client against the given server URL .
151+ Run the conformance test client with authorization code flow .
117152
118153 This function:
119- 1. Connects to the MCP server with OAuth authentication
154+ 1. Connects to the MCP server with OAuth authorization code flow
120155 2. Initializes the session
121156 3. Lists available tools
122157 4. Calls a test tool
123158 """
124- logger .debug (f"Starting conformance auth client for { server_url } " )
159+ logger .debug (f"Starting conformance auth client (authorization_code) for { server_url } " )
125160
126161 # Create callback handler that will automatically fetch auth codes
127162 callback_handler = ConformanceOAuthCallbackHandler ()
@@ -140,6 +175,89 @@ async def run_client(server_url: str) -> None:
140175 callback_handler = callback_handler .handle_callback ,
141176 )
142177
178+ await _run_session (server_url , oauth_auth )
179+
180+
181+ async def run_client_credentials_jwt_client (server_url : str ) -> None :
182+ """
183+ Run the conformance test client with client credentials flow using private_key_jwt (SEP-1046).
184+
185+ This function:
186+ 1. Connects to the MCP server with OAuth client_credentials grant
187+ 2. Uses private_key_jwt authentication with credentials from MCP_CONFORMANCE_CONTEXT
188+ 3. Initializes the session
189+ 4. Lists available tools
190+ 5. Calls a test tool
191+ """
192+ logger .debug (f"Starting conformance auth client (client_credentials_jwt) for { server_url } " )
193+
194+ # Load credentials from environment
195+ context = get_conformance_context ()
196+ client_id = context .get ("client_id" )
197+ private_key_pem = context .get ("private_key_pem" )
198+ signing_algorithm = context .get ("signing_algorithm" , "ES256" )
199+
200+ if not client_id :
201+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_id'" )
202+ if not private_key_pem :
203+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'private_key_pem'" )
204+
205+ # Create JWT parameters for SDK-signed assertions
206+ jwt_params = SignedJWTParameters (
207+ issuer = client_id ,
208+ subject = client_id ,
209+ signing_algorithm = signing_algorithm ,
210+ signing_key = private_key_pem ,
211+ )
212+
213+ # Create OAuth provider for client_credentials with private_key_jwt
214+ oauth_auth = PrivateKeyJWTOAuthProvider (
215+ server_url = server_url ,
216+ storage = InMemoryTokenStorage (),
217+ client_id = client_id ,
218+ assertion_provider = jwt_params .create_assertion_provider (),
219+ )
220+
221+ await _run_session (server_url , oauth_auth )
222+
223+
224+ async def run_client_credentials_basic_client (server_url : str ) -> None :
225+ """
226+ Run the conformance test client with client credentials flow using client_secret_basic.
227+
228+ This function:
229+ 1. Connects to the MCP server with OAuth client_credentials grant
230+ 2. Uses client_secret_basic authentication with credentials from MCP_CONFORMANCE_CONTEXT
231+ 3. Initializes the session
232+ 4. Lists available tools
233+ 5. Calls a test tool
234+ """
235+ logger .debug (f"Starting conformance auth client (client_credentials_basic) for { server_url } " )
236+
237+ # Load credentials from environment
238+ context = get_conformance_context ()
239+ client_id = context .get ("client_id" )
240+ client_secret = context .get ("client_secret" )
241+
242+ if not client_id :
243+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_id'" )
244+ if not client_secret :
245+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_secret'" )
246+
247+ # Create OAuth provider for client_credentials with client_secret_basic
248+ oauth_auth = ClientCredentialsOAuthProvider (
249+ server_url = server_url ,
250+ storage = InMemoryTokenStorage (),
251+ client_id = client_id ,
252+ client_secret = client_secret ,
253+ token_endpoint_auth_method = "client_secret_basic" ,
254+ )
255+
256+ await _run_session (server_url , oauth_auth )
257+
258+
259+ async def _run_session (server_url : str , oauth_auth : OAuthClientProvider ) -> None :
260+ """Common session logic for all OAuth flows."""
143261 # Connect using streamable HTTP transport with OAuth
144262 async with streamablehttp_client (
145263 url = server_url ,
@@ -168,14 +286,26 @@ async def run_client(server_url: str) -> None:
168286
169287def main () -> None :
170288 """Main entry point for the conformance auth client."""
171- if len (sys .argv ) != 2 :
172- print (f"Usage: { sys .argv [0 ]} <server-url>" , file = sys .stderr )
289+ if len (sys .argv ) != 3 :
290+ print (f"Usage: { sys .argv [0 ]} <scenario> <server-url>" , file = sys .stderr )
291+ print ("" , file = sys .stderr )
292+ print ("Scenarios:" , file = sys .stderr )
293+ print (" auth/* - Authorization code flow (default)" , file = sys .stderr )
294+ print (" auth/client-credentials-jwt - Client credentials with JWT auth (SEP-1046)" , file = sys .stderr )
295+ print (" auth/client-credentials-basic - Client credentials with client_secret_basic" , file = sys .stderr )
173296 sys .exit (1 )
174297
175- server_url = sys .argv [1 ]
298+ scenario = sys .argv [1 ]
299+ server_url = sys .argv [2 ]
176300
177301 try :
178- asyncio .run (run_client (server_url ))
302+ if scenario == "auth/client-credentials-jwt" :
303+ asyncio .run (run_client_credentials_jwt_client (server_url ))
304+ elif scenario == "auth/client-credentials-basic" :
305+ asyncio .run (run_client_credentials_basic_client (server_url ))
306+ else :
307+ # Default to authorization code flow for all other auth/* scenarios
308+ asyncio .run (run_authorization_code_client (server_url ))
179309 except Exception :
180310 logger .exception ("Client failed" )
181311 sys .exit (1 )
0 commit comments