Conversation
|
If review wanted, happy to review it. |
|
@Kludex its work in progress as far as I undersatnd but feel free to take a review pass. |
| def validate_scope(requested_scope: str | None, client: OAuthClientInformationFull) -> list[str] | None: | ||
| if requested_scope is None: | ||
| return None | ||
| requested_scopes = requested_scope.split(" ") | ||
| allowed_scopes = [] if client.scope is None else client.scope.split(" ") | ||
| for scope in requested_scopes: | ||
| if scope not in allowed_scopes: | ||
| raise InvalidRequestError(f"Client was not registered with scope {scope}") | ||
| return requested_scopes |
There was a problem hiding this comment.
This feels like a method on OAuthClientInformationFull or the underlying scope model, given we exclusively operate on client.scope here.
There was a problem hiding this comment.
[opinion] after hacking around in here, I actually think we should be thinking about OAuthClientInformationFull as a type similar to the ones in mcp.types, and it would be odd to add methods there
| redirect_uri=redirect_uri, | ||
| ) | ||
|
|
||
| response = RedirectResponse(url="", status_code=302, headers={"Cache-Control": "no-store"}) |
There was a problem hiding this comment.
We should probalby not redirect without a URI?
| if "Authorization" not in conn.headers: | ||
| return None | ||
|
|
||
| auth_header = conn.headers["Authorization"] | ||
| if not auth_header.startswith("Bearer "): | ||
| return None | ||
|
|
||
| token = auth_header[7:] # Remove "Bearer " prefix |
There was a problem hiding this comment.
I think this can be simplified by
| if "Authorization" not in conn.headers: | |
| return None | |
| auth_header = conn.headers["Authorization"] | |
| if not auth_header.startswith("Bearer "): | |
| return None | |
| token = auth_header[7:] # Remove "Bearer " prefix | |
| auth_header = conn.headers.get("Authorization") | |
| if not auth_header or not auth_header.startswith("Bearer "): | |
| return None | |
| token = auth_header[7:] # Remove "Bearer " prefix |
| # Validate the token with the provider | ||
| auth_info = await self.provider.verify_access_token(token) | ||
|
|
||
| if auth_info.expires_at and auth_info.expires_at < int(time.time()): |
There was a problem hiding this comment.
We should use datetime comparisions and not rely on timestamps.
src/mcp/shared/auth.py
Outdated
| client_id: str | ||
| client_secret: Optional[str] = None | ||
| client_id_issued_at: Optional[int] = None | ||
| client_secret_expires_at: Optional[int] = None |
There was a problem hiding this comment.
Probably should be parsed as a Timedelta
There was a problem hiding this comment.
A timedelta, or a datetime? Don't we want an absolute time here?
src/mcp/shared/auth.py
Outdated
| """ | ||
| access_token: str | ||
| token_type: str | ||
| expires_in: Optional[int] = None |
There was a problem hiding this comment.
Also here, probably watn to parse as a timedelta.
src/mcp/shared/auth.py
Outdated
| Corresponds to OAuthTokensSchema in src/shared/auth.ts | ||
| """ | ||
| access_token: str | ||
| token_type: str |
There was a problem hiding this comment.
Should we enumerate the potential types here to ensure pydnatic validation?
| from httpx._transports.base import AsyncBaseTransport | ||
| from httpx._models import Request, Response | ||
| from httpx._types import AsyncByteStream | ||
| import asyncio |
There was a problem hiding this comment.
we don't use asyncio for testing.
jerome3o-anthropic
left a comment
There was a problem hiding this comment.
Working through the changes now
src/mcp/server/auth/json_response.py
Outdated
|
|
||
| class PydanticJSONResponse(JSONResponse): | ||
| def render(self, content: Any) -> bytes: | ||
| return content.model_dump_json(exclude_none=True).encode("utf-8") No newline at end of file |
There was a problem hiding this comment.
Lemme add a comment - this uses Pydantic's JSON serialization, since the stock json doesn't know how to serialize eg: AnyHttpUrl
65db7b6 to
5e7a0bf
Compare
|
The auth part on the client should be implemented using the httpx.Auth class. |
bhosmer-ant
left a comment
There was a problem hiding this comment.
New changes LGTM (and things look relatively undisturbed from the last time it was looked over in some depth) - based on that, tests passing, and your e2e check passing, I'm good with landing it. (would ofc benefit from @jerome3o-anthropic also looking before you push the button, mod available time)
src/mcp/server/sse.py
Outdated
| ) | ||
|
|
||
| # Ensure all streams are properly closed | ||
| async with read_stream, write_stream, read_stream_writer, sse_stream_reader: |
There was a problem hiding this comment.
ooc was the outer with unnecessary or was it actively messing things up?
There was a problem hiding this comment.
I just reverted all the changes to see transport, returning response was not needed at all.
The fun part for the top async with was that it was masking "bad test" and sse tests were just hanging. When reverted all the sse changes notices that some other test randomly fails with " ResourceWarning: Unclosed <MemoryObjectReceiveStream at 106c0dd20>"
There was a problem hiding this comment.
I think it's a nice update, I would prefer to have it in a separate PR and test separately. There are a bunch of open PRs addressing different parts of memory leaks, maybe we can just bundle all of them together
| ) | ||
|
|
||
|
|
||
| class TestFastMCPWithAuth: |
There was a problem hiding this comment.
was this the test that leaked and made the other ones hang?
There was a problem hiding this comment.
yes, basically this :
async with aconnect_sse(
test_client, "GET", "/sse", headers={"Authorization": authorization}
) as event_source:
jerome3o-anthropic
left a comment
There was a problem hiding this comment.
Thanks for pushing this over the line @ihrpr - it's awesome to see it
I've looked over this, not with a fine-toothed comb but for the general shapes, and it LGTM!
| providing an implementation of the `OAuthServerProvider` protocol. | ||
|
|
||
| ``` | ||
| mcp = FastMCP("My App", |
There was a problem hiding this comment.
[non blocking] wonder if its worth noting that this wont scale beyond one instance
There was a problem hiding this comment.
I think that's something we can add to the example
Motivation and Context
Implements https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/ from the specification.
How Has This Been Tested?
I've added some integration tests as part of this branch. I've also implemented a new MCP server (outside of this repo), and integrated it against the new parts of the the SDK, to play around with the interface a bit and see how it feels.
Tested with inspector:
Breaking Changes
No - this is strictly additive.
Types of changes
Checklist
Additional context