Skip to content
Open
Show file tree
Hide file tree
Changes from 103 commits
Commits
Show all changes
225 commits
Select commit Hold shift + click to select a range
833a105
Add client credentials OAuth grant
SoldierSacha Jun 3, 2025
66c7e67
Merge pull request #1 from sacha-development-stuff/codex/add-support-…
SoldierSacha Jun 3, 2025
813168a
Allow client credentials in dynamic registration
SoldierSacha Jun 3, 2025
dbbc6ce
Merge pull request #2 from sacha-development-stuff/codex/review-and-i…
SoldierSacha Jun 3, 2025
3f2a351
Refactor OAuth helpers
SoldierSacha Jun 3, 2025
62c729d
Merge pull request #3 from sacha-development-stuff/codex/review-imple…
SoldierSacha Jun 3, 2025
5212ce0
clean up code
SoldierSacha Jun 3, 2025
d9c751f
linting
SoldierSacha Jun 4, 2025
7848e68
Fix tests and pyright errors
SoldierSacha Jun 4, 2025
e325b95
Merge pull request
SoldierSacha Jun 4, 2025
3a45cf8
work
SoldierSacha Jun 4, 2025
2132cde
test
SoldierSacha Jun 4, 2025
5c87fb3
test
SoldierSacha Jun 4, 2025
103e201
test
SoldierSacha Jun 4, 2025
ad59c92
Fix async fixture usage in OAuth tests
SoldierSacha Jun 4, 2025
e18e606
Merge pull request #5 from sacha-development-stuff/codex/fix-attribut…
SoldierSacha Jun 4, 2025
49fa6c2
Fix resumption token updates
SoldierSacha Jun 4, 2025
b46aac4
Merge pull request
SoldierSacha Jun 4, 2025
2daea3f
Add OAuth token exchange support
SoldierSacha Jun 10, 2025
94850e7
implement-rfc-8693-token-exchange-in-mcp-sdk
SoldierSacha Jun 10, 2025
627eebd
work
SoldierSacha Jun 10, 2025
beeb244
Merge branch 'main' into main
SoldierSacha Jun 10, 2025
e92e61d
docs: document token-exchange support
SoldierSacha Jun 10, 2025
5976e77
docs
SoldierSacha Jun 10, 2025
bde2448
test: update expectations for token-exchange
SoldierSacha Jun 10, 2025
a98f33f
Merge pull request #9 from sacha-development-stuff/codex/fix-token-ex…
SoldierSacha Jun 10, 2025
b3b0509
Fix pyright token type errors
SoldierSacha Jun 10, 2025
a3edbeb
fix-argument-type-and-abstract-class-errors
SoldierSacha Jun 10, 2025
9b5ef4d
work
SoldierSacha Jun 10, 2025
a0d24ca
Strip whitespace from SSE resumption token
SoldierSacha Jun 10, 2025
7e02ddd
fix-test_streamablehttp_client_resumption-failure
SoldierSacha Jun 10, 2025
d04d17c
Merge branch 'main' into main
SoldierSacha Jun 13, 2025
2d6c062
merge with recent branch
SoldierSacha Jun 13, 2025
02597a2
feat: support combined client creds and token exchange
SoldierSacha Jun 14, 2025
e717dbe
adding token exchange + client credentials as a valid registration gr…
SoldierSacha Jun 14, 2025
1f23248
merge with recent branch
SoldierSacha Jun 14, 2025
ded6b89
Handle closed stream when sending notifications
SoldierSacha Jun 14, 2025
05c46e6
fix-test_streamablehttp_client_resumption-failure
SoldierSacha Jun 14, 2025
bb480a2
Merge branch 'main' into main
SoldierSacha Jun 16, 2025
22c5ef2
Merge branch 'main' into main
SoldierSacha Jun 18, 2025
8fdc5f9
merge with recent branch
SoldierSacha Jun 18, 2025
9f7ae6c
test: stabilize resumption notifications
SoldierSacha Jun 18, 2025
2ab4ad0
Merge pull request #14 from sacha-development-stuff/codex/fix-notific…
SoldierSacha Jun 18, 2025
9e06753
Merge branch 'main' into main
SoldierSacha Jun 23, 2025
b935a6f
Resolve merge conflicts and integrate client credential features
SoldierSacha Jun 24, 2025
c1d0acc
Merge pull request #15 from sacha-development-stuff/codex/fix-merge-c…
SoldierSacha Jun 24, 2025
94cefe3
test: restore missing fixtures
SoldierSacha Jun 24, 2025
482c05e
Merge pull request #16 from sacha-development-stuff/codex/resolve-mer…
SoldierSacha Jun 24, 2025
a41187e
merge with recent branch
SoldierSacha Jun 24, 2025
b7d1aad
merge with recent branch
SoldierSacha Jun 24, 2025
1329ab7
merge with recent branch
SoldierSacha Jun 24, 2025
6d1305d
merge with recent branch
SoldierSacha Jun 24, 2025
f61e57e
merge with recent branch
SoldierSacha Jun 24, 2025
f402804
merge with recent branch
SoldierSacha Jun 25, 2025
b1b34e5
Merge branch 'main' into main
SoldierSacha Jun 25, 2025
4a8294c
docs: document client credentials and introspection
SoldierSacha Jun 25, 2025
75ca216
Merge pull request #17 from sacha-development-stuff/codex/add-client-…
SoldierSacha Jun 25, 2025
0a95397
merge with recent branch
SoldierSacha Jun 25, 2025
30e3c79
Merge branch 'main' into main
SoldierSacha Jun 26, 2025
44bc5a0
Merge branch 'main' into main
SoldierSacha Jun 27, 2025
ceb1e19
Merge branch 'main' into main
SoldierSacha Jun 29, 2025
3bf695c
merge with recent branch
SoldierSacha Jun 29, 2025
a7a7a43
merge with recent branch
SoldierSacha Jun 29, 2025
5e77e28
merge with recent branch
SoldierSacha Jun 29, 2025
9057f02
Merge branch 'main' into main
SoldierSacha Jul 8, 2025
26627c1
merge with recent branch
SoldierSacha Jul 8, 2025
b8c0ba3
merge with recent branch
SoldierSacha Jul 8, 2025
4360875
merge with recent branch
SoldierSacha Jul 8, 2025
1704192
Merge branch 'main' into main
SoldierSacha Jul 8, 2025
4b5eaf2
merge with recent branch
SoldierSacha Jul 8, 2025
ff9d079
merge with recent branch
SoldierSacha Jul 8, 2025
f87b7b6
merge with recent branch
SoldierSacha Jul 8, 2025
6182ac2
Merge branch 'main' into main
SoldierSacha Jul 9, 2025
3e8365b
Merge branch 'main' into main
SoldierSacha Jul 10, 2025
1099d6a
Merge branch 'main' into main
SoldierSacha Jul 11, 2025
5be78af
Merge branch 'main' into main
SoldierSacha Jul 14, 2025
29a6b81
merge with recent branch
SoldierSacha Jul 15, 2025
a81eb50
Merge branch 'main' into main
SoldierSacha Jul 16, 2025
c4900ea
Merge branch 'main' into main
SoldierSacha Jul 17, 2025
add5f08
Merge branch 'main' into main
SoldierSacha Jul 18, 2025
e2b27ff
merge with recent branch
SoldierSacha Jul 18, 2025
b3c6dc4
merge with recent branch
SoldierSacha Jul 18, 2025
12d0662
Merge branch 'main' into main
SoldierSacha Jul 22, 2025
78868cc
merge with recent branch
SoldierSacha Jul 22, 2025
7dff18a
merge with recent branch
SoldierSacha Jul 22, 2025
8c9f31f
merge with recent branch
SoldierSacha Jul 22, 2025
31edf42
Merge branch 'main' into main
SoldierSacha Jul 25, 2025
ac02caf
Merge branch 'main' into main
SoldierSacha Jul 26, 2025
a6f77c4
merge with recent branch
SoldierSacha Jul 26, 2025
52fee87
Merge branch 'main' into main
SoldierSacha Aug 4, 2025
545d047
Merge branch 'main' into main
SoldierSacha Aug 4, 2025
d1bbbac
Merge branch 'main' into main
SoldierSacha Aug 15, 2025
710a567
merge with recent branch
SoldierSacha Aug 15, 2025
bafa7a8
refactor: unify OAuth providers and support basic auth
SoldierSacha Aug 15, 2025
10a7a79
Merge pull request #18 from sacha-development-stuff/codex/analyze-cod…
SoldierSacha Aug 15, 2025
382d37d
Merge branch 'main' into main
SoldierSacha Aug 22, 2025
da31a3a
Merge branch 'main' into main
SoldierSacha Aug 26, 2025
0f7aafb
merge with recent branch
SoldierSacha Aug 26, 2025
f837afa
Merge branch 'main' into main
SoldierSacha Aug 31, 2025
6a3db67
Merge branch 'main' into main
SoldierSacha Sep 1, 2025
dc7798f
Merge branch 'main' into main
SoldierSacha Sep 2, 2025
ff07d0d
Merge branch 'main' into main
SoldierSacha Sep 5, 2025
6693675
Merge branch 'main' into main
SoldierSacha Sep 11, 2025
a5c25b1
Merge branch 'main' into main
SoldierSacha Sep 17, 2025
edffa10
Refactor token handler helper flows
SoldierSacha Sep 17, 2025
89265e7
Merge pull request #20 from sacha-development-stuff/codex/fix-too-man…
SoldierSacha Sep 17, 2025
75fbbe5
merge with recent branch
SoldierSacha Sep 17, 2025
7a6862f
Merge branch 'main' into main
SoldierSacha Sep 18, 2025
d0e7e4b
Merge branch 'main' into main
SoldierSacha Sep 23, 2025
cd36ee4
Merge branch 'main' into main
SoldierSacha Sep 23, 2025
b0751c4
Merge branch 'main' into main
SoldierSacha Sep 23, 2025
893a93b
Merge branch 'main' into main
SoldierSacha Sep 24, 2025
41a1973
Merge branch 'main' into main
SoldierSacha Sep 26, 2025
f8a5778
Merge branch 'main' into main
SoldierSacha Sep 27, 2025
1cf50f0
Merge branch 'main' into main
SoldierSacha Sep 29, 2025
16f742a
Allow additional grant types during client registration
SoldierSacha Sep 29, 2025
d0610d1
Merge pull request #21 from sacha-development-stuff/codex/fix-failing…
SoldierSacha Sep 29, 2025
d07d77e
merge with recent branch
SoldierSacha Sep 29, 2025
cb929ea
merge with recent branch
SoldierSacha Sep 29, 2025
f857e0c
Merge branch 'main' into main
SoldierSacha Sep 30, 2025
2ffe38a
Merge branch 'main' into main
SoldierSacha Oct 1, 2025
57e5337
Merge branch 'main' into main
SoldierSacha Oct 4, 2025
6c24b63
Merge branch 'main' into main
SoldierSacha Oct 7, 2025
f9589d6
Merge branch 'main' into main
SoldierSacha Oct 13, 2025
5896e17
Resolve OAuth auth flow merge conflicts
SoldierSacha Oct 13, 2025
e80c285
Merge pull request #22 from sacha-development-stuff/codex/resolve-mer…
SoldierSacha Oct 13, 2025
84860f8
merge with recent branch
SoldierSacha Oct 13, 2025
3e0c70c
Handle closed stdin in stdio client
SoldierSacha Oct 13, 2025
04324e9
Merge pull request #23 from sacha-development-stuff/codex/fix-connect…
SoldierSacha Oct 13, 2025
3ce6094
Merge branch 'main' into main
SoldierSacha Oct 18, 2025
bc9a887
Merge branch 'main' into main
SoldierSacha Oct 22, 2025
fdfbbb7
Merge branch 'main' into main
SoldierSacha Oct 27, 2025
cd87f36
Merge remote-tracking branch 'upstream/main'
SoldierSacha Nov 4, 2025
1999135
Resolve merge conflicts for OAuth enhancements
SoldierSacha Nov 4, 2025
6b3f869
Merge pull request #24 from sacha-development-stuff/codex/resolve-git…
SoldierSacha Nov 4, 2025
7104629
merge with recent branch
SoldierSacha Nov 4, 2025
394a0a0
merge with recent branch
SoldierSacha Nov 4, 2025
9482412
merge with recent branch
SoldierSacha Nov 4, 2025
67f0f2d
Merge branch 'main' into main
SoldierSacha Nov 4, 2025
50833e0
Merge branch 'main' into main
SoldierSacha Nov 11, 2025
74c5d48
Resolve merge conflicts and retain OAuth grant support
SoldierSacha Nov 11, 2025
c836219
Merge pull request #25 from sacha-development-stuff/codex/fix-merge-c…
SoldierSacha Nov 11, 2025
4b00cbc
merge with recent branch
SoldierSacha Nov 11, 2025
20447e4
Merge branch 'main' of https://github.com/sacha-development-stuff/mcp…
SoldierSacha Nov 11, 2025
7352bfa
merge with recent branch
SoldierSacha Nov 11, 2025
cacb93e
merge with recent branch
SoldierSacha Nov 11, 2025
00d1b74
merge with recent branch
SoldierSacha Nov 11, 2025
3af52da
Fix OAuth authorization flow to use auth code exchange
SoldierSacha Nov 11, 2025
d83f184
Merge pull request #26 from sacha-development-stuff/codex/fix-oauthto…
SoldierSacha Nov 11, 2025
7a30a8a
Revert "Fix OAuth authorization flow to use auth code exchange"
SoldierSacha Nov 11, 2025
d9e0243
Merge pull request #27 from sacha-development-stuff/revert-26-codex/f…
SoldierSacha Nov 11, 2025
a9a64d0
Fix OAuth flow response handling
SoldierSacha Nov 12, 2025
58928e1
Merge pull request #28 from sacha-development-stuff/codex/fix-failing…
SoldierSacha Nov 12, 2025
6db5d60
Revert "Improve OAuth token response handling"
SoldierSacha Nov 12, 2025
a7abebd
Merge pull request #30 from sacha-development-stuff/revert-28-codex/f…
SoldierSacha Nov 12, 2025
4df7d48
Fix token response parsing after discovery fallback
SoldierSacha Nov 12, 2025
541d0b9
Merge pull request #31 from sacha-development-stuff/codex/fix-token-v…
SoldierSacha Nov 12, 2025
a5b45f7
Fix OAuth registration skip when client info present
SoldierSacha Nov 12, 2025
654b30d
Merge pull request #32 from sacha-development-stuff/codex/fix-failing…
SoldierSacha Nov 12, 2025
44e59b6
merge with recent branch
SoldierSacha Nov 12, 2025
0b58a94
merge with recent branch
SoldierSacha Nov 12, 2025
c606f6c
merge with recent branch
SoldierSacha Nov 12, 2025
ed4d93e
Add targeted tests for OAuth flows and coverage gaps
SoldierSacha Nov 12, 2025
df6b26d
Merge pull request #33 from sacha-development-stuff/codex/fix-code-co…
SoldierSacha Nov 12, 2025
b2a3b27
merge with recent branch
SoldierSacha Nov 12, 2025
20215fb
Fix pyright typing issues in tests
SoldierSacha Nov 12, 2025
6592391
Merge pull request #34 from sacha-development-stuff/codex/fix-type-an…
SoldierSacha Nov 12, 2025
00340ac
merge with recent branch
SoldierSacha Nov 12, 2025
6602b3a
merge with recent branch
SoldierSacha Nov 12, 2025
da94a6b
Add tests covering OAuth2 client flows
SoldierSacha Nov 12, 2025
f879ec8
Merge pull request #36 from sacha-development-stuff/codex/increase-te…
SoldierSacha Nov 12, 2025
9945699
merge with recent branch
SoldierSacha Nov 12, 2025
a09f355
Use AsyncMock in OAuth2 provider tests
SoldierSacha Nov 12, 2025
6d7cc85
Merge pull request #37 from sacha-development-stuff/codex/investigate…
SoldierSacha Nov 12, 2025
3996fc7
Add tests covering additional OAuth flows
SoldierSacha Nov 12, 2025
3aef782
Merge pull request #38 from sacha-development-stuff/codex/fix-code-co…
SoldierSacha Nov 12, 2025
c33cc00
Fix pyright issues in OAuth tests
SoldierSacha Nov 12, 2025
cc8fbd8
Merge pull request #39 from sacha-development-stuff/codex/fix-pyright…
SoldierSacha Nov 12, 2025
308bc63
work
SoldierSacha Nov 12, 2025
fedadb3
Add tests covering OAuth scope and discovery branches
SoldierSacha Nov 12, 2025
7676c41
Merge pull request #40 from sacha-development-stuff/codex/fix-coverag…
SoldierSacha Nov 12, 2025
6ce78d1
Revert "Add coverage tests for OAuth scope handling and discovery fal…
SoldierSacha Nov 12, 2025
ea8b2ef
Merge pull request #41 from sacha-development-stuff/revert-40-codex/f…
SoldierSacha Nov 12, 2025
d8bef42
Add coverage tests for OAuth token flows
SoldierSacha Nov 12, 2025
6263b53
Merge pull request #42 from sacha-development-stuff/codex/fix-coverag…
SoldierSacha Nov 12, 2025
699f011
merge
SoldierSacha Nov 12, 2025
1286359
Add coverage for client credentials and refresh token flows
SoldierSacha Nov 12, 2025
6399e3a
Merge pull request #43 from sacha-development-stuff/codex/investigate…
SoldierSacha Nov 12, 2025
2148c93
merge
SoldierSacha Nov 12, 2025
f04b778
Add tests for OAuth metadata fallback and refresh dispatch
SoldierSacha Nov 12, 2025
f2fcdc7
Merge pull request #44 from sacha-development-stuff/codex/fix-coverag…
SoldierSacha Nov 12, 2025
ba3621e
Revert "Add branch coverage tests for OAuth metadata and refresh hand…
SoldierSacha Nov 12, 2025
a5138c3
Merge pull request #45 from sacha-development-stuff/revert-44-codex/f…
SoldierSacha Nov 12, 2025
671ddd2
Add tests for OAuth discovery redirect handling and refresh branch
SoldierSacha Nov 12, 2025
5bb3338
Merge pull request #46 from sacha-development-stuff/codex/fix-coverag…
SoldierSacha Nov 12, 2025
921da82
Revert "Add coverage for OAuth discovery redirects and refresh tokens"
SoldierSacha Nov 12, 2025
5b2c9ac
Merge pull request #47 from sacha-development-stuff/revert-46-codex/f…
SoldierSacha Nov 12, 2025
5db0e7e
Test retry after invalid OAuth metadata
SoldierSacha Nov 12, 2025
27fb312
Merge pull request #48 from sacha-development-stuff/codex/fix-coverag…
SoldierSacha Nov 12, 2025
72cca2c
Revert "Add branch coverage tests for OAuth metadata and refresh hand…
SoldierSacha Nov 12, 2025
04b8f53
Merge pull request #49 from sacha-development-stuff/revert-48-codex/f…
SoldierSacha Nov 12, 2025
26fb647
Add token exchange metadata fallbacks and refresh match coverage
SoldierSacha Nov 12, 2025
dc90d27
Merge pull request #50 from sacha-development-stuff/codex/fix-coverag…
SoldierSacha Nov 12, 2025
bcf53b7
Add unit tests for streamable HTTP SSE handling
SoldierSacha Nov 12, 2025
2a66306
Merge pull request #51 from sacha-development-stuff/codex/fix-code-co…
SoldierSacha Nov 12, 2025
ee1c1ea
merge
SoldierSacha Nov 12, 2025
2bdfc7e
merge
SoldierSacha Nov 12, 2025
c520514
Mark Windows-specific paths as no cover
SoldierSacha Nov 12, 2025
cf9a6e6
Merge pull request #52 from sacha-development-stuff/codex/investigate…
SoldierSacha Nov 12, 2025
7876175
merge
SoldierSacha Nov 12, 2025
90bc91a
Merge branch 'main' of https://github.com/sacha-development-stuff/mcp…
SoldierSacha Nov 12, 2025
62fe061
merge
SoldierSacha Nov 12, 2025
0ddbe10
merge
SoldierSacha Nov 12, 2025
a91a038
merge
SoldierSacha Nov 12, 2025
f049650
Merge branch 'main' into main
SoldierSacha Nov 14, 2025
b0674ab
Fix merge conflicts in OAuth2 auth flow
SoldierSacha Nov 14, 2025
738f2c5
Merge pull request #53 from sacha-development-stuff/codex/fix-merge-c…
SoldierSacha Nov 14, 2025
219b71f
Fix OAuth discovery fallbacks
SoldierSacha Nov 14, 2025
d6acc58
Merge pull request #54 from sacha-development-stuff/codex/fix-nameerr…
SoldierSacha Nov 14, 2025
1bb4bc9
merge
SoldierSacha Nov 14, 2025
4d3b51e
Align OAuth metadata handler return types
SoldierSacha Nov 14, 2025
35b30fa
Merge pull request #55 from sacha-development-stuff/codex/fix-incompa…
SoldierSacha Nov 14, 2025
d210a25
Fix OAuth metadata handler stub in auth flow test
SoldierSacha Nov 14, 2025
325f3a3
Merge pull request #56 from sacha-development-stuff/codex/fix-test-ca…
SoldierSacha Nov 14, 2025
d56f550
merge
SoldierSacha Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
The Python SDK exposes the entire `mcp` package for use in your own projects.
It includes an OAuth server implementation with support for the RFC 8693
`token_exchange` grant type.

::: mcp
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
This is the MCP Server implementation in Python.

It only contains the [API Reference](api.md) for the time being.

The built-in OAuth server supports the RFC 8693 `token_exchange` grant type,
allowing clients to exchange user tokens from external providers for MCP
access tokens.
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,52 @@ async def exchange_authorization_code(
scope=" ".join(authorization_code.scopes),
)

async def exchange_client_credentials(self, client: OAuthClientInformationFull, scopes: list[str]) -> OAuthToken:
"""Exchange client credentials for an MCP access token."""
mcp_token = f"mcp_{secrets.token_hex(32)}"
self.tokens[mcp_token] = AccessToken(
token=mcp_token,
client_id=client.client_id,
scopes=scopes,
expires_at=int(time.time()) + 3600,
)
return OAuthToken(
access_token=mcp_token,
token_type="Bearer",
expires_in=3600,
scope=" ".join(scopes),
)

async def exchange_token(
self,
client: OAuthClientInformationFull,
subject_token: str,
subject_token_type: str,
actor_token: str | None,
actor_token_type: str | None,
scope: list[str] | None,
audience: str | None,
resource: str | None,
) -> OAuthToken:
"""Exchange an external token for an MCP access token."""
if not subject_token:
raise ValueError("Invalid subject token")

mcp_token = f"mcp_{secrets.token_hex(32)}"
self.tokens[mcp_token] = AccessToken(
token=mcp_token,
client_id=client.client_id,
scopes=scope or [self.settings.mcp_scope],
expires_at=int(time.time()) + 3600,
resource=resource,
)
return OAuthToken(
access_token=mcp_token,
token_type="Bearer",
expires_in=3600,
scope=" ".join(scope or [self.settings.mcp_scope]),
)

