Skip to content

feat(mcp/py): support SEP-2663 tasks - #4125

Merged
poshinchen merged 8 commits into
strands-agents:mainfrom
gautamsirdeshmukh:agent-tasks/sep-2663-python
Sep 4, 2026
Merged

poshinchen merged 8 commits into
strands-agents:mainfrom
gautamsirdeshmukh:agent-tasks/sep-2663-python

Conversation

@gautamsirdeshmukh

@gautamsirdeshmukh gautamsirdeshmukh commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

This is the SEP-2663 Tasks slice of the Python SDK's MCP 2026-07-28 adoption tracked in #1659. Earlier PRs prepared the SDK for MCP 2.x model and instrumentation changes (#3528, #3611), established compatibility and server/discover negotiation (#3708, #3821), and added pagination, tool-call, MRTR input, and list-changed subscription support (#3984, #4090, #4094, #4112).

With those foundations in place, this PR adds opt-in support for finalized task handles and the tasks/get, tasks/update, and tasks/cancel lifecycle. Tasks now surface in two ways:

  • Automatic (existing API, signatures unchanged). call_tool_sync / call_tool_async — and tool calls an Agent makes through MCPAgentTool — keep returning a terminal tool result. When tasks_config is set and the server advertises task support, the client routes under the hood by installed mcp line: on 2.x it drives the finalized SEP-2663 lifecycle for you (create → poll tasks/get → answer inputRequests via tasks/update → terminal result); on 1.x it keeps the legacy 2025-11-25 flow. Callers see no behavioral difference between the two lines.
  • Manual (new API, mcp 2.x only). New lifecycle methods for callers who want to hold the task handle themselves — fire a long-running tool, check on it later, feed it input, or cancel it. These do not route: on the 1.x runtime pin the finalized models cannot round-trip server JSON, so the methods raise RuntimeError before sending anything.

Public API Changes

New MCPClient methods, each as a sync/async pair:

from strands.tools.mcp import MCPClient, MCPCreateTaskResult

client = MCPClient(transport, tasks_config={})  # opt-in, as before

# Calls the tool once and never polls: returns the direct result
# or, if the server created a task, the task handle.
result = client.call_tool_with_task_sync("long-running-tool")

if isinstance(result, MCPCreateTaskResult):
    state = client.get_task_sync(result.task_id)                 # tasks/get
    client.update_task_sync(result.task_id, input_responses)    # tasks/update
    client.cancel_task_sync(result.task_id)                     # tasks/cancel
Method (*_sync / *_async) Returns
call_tool_with_task_* MCPCallToolResult | MCPCreateTaskResult
get_task_* MCPGetTaskResult
update_task_* MCPUpdateTaskResult
cancel_task_* MCPCancelTaskResult

New types exported from strands.tools.mcp (experimental, like the existing tasks surface):

  • MCPTask — base task state shared by task results (task_id, status, timestamps, ttl_ms, poll_interval_ms), with MCPTaskStatus as its status literal
  • MCPCreateTaskResult — task handle returned instead of an immediate tool result
  • MCPGetTaskResult — status-specific state from tasks/get (input_requests / result / error), validated against the task's status
  • MCPUpdateTaskResult, MCPCancelTaskResult — validated empty acknowledgements
  • MCPTaskError — JSON-RPC error stored by a failed task
  • Aliases for lifecycle payloads: MCPCallToolResult, MCPInputRequest, MCPInputRequests, MCPInputResponse, MCPInputResponses

No existing signature changes; the 1.x path and the terminal-result behavior of call_tool_sync / call_tool_async are preserved.

Related Issues

Documentation PR

N/A — no site documentation changes in this PR.

Type of Change

New feature

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce new warnings.

  • I ran hatch run prepare
  • I ran the MCP 2.x compatibility and task lifecycle suites against mcp==2.0.1, including the public lifecycle over an in-process transport

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions github-actions Bot added python Pull requests that update python code enhancement New feature or request area-mcp MCP related labels Sep 2, 2026
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.69136% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
strands-py/src/strands/tools/mcp/mcp_client.py 99.49% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@gautamsirdeshmukh
gautamsirdeshmukh marked this pull request as ready for review September 2, 2026 16:35
@gautamsirdeshmukh
gautamsirdeshmukh requested a review from a team as a code owner September 2, 2026 16:35
@gautamsirdeshmukh
gautamsirdeshmukh requested review from mkmeral and poshinchen and removed request for mkmeral September 2, 2026 16:35
@gautamsirdeshmukh

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review, please!

