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+
61+
2662# Set up logging to stderr (stdout is for conformance test output)
2763logging .basicConfig (
2864 level = logging .DEBUG ,
@@ -111,17 +147,17 @@ async def handle_callback(self) -> tuple[str, str | None]:
111147 return auth_code , state
112148
113149
114- async def run_client (server_url : str ) -> None :
150+ async def run_authorization_code_client (server_url : str ) -> None :
115151 """
116- Run the conformance test client against the given server URL .
152+ Run the conformance test client with authorization code flow .
117153
118154 This function:
119- 1. Connects to the MCP server with OAuth authentication
155+ 1. Connects to the MCP server with OAuth authorization code flow
120156 2. Initializes the session
121157 3. Lists available tools
122158 4. Calls a test tool
123159 """
124- logger .debug (f"Starting conformance auth client for { server_url } " )
160+ logger .debug (f"Starting conformance auth client (authorization_code) for { server_url } " )
125161
126162 # Create callback handler that will automatically fetch auth codes
127163 callback_handler = ConformanceOAuthCallbackHandler ()
@@ -140,6 +176,89 @@ async def run_client(server_url: str) -> None:
140176 callback_handler = callback_handler .handle_callback ,
141177 )
142178
179+ await _run_session (server_url , oauth_auth )
180+
181+
182+ async def run_client_credentials_jwt_client (server_url : str ) -> None :
183+ """
184+ Run the conformance test client with client credentials flow using private_key_jwt (SEP-1046).
185+
186+ This function:
187+ 1. Connects to the MCP server with OAuth client_credentials grant
188+ 2. Uses private_key_jwt authentication with credentials from MCP_CONFORMANCE_CONTEXT
189+ 3. Initializes the session
190+ 4. Lists available tools
191+ 5. Calls a test tool
192+ """
193+ logger .debug (f"Starting conformance auth client (client_credentials_jwt) for { server_url } " )
194+
195+ # Load credentials from environment
196+ context = get_conformance_context ()
197+ client_id = context .get ("client_id" )
198+ private_key_pem = context .get ("private_key_pem" )
199+ signing_algorithm = context .get ("signing_algorithm" , "ES256" )
200+
201+ if not client_id :
202+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_id'" )
203+ if not private_key_pem :
204+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'private_key_pem'" )
205+
206+ # Create JWT parameters for SDK-signed assertions
207+ jwt_params = SignedJWTParameters (
208+ issuer = client_id ,
209+ subject = client_id ,
210+ signing_algorithm = signing_algorithm ,
211+ signing_key = private_key_pem ,
212+ )
213+
214+ # Create OAuth provider for client_credentials with private_key_jwt
215+ oauth_auth = PrivateKeyJWTOAuthProvider (
216+ server_url = server_url ,
217+ storage = InMemoryTokenStorage (),
218+ client_id = client_id ,
219+ assertion_provider = jwt_params .create_assertion_provider (),
220+ )
221+
222+ await _run_session (server_url , oauth_auth )
223+
224+
225+ async def run_client_credentials_basic_client (server_url : str ) -> None :
226+ """
227+ Run the conformance test client with client credentials flow using client_secret_basic.
228+
229+ This function:
230+ 1. Connects to the MCP server with OAuth client_credentials grant
231+ 2. Uses client_secret_basic authentication with credentials from MCP_CONFORMANCE_CONTEXT
232+ 3. Initializes the session
233+ 4. Lists available tools
234+ 5. Calls a test tool
235+ """
236+ logger .debug (f"Starting conformance auth client (client_credentials_basic) for { server_url } " )
237+
238+ # Load credentials from environment
239+ context = get_conformance_context ()
240+ client_id = context .get ("client_id" )
241+ client_secret = context .get ("client_secret" )
242+
243+ if not client_id :
244+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_id'" )
245+ if not client_secret :
246+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_secret'" )
247+
248+ # Create OAuth provider for client_credentials with client_secret_basic
249+ oauth_auth = ClientCredentialsOAuthProvider (
250+ server_url = server_url ,
251+ storage = InMemoryTokenStorage (),
252+ client_id = client_id ,
253+ client_secret = client_secret ,
254+ token_endpoint_auth_method = "client_secret_basic" ,
255+ )
256+
257+ await _run_session (server_url , oauth_auth )
258+
259+
260+ async def _run_session (server_url : str , oauth_auth : OAuthClientProvider ) -> None :
261+ """Common session logic for all OAuth flows."""
143262 # Connect using streamable HTTP transport with OAuth
144263 async with streamablehttp_client (
145264 url = server_url ,
@@ -168,14 +287,26 @@ async def run_client(server_url: str) -> None:
168287
169288def main () -> None :
170289 """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 )
290+ if len (sys .argv ) != 3 :
291+ print (f"Usage: { sys .argv [0 ]} <scenario> <server-url>" , file = sys .stderr )
292+ print ("" , file = sys .stderr )
293+ print ("Scenarios:" , file = sys .stderr )
294+ print (" auth/* - Authorization code flow (default)" , file = sys .stderr )
295+ print (" auth/client-credentials-jwt - Client credentials with JWT auth (SEP-1046)" , file = sys .stderr )
296+ print (" auth/client-credentials-basic - Client credentials with client_secret_basic" , file = sys .stderr )
173297 sys .exit (1 )
174298
175- server_url = sys .argv [1 ]
299+ scenario = sys .argv [1 ]
300+ server_url = sys .argv [2 ]
176301
177302 try :
178- asyncio .run (run_client (server_url ))
303+ if scenario == "auth/client-credentials-jwt" :
304+ asyncio .run (run_client_credentials_jwt_client (server_url ))
305+ elif scenario == "auth/client-credentials-basic" :
306+ asyncio .run (run_client_credentials_basic_client (server_url ))
307+ else :
308+ # Default to authorization code flow for all other auth/* scenarios
309+ asyncio .run (run_authorization_code_client (server_url ))
179310 except Exception :
180311 logger .exception ("Client failed" )
181312 sys .exit (1 )
0 commit comments