async def load_access_token(self, token: str) -> AccessToken | None:
"""Load and validate an access token."""
access_token = self.tokens.get(token)
Expand Down
473 changes: 388 additions & 85 deletions src/mcp/client/auth.py

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions src/mcp/client/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,11 @@ async def _handle_sse_event(
session_message = SessionMessage(message)
await read_stream_writer.send(session_message)

# Call resumption token callback if we have an ID
if sse.id and resumption_callback:
await resumption_callback(sse.id)
# Call resumption token callback if we have an ID. Only update
# the resumption token on notifications to avoid overwriting it
# with the token from the final response.
if sse.id and resumption_callback and not isinstance(message.root, JSONRPCResponse | JSONRPCError):
await resumption_callback(sse.id.strip())

# If this is a response or error return True indicating completion
# Otherwise, return False to continue listening
Expand Down Expand Up @@ -221,7 +223,7 @@ async def _handle_resumption_request(self, ctx: RequestContext) -> None:
"""Handle a resumption request using GET with SSE."""
headers = self._prepare_request_headers(ctx.headers)
if ctx.metadata and ctx.metadata.resumption_token:
headers[LAST_EVENT_ID] = ctx.metadata.resumption_token
headers[LAST_EVENT_ID] = ctx.metadata.resumption_token.strip()
else:
raise ResumptionError("Resumption request requires a resumption token")

Expand Down
15 changes: 13 additions & 2 deletions src/mcp/server/auth/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,22 @@ async def handle(self, request: Request) -> Response:
),
status_code=400,
)
if set(client_metadata.grant_types) != {"authorization_code", "refresh_token"}:
grant_types_set: set[str] = set(client_metadata.grant_types)
valid_sets = [
{"authorization_code", "refresh_token"},
{"client_credentials"},
{"token_exchange"},
{"client_credentials", "token_exchange"},
]

if grant_types_set not in valid_sets:
return PydanticJSONResponse(
content=RegistrationErrorResponse(
error="invalid_client_metadata",
error_description="grant_types must be authorization_code and refresh_token",
error_description=(
"grant_types must be authorization_code and refresh_token "
"or client_credentials or token exchange or client_credentials and token_exchange"
),
),
status_code=400,
)
Expand Down
68 changes: 65 additions & 3 deletions src/mcp/server/auth/handlers/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,39 @@ class RefreshTokenRequest(BaseModel):
resource: str | None = Field(None, description="Resource indicator for the token")


class ClientCredentialsRequest(BaseModel):
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
grant_type: Literal["client_credentials"]
scope: str | None = Field(None, description="Optional scope parameter")
client_id: str
client_secret: str | None = None


class TokenExchangeRequest(BaseModel):
"""RFC 8693 token exchange request."""

grant_type: Literal["token_exchange"]
subject_token: str = Field(..., description="Token to exchange")
subject_token_type: str = Field(..., description="Type of the subject token")
actor_token: str | None = Field(None, description="Optional actor token")
actor_token_type: str | None = Field(None, description="Type of the actor token if provided")
resource: str | None = None
audience: str | None = None
scope: str | None = None
client_id: str
client_secret: str | None = None


