Skip to content

Add support for MCP background tasks (SEP-1686)#5192

Merged
DouweM merged 6 commits into
pydantic:mainfrom
Alex-Resch:feature/fastmcp-background-tasks
May 21, 2026
Merged

Add support for MCP background tasks (SEP-1686)#5192
DouweM merged 6 commits into
pydantic:mainfrom
Alex-Resch:feature/fastmcp-background-tasks

Conversation

@Alex-Resch
Copy link
Copy Markdown
Contributor

@Alex-Resch Alex-Resch commented Apr 24, 2026

Adds SEP-1686 task-augmented execution support to MCPToolset. Tools whose server config declares task=TaskConfig(mode='required'|'optional') now run via the durable, cancelable, pollable task path — client.call_tool(task=True)tool_task.result() — instead of failing with -32601: requires task-augmented execution.

Usage

No client-side configuration is needed — MCPToolset honors the server's per-tool execution.taskSupport declaration automatically:

execution.taskSupport Behavior
"required" Always calls with task=True (the SEP requires it).
"optional" Always calls with task=True to opt in to durability, cancellation, and progress notifications.
"forbidden" or absent Calls normally.
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset

agent = Agent('openai:gpt-5', toolsets=[MCPToolset('http://localhost:8000/mcp')])

What's in scope

  • MCPToolset.get_tools reads mcp_tool.execution.taskSupport and exposes a selector-friendly task: bool on ToolDefinition.metadata.
  • MCPToolset.call_tool routes through the task path iff the tool's task metadata is set (i.e. the server declared required/optional).
  • MCPToolset.direct_call_tool gains a keyword-only use_task=False low-level escape hatch for callers who invoke without a ToolDefinition.
  • process_tool_call gets a partial(direct_call_tool, use_task=...) so a custom wrapper preserves the task path without needing to know about it.
  • Docs: new "Background tasks" section in docs/mcp/client.md.

Notes

  • Built on the new MCPToolset (Add MCPToolset that uses fastmcp-slim[client], deprecate MCPServer* and FastMCPToolset #5325), not the now-deprecated FastMCPToolset — the original community PR targeted FastMCPToolset; relocated here as part of the takeover.
  • Server-side task execution is purely the MCP server's decision, so there's no client-side opt-in and no coupling to BackgroundTools (pydantic-ai-harness#222) — that capability addresses a different layer (client-side fire-and-forget asyncio tasks). Overriding the server's decision (forcing or skipping the task path) is left to a future generic capability.

Closes #4266.
Co-authored with the original community PR by @Alex-Resch (#5192).

Checklist

  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • No breaking changes in accordance with the version policy.
  • PR title is fit for the release changelog.

Copilot AI review requested due to automatic review settings April 24, 2026 13:25
@github-actions github-actions Bot added the size: M Medium PR (101-500 weighted lines) label Apr 24, 2026
@github-actions github-actions Bot added the feature New feature request, or PR implementing a feature (enhancement) label Apr 24, 2026
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread docs/mcp/fastmcp-client.md Outdated

if not pydocket_installed and execution is not None and getattr(execution, 'taskSupport', None) == 'required':
raise UserError(
f"Tool '{name}' requires task-augmented execution but 'pydocket' is not installed. "
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot Apr 24, 2026

Choose a reason for hiding this comment

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

🟡 Double-quoting of tool name in error message due to !r combined with literal quotes

At pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py:296, the f-string f"Tool '{name!r}'" wraps name!r in additional literal single quotes. Since !r already adds quotes (e.g., 'task_required_tool'), the result is double-quoted: Tool ''task_required_tool'' requires.... This violates the repository's coding guideline rule:32 in agent_docs/index.md which explicitly states: "Use !r format specifier for identifiers in error messages (e.g., f'Tool {name!r}' not f'Tool \{name}`'`)".

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread docs/mcp/fastmcp-client.md Outdated
Comment thread pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds MCP SEP-1686 “task-augmented execution” support to the FastMCPToolset, enabling tools that declare TaskConfig(mode="required"/"optional") to run via background tasks when pydocket is available, and improving error reporting when it is not.

Changes:

  • Extend FastMCPToolset.call_tool()/direct_call_tool() to optionally call FastMCP with task=True and await the task result.
  • Include tool execution metadata in tool definitions to drive task-mode selection.
  • Add tests + docs covering required/optional task tools and pydocket availability behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py Adds runtime detection for pydocket, includes execution metadata, and introduces task-augmented tool calling via ToolTask.result().
tests/test_fastmcp.py Adds server tools that require/optionally support background tasks plus tests for task execution and missing-pydocket behavior.
docs/mcp/fastmcp-client.md Documents background task support and the optional pydocket requirement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'meta': mcp_tool.meta,
'annotations': mcp_tool.annotations.model_dump() if mcp_tool.annotations else None,
'output_schema': mcp_tool.outputSchema or None,
'execution': mcp_tool.execution,
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

execution is being stored in ToolDefinition.metadata as the raw mcp_tool.execution object. Tool metadata is used by matches_tool_selector via deep dict inclusion, and this will not work well with a pydantic model/object (e.g. selectors like {'execution': {'taskSupport': 'required'}} will never match). Consider storing a JSON-compatible dict instead (e.g. mcp_tool.execution.model_dump() / None) to keep metadata filterable and serializable, consistent with how annotations is handled.

Suggested change
'execution': mcp_tool.execution,
'execution': mcp_tool.execution.model_dump() if mcp_tool.execution else None,

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +290
execution = tool.tool_def.metadata.get('execution') if tool.tool_def.metadata else None
use_task = pydocket_installed and execution is not None

if not pydocket_installed and execution is not None and getattr(execution, 'taskSupport', None) == 'required':
raise UserError(
f"Tool '{name}' requires task-augmented execution but 'pydocket' is not installed. "
'Install it with: pip install pydocket'
)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

use_task is enabled whenever execution metadata is present, but the docstring says it should only be set when the tool advertises task support (taskSupport is required/optional). To avoid sending task=True for tools whose execution metadata is unrelated to tasks, consider gating on execution.taskSupport in {'required','optional'} (and adjust the required-mode check accordingly).

Copilot uses AI. Check for mistakes.
Comment thread tests/test_fastmcp.py Outdated
Comment on lines +717 to +734
async def test_call_tool_with_task_required(
self, fastmcp_toolset: FastMCPToolset[None], run_context: RunContext[None]
):
async with fastmcp_toolset:
tools = await fastmcp_toolset.get_tools(run_context)
task_tool = tools['task_required_tool']
result = await fastmcp_toolset.call_tool('task_required_tool', {}, run_context, task_tool)
assert result == snapshot('task_required_completed')

async def test_call_tool_with_task_optional(
self, fastmcp_toolset: FastMCPToolset[None], run_context: RunContext[None]
):
async with fastmcp_toolset:
tools = await fastmcp_toolset.get_tools(run_context)
task_tool = tools['task_optional_tool']
result = await fastmcp_toolset.call_tool('task_optional_tool', {}, run_context, task_tool)
assert result == snapshot('task_optional_completed')

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

These background-task tests will fail in any environment where fastmcp is installed but pydocket is not (since task_required_tool will raise UserError when pydocket_installed is false). Since pydocket is intentionally optional and is not listed in the repo's dev dependency group, consider skipping these tests when pydocket/docket isn't importable (or adding pydocket as an explicit test dependency).

Copilot uses AI. Check for mistakes.
Comment thread tests/test_fastmcp.py Outdated
assert fastmcp_module.pydocket_installed is False

importlib.reload(fastmcp_module)
assert fastmcp_module.pydocket_installed is True
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

test_pydocket_not_installed asserts pydocket_installed is True after reloading the module, which will fail if pydocket isn't actually installed in the environment (even though that's a supported configuration). Consider making the final assertion conditional on whether docket can be imported/found (or skip this test when pydocket isn't present).

Suggested change
assert fastmcp_module.pydocket_installed is True
assert fastmcp_module.pydocket_installed is (importlib.util.find_spec('docket') is not None)

Copilot uses AI. Check for mistakes.
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review


try:
from fastmcp.client import Client
from fastmcp.client.tasks import ToolTask
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.

🚩 Unconditional ToolTask import may break entire FastMCPToolset for older fastmcp versions

The from fastmcp.client.tasks import ToolTask at pydantic_ai_slim/pydantic_ai/toolsets/fastmcp.py:22 is inside the main try/except ImportError block that raises a generic 'Please install the fastmcp package' error. If a user has fastmcp installed but at a version that predates fastmcp.client.tasks, the entire FastMCPToolset would fail to import with a misleading error message suggesting fastmcp isn't installed at all. This contrasts with how docket is handled (separate try/except with a boolean flag at lines 53-58). While this works with the current minimum version (>=3.2.4), it's fragile — a version bump in the constraint or a user pinning an older version could trigger this. Consider guarding the ToolTask import separately or bumping the minimum fastmcp version if task support requires it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread docs/mcp/fastmcp-client.md Outdated
!!! note
Background task support requires the `pydocket` package. Install it with `pip install pydocket` or `pip install "pydantic-ai-slim[fastmcp-tasks]"`.

```python {test="skip"}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lets try to not skip this one

@github-actions github-actions Bot added size: XL Extra large PR (>1500 weighted lines) and removed size: M Medium PR (101-500 weighted lines) labels Apr 29, 2026
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1412 to +1424

def _apply_explicit_message_caching(
self,
model_settings: AnthropicModelSettings,
anthropic_messages: list[BetaMessageParam],
) -> None:
"""Apply per-block message caching when `anthropic_cache_messages` is enabled.

Mutually exclusive with `anthropic_cache` (enforced by `_build_automatic_cache_control`).
"""
if cache_messages := model_settings.get('anthropic_cache_messages'):
ttl: Literal['5m', '1h'] = '5m' if cache_messages is True else cache_messages
self._apply_message_cache_control(anthropic_messages, ttl)
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.

🚩 anthropic_cache_messages semantics changed from deprecated alias to distinct feature

Previously anthropic_cache_messages was a deprecated alias for anthropic_cache — it emitted a DeprecationWarning and mapped to top-level automatic caching. Now it's a distinct feature that applies per-block cache_control to the last message content block, intended for Anthropic-compatible gateways that don't support the top-level caching parameter. The deprecation warning was removed, and the behavior silently changed. Users who were relying on anthropic_cache_messages=True to get top-level automatic caching will now get per-block caching instead. The docs and tests were updated to reflect this, and the _build_automatic_cache_control still enforces mutual exclusivity with anthropic_cache. This is an intentional semantic change but could surprise users upgrading who relied on the deprecated behavior.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +956 to +963
if self._next_node is not None:
return self._next_node
# If the stream raised an error that was caught by an external consumer
# (e.g. UIEventStream.transform_stream), _next_node will not have been set.
# Re-raise the original error instead of a confusing assertion.
if self._stream_error is not None:
raise self._stream_error.with_traceback(self._stream_error.__traceback__)
raise exceptions.AgentRunError('the stream should set `self._next_node` before it ends') # pragma: no cover
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.

🚩 _stream_error re-raise path in HandleResponseNode.run() may be unreachable

The new _stream_error handling in run() at pydantic_ai_slim/pydantic_ai/_agent_graph.py:961-963 checks for a stored error if _next_node is None. However, in the run() flow (async with self.stream(ctx): pass), if the stream raises a BaseException, it's caught by the except at line 1140, stored in _stream_error, and re-raised. The @asynccontextmanager's __aexit__ propagates the exception, so the code after async with is unreachable when the stream errors. The _stream_error check seems designed for the case where an external consumer (e.g. UIEventStream.transform_stream) catches and suppresses the error before it reaches the context manager boundary, but through the run() path this shouldn't happen. The test_event_stream_handler_propagates_tool_error test does validate the overall error propagation behavior.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented Apr 29, 2026

@Alex-Resch Thanks for working on this! I've been making progress on a different flavor of background/async tools as well:

Any opinions on how the features should interact?

@Alex-Resch
Copy link
Copy Markdown
Contributor Author

Hey @DouweM, thanks for the review! I think I could change my PR so that — similar to what you did in #222 — FastMCPToolset wouldn’t await the task result anymore. Instead, the agent would get an immediate ack like “task started” and could work on other things in the meantime. Once the server-side task completes, the result would get enqueued as a follow-up via #4980. This would be opt-in, so the current blocking behavior stays the default.
Does that sound like the direction you had in mind? Or am I misunderstanding something here?

@github-actions github-actions Bot added size: M Medium PR (101-500 weighted lines) and removed size: XL Extra large PR (>1500 weighted lines) labels Apr 30, 2026
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 16 additional findings in Devin Review.

Open in Devin Review

Comment thread pydantic_ai_slim/pyproject.toml Outdated
mcp = ["mcp>=1.25.0,<2.0"]
# FastMCP
fastmcp = ["fastmcp>=3.2.4"]
fastmcp-tasks = ["fastmcp>=3.2.4", "pydocket>=0.19.0"]
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.

🚩 The fastmcp-tasks optional group requires fastmcp but does not add it to the main fastmcp extra

The new fastmcp-tasks extra at pydantic_ai_slim/pyproject.toml:116 is ["fastmcp>=3.2.4", "pydocket>=0.19.0"], which is a standalone group. This means users must explicitly install pydantic-ai-slim[fastmcp-tasks] to get task support — installing pydantic-ai-slim[fastmcp] alone won't include pydocket. This is intentional (task support is opt-in) and consistent with how the pydocket_installed check works at runtime. The main pydantic-ai meta-package at pyproject.toml:50 does not include fastmcp-tasks either, so users of the full package won't get it automatically. This is worth noting for reviewers to confirm it's the intended UX.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread docs/mcp/fastmcp-client.md Outdated
Comment on lines +128 to +134
agent = Agent(
TestModel(custom_output_text='completed'),
toolsets=[FastMCPToolset(mcp)],
)
result = await agent.run('Run the long task')
print(result.output)
#> completed
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.

🚩 Doc example uses TestModel instead of a frontier model

The new documentation example at docs/mcp/fastmcp-client.md:128-134 uses TestModel(custom_output_text='completed') rather than a frontier model like openai:gpt-5.2 (as used in the earlier examples on the same page). The coding guidelines (agent_docs/index.md rule:132) say to use latest/frontier models in docs. However, this example specifically demonstrates background task behavior where a real model would be overkill and make the example non-runnable without credentials, so TestModel is arguably the right choice here. Reviewer should confirm this is acceptable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented Apr 30, 2026

@Alex-Resch Yep that's exactly what I was thinking. I wanted to hear from someone who actually needs the feature and is more aware of what the MCP background task SEP prescribes, what their expected interaction (and opt-in vs opt-out) behavior would be :)

I believe it would be possible to use "background tools" for MCP tasks without any changes to this particular code, since this sets an execution field on ToolDefinition.metadata, meaning that opting in to the harnesses's BackgroundTools behavior becomes adding a new capability SetToolMetadata(tools={'execution': {'taskSupport': 'required'}}, metadata={'background': True}), so that all the background task tools also get the background flag that BackgroundTools looks for. I suppose you would then also need to target 'taskSupport': 'optional', which is a bit annoying. I think it'd be worth adding a top-level metadata field in this PR that indicates whether the MCP toolset will be running it in task or not (i.e. True for required, True/False for optional depending on whether the library was installed). Then it'll be SetToolMetadata({'task': True}, {'background': True}).

Could you verify that this PR works as expected with those other 2, by bringing the branches together in a local project?

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented Apr 30, 2026

@Alex-Resch Do we need pydocket? Is there any way we can do this without new dependencies? Perhaps with an way to specify a different executor?

Note that there's also #4942

@Alex-Resch
Copy link
Copy Markdown
Contributor Author

Hey @DouweM, I pulled all branches into one and all current relevant tests pass. So this works.

For the pydocket question: pydantic-ai's code doesn't actually need pydocket, because it's a server-side requirement. Only the server needs it, the client doesn't.

I verified this locally with two separate venvs:

  • Server venv with fastmcp[tasks] (pydocket installed): runs a @mcp.tool(task=TaskConfig(mode="required")) long_task over streamable-http on port 8000.
  • Client venv with only base fastmcp (no pydocket — import docket raises ModuleNotFoundError): calls client.call_tool("long_task", {}, task=True), gets a ToolTask back with returned_immediately=False, then await task.result() returns the actual server result.

So the task path runs end-to-end with no pydocket on the client side. The earlier ImportError I had seen came from the in-process test fixture registering a server-side TaskConfig, not from the client call:

ImportError: FastMCP background tasks require the `tasks` extra. Install with: pip install 'fastmcp[tasks]'. (Triggered by `task=True` on function 'task_required_tool') ​

That error fires from fastmcp/server/dependencies.py when the server-side decorator runs, which is why the in-process fixture trips on it but a real client doesn't. So I think pydocket, the pydocket_installed check, the UserError for mode="required", and the fastmcp-tasks dependency aren't needed.

Is that what you asked for? Let me know if I'm missing something.

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented May 5, 2026

@Alex-Resch Yep sounds reasonable, can you make those updates to the PR please?

If task == required, what do you think the right behavior should be, if not an error? I haven't read https://modelcontextprotocol.io/seps/1686-tasks to see if it prescribes something specific. But it seems to me that just getting the task and then immediately doing a blocking await (blocking from the agent loop perspective), wouldn't match the intent. Should we error unless the user is using Background Tools mode from pydantic/pydantic-ai-harness#222?

@Alex-Resch
Copy link
Copy Markdown
Contributor Author

Hey DouweM, I'd love to implement those changes!

I read the SEP and it doesn't prescribe any specific client behavior for this case — mode="required" is a fastmcp-specific concept, the SEP doesn't know about it. If the server doesn't know a task it just ignores the task metadata.
I think throwing an error for mode="required" is the best approach here — the server explicitly requires background execution, so the user should know they need to activate BackgroundTools.

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented May 5, 2026

@Alex-Resch OK that makes sense to me then!

@Alex-Resch Alex-Resch force-pushed the feature/fastmcp-background-tasks branch from c62eb0f to c692fd2 Compare May 7, 2026 10:49
@Alex-Resch
Copy link
Copy Markdown
Contributor Author

@DouweM I updated the PR with your suggestions.

I verified end-to-end locally by combining this PR with #4980 and pydantic-ai-harness#222 — agent gets the immediate ack, the task runs in the background on the server, and the result comes back as a follow-up message. I confirmed that everything works as expected.

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented May 8, 2026

@Alex-Resch Thanks, I'll have a look next week!

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented May 12, 2026

FYI, I'll likely get #5325 in, and then this.

@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented May 15, 2026

#5325 has been merged, I'll take this PR from here!

@DouweM DouweM self-assigned this May 15, 2026
DouweM added a commit to Alex-Resch/pydantic-ai that referenced this pull request May 15, 2026
DouweM added a commit to Alex-Resch/pydantic-ai that referenced this pull request May 15, 2026
Move SEP-1686 task-augmented execution from the now-deprecated
FastMCPToolset (PR pydantic#5192's original target, deprecated by pydantic#5325) to the
new MCPToolset in pydantic_ai/mcp.py.

- MCPToolset.get_tools exposes `task` and `task_required` flags on
  ToolDefinition.metadata so capabilities like SetToolMetadata can
  target task-supporting tools.
- MCPToolset.direct_call_tool gains a keyword-only `use_task` that sends
  `task=True` to the server and awaits `tool_task.result()`.
- MCPToolset.call_tool reads `background` from the runtime tool metadata
  (post-capability augmentation). `task_required` + no opt-in raises
  UserError pointing at SetToolMetadata (no BackgroundTools coupling).
- Drop the deep-dict `execution` metadata field (per Copilot's day-1
  selector-friendliness note; `task` and `task_required` carry the
  needed signal).
- Drop the `fastmcp-tasks` extra — clients don't need pydocket
  (server-side concern only). Keep pydocket in dev-deps (the in-process
  test fixture needs the server-side extra) and bump to 0.20.2 for
  fastmcp 3.3.0 compatibility.
- Drop the FastMCPToolset edits in toolsets/fastmcp.py and
  tests/test_fastmcp.py — the deprecated path doesn't get new features.
- Drop the docs/mcp/fastmcp-client.md "Background Tasks" section —
  fastmcp-client.md documents the deprecated path; MCPToolset's
  docstring now carries the user-facing example.
- Fix two merge artifacts that survived the auto-merge: duplicate
  'HandleDeferredToolCalls' in capabilities/__init__.py __all__, and a
  duplicated `if deferred_tool_results is None` block in
  ui/_adapter.py:run_stream_native.

Closes pydantic#4266.
@github-actions github-actions Bot added size: XL Extra large PR (>1500 weighted lines) and removed size: M Medium PR (101-500 weighted lines) labels May 15, 2026
@DouweM DouweM changed the title FastMCPToolset: add support for MCP background tasks (SEP-1686) MCPToolset: add support for MCP background tasks (SEP-1686) May 15, 2026
Alex-Resch and others added 2 commits May 15, 2026 23:11
Tools whose server config declares `task=TaskConfig(mode='required'|'optional')`
can now run via the durable, cancelable, pollable MCP task path —
`client.call_tool(task=True)` → `tool_task.result()` — instead of failing with
`-32601: requires task-augmented execution`.

- `MCPToolset.get_tools` reads `mcp_tool.execution.taskSupport` and exposes
  `task` / `task_required` flags on `ToolDefinition.metadata` so capabilities
  like `SetToolMetadata` can target task-supporting tools.
- `MCPToolset.direct_call_tool` gains a keyword-only `use_task=False` that
  routes through `client.call_tool(task=True)` and awaits `tool_task.result()`.
- `MCPToolset.call_tool` reads `background` from the runtime tool metadata
  (post-capability augmentation) and gates the task path on it; raises
  `UserError` for `task_required` tools with no opt-in.
- `process_tool_call` gets a `partial(direct_call_tool, use_task=...)` so a
  custom wrapper preserves the task path without needing to know about it.

Opt in via `SetToolMetadata(tools={'task': True}, background=True)`.

Bumps the `pydocket` dev dep to `>=0.20.2` for fastmcp 3.3.0 compat (the
in-process test fixture instantiates a FastMCP server with `TaskConfig` tools,
which needs the server-side `tasks` extra).

Closes pydantic#4266.
@DouweM DouweM force-pushed the feature/fastmcp-background-tasks branch from 5a0e43a to 5b5f474 Compare May 18, 2026 21:53
@github-actions github-actions Bot added size: M Medium PR (101-500 weighted lines) and removed size: XL Extra large PR (>1500 weighted lines) labels May 18, 2026
Comment thread pydantic_ai_slim/pydantic_ai/mcp.py Outdated
Comment on lines +2125 to +2126
'task': task_support in ('required', 'optional'),
'task_required': task_support == 'required',
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.

The docs/mcp/client.md "Tool metadata" section explicitly lists the metadata fields (meta, annotations, output_schema) but doesn't mention the new task and task_required fields added here. That section should be updated.

More broadly, a "Background Tasks" section should be added to docs/mcp/client.md (alongside "MCP Sampling", "Elicitation", etc.) to make the feature discoverable where users naturally look — the docstring documentation is thorough, but the project's documentation guidelines call for dedicated sections in the user-facing docs page, not just API reference docstrings.

@DouweM — fine to defer to a follow-up if you'd prefer, but flagging since the docs guidelines are clear on this.

DouweM added 3 commits May 19, 2026 00:55
The previous design gated MCP task-augmented execution on a `background=True`
metadata flag — overloading the harness's `BackgroundTools` concept, which is
about client-side fire-and-forget asyncio tasks (a different layer entirely).
Server-side task durability/cancellation/progress is purely an MCP-server
decision; the client should just honor it.

- `MCPToolset.call_tool` now routes via the task path iff the server declares
  `execution.taskSupport in ('required', 'optional')`. No `background=True`
  check, no `UserError`, no opt-in.
- Drop `task_required` from `ToolDefinition.metadata`; keep `task: bool` as a
  selector-friendly fact for downstream capabilities that want to target
  task-supporting MCP tools.
- Move user-facing docs from the `MCPToolset` docstring into a new
  "Background tasks" section in `docs/mcp/client.md`, alongside the existing
  "Tool metadata" section.

Users who want to override the server's decision (e.g. force task path for
a tool the server didn't mark, or skip the task path for an `optional` tool)
should use a separate, generic capability — not something MCP-specific.
Comment thread docs/mcp/client.md Outdated
@@ -354,6 +354,50 @@ agent = Agent('openai:gpt-5.2', toolsets=[server])

MCP tools can include metadata that provides additional information about the tool's characteristics, which can be useful when [filtering tools][pydantic_ai.toolsets.FilteredToolset]. The `meta`, `annotations`, and `output_schema` fields can be found on the `metadata` dict on the [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] object that's passed to filter functions.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The bit about output_schema is out of date now, right? ToolDef just has a return_schema field now I believe.

Comment thread docs/mcp/client.md Outdated
agent = Agent('openai:gpt-5', toolsets=[toolset])
```

See [Streamable HTTP Client](#streamable-http-client) for the full `agent.run` plumbing.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Drop this

Comment thread uv.lock
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Help me understand why adding pydocket to dev dependencies, results in -109 / +25 on uv.lock. Seems like more than we need.

Comment thread docs/mcp/client.md Outdated
from pydantic_ai.mcp import MCPToolset

toolset = MCPToolset('http://localhost:8000/mcp')
agent = Agent('openai:gpt-5', toolsets=[toolset])
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.

Every other example in this file uses openai:gpt-5.2 (the latest frontier model per the project's docs guidelines). This one should match for consistency:

agent = Agent('openai:gpt-5.2', toolsets=[toolset])

…f, use gpt-5.2

- client.md: the MCPToolset metadata dict exposes `meta`/`annotations`/`task`;
  the output schema is on `ToolDefinition.return_schema`, not in metadata.
- tools.py: update the `metadata` docstring to match the new MCPToolset.
- Drop the redundant "See Streamable HTTP Client" line and match the example
  model to the rest of the docs.
@DouweM DouweM changed the title MCPToolset: add support for MCP background tasks (SEP-1686) Add support for MCP background tasks (SEP-1686) May 21, 2026
@DouweM DouweM merged commit 6c53be1 into pydantic:main May 21, 2026
46 checks passed
@DouweM
Copy link
Copy Markdown
Collaborator

DouweM commented May 21, 2026

@Alex-Resch Thanks Alex!

DouweM added a commit that referenced this pull request May 21, 2026
Adapts incoming main changes to v2 idioms:
- Bedrock adaptive-thinking/effort (#5326): rewrote `bedrock_anthropic_model_profile` and `BedrockModelProfile` flag reads to the v2 TypedDict shape (`merge_profile`, `.get(...)`) instead of `.update()`/attribute access; fixed the two new profile fields that were merged in as dataclass-style defaults.
- XSearch model-agnostic refactor (#5120): adopted the moved `capabilities/x_search.py` + `common_tools/x_search.py`, stripping the v1 `builtin`/`native` deprecation shims (removed in v2) and reconciling the XSearch test suite.
- MCP background tasks / SEP-1686 (#5192): kept v2's runtime fastmcp import structure plus the new `ToolTask` import; relocated the new background-task tests into `tests/test_mcp.py` (v2 dropped `tests/test_mcp_toolset.py`).
- xAI tool result IDs (#5355): tracks the latest `xai-sdk` (1.12.2), which keeps `tool_result(result, tool_call_id=...)`; all 112 `tests/models/test_xai.py` pass against it.
- Reconciled capabilities/MCP docs to keep v2 default-behavior wording while adding the XSearch entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature request, or PR implementing a feature (enhancement) size: M Medium PR (101-500 weighted lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FastMCPToolset: add support for MCP background tasks (SEP-1686)

4 participants