Skip to content

Commit 231990b

Browse files
yakub268claude
andcommitted
feat: add subject and claims fields to AccessToken
Adds `subject: str | None` (JWT sub claim) and `claims: dict[str, Any] | None` (arbitrary JWT claims) to AccessToken to enable servers to identify the end-user behind an access token, not just the OAuth client. Also exposes `ctx.subject` on the MCPServer Context class via `get_access_token()` from the auth middleware context, following the same pattern as `ctx.client_id`. Fixes #1038 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dd52713 commit 231990b

File tree

4 files changed

+38
-1
lines changed

4 files changed

+38
-1
lines changed

src/mcp/server/auth/provider.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import Generic, Literal, Protocol, TypeVar
2+
from typing import Any, Generic, Literal, Protocol, TypeVar
33
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
44

55
from pydantic import AnyUrl, BaseModel
@@ -40,6 +40,8 @@ class AccessToken(BaseModel):
4040
scopes: list[str]
4141
expires_at: int | None = None
4242
resource: str | None = None # RFC 8707 resource indicator
43+
subject: str | None = None # JWT sub claim — identifies the end-user
44+
claims: dict[str, Any] | None = None # arbitrary JWT claims from the token
4345

4446

4547
RegistrationErrorCode = Literal[

src/mcp/server/mcpserver/context.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,18 @@ def client_id(self) -> str | None:
218218
"""Get the client ID if available."""
219219
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
220220

221+
@property
222+
def subject(self) -> str | None:
223+
"""Get the authenticated user's subject (JWT sub claim) if available.
224+
225+
This returns the subject claim from the OAuth access token, identifying
226+
the end-user on whose behalf the request is made.
227+
"""
228+
from mcp.server.auth.middleware.auth_context import get_access_token
229+
230+
token = get_access_token()
231+
return token.subject if token else None
232+
221233
@property
222234
def request_id(self) -> str:
223235
"""Get the unique ID for this request."""

tests/server/auth/middleware/test_auth_context.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def valid_access_token() -> AccessToken:
4141
client_id="test_client",
4242
scopes=["read", "write"],
4343
expires_at=int(time.time()) + 3600, # 1 hour from now
44+
subject="user_123",
4445
)
4546

4647

@@ -83,6 +84,27 @@ async def send(message: Message) -> None: # pragma: no cover
8384
assert get_access_token() is None
8485

8586

87+
@pytest.mark.anyio
88+
async def test_auth_context_middleware_subject_preserved(valid_access_token: AccessToken):
89+
"""Test that subject field on AccessToken is available via get_access_token()."""
90+
app = MockApp()
91+
middleware = AuthContextMiddleware(app)
92+
93+
user = AuthenticatedUser(valid_access_token)
94+
scope: Scope = {"type": "http", "user": user}
95+
96+
async def receive() -> Message: # pragma: no cover
97+
return {"type": "http.request"}
98+
99+
async def send(message: Message) -> None: # pragma: no cover
100+
pass
101+
102+
await middleware(scope, receive, send)
103+
104+
assert app.access_token_during_call is not None
105+
assert app.access_token_during_call.subject == "user_123"
106+
107+
86108
@pytest.mark.anyio
87109
async def test_auth_context_middleware_with_no_user():
88110
"""Test middleware with no user in scope."""

tests/server/auth/middleware/test_bearer_auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def valid_access_token() -> AccessToken:
7777
client_id="test_client",
7878
scopes=["read", "write"],
7979
expires_at=int(time.time()) + 3600, # 1 hour from now
80+
subject="user_123",
8081
)
8182

8283

0 commit comments

Comments
 (0)