google / google/adk-python

Support the MCP Tasks extension (SEP-2663) in McpToolset: expose the mcp 2.x client extension seam

Abierto
#6,826 3 comentarios 0 reacciones 1 asignado Reclamado por @sanketpatil06 Ver en GitHub
mcp
Lenguaje dominante
Python
Estrellas
21.5k
Forks
4k
Merge medio
1 d 14 h
PR fusionados (30 d)
37

Descripción

** 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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.