Comment thread strands-py/src/strands/tools/mcp/mcp_client.py Outdated

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested — the SEP-2663 slice matches the spec and the 1.x path is genuinely preserved, but the new poll loop leaks server-side tasks on every exit that isn't a timeout (a protection the legacy path had), several public docstrings now describe behavior the 2.x path doesn't have, and the surface is large enough for the API-review workflow.

  • 1 🔴 + 7 🟡 inline, 11 appendix items, 3 blocking questions below. One pre-existing bug found along the way was filed separately as #4130 (not this PR's problem).
  • API review: 8 new public methods + 12 exported types reads as substantial per team/API_BAR_RAISING.md — it likely needs api/needs-review and a reviewer session. Note the green check-api-review-label check skips when no label is present, so it doesn't mean review happened.
  • Reachability caveat: with the runtime pin mcp<2.0.0, no released-SDK user can reach the 2.x task path yet — that's why most findings are 🟡. The one 🔴 is a dropped protection with no test, live the moment the pin lifts.
What was verified
  • ✅ Reviewed head ebb3755d4 (includes the modern→v2 rename).
  • ✅ 1.x (mcp 1.29.1): tests/strands/tools/mcp306 passed / 28 skipped — the 1.x-preservation claim holds against the existing suite.
  • ✅ 2.x (mcp 2.0.1, mirroring the CI job): test__compat.py + test_mcp_client_tasks_v2.py63 passed / 1 skipped.
  • ✅ 8 mutants run against the 2.x selection; 4 survived (input dedupe, server pollIntervalMs, all four lifecycle guards, createdAt check) — each survivor maps to an inline finding.
  • ✅ ~10 adversarial repros against an in-process mcp 2.0.1 tasks server; key ones re-run at head. Robustness that held: status flapping with distinct keys, pollIntervalMs=0 floor, stop() mid-poll (no thread/loop leak), all four *_async methods from a normal asyncio loop (no deadlock).
  • ruff format --check (638 files) + ruff check pass. ✅ Exports in __init__.py exactly match mcp_tasks.__all__.
  • ✅ Verified on both mcp lines: the same task JSON parses on 2.0.1 and fails with 5 validation errors on 1.29.1 (inline finding on the exports).
  • ⚠️ mypy not run locally (two sandbox attempts failed); one open check rides on CI: MCPError.from_error_data is 2.x-only, used without an ignore, and mcp_client.py isn't in the warn_unused_ignores exemption.
  • ⚠️ CI at this head was still pending when reviewed.
  • Repro scripts/logs and the mutation-test log are uploaded to the review artifacts store (strands-agents/harness-sdk/pr/4125/).
Questions

Blocking

  1. tasks_config={} is the only way to reach the new lifecycle API, and it also routes every call_tool_sync on a tasks-advertising server through the task path (tool-level taskSupport is no longer consulted on 2.x). Is that coupling intended, or should the manual API get its own opt-in?
  2. What's the intended public timeout story? read_timeout_seconds now means per-round on a direct call but whole-task on a task call, with the per-round bound only reachable via the new request_timeout config — while poll_timeout already means "whole task". If deliberate, it's a behavior change to existing public API on 2.x and worth a line in the API review.
  3. Is dropping the experimental markers a decision or a casualty of the rewrite? Either is fine — it needs to be on the record (see inline).

