Skip to content

Commit 02597a2

Browse files
committed
feat: support combined client creds and token exchange
1 parent 2d6c062 commit 02597a2

File tree

3 files changed

+38
-3
lines changed

3 files changed

+38
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ async def main():
866866
client_metadata=OAuthClientMetadata(
867867
client_name="My Client",
868868
redirect_uris=["http://localhost:3000/callback"],
869-
grant_types=["token_exchange"],
869+
grant_types=["client_credentials", "token_exchange"],
870870
response_types=["code"],
871871
),
872872
storage=CustomTokenStorage(),

src/mcp/server/auth/handlers/register.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ async def handle(self, request: Request) -> Response:
7373
{"authorization_code", "refresh_token"},
7474
{"client_credentials"},
7575
{"token_exchange"},
76+
{"client_credentials", "token_exchange"},
7677
]
7778

7879
if grant_types_set not in valid_sets:
@@ -81,7 +82,7 @@ async def handle(self, request: Request) -> Response:
8182
error="invalid_client_metadata",
8283
error_description=(
8384
"grant_types must be authorization_code and refresh_token "
84-
"or client_credentials or token exchange"
85+
"or client_credentials or token exchange or client_credentials and token_exchange"
8586
),
8687
),
8788
status_code=400,

tests/server/fastmcp/auth/test_auth_integration.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,11 @@ async def test_client_registration_invalid_grant_type(self, test_client: httpx.A
976976
assert error_data["error"] == "invalid_client_metadata"
977977
assert (
978978
error_data["error_description"]
979-
== "grant_types must be authorization_code and refresh_token or client_credentials or token exchange"
979+
== (
980+
"grant_types must be authorization_code and refresh_token "
981+
"or client_credentials or token exchange or "
982+
"client_credentials and token_exchange"
983+
)
980984
)
981985

982986
@pytest.mark.anyio
@@ -1336,3 +1340,33 @@ async def test_token_exchange_invalid_subject(self, test_client: httpx.AsyncClie
13361340
assert response.status_code == 400
13371341
data = response.json()
13381342
assert data["error"] == "invalid_grant"
1343+
1344+
@pytest.mark.anyio
1345+
@pytest.mark.parametrize(
1346+
"registered_client",
1347+
[{"grant_types": ["client_credentials", "token_exchange"]}],
1348+
indirect=True,
1349+
)
1350+
async def test_client_credentials_and_token_exchange(self, test_client: httpx.AsyncClient, registered_client):
1351+
cc_response = await test_client.post(
1352+
"/token",
1353+
data={
1354+
"grant_type": "client_credentials",
1355+
"client_id": registered_client["client_id"],
1356+
"client_secret": registered_client["client_secret"],
1357+
"scope": "read write",
1358+
},
1359+
)
1360+
assert cc_response.status_code == 200
1361+
1362+
te_response = await test_client.post(
1363+
"/token",
1364+
data={
1365+
"grant_type": "token_exchange",
1366+
"client_id": registered_client["client_id"],
1367+
"client_secret": registered_client["client_secret"],
1368+
"subject_token": "good_token",
1369+
"subject_token_type": "access_token",
1370+
},
1371+
)
1372+
assert te_response.status_code == 200

0 commit comments

Comments
 (0)