99Usage:
1010 python -m mcp_conformance_auth_client <scenario> <server-url>
1111
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+
1221Scenarios:
1322 auth/* - Authorization code flow scenarios (default behavior)
1423 auth/client-credentials-jwt - Client credentials with JWT authentication (SEP-1046)
1524 auth/client-credentials-basic - Client credentials with client_secret_basic
1625"""
1726
1827import asyncio
28+ import json
1929import logging
30+ import os
2031import sys
2132from datetime import timedelta
2233from urllib .parse import ParseResult , parse_qs , urlparse
2940from mcp .shared .auth import OAuthClientInformationFull , OAuthClientMetadata , OAuthToken
3041from pydantic import AnyUrl
3142
32- # Well-known test private key for conformance testing (EC P-256).
33- # This corresponds to the public key in the conformance test framework.
34- # The key is intentionally public for conformance testing purposes.
35- CONFORMANCE_TEST_PRIVATE_KEY_PEM = """-----BEGIN EC PRIVATE KEY-----
36- MHcCAQEEIAIbFaPsD3ShCQpvGzgyEIvLQo8pLOsEMwQWcRq+CwWGoAoGCCqGSM49
37- AwEHoUQDQgAEiYsmK6YG68IEZ4k0vVSg1Cy8PRwsmz/XweRkTsYof+yVMH+FYm5P
38- 0IhAPaKV/4DjucO6UcQ+d/8q7i//mT7WQA==
39- -----END EC PRIVATE KEY-----"""
40-
41- # The client_id expected by the conformance test
42- CONFORMANCE_TEST_CLIENT_ID = "conformance-test-client"
4343
44- # Static credentials for client_secret_basic flow
45- CONFORMANCE_CLIENT_SECRET = "conformance-test-secret"
44+ def get_conformance_context () -> dict :
45+ """Load conformance test context from MCP_CONFORMANCE_CONTEXT environment variable."""
46+ context_json = os .environ .get ("MCP_CONFORMANCE_CONTEXT" )
47+ if not context_json :
48+ raise RuntimeError (
49+ "MCP_CONFORMANCE_CONTEXT environment variable not set. "
50+ "Expected JSON with client_id, client_secret, and/or private_key_pem."
51+ )
52+ try :
53+ return json .loads (context_json )
54+ except json .JSONDecodeError as e :
55+ raise RuntimeError (f"Failed to parse MCP_CONFORMANCE_CONTEXT as JSON: { e } " ) from e
4656
4757# Set up logging to stderr (stdout is for conformance test output)
4858logging .basicConfig (
@@ -170,35 +180,50 @@ async def run_client_credentials_jwt_client(server_url: str) -> None:
170180
171181 This function:
172182 1. Connects to the MCP server with OAuth client_credentials grant
173- 2. Uses private_key_jwt authentication with the well-known test key
183+ 2. Uses private_key_jwt authentication with credentials from MCP_CONFORMANCE_CONTEXT
174184 3. Initializes the session
175185 4. Lists available tools
176186 5. Calls a test tool
177187 """
178188 logger .debug (f"Starting conformance auth client (client_credentials_jwt) for { server_url } " )
179189
190+ # Load credentials from environment
191+ context = get_conformance_context ()
192+ client_id = context .get ("client_id" )
193+ private_key_pem = context .get ("private_key_pem" )
194+ signing_algorithm = context .get ("signing_algorithm" , "ES256" )
195+
196+ if not client_id :
197+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_id'" )
198+ if not private_key_pem :
199+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'private_key_pem'" )
200+
180201 # Create JWT parameters for private_key_jwt authentication
181202 jwt_params = JWTParameters (
182- issuer = CONFORMANCE_TEST_CLIENT_ID ,
183- subject = CONFORMANCE_TEST_CLIENT_ID ,
184- jwt_signing_algorithm = "ES256" ,
185- jwt_signing_key = CONFORMANCE_TEST_PRIVATE_KEY_PEM ,
203+ issuer = client_id ,
204+ subject = client_id ,
205+ jwt_signing_algorithm = signing_algorithm ,
206+ jwt_signing_key = private_key_pem ,
186207 )
187208
188209 # Create OAuth authentication handler for client_credentials flow
189210 # Note: redirect_uris is required by the model but not used in client_credentials flow
190- oauth_auth = RFC7523OAuthClientProvider (
191- server_url = server_url ,
192- client_metadata = OAuthClientMetadata (
193- client_name = CONFORMANCE_TEST_CLIENT_ID ,
194- redirect_uris = [AnyUrl ("http://localhost:0/unused" )], # Required but unused
195- grant_types = ["client_credentials" ],
196- response_types = [],
197- token_endpoint_auth_method = "private_key_jwt" ,
198- ),
199- storage = InMemoryTokenStorage (),
200- jwt_parameters = jwt_params ,
201- )
211+ import warnings
212+
213+ with warnings .catch_warnings ():
214+ warnings .simplefilter ("ignore" , DeprecationWarning )
215+ oauth_auth = RFC7523OAuthClientProvider (
216+ server_url = server_url ,
217+ client_metadata = OAuthClientMetadata (
218+ client_name = client_id ,
219+ redirect_uris = [AnyUrl ("http://localhost:0/unused" )], # Required but unused
220+ grant_types = ["client_credentials" ],
221+ response_types = [],
222+ token_endpoint_auth_method = "private_key_jwt" ,
223+ ),
224+ storage = InMemoryTokenStorage (),
225+ jwt_parameters = jwt_params ,
226+ )
202227
203228 await _run_session (server_url , oauth_auth )
204229
@@ -209,36 +234,50 @@ async def run_client_credentials_basic_client(server_url: str) -> None:
209234
210235 This function:
211236 1. Connects to the MCP server with OAuth client_credentials grant
212- 2. Uses client_secret_basic authentication with static credentials
237+ 2. Uses client_secret_basic authentication with credentials from MCP_CONFORMANCE_CONTEXT
213238 3. Initializes the session
214239 4. Lists available tools
215240 5. Calls a test tool
216241 """
217242 logger .debug (f"Starting conformance auth client (client_credentials_basic) for { server_url } " )
218243
219- # Create storage pre-populated with static client credentials
244+ # Load credentials from environment
245+ context = get_conformance_context ()
246+ client_id = context .get ("client_id" )
247+ client_secret = context .get ("client_secret" )
248+
249+ if not client_id :
250+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_id'" )
251+ if not client_secret :
252+ raise RuntimeError ("MCP_CONFORMANCE_CONTEXT missing 'client_secret'" )
253+
254+ # Create storage pre-populated with client credentials
220255 storage = InMemoryTokenStorage ()
221256 await storage .set_client_info (
222257 OAuthClientInformationFull (
223- client_id = CONFORMANCE_TEST_CLIENT_ID ,
224- client_secret = CONFORMANCE_CLIENT_SECRET ,
258+ client_id = client_id ,
259+ client_secret = client_secret ,
225260 redirect_uris = [AnyUrl ("http://localhost:0/unused" )],
226261 token_endpoint_auth_method = "client_secret_basic" ,
227262 )
228263 )
229264
230265 # Create OAuth authentication handler for client_credentials flow with basic auth
231- oauth_auth = RFC7523OAuthClientProvider (
232- server_url = server_url ,
233- client_metadata = OAuthClientMetadata (
234- client_name = CONFORMANCE_TEST_CLIENT_ID ,
235- redirect_uris = [AnyUrl ("http://localhost:0/unused" )], # Required but unused
236- grant_types = ["client_credentials" ],
237- response_types = [],
238- token_endpoint_auth_method = "client_secret_basic" ,
239- ),
240- storage = storage ,
241- )
266+ import warnings
267+
268+ with warnings .catch_warnings ():
269+ warnings .simplefilter ("ignore" , DeprecationWarning )
270+ oauth_auth = RFC7523OAuthClientProvider (
271+ server_url = server_url ,
272+ client_metadata = OAuthClientMetadata (
273+ client_name = client_id ,
274+ redirect_uris = [AnyUrl ("http://localhost:0/unused" )], # Required but unused
275+ grant_types = ["client_credentials" ],
276+ response_types = [],
277+ token_endpoint_auth_method = "client_secret_basic" ,
278+ ),
279+ storage = storage ,
280+ )
242281
243282 await _run_session (server_url , oauth_auth )
244283
0 commit comments