Skip to content

Commit 7e542e9

Browse files
committed
Clarify subject docstrings and add Context.claims
- Reword AccessToken.subject's client_credentials note to match RFC 9068 §2.2 (sub may identify the client itself rather than being unset). - Add Context.claims so handlers can read iss/act/etc without importing get_access_token and handling the raw token model. - Document that Context.client_id reads MCP request _meta, not the OAuth client_id, so it isn't mistaken for the pair to Context.subject.
1 parent c4fe4d2 commit 7e542e9

3 files changed

Lines changed: 49 additions & 3 deletions

File tree

src/mcp/server/auth/provider.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ class AccessToken(BaseModel):
4747
`sub` from a JWT (RFC 9068) or introspection response (RFC 7662). Token
4848
verifiers should populate this whenever an end-user is involved so request
4949
handlers and transports can distinguish users that share an OAuth client.
50-
Conventionally unset for `client_credentials` tokens."""
50+
For `client_credentials` grants there is no end-user; `sub` may then
51+
identify the client itself (RFC 9068 §2.2) or be absent, depending on the
52+
authorization server."""
5153
claims: dict[str, Any] | None = None
5254
"""Additional verified claims (e.g. `iss`, `act`) for request handlers."""
5355

src/mcp/server/mcpserver/context.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,15 @@ async def log(
211211

212212
@property
213213
def client_id(self) -> str | None:
214-
"""Get the client ID if available."""
214+
"""Get the client ID if available.
215+
216+
Note: this reads from the MCP request's `_meta` params, not from the
217+
OAuth bearer token. It is unrelated to `subject` and `claims` below,
218+
which come from the authenticated `AccessToken`. For the OAuth
219+
`client_id`, use `get_access_token().client_id`.
220+
221+
TODO(maxisbey): see if this is needed otherwise remove
222+
"""
215223
return self.request_context.meta.get("client_id") if self.request_context.meta else None # pragma: no cover
216224

217225
@property
@@ -225,6 +233,18 @@ def subject(self) -> str | None:
225233
token = get_access_token()
226234
return token.subject if token is not None else None
227235

236+
@property
237+
def claims(self) -> dict[str, Any] | None:
238+
"""Additional verified claims from the bearer token, if any.
239+
240+
Returns `AccessToken.claims` for the current request so handlers can
241+
read values like `iss` or `act` without importing `get_access_token`
242+
and handling the raw token directly. `None` when unauthenticated or
243+
when the token verifier did not populate claims.
244+
"""
245+
token = get_access_token()
246+
return token.claims if token is not None else None
247+
228248
@property
229249
def request_id(self) -> str:
230250
"""Get the unique ID for this request."""

tests/server/mcpserver/auth/test_context_subject.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Context.subject reads the resource owner from the request's access token."""
1+
"""Context.subject and Context.claims read from the request's access token."""
22

33
from mcp.server.auth.middleware.auth_context import auth_context_var
44
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
@@ -40,3 +40,27 @@ def test_subject_tracks_current_auth_context():
4040
auth_context_var.reset(cv_token)
4141

4242
assert ctx.subject is None
43+
44+
45+
def test_claims_is_none_when_unauthenticated():
46+
assert Context().claims is None
47+
48+
49+
def test_claims_is_none_when_token_has_no_claims():
50+
user = AuthenticatedUser(AccessToken(token="t", client_id="c", scopes=[]))
51+
cv_token = auth_context_var.set(user)
52+
try:
53+
assert Context().claims is None
54+
finally:
55+
auth_context_var.reset(cv_token)
56+
57+
58+
def test_claims_reads_from_access_token():
59+
user = AuthenticatedUser(
60+
AccessToken(token="t", client_id="c", scopes=[], claims={"iss": "https://auth.example.com"})
61+
)
62+
cv_token = auth_context_var.set(user)
63+
try:
64+
assert Context().claims == {"iss": "https://auth.example.com"}
65+
finally:
66+
auth_context_var.reset(cv_token)

0 commit comments

Comments
 (0)