Non-blocking
4. Are task status notifications (notifications/tasks via subscriptions) a planned follow-up? It's the one SEP-2663 piece this slice leaves out and the actual ask in #1812; #4112 already built the subscription plumbing. A line in the body would close the loop.
5. The manual API exposes create/get/update/cancel but keeps the waiting logic private, so "start now, join later" means re-implementing ~50 subtle lines of _complete_v2_task. Worth exposing a wait_for_task_* over the machinery that already exists?
6. The claim pins protocol_versions={"2026-07-28"} at session construction and the guard demands exact equality, so the next protocol revision fails closed at start()/first call. Intended posture? CI pins mcp==2.0.1 and 2.1.x is already on PyPI, so nothing would notice drift.
7. _fulfill_task_input sends one tasks/update per key while the MRTR path batches all responses into one round. Deliberate (a declining callback doesn't discard collected answers)? A comment would settle it.
8. Naming: call_tool_with_task_* is the odd one out in a <verb>_task_* set and doesn't convey "returns without waiting" — MCP's own vocabulary says "call tool as task". And update_task reads like "modify the task" rather than "answer its pending input requests".
9. Four new *_async methods with no tests, on a client that's otherwise sync-only for prompts/resources/list_tools. Commit to async pairs across the client, or trim to sync until asked?

Reading order
  1. mcp_tasks.py — wire models and TasksConfig; everything else is written against these.
  2. _compat.py (new block at the end) — how a server is judged task-capable on each mcp line, and how the 2.x extension claim is installed at session build.
  3. mcp_client.py gates — _should_use_task (where 2.x stops consulting tool-level taskSupport) and _require_v2_task_lifecycle (the four guards).
  4. mcp_client.py engine — _complete_v2_task, _reconcile_v2_task_state, _call_tool_with_task_and_poll_async. Most findings live here; read against the untouched legacy _call_tool_as_task_and_poll_async directly below, which shows what the new path chose not to keep.
  5. The 8 new public methods — thin wrappers over 3–4.
  6. mcp/__init__.py and the call_tool_sync/call_tool_async docstrings — the public surface and its stated contract.
  7. Last: the new test file and the workflow change — what is actually pinned, and where it runs.
Appendix — non-blocking (11)
  • ⚪ The new task path emits zero log lines; the legacy path logs 11, including the created task id — an operator's only handle for out-of-band tasks/get/tasks/cancel.
  • ⚪ A failed task's JSON-RPC code/data are dropped; only the message survives, and a task failing with -32042 misses the elicitation-required handling the direct path gives the same code.
  • pollIntervalMs has a floor but no ceiling and no clamp to remaining budget: pollIntervalMs: 3600000 sleeps past the whole poll_timeout without a single tasks/get (demonstrated; bounded by the deadline).
  • ⚪ Raw pydantic validation text reaches the tool result — and hence the model's context ("1 validation error for tagged-union[…]"). Fails closed, but the message is written for a pydantic user.
  • isinstance(input_responses, dict)TypeError is defensive code the annotation already covers, and it rejects a non-dict Mapping the wire path would accept.
  • ⚪ Test structure: one 118-line test covers four public methods and both automatic paths (failures localize poorly), and there's a load-bearing assert inside the server handler — prefer asserting recorded params client-side. The real in-process server itself should stay: it's the only thing pinning allow_claimed=True (mutant-verified).
  • ⚪ CI pin mcp==2.0.*==2.0.1 is fine per the job's own "bump deliberately" comment, but nothing in CI notices protocol drift (see Q6) — a scheduled unpinned run or tracking issue would.
  • ⚪ Capability detection ignores the per-extension settings dict; a server advertising the extension but implementing a different revision makes every tool call create-then-fail (demonstrated). Non-blocking because mcp 2.0.1 defines those values as opaque — there's nothing spec'd to check; the residual leak is the 🔴 inline.
  • ⚪ No tests_integ/ addition, while the legacy path has tests_integ/mcp/test_mcp_client_tasks.py + task_echo_server.py.
  • ⚪ The CONTRIBUTING attestation ("I have reviewed and understand every line") is unchecked on a +1265-line PR.
  • Pre-existing (not this PR's problem): per-call remote cancellation silently never fires on the 2.x direct path — filed as #4130.

Review run: triage → context pack → 5 independent specialist passes (correctness, issue-alignment, API bar-raise, adversarial+repro, test-quality) → aggregation. The API and adversarial passes timed out on the first attempt and were re-run successfully on the standard tier. Solid work overall — the survived-attacks list above is real robustness; the asks are focused on cleanup guarantees, the documented contract, and pinning the guards with tests.

Comment thread strands-py/src/strands/tools/mcp/mcp_client.py
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py Outdated
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py Outdated
Comment thread strands-py/src/strands/tools/mcp/mcp_tasks.py
Comment thread strands-py/src/strands/tools/mcp/mcp_tasks.py Outdated
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py Outdated
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py
Comment thread strands-py/src/strands/tools/mcp/mcp_tasks.py
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py
Comment thread strands-py/src/strands/tools/mcp/mcp_tasks.py Outdated
Comment thread strands-py/src/strands/tools/mcp/mcp_client.py Outdated
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Issue (API bar-raising): This PR adds a substantial new public surface — 8 methods (call_tool_with_task_sync/async, get_task_sync/async, update_task_sync/async, cancel_task_sync/async) plus ~11 exported types — but is not tagged api/needs-review. Two design points worth an explicit reviewer sign-off:

  1. Union return type. call_tool_with_task_* returns MCPCallToolResult | MCPCreateTaskResult, forcing every caller into an isinstance branch. That mirrors the wire protocol, but please confirm it's the ergonomics we want to commit to as public API.
  2. tasks_config={} as the opt-in. Enabling a feature by passing an empty dict is somewhat implicit; a reviewer should confirm this reads well alongside the rest of the SDK's config conventions.

Suggestion: Add the api/needs-review label and include the API section the template asks for (use cases, signatures with defaults, module exports) so this gets a proper bar-raising pass before merge. See team/API_BAR_RAISING.md.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Assessment: Comment

Solid, well-structured addition of SEP-2663 task support that preserves the MCP 1.x path and keeps the feature opt-in. The task result models have thorough validation and the malformed-shape/reconciliation tests are a highlight. Main themes below before merge.

Review Categories
  • API review: New public surface (8 methods + ~11 types) lacks the api/needs-review label and the template's API section; the union return type and tasks_config={} opt-in deserve an explicit bar-raising sign-off.
  • Complexity: complexity/high label is driven by _complete_v2_task's nested poll loop — extracting the terminal-state and input-required handling would flatten it and make each branch testable.
  • Testing: 62.5% patch coverage on mcp_client.py; the public wrappers' guard-clause Raises: contracts are untested.
  • Consistency: ttl_ms required-but-nullable vs. optional poll_interval_ms, and duplicated ISO-8601 timestamp parsing across two files.

Nice work threading this through the existing compat layer without disturbing 1.x behavior.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review reconciliation / verdict correction

Cross-referencing the more thorough review already on this PR (the "Changes requested" review with the 🔴 task-leak finding): that review is the authoritative one, and I'm aligning to it.

Two corrections to my earlier pass:

  1. Verdict: my earlier summary said Comment — that undersold the state. With a confirmed resource-leak bug (server-side tasks left running to TTL on any poll-loop exit that isn't a timeout, and no cancellation if the deadline lands during the create round), the correct verdict is Request Changes. Please treat that finding as blocking.

  2. Overlap: my inline notes on API bar-raising, patch coverage, _complete_v2_task complexity, and the duplicated timestamp parsing are subsumed by that review — no need to action them twice. The one item I'd still call out independently is the ttl_ms required-but-nullable vs. optional poll_interval_ms asymmetry, which the other review didn't cover.

Apologies for the noise from the duplicate pass.

@gautamsirdeshmukh
gautamsirdeshmukh force-pushed the agent-tasks/sep-2663-python branch from ebb3755 to 1ca5592 Compare September 2, 2026 19:56
@gautamsirdeshmukh
gautamsirdeshmukh force-pushed the agent-tasks/sep-2663-python branch from 1e629ae to f1f47b1 Compare September 2, 2026 20:06
@gautamsirdeshmukh

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent please review oncemore

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Assessment: Comment — code is in good shape; one process gate remains.

Independent re-review at 1171b5aae. Rather than repeat the earlier passes, I checked out the branch and verified the substantive concerns are closed and that the latest test commit (785949144) holds up.

Verified locally against mcp==2.0.1 (overriding the <2.0.0 runtime pin, as CI does)
  • test_mcp_client_tasks_v2.py38 passed
  • test__compat.py51 passed, 1 skipped
  • Legacy test_mcp_client_tasks.py fails under 2.x — expected, these are the 1.x-only tests the 2.x CI job excludes by design (per the workflow comment); not a regression.
  • Prior 🔴 task-leak fix confirmed in code: _create_v2_task shields the create round + grace-window/detached-callback cleanup, and _call_tool_with_task_and_poll_async cancels the server task on both the timeout path and except BaseException once a handle exists.
  • ttl_ms vs poll_interval_ms asymmetry now carries an explanatory comment (mcp_tasks.py L86–88) — settles the last open code nit.
  • New sync/async pairs correctly use asyncio.wrap_future (no event-loop blocking); the new cancellation-edge tests (grace-period inline cancel, direct-result skip, failed create round, input-request ErrorData) are well-targeted.

Remaining gate before merge:

  • ⚠️ API review — the PR still has no api/needs-review label despite a substantial new public surface (8 lifecycle methods + 13 exported types per team/API_BAR_RAISING.md). The good news is the PR description now includes the required API section (use cases, signatures with defaults, module exports), so a designated reviewer can evaluate it straight from the description. Two design points worth an explicit sign-off: the MCPCallToolResult | MCPCreateTaskResult union return that forces an isinstance branch, and tasks_config={} as the enable-by-empty-dict opt-in. Please add the label and get an API reviewer to confirm (note the check-api-review-label check skips when no label is present, so a green check isn't evidence of review).

Nice work closing the cancellation window cleanly and pinning it with regression tests.

Comment thread strands-py/src/strands/tools/mcp/mcp_client.py Outdated
@gautamsirdeshmukh
gautamsirdeshmukh force-pushed the agent-tasks/sep-2663-python branch from 54da93c to 5e0f7ec Compare September 4, 2026 16:52
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Issue: The PR description's Public API Changes section is now stale after the call_tool_with_task_*submit_tool_* rename (agreed with @pgrayy and already applied in the code and strands-py/AGENTS.md). The prose example still calls client.call_tool_with_task_sync(...) and the method table still lists call_tool_with_task_*, but the shipped public methods are submit_tool_sync / submit_tool_async.

Suggestion: Update the description to submit_tool_*. This matters beyond cosmetics: prior reviews noted the API bar-raising sign-off will be done straight from this description, so the signatures/examples there need to match the real surface.


On the one remaining process gate (raised in earlier passes, still open): the PR carries no api/needs-review / api/review-complete label despite a substantial new public surface (4 sync/async method pairs + ~13 exported types). Per team/API_BAR_RAISING.md this warrants an explicit reviewer sign-off — note the two design points earlier reviews called out for confirmation: the MCPCallToolResult | MCPCreateTaskResult union return (forces an isinstance branch on callers) and tasks_config={} as the enable-by-empty-dict opt-in. Adding the label and getting a designated API reviewer to confirm is the last gate before merge.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Assessment: Comment

The implementation is in strong shape after several review passes — the substantive code concerns (task-leak on cancellation/timeout, 2.x timeout docstrings, patch coverage, timestamp dedup, ttl_ms asymmetry) are all resolved and pinned by well-targeted regression tests. I re-read the finalized surface and found no new code defects; the create-window shielding and the decomposed _create_v2_task / _complete_v2_task / _reconcile_v2_task_state helpers read cleanly.

Review notes
  • Docs accuracy (Important): PR description still documents the pre-rename call_tool_with_task_* API; code and AGENTS.md are on submit_tool_*. Update the description so the API-bar-raising reviewer evaluates the real signatures. (see general comment)
  • Process gate (Important): no api/needs-review label yet — union return type and tasks_config={} opt-in are the two points flagged for explicit sign-off.
  • Code quality: solid validation in mcp_tasks.py, thorough cancellation-edge test coverage; nothing blocking.

Nice work closing the cancellation guarantees and backing them with regression tests — the remaining items are the stale description and the API-review sign-off, not code correctness.

Comment thread strands-py/tests/strands/tools/mcp/test_mcp_client_tasks_v2.py
Comment thread strands-py/tests/strands/tools/mcp/test_mcp_client_tasks_v2.py
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Assessment: Request Changes

The implementation is well-structured and thoroughly documented, with strong unit coverage of the SEP-2663 lifecycle, cancellation races, and model validation. The blocker is in the tests, not the production code: the one test that exercises the real MCP 2.x transport end to end has import typos (httpx2, mcp_types) that prevent it from running, so the most important integration guarantee is currently unverified.

Review themes
  • Testing (blocking): test_public_task_lifecycle_over_real_mcp_transport won't execute under mcp 2.x due to import httpx2 and from mcp_types import ... (inline comments). Worth confirming the forced-2.x CI job actually collects and runs this file, since these would otherwise fail there.
  • PR description drift: The "Public API Changes" section and the method table still show call_tool_with_task_*, but the code and tests were renamed to submit_tool_* (per the resolved thread with @pgrayy). Since the description is the reference doc for API bar-raising, please update the snippet and table to match.
  • API bar-raising: Already raised earlier — this substantial public surface (8 methods + ~13 exported types) still lacks the api/needs-review label. Flagging only so it isn't lost.

Nice work on the cancellation-during-create handling and the timestamp-reconciliation logic — those edge cases are easy to miss and are handled carefully here.

@gautamsirdeshmukh

Copy link
Copy Markdown
Contributor Author

Disregard the bot comments, httpx2 and mcp_types are both real packages and hard deps of mcp 2.0.1, tests passed locally and in CI

Comment thread .github/workflows/sync-fork.yml Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-mcp MCP related complexity/medium Touched functions have moderate cognitive complexity (11-25) enhancement New feature or request python Pull requests that update python code size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants