Skip to content

MCP task proxies bypass source tool semantics; MCP tools omit strict:false #6208

Description

@julgr

Summary

Two MCP argument-shaping gaps cause otherwise valid calls to strict MCP servers to fail:

  1. MCPTool and DeferredMCPTool do not explicitly declare strict: false. OpenAI-family serializers preserve explicit false after 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.
  2. 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:

{
  "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 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.

Current classes:

  • packages/coding-agent/src/mcp/tool-bridge.tsMCPTool
  • packages/coding-agent/src/mcp/tool-bridge.tsDeferredMCPTool

Neither declares an explicit strict value.

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:

const input = {
  body: "x",
  optional: "",
  i: "js prelude",
};

Expected outbound tools/call arguments:

{ "body": "x" }

Current Task-proxy outbound arguments include the undeclared internal key:

{ "body": "x", "optional": "", "i": "js prelude" }

A server using additionalProperties: false rejects the request with an error such as:

-32602 Input validation error: unrecognized_keys ["i"]

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

  1. Mark MCPTool and DeferredMCPTool explicitly non-strict:
readonly strict = false as const;
  1. Propagate that property on Task proxy definitions.
  2. Make Task proxies retain and delegate to the current source MCPTool/DeferredMCPTool rather than rebuilding a raw callTool request.
  3. Preserve the existing Task timeout by combining its abort signal with the caller signal around source-tool execution.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentAgent runtime planning and orchestrationbugSomething isn't workingprio:p2Medium: important but not urgenttoolTool behavior and integrationstriaged

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions