Skip to content

feat: add token refresher#179

Open
radugheo wants to merge 1 commit intomainfrom
feat/add-token-refresh-mechanism
Open

feat: add token refresher#179
radugheo wants to merge 1 commit intomainfrom
feat/add-token-refresh-mechanism

Conversation

@radugheo
Copy link
Collaborator

@radugheo radugheo commented Feb 24, 2026

This pull request introduces a token refresh mechanism for long-lived MCP runtime connections, ensuring that authentication tokens are automatically refreshed before expiration.

@radugheo radugheo closed this Feb 24, 2026
@radugheo radugheo reopened this Feb 24, 2026
@radugheo radugheo marked this pull request as draft February 24, 2026 14:16
@radugheo radugheo changed the title feat: add token refresher 🚧feat: add token refresher Feb 24, 2026
@radugheo radugheo changed the base branch from feat/support-streamable-http to main February 24, 2026 15:24
@radugheo radugheo force-pushed the feat/add-token-refresh-mechanism branch 3 times, most recently from 6e8bc21 to ccf8be8 Compare February 25, 2026 09:07
uses: actions/checkout@v4

- name: Setup uv
uses: astral-sh/setup-uv@v5

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow Medium test

Unpinned 3rd party Action 'Test' step
Uses Step
uses 'astral-sh/setup-uv' with ref 'v5', not a pinned commit hash
@radugheo radugheo force-pushed the feat/add-token-refresh-mechanism branch 3 times, most recently from affe586 to cca55cb Compare February 25, 2026 09:21
@radugheo radugheo marked this pull request as ready for review February 25, 2026 09:30
Copilot AI review requested due to automatic review settings February 25, 2026 09:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TokenRefresher and starts/stops it from the MCP runtime lifecycle.
  • Refactors session servers to share a runtime-provided UiPath instance (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_output is declared as a class attribute here; the instance attribute is only created once start() runs. This can be confusing and can lead to shared state if output is accessed before start(). Prefer initializing it as an instance attribute (e.g., in an explicit __init__) or at least set it on self during 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.

Comment on lines 211 to +213
bearer_token = self._uipath._config.secret
self._token_refresher = TokenRefresher(self._uipath)

Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@radugheo radugheo force-pushed the feat/add-token-refresh-mechanism branch from cca55cb to 93aecb9 Compare February 25, 2026 10:21
@radugheo radugheo changed the title 🚧feat: add token refresher feat: add token refresher Feb 25, 2026
@radugheo radugheo requested a review from edis-uipath February 25, 2026 10:22
response.raise_for_status()
return TokenData.model_validate(response.json())

def _propagate_token(self, token_data: TokenData) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

@radugheo radugheo Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

@radugheo radugheo Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it like this to ensure consistency across our other SDKs. I think we could let the whole test matrix run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants