feat(mcp): long-running UiPath jobs over MCP (uipath.com/job) — self-contained, no uipath-python dep#907
Open
edis-uipath wants to merge 3 commits into
Open
Conversation
Upgrades the `mcp` SDK from 1.26.0 to the latest stable 1.27.2, whose LATEST_PROTOCOL_VERSION is "2025-11-25". The MCP client negotiates the SDK's latest protocol version on initialize, so this is what lets UiPath MCP servers offer tasks to the agent (prerequisite for suspend-on-UiPath-task). The forked streamable-HTTP transport (which adds UiPath's session-id save/provide layer, needed so a suspended agent resumes onto the same MCP session) is kept as-is: the official client/streamable_http.py is byte-identical between 1.26.0 and 1.27.2, so there is nothing to re-sync, and the official client still offers no way to provide an initial session id through its public entrypoint. Adds a guard test that LATEST_PROTOCOL_VERSION is 2025-11-25, the task result types (CreateTaskResult / GetTaskResult / TaskMetadata) are importable, and Result exposes _meta. Full test suite green (2081 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes tests/agent/tools/test_mcp/test_protocol_version.py. It pinned task-type imports (CreateTaskResult / TaskMetadata) and the 2025-11-25 constant — assumptions the long-running-job design has moved away from: the chosen _meta-based contract doesn't use the SDK task types (which 2.x removes anyway). The version bump stands on the rest of the mcp suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds client-side support for long-running UiPath Orchestrator jobs surfaced via MCP tools (uipath.com/job) by negotiating capability on initialize, sending START/FETCH _meta, and (by default) suspending/resuming a LangGraph agent instead of blocking.
Changes:
- Introduces a framework-neutral
uipath.com/job_metacontract layer (jobs.py) plus a polling fallback executor. - Extends
McpClientto parse job capability advertisement and to thread request_metathroughcall_tool(..., meta=...). - Routes tool invocations through an injected job executor when the server advertises job support; adds a LangGraph suspend/resume executor and corresponding tests/docs.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/agent/tools/test_mcp/test_mcp_tool.py | Updates existing MCP tool factory tests to account for job-awareness gating. |
| tests/agent/tools/test_mcp/test_mcp_jobs.py | Adds new test coverage for advertisement parsing, START/FETCH _meta, and LangGraph executor behavior. |
| tests/agent/tools/test_mcp/claude.md | Updates test inventory documentation to include new job tests. |
| src/uipath_langchain/agent/tools/mcp/mcp_tool.py | Adds job-aware invocation path, START/FETCH meta wiring, and executor injection/defaulting. |
| src/uipath_langchain/agent/tools/mcp/mcp_client.py | Parses initialize job advertisement and adds meta passthrough on call_tool. |
| src/uipath_langchain/agent/tools/mcp/jobs.py | New framework-neutral job _meta helpers and a blocking (polling) executor. |
| src/uipath_langchain/agent/tools/mcp/job_executor.py | New LangGraph-specific executor that suspends on WaitJobRaw and fetches on resume. |
| src/uipath_langchain/agent/tools/mcp/claude.md | Documents the uipath.com/job flow and where each piece lives. |
| src/uipath_langchain/agent/tools/mcp/init.py | Exposes new job-related public exports. |
Comment on lines
+265
to
+269
| version = read_job_version(init_result.meta) | ||
| if version is not None: | ||
| self._job_aware = True | ||
| self._job_version = version | ||
| logger.info(f"MCP server advertised uipath.com/job v{version}") |
Comment on lines
+195
to
+199
| async def _wait_until_terminal(self, handle: UiPathJobHandle) -> None: | ||
| jobs = self._jobs_service() | ||
| loop = asyncio.get_event_loop() | ||
| deadline = None if self._timeout is None else loop.time() + self._timeout | ||
| while True: |
…th.com/job), self-contained Implements Option 6 of the long-running-jobs design entirely within uipath-langchain, with NO change to base uipath-python. A UiPath MCP server that backs a tool with an Orchestrator job advertises `uipath.com/job` on initialize; the client opts in per call and suspends the LangGraph agent while the job runs, resuming with the result — all over MCP `_meta`, so deployed agents gain the behavior on package upgrade with no agent.json change. Why self-contained (vs the base-SDK core in uipath-python #1717): base uipath-platform has zero MCP-protocol code today (only WaitJob*/CreateTask* wait-models and McpService CRUD). All MCP protocol code and every framework tool (process_tool, escalation_tool) already live here; an McpJobExecutor is the same kind of glue. This keeps base generic, removes the cross-package release coupling, and the base already provides the generic durable primitive we build on (WaitJobRaw + jobs service). See the §3b placement debate in mcp-longrunning-jobs-design.md. - `jobs.py` (new): the `uipath.com/job` `_meta` helpers + `UiPathJobHandle`, `JobStart`, `McpJobExecutor` Protocol, `BlockingJobExecutor` — framework- and MCP-SDK-neutral (plain dicts; imports only the already-published `Job`/`JobState`/jobs service). - `mcp_client.py`: read the initialize advertisement (`is_job_aware`/`job_version`), thread request `_meta` through `call_tool(..., meta=)`. - `mcp_tool.py`: `_invoke_job_aware` sends START `_meta`, parses the handle, delegates to the injected `McpJobExecutor` with neutral `start`/`fetch` closures. - `job_executor.py`: `LangGraphJobExecutor` (default) — START inside `@durable_interrupt`, interrupt with `WaitJobRaw`, on resume re-derive the handle and FETCH. `interrupt` is confined here. No `uipath-platform` floor bump, no #1717 dependency → CI is green standalone. This is an alternative to #906 (which stacks on #1717); pick one. Tests: 9 new (advertisement, START/FETCH `_meta`, executor suspend/resume/faulted); full test_mcp suite green (233 incl. circular-import guard); ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7133dc6 to
a046bbc
Compare
ccb95d3 to
9212b63
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Suspend/resume the agent on long-running UiPath jobs (
uipath.com/job) — self-containedImplements Option 6 of the design entirely within uipath-langchain, with no change to base uipath-python. A UiPath MCP server that backs a tool with an Orchestrator job advertises
uipath.com/joboninitialize; the client opts in per call and suspends the LangGraph agent while the job runs, resuming with the result — all over MCP_meta, so a deployed agent gains the behavior on package upgrade with noagent.jsonchange.Why self-contained
Base
uipath-platformhas zero MCP-protocol code today — only the genericWaitJob/WaitJobRaw/CreateTask/WaitEscalationwait-models andMcpService(MCP-server-as-resource CRUD). All MCP protocol code (thestreamable_http/mcp_client/mcp_toolfork) and every framework tool (process_tool,escalation_tool,context_tool) already live in this package and consume a generic base wait-model. AnMcpJobExecutoris the same kind of glue, and the generic durable primitive it builds on (WaitJobRaw+ the jobs service) already ships in base. Putting MCP_metasemantics in base would be the first MCP-protocol code there — against the grain — and would add cross-package release coupling. (Full reasoning: §3b "placement debate" in the design doc;uipath-agentsalready imports the MCP layer from this package, not from base.)What's here
jobs.py(new): theuipath.com/job_metahelpers +UiPathJobHandle,JobStart,McpJobExecutorProtocol,BlockingJobExecutor— framework- and MCP-SDK-neutral (plain dicts; imports only the already-publishedJob/JobState/jobs service).mcp_client.py: reads the initialize advertisement (is_job_aware/job_version); threads request_metathroughcall_tool(..., meta=).mcp_tool.py:_invoke_job_awaresends START_meta, parses the handle, delegates to the injectedMcpJobExecutorwith neutralstart/fetchclosures.job_executor.py:LangGraphJobExecutor(default) — START inside@durable_interrupt, interrupt withWaitJobRaw, on resume re-derive the handle and FETCH.interruptis confined here; the neutral core never imports langgraph.Non-job tools on a job-aware server resolve via
SkipInterruptValue(no real suspend; durable index stays aligned). Old/non-UiPath servers never advertise → today's blocking path (back-compat).Testing
test_mcp_jobs.py); fulltest_mcpsuite green (233 incl. circular-import guard);ruff+mypyclean.Base:
chore/upgrade-mcp-1.27.2(#904). Pairs with AgentHubService #1332 (server side).🤖 Generated with Claude Code