class TokenRequest(
RootModel[
Annotated[
AuthorizationCodeRequest | RefreshTokenRequest,
AuthorizationCodeRequest | RefreshTokenRequest | ClientCredentialsRequest | TokenExchangeRequest,
Field(discriminator="grant_type"),
]
]
):
root: Annotated[
AuthorizationCodeRequest | RefreshTokenRequest,
AuthorizationCodeRequest | RefreshTokenRequest | ClientCredentialsRequest | TokenExchangeRequest,
Field(discriminator="grant_type"),
]

Expand Down Expand Up @@ -192,10 +215,49 @@ async def handle(self, request: Request):
)
)

case ClientCredentialsRequest():
scopes = (
token_request.scope.split(" ")
if token_request.scope
else client_info.scope.split(" ")
if client_info.scope
else []
)
try:
tokens = await self.provider.exchange_client_credentials(client_info, scopes)
except TokenError as e:
return self.response(
TokenErrorResponse(
error=e.error,
error_description=e.error_description,
)
)

case TokenExchangeRequest():
scopes = token_request.scope.split(" ") if token_request.scope else []
try:
tokens = await self.provider.exchange_token(
client_info,
token_request.subject_token,
token_request.subject_token_type,
token_request.actor_token,
token_request.actor_token_type,
scopes,
token_request.audience,
token_request.resource,
)
except TokenError as e:
return self.response(
TokenErrorResponse(
error=e.error,
error_description=e.error_description,
)
)

case RefreshTokenRequest():
refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)
if refresh_token is None or refresh_token.client_id != token_request.client_id:
# if token belongs to different client, pretend it doesn't exist
# if token belongs to a different client, pretend it doesn't exist
return self.response(
TokenErrorResponse(
error="invalid_grant",
Expand Down
19 changes: 19 additions & 0 deletions src/mcp/server/auth/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class AuthorizeError(Exception):
"unauthorized_client",
"unsupported_grant_type",
"invalid_scope",
"invalid_target",
]


Expand Down Expand Up @@ -248,6 +249,24 @@ async def exchange_refresh_token(
"""
...

async def exchange_client_credentials(self, client: OAuthClientInformationFull, scopes: list[str]) -> OAuthToken:
"""Exchange client credentials for an MCP access token."""
...

async def exchange_token(
self,
client: OAuthClientInformationFull,
subject_token: str,
subject_token_type: str,
actor_token: str | None,
actor_token_type: str | None,
scope: list[str] | None,
audience: str | None,
resource: str | None,
) -> OAuthToken:
"""Exchange an external token for an MCP access token."""
...

async def load_access_token(self, token: str) -> AccessTokenT | None:
"""
Loads an access token by its token.
Expand Down
7 changes: 6 additions & 1 deletion src/mcp/server/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ def build_metadata(
scopes_supported=client_registration_options.valid_scopes,
response_types_supported=["code"],
response_modes_supported=None,
grant_types_supported=["authorization_code", "refresh_token"],
grant_types_supported=[
"authorization_code",
"refresh_token",
"client_credentials",
"token_exchange",
],
token_endpoint_auth_methods_supported=["client_secret_post"],
token_endpoint_auth_signing_alg_values_supported=None,
service_documentation=service_documentation_url,
Expand Down
45 changes: 39 additions & 6 deletions src/mcp/shared/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class OAuthToken(BaseModel):
expires_in: int | None = None
scope: str | None = None
refresh_token: str | None = None
issued_token_type: str | None = None

@field_validator("token_type", mode="before")
@classmethod
Expand Down Expand Up @@ -46,8 +47,15 @@ class OAuthClientMetadata(BaseModel):
# client_secret_post;
# ie: we do not support client_secret_basic
token_endpoint_auth_method: Literal["none", "client_secret_post"] = "client_secret_post"
# grant_types: this implementation only supports authorization_code & refresh_token
grant_types: list[Literal["authorization_code", "refresh_token"]] = [
# grant_types: this implementation supports authorization_code, refresh_token, client_credentials, & token_exchange
grant_types: list[
Literal[
"authorization_code",
"refresh_token",
"client_credentials",
"token_exchange",
]
] = [
"authorization_code",
"refresh_token",
]
Expand Down Expand Up @@ -114,10 +122,35 @@ class OAuthMetadata(BaseModel):
registration_endpoint: AnyHttpUrl | None = None
scopes_supported: list[str] | None = None
response_types_supported: list[str] = ["code"]
response_modes_supported: list[str] | None = None
grant_types_supported: list[str] | None = None
token_endpoint_auth_methods_supported: list[str] | None = None
token_endpoint_auth_signing_alg_values_supported: list[str] | None = None
response_modes_supported: (
list[
Literal[
"query",
"fragment",
"form_post",
"query.jwt",
"fragment.jwt",
"form_post.jwt",
"jwt",
]
]
| None
) = None
grant_types_supported: (
list[
Literal[
"authorization_code",
"refresh_token",
"client_credentials",
"token_exchange",
]
]
| None
) = None
token_endpoint_auth_methods_supported: list[Literal["none", "client_secret_post", "client_secret_basic"]] | None = (
None
)
token_endpoint_auth_signing_alg_values_supported: None = None
service_documentation: AnyHttpUrl | None = None
ui_locales_supported: list[str] | None = None
op_policy_uri: AnyHttpUrl | None = None
Expand Down
5 changes: 4 additions & 1 deletion src/mcp/shared/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,10 @@ async def send_notification(
message=JSONRPCMessage(jsonrpc_notification),
metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None,
)
await self._write_stream.send(session_message)
try:
await self._write_stream.send(session_message)
except (anyio.BrokenResourceError, anyio.ClosedResourceError):
logging.debug("Discarding notification due to closed stream")

async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
if isinstance(response, ErrorData):
Expand Down
Loading
Loading