You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Task/subagent MCP proxies reconstruct a raw tools/call request instead of executing through the current source MCPTool. This bypasses source-owned argument normalization, internal-intent stripping, local-URL resolution, reconnect behavior, abort handling, and result metadata.
These are separate layers of one parity problem: the same MCP tool does not have the same model-facing definition or execution semantics when used directly and through a Task subagent.
Affected version
Confirmed in @oh-my-pi/pi-coding-agent 17.0.7 and current main as of 2026-07-21.
Failure 1: model-generated optional overfill
Consider an MCP operation with mutually exclusive optional targets, conceptually:
OMP correctly removes empty optional placeholders, but it cannot safely remove statusUpdateType: "project": that value is legitimate when paired with a real statusUpdateId. The server therefore receives an invalid target combination and rejects the call.
#4336/#4340 established that explicit tool.strict === false must be preserved on OpenAI-family payloads because omitted and explicit-false are distinct. The remaining gap is that ordinary active and deferred MCP tools do not set strict: false, so there is no value for the serializer to preserve.
Failure 2: Task proxy bypasses the source MCP boundary
The JS eval bridge supplies the harness-internal intent field i to tool execution. A direct parent MCP call reaches the source MCPTool, whose outbound preparation removes i unless the MCP server genuinely declares a parameter by that name.
The Task/subagent proxy path does not execute through that source tool. createMCPProxyTools in packages/coding-agent/src/task/executor.ts locates the connection and calls the low-level MCP callTool(...) function with the received parameters.
Conceptual reproduction with a strict fake MCP tool:
The bypass also means parent and Task paths can diverge for:
optional placeholder removal;
local-URL argument resolution;
current/reconnected connection ownership;
provider/source metadata;
MCP error details;
future behavior added to MCPTool.execute.
Expected behavior
For one MCP tool and one input:
direct model calls, parent eval calls, and Task-subagent eval calls expose the same intended strictness;
all paths execute through the same authoritative outbound-argument boundary;
internal i is removed unless the MCP input schema declares a real i parameter;
empty non-required placeholders are removed;
legitimate nonempty optional values are preserved without server-specific guessing;
Task timeout and caller abort behavior remain bounded;
local URLs, reconnects, result content, and details match the source MCP tool.
Suggested fix shape
Mark MCPTool and DeferredMCPTool explicitly non-strict:
readonlystrict=falseasconst;
Propagate that property on Task proxy definitions.
Make Task proxies retain and delegate to the current source MCPTool/DeferredMCPTool rather than rebuilding a raw callTool request.
Preserve the existing Task timeout by combining its abort signal with the caller signal around source-tool execution.
Resolve source-tool identity by raw MCP server/tool metadata, not only a normalized display name, so reconnect replacements remain correct.
The key invariant is that argument shaping stays owned by the source MCP tool. Copying selected normalization steps into executor.ts would leave the two paths able to drift again.
Regression coverage
Useful focused cases:
MCPTool.strict === false;
DeferredMCPTool.strict === false;
Task proxy definition preserves strict: false;
OpenAI/Codex payload contains explicit strict: false for parent and Task-proxied MCP tools;
strict fake server receives exactly { body: "x" } from { body: "x", optional: "", i: "js prelude" } through parent and Task paths;
a server schema that declares a real i parameter still receives it;
parent direct, parent eval, and Task-subagent eval produce identical outbound arguments;
Task proxy retains local URL resolution, reconnect replacement, abort, timeout, result content, and error details.
Related reports
These cover adjacent layers but not the combined defect:
A tested implementation and parity suite are available in commit 34bbbb0e. The associated PR #5867 was automatically closed by the contributor-vouch gate before technical review.
Summary
Two MCP argument-shaping gaps cause otherwise valid calls to strict MCP servers to fail:
MCPToolandDeferredMCPTooldo not explicitly declarestrict: false. OpenAI-family serializers preserve explicitfalseafter Preserve explicit strict:false when serializing function tools #4336/fix(ai): preserve explicit tool.strict:false on OpenAI-family payloads #4340, but MCP-backed tools never supply it. Models can therefore synthesize nonempty values for unrelated optional or mutually exclusive fields.tools/callrequest instead of executing through the current sourceMCPTool. This bypasses source-owned argument normalization, internal-intent stripping, local-URL resolution, reconnect behavior, abort handling, and result metadata.These are separate layers of one parity problem: the same MCP tool does not have the same model-facing definition or execution semantics when used directly and through a Task subagent.
Affected version
Confirmed in
@oh-my-pi/pi-coding-agent17.0.7 and currentmainas of 2026-07-21.Failure 1: model-generated optional overfill
Consider an MCP operation with mutually exclusive optional targets, conceptually:
{ "type": "object", "properties": { "body": { "type": "string" }, "issueId": { "type": "string" }, "statusUpdateId": { "type": "string" }, "statusUpdateType": { "enum": ["project", "initiative"] } }, "required": ["body"], "additionalProperties": false }An issue comment should be:
{ "body": "message", "issueId": "ISSUE-123" }On an OpenAI/Codex path, the model can instead emit all optional slots, including a nonempty enum:
{ "body": "message", "issueId": "ISSUE-123", "statusUpdateId": "", "statusUpdateType": "project" }OMP correctly removes empty optional placeholders, but it cannot safely remove
statusUpdateType: "project": that value is legitimate when paired with a realstatusUpdateId. The server therefore receives an invalid target combination and rejects the call.#4336/#4340 established that explicit
tool.strict === falsemust be preserved on OpenAI-family payloads because omitted and explicit-false are distinct. The remaining gap is that ordinary active and deferred MCP tools do not setstrict: false, so there is no value for the serializer to preserve.Current classes:
packages/coding-agent/src/mcp/tool-bridge.ts—MCPToolpackages/coding-agent/src/mcp/tool-bridge.ts—DeferredMCPToolNeither declares an explicit
strictvalue.Failure 2: Task proxy bypasses the source MCP boundary
The JS eval bridge supplies the harness-internal intent field
ito tool execution. A direct parent MCP call reaches the sourceMCPTool, whose outbound preparation removesiunless the MCP server genuinely declares a parameter by that name.The Task/subagent proxy path does not execute through that source tool.
createMCPProxyToolsinpackages/coding-agent/src/task/executor.tslocates the connection and calls the low-level MCPcallTool(...)function with the received parameters.Conceptual reproduction with a strict fake MCP tool:
Expected outbound
tools/callarguments:{ "body": "x" }Current Task-proxy outbound arguments include the undeclared internal key:
{ "body": "x", "optional": "", "i": "js prelude" }A server using
additionalProperties: falserejects the request with an error such as:The bypass also means parent and Task paths can diverge for:
MCPTool.execute.Expected behavior
For one MCP tool and one input:
iis removed unless the MCP input schema declares a realiparameter;Suggested fix shape
MCPToolandDeferredMCPToolexplicitly non-strict:MCPTool/DeferredMCPToolrather than rebuilding a rawcallToolrequest.The key invariant is that argument shaping stays owned by the source MCP tool. Copying selected normalization steps into
executor.tswould leave the two paths able to drift again.Regression coverage
Useful focused cases:
MCPTool.strict === false;DeferredMCPTool.strict === false;strict: false;strict: falsefor parent and Task-proxied MCP tools;{ body: "x" }from{ body: "x", optional: "", i: "js prelude" }through parent and Task paths;iparameter still receives it;Related reports
These cover adjacent layers but not the combined defect:
tool.*bridge forwards the harnessiintent arg to MCP servers — strict-schema servers reject every call (unrecognized_keys: ["i"]) #3575 — internalistripping for direct eval/MCP execution;strict: false;A tested implementation and parity suite are available in commit
34bbbb0e. The associated PR #5867 was automatically closed by the contributor-vouch gate before technical review.