Skip to content

Commit 754c979

Browse files
committed
Omit optional fields with empty strings from tool calls
1 parent 42aa95b commit 754c979

File tree

4 files changed

+268
-14
lines changed

4 files changed

+268
-14
lines changed

client/src/App.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
2222
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
2323
import { OAuthStateMachine } from "./lib/oauth-state-machine";
2424
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
25+
import { cleanParams } from "./utils/paramUtils";
2526
import React, {
2627
Suspense,
2728
useCallback,
@@ -777,12 +778,18 @@ const App = () => {
777778
lastToolCallOriginTabRef.current = currentTabRef.current;
778779

779780
try {
781+
// Find the tool schema to clean parameters properly
782+
const tool = tools.find(t => t.name === name);
783+
const cleanedParams = tool?.inputSchema
784+
? cleanParams(params, tool.inputSchema as any)
785+
: params;
786+
780787
const response = await sendMCPRequest(
781788
{
782789
method: "tools/call" as const,
783790
params: {
784791
name,
785-
arguments: params,
792+
arguments: cleanedParams,
786793
_meta: {
787794
progressToken: progressTokenRef.current++,
788795
},

client/src/components/ToolsTab.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,22 @@ const ToolsTab = ({
160160
name={key}
161161
placeholder={prop.description}
162162
value={(params[key] as string) ?? ""}
163-
onChange={(e) =>
164-
setParams({
165-
...params,
166-
[key]:
167-
e.target.value === ""
168-
? undefined
169-
: e.target.value,
170-
})
171-
}
163+
onChange={(e) => {
164+
const value = e.target.value;
165+
if (value === "" && !required) {
166+
// Optional field cleared - set to undefined to omit from request
167+
setParams({
168+
...params,
169+
[key]: undefined,
170+
});
171+
} else {
172+
// Field has value or is required - keep as string
173+
setParams({
174+
...params,
175+
[key]: value,
176+
});
177+
}
178+
}}
172179
className="mt-1"
173180
/>
174181
) : prop.type === "object" || prop.type === "array" ? (
@@ -202,10 +209,22 @@ const ToolsTab = ({
202209
value={(params[key] as string) ?? ""}
203210
onChange={(e) => {
204211
const value = e.target.value;
205-
setParams({
206-
...params,
207-
[key]: value === "" ? "" : Number(value),
208-
});
212+
if (value === "" && !required) {
213+
// Optional field cleared - set to undefined to omit from request
214+
setParams({
215+
...params,
216+
[key]: undefined,
217+
});
218+
} else {
219+
// Field has value or is required - convert to number
220+
const num = Number(value);
221+
if (!isNaN(num) || value === "") {
222+
setParams({
223+
...params,
224+
[key]: value === "" ? "" : num,
225+
});
226+
}
227+
}
209228
}}
210229
className="mt-1"
211230
/>
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { cleanParams } from "../paramUtils";
2+
import type { JsonSchemaType } from "../jsonUtils";
3+
4+
describe("cleanParams", () => {
5+
it("should preserve required fields even when empty", () => {
6+
const schema: JsonSchemaType = {
7+
type: "object",
8+
required: ["requiredString", "requiredNumber"],
9+
properties: {
10+
requiredString: { type: "string" },
11+
requiredNumber: { type: "number" },
12+
optionalString: { type: "string" },
13+
optionalNumber: { type: "number" },
14+
},
15+
};
16+
17+
const params = {
18+
requiredString: "",
19+
requiredNumber: 0,
20+
optionalString: "",
21+
optionalNumber: undefined,
22+
};
23+
24+
const cleaned = cleanParams(params, schema);
25+
26+
expect(cleaned).toEqual({
27+
requiredString: "",
28+
requiredNumber: 0,
29+
// optionalString and optionalNumber should be omitted
30+
});
31+
});
32+
33+
it("should omit optional fields with empty strings", () => {
34+
const schema: JsonSchemaType = {
35+
type: "object",
36+
required: [],
37+
properties: {
38+
optionalString: { type: "string" },
39+
optionalNumber: { type: "number" },
40+
},
41+
};
42+
43+
const params = {
44+
optionalString: "",
45+
optionalNumber: "",
46+
};
47+
48+
const cleaned = cleanParams(params, schema);
49+
50+
expect(cleaned).toEqual({});
51+
});
52+
53+
it("should omit optional fields with undefined values", () => {
54+
const schema: JsonSchemaType = {
55+
type: "object",
56+
required: [],
57+
properties: {
58+
optionalString: { type: "string" },
59+
optionalNumber: { type: "number" },
60+
},
61+
};
62+
63+
const params = {
64+
optionalString: undefined,
65+
optionalNumber: undefined,
66+
};
67+
68+
const cleaned = cleanParams(params, schema);
69+
70+
expect(cleaned).toEqual({});
71+
});
72+
73+
it("should omit optional fields with null values", () => {
74+
const schema: JsonSchemaType = {
75+
type: "object",
76+
required: [],
77+
properties: {
78+
optionalString: { type: "string" },
79+
optionalNumber: { type: "number" },
80+
},
81+
};
82+
83+
const params = {
84+
optionalString: null,
85+
optionalNumber: null,
86+
};
87+
88+
const cleaned = cleanParams(params, schema);
89+
90+
expect(cleaned).toEqual({});
91+
});
92+
93+
it("should preserve optional fields with meaningful values", () => {
94+
const schema: JsonSchemaType = {
95+
type: "object",
96+
required: [],
97+
properties: {
98+
optionalString: { type: "string" },
99+
optionalNumber: { type: "number" },
100+
optionalBoolean: { type: "boolean" },
101+
},
102+
};
103+
104+
const params = {
105+
optionalString: "hello",
106+
optionalNumber: 42,
107+
optionalBoolean: false, // false is a meaningful value
108+
};
109+
110+
const cleaned = cleanParams(params, schema);
111+
112+
expect(cleaned).toEqual({
113+
optionalString: "hello",
114+
optionalNumber: 42,
115+
optionalBoolean: false,
116+
});
117+
});
118+
119+
it("should handle mixed required and optional fields", () => {
120+
const schema: JsonSchemaType = {
121+
type: "object",
122+
required: ["requiredField"],
123+
properties: {
124+
requiredField: { type: "string" },
125+
optionalWithValue: { type: "string" },
126+
optionalEmpty: { type: "string" },
127+
optionalUndefined: { type: "number" },
128+
},
129+
};
130+
131+
const params = {
132+
requiredField: "",
133+
optionalWithValue: "test",
134+
optionalEmpty: "",
135+
optionalUndefined: undefined,
136+
};
137+
138+
const cleaned = cleanParams(params, schema);
139+
140+
expect(cleaned).toEqual({
141+
requiredField: "",
142+
optionalWithValue: "test",
143+
});
144+
});
145+
146+
it("should handle schema without required array", () => {
147+
const schema: JsonSchemaType = {
148+
type: "object",
149+
properties: {
150+
field1: { type: "string" },
151+
field2: { type: "number" },
152+
},
153+
};
154+
155+
const params = {
156+
field1: "",
157+
field2: undefined,
158+
};
159+
160+
const cleaned = cleanParams(params, schema);
161+
162+
expect(cleaned).toEqual({});
163+
});
164+
165+
it("should preserve zero values for numbers", () => {
166+
const schema: JsonSchemaType = {
167+
type: "object",
168+
required: [],
169+
properties: {
170+
optionalNumber: { type: "number" },
171+
},
172+
};
173+
174+
const params = {
175+
optionalNumber: 0,
176+
};
177+
178+
const cleaned = cleanParams(params, schema);
179+
180+
expect(cleaned).toEqual({
181+
optionalNumber: 0,
182+
});
183+
});
184+
});

client/src/utils/paramUtils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { JsonSchemaType } from "./jsonUtils";
2+
3+
/**
4+
* Cleans parameters by removing undefined, null, and empty string values for optional fields
5+
* while preserving all values for required fields.
6+
*
7+
* @param params - The parameters object to clean
8+
* @param schema - The JSON schema defining which fields are required
9+
* @returns Cleaned parameters object with optional empty fields omitted
10+
*/
11+
export function cleanParams(
12+
params: Record<string, unknown>,
13+
schema: JsonSchemaType
14+
): Record<string, unknown> {
15+
const cleaned: Record<string, unknown> = {};
16+
const required = schema.required || [];
17+
18+
for (const [key, value] of Object.entries(params)) {
19+
const isFieldRequired = required.includes(key);
20+
21+
if (isFieldRequired) {
22+
// Required fields: always include, even if empty string or falsy
23+
cleaned[key] = value;
24+
} else {
25+
// Optional fields: only include if they have meaningful values
26+
if (value !== undefined && value !== "" && value !== null) {
27+
cleaned[key] = value;
28+
}
29+
// Empty strings, undefined, null for optional fields → omit completely
30+
}
31+
}
32+
33+
return cleaned;
34+
}
35+
36+
/**
37+
* Checks if a field should be set to undefined when cleared
38+
* @param isRequired - Whether the field is required
39+
* @param value - The current value
40+
* @returns Whether to set the field to undefined
41+
*/
42+
export function shouldSetToUndefined(isRequired: boolean, value: string): boolean {
43+
return !isRequired && value === "";
44+
}

0 commit comments

Comments
 (0)