Skip to content

Commit 623ffb0

Browse files
committed
Load conformance test credentials from MCP_CONFORMANCE_CONTEXT env var
Instead of hardcoding test credentials (private key, client_id, client_secret), the conformance auth client now loads them from MCP_CONFORMANCE_CONTEXT environment variable as a JSON object: { "client_id": "...", "client_secret": "...", // For client_secret_basic flow "private_key_pem": "...", // For private_key_jwt flow "signing_algorithm": "ES256" // Optional, defaults to ES256 } This allows the conformance test framework to provide credentials dynamically.
1 parent e56ad96 commit 623ffb0

File tree

1 file changed

+84
-45
lines changed
  • examples/clients/conformance-auth-client/mcp_conformance_auth_client

1 file changed

+84
-45
lines changed

examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py

Lines changed: 84 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,25 @@
99
Usage:
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+
1221
Scenarios:
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

1827
import asyncio
28+
import json
1929
import logging
30+
import os
2031
import sys
2132
from datetime import timedelta
2233
from urllib.parse import ParseResult, parse_qs, urlparse
@@ -29,20 +40,19 @@
2940
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
3041
from 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)
4858
logging.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

Comments
 (0)