Omit optional fields with empty strings from tool calls
This commit is contained in:
parent
42aa95b343
commit
754c97956f
4 changed files with 268 additions and 14 deletions
|
|
@ -22,6 +22,7 @@ import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
|
||||||
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
|
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
|
||||||
import { OAuthStateMachine } from "./lib/oauth-state-machine";
|
import { OAuthStateMachine } from "./lib/oauth-state-machine";
|
||||||
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
||||||
|
import { cleanParams } from "./utils/paramUtils";
|
||||||
import React, {
|
import React, {
|
||||||
Suspense,
|
Suspense,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
|
@ -777,12 +778,18 @@ const App = () => {
|
||||||
lastToolCallOriginTabRef.current = currentTabRef.current;
|
lastToolCallOriginTabRef.current = currentTabRef.current;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Find the tool schema to clean parameters properly
|
||||||
|
const tool = tools.find(t => t.name === name);
|
||||||
|
const cleanedParams = tool?.inputSchema
|
||||||
|
? cleanParams(params, tool.inputSchema as any)
|
||||||
|
: params;
|
||||||
|
|
||||||
const response = await sendMCPRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "tools/call" as const,
|
method: "tools/call" as const,
|
||||||
params: {
|
params: {
|
||||||
name,
|
name,
|
||||||
arguments: params,
|
arguments: cleanedParams,
|
||||||
_meta: {
|
_meta: {
|
||||||
progressToken: progressTokenRef.current++,
|
progressToken: progressTokenRef.current++,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -160,15 +160,22 @@ const ToolsTab = ({
|
||||||
name={key}
|
name={key}
|
||||||
placeholder={prop.description}
|
placeholder={prop.description}
|
||||||
value={(params[key] as string) ?? ""}
|
value={(params[key] as string) ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setParams({
|
const value = e.target.value;
|
||||||
...params,
|
if (value === "" && !required) {
|
||||||
[key]:
|
// Optional field cleared - set to undefined to omit from request
|
||||||
e.target.value === ""
|
setParams({
|
||||||
? undefined
|
...params,
|
||||||
: e.target.value,
|
[key]: undefined,
|
||||||
})
|
});
|
||||||
}
|
} else {
|
||||||
|
// Field has value or is required - keep as string
|
||||||
|
setParams({
|
||||||
|
...params,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
) : prop.type === "object" || prop.type === "array" ? (
|
) : prop.type === "object" || prop.type === "array" ? (
|
||||||
|
|
@ -202,10 +209,22 @@ const ToolsTab = ({
|
||||||
value={(params[key] as string) ?? ""}
|
value={(params[key] as string) ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setParams({
|
if (value === "" && !required) {
|
||||||
...params,
|
// Optional field cleared - set to undefined to omit from request
|
||||||
[key]: value === "" ? "" : Number(value),
|
setParams({
|
||||||
});
|
...params,
|
||||||
|
[key]: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Field has value or is required - convert to number
|
||||||
|
const num = Number(value);
|
||||||
|
if (!isNaN(num) || value === "") {
|
||||||
|
setParams({
|
||||||
|
...params,
|
||||||
|
[key]: value === "" ? "" : num,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
184
client/src/utils/__tests__/paramUtils.test.ts
Normal file
184
client/src/utils/__tests__/paramUtils.test.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { cleanParams } from "../paramUtils";
|
||||||
|
import type { JsonSchemaType } from "../jsonUtils";
|
||||||
|
|
||||||
|
describe("cleanParams", () => {
|
||||||
|
it("should preserve required fields even when empty", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: ["requiredString", "requiredNumber"],
|
||||||
|
properties: {
|
||||||
|
requiredString: { type: "string" },
|
||||||
|
requiredNumber: { type: "number" },
|
||||||
|
optionalString: { type: "string" },
|
||||||
|
optionalNumber: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
requiredString: "",
|
||||||
|
requiredNumber: 0,
|
||||||
|
optionalString: "",
|
||||||
|
optionalNumber: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({
|
||||||
|
requiredString: "",
|
||||||
|
requiredNumber: 0,
|
||||||
|
// optionalString and optionalNumber should be omitted
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should omit optional fields with empty strings", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: [],
|
||||||
|
properties: {
|
||||||
|
optionalString: { type: "string" },
|
||||||
|
optionalNumber: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
optionalString: "",
|
||||||
|
optionalNumber: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should omit optional fields with undefined values", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: [],
|
||||||
|
properties: {
|
||||||
|
optionalString: { type: "string" },
|
||||||
|
optionalNumber: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
optionalString: undefined,
|
||||||
|
optionalNumber: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should omit optional fields with null values", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: [],
|
||||||
|
properties: {
|
||||||
|
optionalString: { type: "string" },
|
||||||
|
optionalNumber: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
optionalString: null,
|
||||||
|
optionalNumber: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve optional fields with meaningful values", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: [],
|
||||||
|
properties: {
|
||||||
|
optionalString: { type: "string" },
|
||||||
|
optionalNumber: { type: "number" },
|
||||||
|
optionalBoolean: { type: "boolean" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
optionalString: "hello",
|
||||||
|
optionalNumber: 42,
|
||||||
|
optionalBoolean: false, // false is a meaningful value
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({
|
||||||
|
optionalString: "hello",
|
||||||
|
optionalNumber: 42,
|
||||||
|
optionalBoolean: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed required and optional fields", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: ["requiredField"],
|
||||||
|
properties: {
|
||||||
|
requiredField: { type: "string" },
|
||||||
|
optionalWithValue: { type: "string" },
|
||||||
|
optionalEmpty: { type: "string" },
|
||||||
|
optionalUndefined: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
requiredField: "",
|
||||||
|
optionalWithValue: "test",
|
||||||
|
optionalEmpty: "",
|
||||||
|
optionalUndefined: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({
|
||||||
|
requiredField: "",
|
||||||
|
optionalWithValue: "test",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle schema without required array", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
field1: { type: "string" },
|
||||||
|
field2: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
field1: "",
|
||||||
|
field2: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve zero values for numbers", () => {
|
||||||
|
const schema: JsonSchemaType = {
|
||||||
|
type: "object",
|
||||||
|
required: [],
|
||||||
|
properties: {
|
||||||
|
optionalNumber: { type: "number" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
optionalNumber: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = cleanParams(params, schema);
|
||||||
|
|
||||||
|
expect(cleaned).toEqual({
|
||||||
|
optionalNumber: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
44
client/src/utils/paramUtils.ts
Normal file
44
client/src/utils/paramUtils.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { JsonSchemaType } from "./jsonUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans parameters by removing undefined, null, and empty string values for optional fields
|
||||||
|
* while preserving all values for required fields.
|
||||||
|
*
|
||||||
|
* @param params - The parameters object to clean
|
||||||
|
* @param schema - The JSON schema defining which fields are required
|
||||||
|
* @returns Cleaned parameters object with optional empty fields omitted
|
||||||
|
*/
|
||||||
|
export function cleanParams(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
schema: JsonSchemaType
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const cleaned: Record<string, unknown> = {};
|
||||||
|
const required = schema.required || [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
const isFieldRequired = required.includes(key);
|
||||||
|
|
||||||
|
if (isFieldRequired) {
|
||||||
|
// Required fields: always include, even if empty string or falsy
|
||||||
|
cleaned[key] = value;
|
||||||
|
} else {
|
||||||
|
// Optional fields: only include if they have meaningful values
|
||||||
|
if (value !== undefined && value !== "" && value !== null) {
|
||||||
|
cleaned[key] = value;
|
||||||
|
}
|
||||||
|
// Empty strings, undefined, null for optional fields → omit completely
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a field should be set to undefined when cleared
|
||||||
|
* @param isRequired - Whether the field is required
|
||||||
|
* @param value - The current value
|
||||||
|
* @returns Whether to set the field to undefined
|
||||||
|
*/
|
||||||
|
export function shouldSetToUndefined(isRequired: boolean, value: string): boolean {
|
||||||
|
return !isRequired && value === "";
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue