feat: deliver MCP toolsets via a pydantic-ai capability (McpToolsets) - #834
Open
mpfaffenberger wants to merge 2 commits into
Open
feat: deliver MCP toolsets via a pydantic-ai capability (McpToolsets)#834mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
Seventh in the capability series: promotes MCP server delivery from the Agent(toolsets=...) constructor kwarg to the first-class get_toolset() capability seam via a new McpToolsets capability in code_puppy/agents/_mcp_toolsets.py. Only delivery moves. Server discovery (load_mcp_servers), bound-server autostart, and the two-pass collision filter (filter_conflicting_mcp_tools) are untouched and still run before the capability is constructed; agent._mcp_servers keeps its plugin-facing contract (agent_run_context, token_usage, acp all unaffected). Parity verified against pydantic-ai 2.31.0 source: - Agent.__aenter__ enters _get_toolset(), which includes capability toolsets, so MCP server lifecycle is identical. - Agent.override(toolsets=...) skips _cap_toolsets exactly as it skips constructor _user_toolsets. This is what keeps the DBOS durable wrapper working: DBOSAgent reads the public agent.toolsets property (capability toolsets included), dbosifies leaves via visit_and_replace, and overrides them in per run. - The kwarg's normalization (AbstractToolsets kept in order, non-toolsets wrapped in DynamicToolset and appended after) is replicated inside the capability, since get_toolset() returns a single toolset. - One inert ordering divergence: capability toolsets append after dynamic toolsets in _build_toolset_list; MCP servers are the only toolsets code_puppy delivers, so the combined run toolset is identical. Not spec-constructible (get_serialization_name -> None): live MCP toolsets backed by subprocesses/HTTP clients owned by the MCP manager.
Review nit from PR #834 pass 1: the builder collision test now runs the built agent against a FunctionModel and asserts the colliding MCP tool name is hidden from the model while the surviving tool from the same server stays visible (membership assertions, since other capabilities like ToolOutputLimits contribute their own tools).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Seventh in the capability series (#828 SteerInjection, #829 HistoryCompaction, #830 PluginMessageTransform, #831 PerModelSettings, #832 AssembledInstructions, #833 ResolvedModel): promotes MCP server delivery from the
Agent(toolsets=...)constructor kwarg to the first-classget_toolset()capability seam, via a newMcpToolsetscapability incode_puppy/agents/_mcp_toolsets.py.Both construction sites converted:
_builder.build_pydantic_agent— probe pass getsMcpToolsets([]), final pass getsMcpToolsets(filtered_mcp_servers)subagent_invocation—McpToolsets(mcp_servers)(unfiltered, exactly as the kwarg was there)What deliberately did NOT move
load_mcp_servers), bound-server autostart, and the two-pass collision filter (filter_conflicting_mcp_tools) still run before the capability is constructed — same restraint as the rest of the series: only delivery changes.agent._mcp_serverskeeps its plugin-facing contract (agent_run_contexthook,token_usage, the acp plugin's mutation) — all unaffected.build_tool_probe_for_agent's stripped probe keepstoolsets=[](refactor: promote system-prompt delivery to an AssembledInstructions capability #832/Promote model delivery to a ResolvedModel capability #833 precedent).Parity (verified against pydantic-ai 2.31.0 source)
Agent.__aenter__enters_get_toolset(), which includes capability toolsets — MCP servers get entered/exited identically.DBOSAgentreads the publicagent.toolsetsproperty (which includes capability toolsets), dbosifies leaves viavisit_and_replace, and applies them per run withoverride(toolsets=...)— andoverrideskips_cap_toolsetsexactly as it skipped constructor_user_toolsets. Same replace semantics; pinned by tests that perform the exact traversal DBOS does.AbstractToolsets (in order) and wrapped anything else inDynamicToolset(toolset_func=...)appended after;get_toolset()returns a single toolset, so the capability replicates that split-then-wrap itself, then combines (0 → None,1 → identity,n → CombinedToolset). Memoized so repeatedget_toolset()calls deliver stable identity, matching the constructor storing its list once._build_toolset_list, and ride wrapped inCapabilityOwnedToolset/CombinedToolsetinsideagent.toolsets. MCP servers are the only toolsets code_puppy delivers and name conflicts raise either way, so the combined run toolset is behaviorally identical; the wrapper chain is transparent tovisit_and_replace(pinned by test).Not spec-constructible (
get_serialization_name() -> None): live MCP toolsets backed by subprocesses/HTTP clients owned by the MCP manager — same precedent as the other live-object capabilities in the series.Tests
13 contract tests in
tests/agents/test_mcp_toolsets_capability.py: pure normalization contract, pydantic-ai integration (public property surfacing,visit_and_replacereachability,overridereplacement,__aenter__lifecycle counters, end-to-endFunctionModeltool call through a capability-delivered toolset), and real-builder integration (delivery + collision filter still applied,_user_toolsetsempty,agent._mcp_serverscontract pinned).Full suite: 7589 passed; the 3 reds are the exact documented pre-existing main failures (
test_render_version_check_current+ the two full-run-order subagent-adjacent flakes, which pass in isolation on this branch).Merge-order note
Like the previous six, this PR grazes the shared
capabilities=[...]blocks in_builder.py+subagent_invocation.py. Whichever of the seven lands last eats trivial rebases. Musical chairs continues.