Conversation
6e8bc21 to
ccf8be8
Compare
affe586 to
cca55cb
Compare
There was a problem hiding this comment.
Pull request overview
Adds an automatic access-token refresh mechanism intended to support long-lived MCP runtime sessions by refreshing auth tokens before expiry and propagating them to consumers.
Changes:
- Introduces
TokenRefresherand starts/stops it from the MCP runtime lifecycle. - Refactors session servers to share a runtime-provided
UiPathinstance (instead of constructing their own). - Adds async-capable tests (pytest-asyncio) plus a CI test workflow.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/uipath_mcp/_cli/_runtime/_token_refresh.py |
Implements background token refresh loop with OAuth and client-credentials strategies. |
src/uipath_mcp/_cli/_runtime/_runtime.py |
Instantiates/starts token refresher and passes shared UiPath instance into session servers. |
src/uipath_mcp/_cli/_runtime/_session.py |
Updates session base class to accept UiPath via DI; adjusts StdioSessionServer internals. |
tests/test_token_refresh.py |
Adds unit tests covering strategy detection, refresh timing, propagation, and retry behavior. |
tests/conftest.py |
Adds autouse fixture to clear relevant env vars between tests. |
pyproject.toml |
Bumps version, adds pytest-asyncio, and configures pytest asyncio mode. |
uv.lock |
Locks pytest-asyncio and bumps local package version entry. |
.github/workflows/test.yml |
Adds a test workflow running pytest on a Python/OS matrix. |
.github/workflows/ci.yml |
Wires the new test workflow into CI. |
Comments suppressed due to low confidence (1)
src/uipath_mcp/_cli/_runtime/_session.py:299
_server_stderr_outputis declared as a class attribute here; the instance attribute is only created oncestart()runs. This can be confusing and can lead to shared state ifoutputis accessed beforestart(). Prefer initializing it as an instance attribute (e.g., in an explicit__init__) or at least set it onselfduring construction to make the per-session lifecycle clear.
class StdioSessionServer(BaseSessionServer):
"""Manages a stdio server process for a specific session."""
_server_stderr_output: str | None = None
@property
def output(self) -> str | None:
"""Returns the captured stderr output from the MCP server process."""
return self._server_stderr_output
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| bearer_token = self._uipath._config.secret | ||
| self._token_refresher = TokenRefresher(self._uipath) | ||
|
|
There was a problem hiding this comment.
The token refresher updates self._uipath._config.secret, but the SignalR client is initialized with a snapshot bearer_token and a fixed Authorization header. If the websocket handshake/connection requires a valid bearer token over time, refreshing the token won’t affect the already-created SignalRClient and the connection may still drop when the original token expires. Consider wiring the refreshed token into the SignalR client (e.g., reconnect on refresh, or supply a header/token callback if the library supports it) so the long-lived runtime connection actually benefits from the refresh mechanism.
There was a problem hiding this comment.
The SignalR client's Authorization header is intentionally left alone. During /negotiate, the server validates our token and returns an Azure SignalR-issued accessToken, which pysignalr uses to replace the Authorization header. Overwriting it with a refreshed UiPath token would cause 401s on WebSocket reconnection.
cca55cb to
93aecb9
Compare
| response.raise_for_status() | ||
| return TokenData.model_validate(response.json()) | ||
|
|
||
| def _propagate_token(self, token_data: TokenData) -> None: |
There was a problem hiding this comment.
TokenRefresher._propagate_token() replaces self._uipath._config with a new UiPathApiConfig. If any downstream code cached headers/clients at construction time, they may still use the old token. If the SDK constructs clients lazily per request, this is fine, but can you confirm this? Tbh I need more context to understand.
It would be nice to have a test that demonstrates refreshed token is actually used by subsequent requests in a session.
There was a problem hiding this comment.
The SDK constructs clients lazily per request indeed. UiPath.api_client is a @property (not @cached_property), so every call to self._uipath.api_client.request_async() creates a fresh ApiClient that reads from the current self._uipath._config.
| ) | ||
|
|
||
| data = { | ||
| "grant_type": "client_credentials", |
There was a problem hiding this comment.
if UIPATH_CLIENT_ID and UIPATH_CLIENT_SECRET exist -> CLIENT_CREDENTIALS
else if auth file has refresh_token ->OAUTH
else NONE
Did i understand correctly?
Is it intended that env vars override user login refresh token always?
Should UIPATH_CLIENT_SCOPE default to OR.Execution for MCP runtime, or should it be configurable/derived?
| - name: Run tests | ||
| run: uv run pytest | ||
|
|
||
| continue-on-error: true |
There was a problem hiding this comment.
If the intent is to enforce tests, remove continue-on-error: true. If the intent is “informational only” until stabilized, add a comment explaining why, and consider making it temporary.
There was a problem hiding this comment.
I left it like this to ensure consistency across our other SDKs. I think we could let the whole test matrix run.
This pull request introduces a token refresh mechanism for long-lived MCP runtime connections, ensuring that authentication tokens are automatically refreshed before expiration.