Skip to content

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

Description

@zeishr

** Please make sure you read the contribution guide and file the issues in the right place. **
Contribution 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 (merged 2026-05-15) and maintained at 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
  • MCPSessionManagermcp_toolset.py:256-262, mcp_session_manager.py:543-545
  • SessionContextmcp_session_manager.py:1011-1019
  • ClientSessionsession_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 (Support for MCP Progress, Notifications, and Logging in MCP Toolset #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:

# 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:

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

Metadata

Metadata

Assignees

Labels

mcp[Component] This issues is related to MCP support

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions