Skip to content

Commit aed01ca

Browse files
committed
Merge remote-tracking branch 'origin/farm/8a720b5a/mcp-tools-omit-unused-optional-args'
2 parents 6591fc2 + 655ab43 commit aed01ca

3 files changed

Lines changed: 120 additions & 2 deletions

File tree

packages/coding-agent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
- Fixed `task.maxConcurrency: 0` serializing subagent spawns instead of running them unbounded. The settings UI labels `0` as "Unlimited", but the session-scoped spawn `Semaphore` clamped `max` via `Math.max(1, max)`, so the second subagent body in a batch always waited for the first to release the seat. The constructor now treats `max <= 0` (and any non-finite input) as unbounded via `Number.POSITIVE_INFINITY`, matching the eval `parallel()`/`pipeline()` worker-pool semantics ([#3305](https://github.com/can1357/oh-my-pi/issues/3305)).
1515

16+
### Fixed
17+
18+
- Fixed MCP tool calls forwarding empty optional placeholder arguments (`""` and `{}`) to `tools/call`; optional placeholders are now omitted while required fields and meaningful falsy values are preserved. ([#3302](https://github.com/can1357/oh-my-pi/issues/3302))
19+
1620
## [16.1.16] - 2026-06-23
1721

1822
### Breaking Changes

packages/coding-agent/src/mcp/tool-bridge.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,31 @@ function normalizeToolArgs(value: unknown): MCPToolArgs {
5858
return value as MCPToolArgs;
5959
}
6060

61+
function isUnusedOptionalPlaceholder(value: unknown): boolean {
62+
return (
63+
value === undefined ||
64+
value === "" ||
65+
(typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0)
66+
);
67+
}
68+
69+
function omitUnusedOptionalArgs(args: MCPToolArgs, inputSchema: MCPToolDefinition["inputSchema"]): MCPToolArgs {
70+
const properties = inputSchema.properties;
71+
if (!properties) return args;
72+
73+
let cleaned: MCPToolArgs | undefined;
74+
const required = new Set(inputSchema.required ?? []);
75+
for (const [key, value] of Object.entries(args)) {
76+
if (required.has(key) || !Object.hasOwn(properties, key) || !isUnusedOptionalPlaceholder(value)) {
77+
continue;
78+
}
79+
cleaned ??= { ...args };
80+
delete cleaned[key];
81+
}
82+
83+
return cleaned ?? args;
84+
}
85+
6186
/** Details included in MCP tool results for rendering */
6287
export interface MCPToolDetails {
6388
/** Server name */
@@ -261,7 +286,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
261286
signal?: AbortSignal,
262287
): Promise<CustomToolResult<MCPToolDetails>> {
263288
throwIfAborted(signal);
264-
const args = normalizeToolArgs(params);
289+
const args = omitUnusedOptionalArgs(normalizeToolArgs(params), this.tool.inputSchema);
265290
const provider = this.connection._source?.provider;
266291
const providerName = this.connection._source?.providerName;
267292

@@ -360,7 +385,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
360385
signal?: AbortSignal,
361386
): Promise<CustomToolResult<MCPToolDetails>> {
362387
throwIfAborted(signal);
363-
const args = normalizeToolArgs(params);
388+
const args = omitUnusedOptionalArgs(normalizeToolArgs(params), this.tool.inputSchema);
364389
const provider = this.#fallbackProvider;
365390
const providerName = this.#fallbackProviderName;
366391

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, expect, it } from "bun:test";
2+
import type { CustomToolContext } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools";
3+
import { DeferredMCPTool, MCPTool, type MCPToolDefinition } from "@oh-my-pi/pi-coding-agent/mcp";
4+
import type { MCPServerConnection } from "@oh-my-pi/pi-coding-agent/mcp/types";
5+
import { createMockConnection, createMockTransport } from "./mcp-test-utils";
6+
7+
type CapturedRequest = {
8+
method: string;
9+
params: Record<string, unknown> | undefined;
10+
};
11+
12+
const unusedContext = {} as CustomToolContext;
13+
14+
function createSearchToolDefinition(): MCPToolDefinition {
15+
return {
16+
name: "search",
17+
description: "Search symbols or file locations",
18+
inputSchema: {
19+
type: "object",
20+
properties: {
21+
symbol: { type: "string" },
22+
language: { type: "string" },
23+
file: { type: "string" },
24+
line: { type: "number" },
25+
column: { type: "number" },
26+
filters: { type: "object" },
27+
exact: { type: "boolean" },
28+
},
29+
required: ["symbol", "language"],
30+
},
31+
};
32+
}
33+
34+
function createCapturedConnection(calls: CapturedRequest[]): MCPServerConnection {
35+
const transport = createMockTransport(
36+
new Map([["tools/call", [{ content: [{ type: "text", text: "ok" }] }]]]),
37+
(method, params) => calls.push({ method, params }),
38+
);
39+
return createMockConnection({ tools: {} }, transport);
40+
}
41+
42+
describe("MCP tool arguments", () => {
43+
it("omits optional empty placeholders before tools/call", async () => {
44+
const calls: CapturedRequest[] = [];
45+
const tool = new MCPTool(createCapturedConnection(calls), createSearchToolDefinition());
46+
47+
await tool.execute(
48+
"call-1",
49+
{ symbol: "Foo", language: "", file: "", line: 0, filters: {}, exact: false },
50+
undefined,
51+
unusedContext,
52+
undefined,
53+
);
54+
55+
expect(calls).toEqual([
56+
{
57+
method: "tools/call",
58+
params: {
59+
name: "search",
60+
arguments: { symbol: "Foo", language: "", line: 0, exact: false },
61+
},
62+
},
63+
]);
64+
});
65+
66+
it("omits optional empty placeholders for deferred MCP tools", async () => {
67+
const calls: CapturedRequest[] = [];
68+
const connection = createCapturedConnection(calls);
69+
const tool = new DeferredMCPTool("intellij-index", createSearchToolDefinition(), async () => connection);
70+
71+
await tool.execute(
72+
"call-1",
73+
{ symbol: "Foo", language: "TypeScript", file: "", column: "", filters: {} },
74+
undefined,
75+
unusedContext,
76+
undefined,
77+
);
78+
79+
expect(calls).toEqual([
80+
{
81+
method: "tools/call",
82+
params: {
83+
name: "search",
84+
arguments: { symbol: "Foo", language: "TypeScript" },
85+
},
86+
},
87+
]);
88+
});
89+
});

0 commit comments

Comments
 (0)