Support the MCP Tasks extension (SEP-2663) in McpToolset: expose the mcp 2.x client extension seam
- 主要语言
- Python
- 星标
- 21.5k
- 派生
- 4k
- 平均合并
- 1 天 14 小时
- 30 天内合并 PR
- 37
描述
** Please make sure you read the contribution guide and file the issues in the right place. **
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)
## 🔴 Required Information
### Is your feature request related to a specific problem?
We run an MCP server that fronts physical hardware. A reboot, a firmware flash or an instrumented run takes minutes, not seconds — and the call is behind an HTTP gateway with its own write timeout, so "just hold the connection open" is not available to us.
Today `McpToolset` has exactly one shape for a tool call: `session.call_tool(...)` awaited to a `CallToolResult` (`mcp_tool.py:470-475`, verified at `main@1cd6f464`). The only relief valves are `sse_read_timeout` (default 300s, `mcp_session_manager.py:218`) and progress notifications (#3398), and neither gives a durable handle — a dropped connection loses the operation outright.
So servers like ours split one operation into `start_x` / `x_status` / `x_result` tools and let the model drive the polling loop. That is unreliable (it depends on prompt engineering), it burns context, and it is the exact anti-pattern the MCP spec set out to remove.
MCP has a standard answer now: the **Tasks extension**, identifier `io.modelcontextprotocol/tasks`, specified in [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663) (merged 2026-05-15) and maintained at [modelcontextprotocol/ext-tasks](https://github.com/modelcontextprotocol/ext-tasks). The server decides per request that the work is long-running and answers `tools/call` with `resultType: "task"` plus a `taskId`; the client polls `tasks/get` at the server's `pollIntervalMs` until a terminal status and reads the final `result`. One tool declaration, no extra tools in the model's context, and the handle survives a reconnect.
`McpToolset` has no path to this. There is no protocol-level `task` anywhere under `src/google/adk/tools/mcp_tool/`, and #3449 has no entry for it.
**To be clear about what this is not:** this is not the experimental `2025-11-25` tasks (SEP-1686). SEP-2663 removed those from the core spec, and in the mcp 1.x SDK both `ClientSession.experimental` and `mcp.client.experimental.task_handlers` are deprecated for removal in 2.0 (`mcp/client/session.py:25-28` in `mcp==1.29.0`). Nothing here asks for that design — in particular not the `Tool.execution.taskSupport` / `tools/list` warmup handshake, which SEP-2663 deliberately dropped in favour of a single capability-level opt-in.
### Describe the Solution You'd Like
Two steps. The first is small and is the one we actually need.
**1. Pass the mcp 2.x client extension seam through `McpToolset`.**
`mcp==2.0.0` gives `ClientSession.__init__` three new keyword arguments — `extensions`, `result_claims`, `notification_bindings` (`mcp/client/session.py:385-387`) — which are precisely the machinery an extension needs: `extensions` advertises the identifier in per-request client capabilities, and a `ResultClaim` keyed on `resultType` folds a non-core result shape into `tools/call` parsing.
ADK already threads `sampling_callback` / `sampling_capabilities` / `elicitation_callback` down exactly the chain those arguments would follow:
- `McpToolset.__init__` — `mcp_toolset.py:171-173`
- → `MCPSessionManager` — `mcp_toolset.py:256-262`, `mcp_session_manager.py:543-545`
- → `SessionContext` — `mcp_session_manager.py:1011-1019`
- → `ClientSession` — `session_context.py:347-369`
Adding the three extension kwargs to that same chain is the same shape of change as the elicitation plumbing in #6422, and it lets Tasks — or any other official extension — be implemented outside ADK without forking the toolset.
This depends on the mcp 2.x bump tracked in #6532 / #6537 and only makes sense on top of it.
**2. Ship Tasks natively in the toolset (follow-up).**
Opt-in, off by default. When it is on and the server advertises `io.modelcontextprotocol/tasks`, `McpToolset` advertises it too; on a `resultType: "task"` response it polls `tasks/get` at the server's `pollIntervalMs` until terminal, returns the `result` as the tool's `CallToolResult`, and sends `tasks/cancel` if the invocation is cancelled. The agent sees the same tool and the same result it sees today — only the wire changes.
For callers that want the handle rather than a blocking poll, `BaseTool.is_long_running` (`base_tool.py:59`) is the natural mapping, but that can wait; blocking-with-polling already solves the timeout and reconnect problems.
### Impact on your work
This is blocking for us. Every hardware operation either has to fit inside an HTTP write timeout or be re-expressed as a start/status/result triplet with the model as the scheduler. Step 1 alone unblocks us, because we can then carry the extension ourselves.
No timeline pressure beyond #6532 — we are not asking for anything before the mcp 2.x bump lands.
### Willingness to contribute
Yes. Happy to send the step-1 pass-through PR on top of #6537, and to prototype step 2 against a Tasks conformance server.
---
## 🟡 Recommended Information
### Describe Alternatives You've Considered
- **start / status / result tool triplets.** What we do today. Model-driven polling, so it depends on prompt engineering to happen at all, and it costs a model turn per poll. This is the motivating problem in SEP-1686 and SEP-2663.
- **Raising `sse_read_timeout` and blocking longer.** Intermediaries cap the request duration regardless, and a blocked call has no crash resilience: if the connection drops, the result is gone with no handle to resume from.
- **Progress notifications (#3398).** They give visibility into a call that is still open. They do not give a durable handle, and the call still has to stay open.
- **Subclassing `McpTool` to poll ourselves.** Two blockers: extension negotiation happens on the `ClientSession`, which the toolset owns and does not expose; and the toolset constructs the `McpTool` instances itself (`mcp_toolset.py:518-527`), so there is no supported injection point.
### Proposed API / Implementation
Step 1, as a pass-through:
```python
# google/adk/tools/mcp_tool/mcp_toolset.py
toolset = McpToolset(
connection_params=StreamableHTTPConnectionParams(url=...),
# forwarded verbatim to mcp.ClientSession, like sampling_callback today
extensions={"io.modelcontextprotocol/tasks": {}},
result_claims={"io.modelcontextprotocol/tasks": [tasks_claim]},
)
```
Step 2, as an opt-in on the same object:
```python
toolset = McpToolset(
connection_params=...,
# advertise the extension, and resolve `resultType: "task"` by polling
# tasks/get until terminal before returning the CallToolResult
enable_tasks=True,
)
```
### Additional Context
- Extension identifier and negotiation:
- SEP-2663 (merged 2026-05-15):
- Extension spec repository: (self-described as experimental; the negotiation mechanism it relies on is stable in `2026-07-28`)
- The Tasks extension is not yet listed in the [extension client matrix](https://modelcontextprotocol.io/extensions/client-matrix), so as far as we can tell no major host ships it yet. Step 1 is what lets ADK users move without waiting on that.
贡献指南
评估
这个 Issue 还没有评估数据。