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+ Scenarios:
13+ auth/* - Authorization code flow scenarios (default behavior)
14+ auth/client-credentials-jwt - Client credentials with JWT authentication (SEP-1046)
1115"""
1216
1317import asyncio
1923import httpx
2024from mcp import ClientSession
2125from mcp .client .auth import OAuthClientProvider , TokenStorage
26+ from mcp .client .auth .extensions .client_credentials import JWTParameters , RFC7523OAuthClientProvider
2227from mcp .client .streamable_http import streamablehttp_client
2328from mcp .shared .auth import OAuthClientInformationFull , OAuthClientMetadata , OAuthToken
2429from pydantic import AnyUrl
2530
31+ # Well-known test private key for conformance testing (EC P-256).
32+ # This corresponds to the public key in the conformance test framework.
33+ # The key is intentionally public for conformance testing purposes.
34+ CONFORMANCE_TEST_PRIVATE_KEY_PEM = """-----BEGIN EC PRIVATE KEY-----
35+ MHcCAQEEIAIbFaPsD3ShCQpvGzgyEIvLQo8pLOsEMwQWcRq+CwWGoAoGCCqGSM49
36+ AwEHoUQDQgAEiYsmK6YG68IEZ4k0vVSg1Cy8PRwsmz/XweRkTsYof+yVMH+FYm5P
37+ 0IhAPaKV/4DjucO6UcQ+d/8q7i//mT7WQA==
38+ -----END EC PRIVATE KEY-----"""
39+
40+ # The client_id expected by the conformance test
41+ CONFORMANCE_TEST_CLIENT_ID = "conformance-test-client"
42+
2643# Set up logging to stderr (stdout is for conformance test output)
2744logging .basicConfig (
2845 level = logging .DEBUG ,
@@ -111,17 +128,17 @@ async def handle_callback(self) -> tuple[str, str | None]:
111128 return auth_code , state
112129
113130
114- async def run_client (server_url : str ) -> None :
131+ async def run_authorization_code_client (server_url : str ) -> None :
115132 """
116- Run the conformance test client against the given server URL .
133+ Run the conformance test client with authorization code flow .
117134
118135 This function:
119- 1. Connects to the MCP server with OAuth authentication
136+ 1. Connects to the MCP server with OAuth authorization code flow
120137 2. Initializes the session
121138 3. Lists available tools
122139 4. Calls a test tool
123140 """
124- logger .debug (f"Starting conformance auth client for { server_url } " )
141+ logger .debug (f"Starting conformance auth client (authorization_code) for { server_url } " )
125142
126143 # Create callback handler that will automatically fetch auth codes
127144 callback_handler = ConformanceOAuthCallbackHandler ()
@@ -140,6 +157,50 @@ async def run_client(server_url: str) -> None:
140157 callback_handler = callback_handler .handle_callback ,
141158 )
142159
160+ await _run_session (server_url , oauth_auth )
161+
162+
163+ async def run_client_credentials_client (server_url : str ) -> None :
164+ """
165+ Run the conformance test client with client credentials flow (SEP-1046).
166+
167+ This function:
168+ 1. Connects to the MCP server with OAuth client_credentials grant
169+ 2. Uses private_key_jwt authentication with the well-known test key
170+ 3. Initializes the session
171+ 4. Lists available tools
172+ 5. Calls a test tool
173+ """
174+ logger .debug (f"Starting conformance auth client (client_credentials) for { server_url } " )
175+
176+ # Create JWT parameters for private_key_jwt authentication
177+ jwt_params = JWTParameters (
178+ issuer = CONFORMANCE_TEST_CLIENT_ID ,
179+ subject = CONFORMANCE_TEST_CLIENT_ID ,
180+ jwt_signing_algorithm = "ES256" ,
181+ jwt_signing_key = CONFORMANCE_TEST_PRIVATE_KEY_PEM ,
182+ )
183+
184+ # Create OAuth authentication handler for client_credentials flow
185+ # Note: redirect_uris is required by the model but not used in client_credentials flow
186+ oauth_auth = RFC7523OAuthClientProvider (
187+ server_url = server_url ,
188+ client_metadata = OAuthClientMetadata (
189+ client_name = CONFORMANCE_TEST_CLIENT_ID ,
190+ redirect_uris = [AnyUrl ("http://localhost:0/unused" )], # Required but unused
191+ grant_types = ["client_credentials" ],
192+ response_types = [],
193+ token_endpoint_auth_method = "private_key_jwt" ,
194+ ),
195+ storage = InMemoryTokenStorage (),
196+ jwt_parameters = jwt_params ,
197+ )
198+
199+ await _run_session (server_url , oauth_auth )
200+
201+
202+ async def _run_session (server_url : str , oauth_auth : OAuthClientProvider ) -> None :
203+ """Common session logic for all OAuth flows."""
143204 # Connect using streamable HTTP transport with OAuth
144205 async with streamablehttp_client (
145206 url = server_url ,
@@ -168,14 +229,23 @@ async def run_client(server_url: str) -> None:
168229
169230def main () -> None :
170231 """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 )
232+ if len (sys .argv ) != 3 :
233+ print (f"Usage: { sys .argv [0 ]} <scenario> <server-url>" , file = sys .stderr )
234+ print ("" , file = sys .stderr )
235+ print ("Scenarios:" , file = sys .stderr )
236+ print (" auth/* - Authorization code flow (default)" , file = sys .stderr )
237+ print (" auth/client-credentials-jwt - Client credentials with JWT auth (SEP-1046)" , file = sys .stderr )
173238 sys .exit (1 )
174239
175- server_url = sys .argv [1 ]
240+ scenario = sys .argv [1 ]
241+ server_url = sys .argv [2 ]
176242
177243 try :
178- asyncio .run (run_client (server_url ))
244+ if scenario == "auth/client-credentials-jwt" :
245+ asyncio .run (run_client_credentials_client (server_url ))
246+ else :
247+ # Default to authorization code flow for all other auth/* scenarios
248+ asyncio .run (run_authorization_code_client (server_url ))
179249 except Exception :
180250 logger .exception ("Client failed" )
181251 sys .exit (1 )
0 commit comments