Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR adds comprehensive SSE (Server-Sent Events) transport support for MCP clients with OAuth authentication and secure token storage. The implementation includes session management, keychain-based token storage with file fallback, and an integrated auth server for OAuth flows.
- Add SSE transport support with OAuth client provider integration
- Implement dual storage strategy for OAuth tokens (keychain + file fallback)
- Add session manager for pre-initialized MCP client connections
Reviewed Changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pyproject.toml | Adds new dependencies for SSE support (fastapi, uvicorn, keyring) and updates MCP version |
| state.py | Integrates token storage management with project state |
| mcp_client/token_storage.py | Implements keychain-based token storage with secure file fallback |
| mcp_client/session_manager.py | Manages pre-initialized MCP client sessions for improved performance |
| auth_server/ | FastAPI-based OAuth callback server for handling authorization flows |
| engine/ | Refactors template engine with improved module organization |
| util/terminal.py | Adds terminal utility functions for hyperlink support |
| tests/ | Updates test templates and improves test isolation |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
mcp_client/token_storage.py
Outdated
| # Write back to file with restricted permissions | ||
| with open(self.fallback_file, "w") as f: | ||
| json.dump(existing_data, f, indent=2) | ||
|
|
||
| # Set restrictive permissions (owner read/write only) | ||
| os.chmod(self.fallback_file, stat.S_IRUSR | stat.S_IWUSR) |
There was a problem hiding this comment.
There's a race condition where the file is created with default permissions before being restricted. The file should be created with restrictive permissions from the start using os.open() with mode parameter or setting umask.
| # Write back to file with restricted permissions | |
| with open(self.fallback_file, "w") as f: | |
| json.dump(existing_data, f, indent=2) | |
| # Set restrictive permissions (owner read/write only) | |
| os.chmod(self.fallback_file, stat.S_IRUSR | stat.S_IWUSR) | |
| # Write back to file with restricted permissions (owner read/write only) | |
| flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | |
| mode = stat.S_IRUSR | stat.S_IWUSR # 0o600 | |
| with os.fdopen(os.open(self.fallback_file, flags, mode), "w") as f: | |
| json.dump(existing_data, f, indent=2) |
There was a problem hiding this comment.
Pull Request Overview
This PR adds SSE (Server-Sent Events) transport support to the MCP client, implements OAuth authentication flows, and introduces keychain-based token storage. The changes enable secure authentication with MCP servers and persistent token storage across sessions.
Key changes:
- SSE transport support with OAuth authentication flows
- Keychain-based token storage with file fallback for security
- FastAPI-based auth server for handling OAuth callbacks
Reviewed Changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pyproject.toml | Added dependencies for FastAPI, keyring, uvicorn, and upgraded MCP version |
| mcp_client/token_storage.py | New keychain storage implementation with file fallback |
| auth_server/auth_server.py | FastAPI-based OAuth callback server |
| mcp_client/session_manager.py | Session management for pre-initialized MCP connections |
| mcp_client/client_session_provider.py | Updated to support SSE with OAuth and token storage |
| tests/e2e/test_e2e.py | Updated test fixtures for better isolation |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| if self._callback_future and not self._callback_future.done(): | ||
| # Schedule the exception in the main event loop | ||
| if self._main_loop: | ||
| self._main_loop.call_soon_threadsafe( | ||
| self._callback_future.set_exception, | ||
| Exception(f"OAuth error: {error} - {error_description}"), | ||
| ) | ||
| return HTMLResponse( | ||
| content=f"<html><body><h2>Authentication Error</h2><p>{error}: {error_description}</p></body></html>", # noqa: E501 | ||
| status_code=400, | ||
| ) | ||
|
|
||
| if not code: | ||
| if self._callback_future and not self._callback_future.done(): | ||
| # Schedule the exception in the main event loop | ||
| if self._main_loop: | ||
| self._main_loop.call_soon_threadsafe( | ||
| self._callback_future.set_exception, Exception("No authorization code received") | ||
| ) |
There was a problem hiding this comment.
The callback error handling logic is duplicated in multiple places. Consider extracting this into a helper method like _set_callback_exception() to reduce code duplication.
| if self._callback_future and not self._callback_future.done(): | |
| # Schedule the exception in the main event loop | |
| if self._main_loop: | |
| self._main_loop.call_soon_threadsafe( | |
| self._callback_future.set_exception, | |
| Exception(f"OAuth error: {error} - {error_description}"), | |
| ) | |
| return HTMLResponse( | |
| content=f"<html><body><h2>Authentication Error</h2><p>{error}: {error_description}</p></body></html>", # noqa: E501 | |
| status_code=400, | |
| ) | |
| if not code: | |
| if self._callback_future and not self._callback_future.done(): | |
| # Schedule the exception in the main event loop | |
| if self._main_loop: | |
| self._main_loop.call_soon_threadsafe( | |
| self._callback_future.set_exception, Exception("No authorization code received") | |
| ) | |
| self._set_callback_exception(Exception(f"OAuth error: {error} - {error_description}")) | |
| return HTMLResponse( | |
| content=f"<html><body><h2>Authentication Error</h2><p>{error}: {error_description}</p></body></html>", # noqa: E501 | |
| status_code=400, | |
| ) | |
| if not code: | |
| self._set_callback_exception(Exception("No authorization code received")) |
Note: This requires a yet to be released version of the python MCP SDK due to a timeout bug in the current version.