mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
Merge 1a2e40111c into 1df25e26cf
This commit is contained in:
commit
008bd0a1ed
7 changed files with 320 additions and 93 deletions
|
|
@ -158,6 +158,78 @@ describe("ToolTestPanel defaults", () => {
|
|||
expect(callButton.closest("form")).not.toBeNull();
|
||||
expect(callButton).toHaveAttribute("type", "button");
|
||||
});
|
||||
|
||||
it("renders inputs for nullable (type-array) schema properties instead of leaving them blank", () => {
|
||||
// A real Azure DevOps MCP tool schema (testplan_test_suite_write) declares its optional
|
||||
// fields exactly this way.
|
||||
const schema: InputSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: ["string", "null"], description: "Optional name" },
|
||||
parentSuiteId: { type: ["integer", "null"], description: "Optional parent suite id" },
|
||||
active: { type: ["boolean", "null"], description: "Optional flag" },
|
||||
},
|
||||
};
|
||||
|
||||
renderPanel(schema);
|
||||
|
||||
expect(screen.getByLabelText("name")).toHaveValue("");
|
||||
expect(screen.getByLabelText("parentSuiteId")).toHaveValue(null);
|
||||
expect(screen.getByLabelText("active")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits an untouched nullable numeric field from the submitted payload instead of sending a synthetic 0", async () => {
|
||||
const schema: InputSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
parentSuiteId: { type: ["integer", "null"], description: "Optional parent suite id" },
|
||||
},
|
||||
};
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
render(
|
||||
<ToolTestPanel
|
||||
tool={buildTool(schema)}
|
||||
onSubmit={onSubmit}
|
||||
isLoading={false}
|
||||
result={null}
|
||||
error={null}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Call Tool" }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it("coerces a nullable integer field to a number on submit, not a string", async () => {
|
||||
const schema: InputSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
parentSuiteId: { type: ["integer", "null"], description: "Optional parent suite id" },
|
||||
},
|
||||
};
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
render(
|
||||
<ToolTestPanel
|
||||
tool={buildTool(schema)}
|
||||
onSubmit={onSubmit}
|
||||
isLoading={false}
|
||||
result={null}
|
||||
error={null}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("parentSuiteId");
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, "42");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Call Tool" }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ parentSuiteId: 42 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolTestPanel argument payload", () => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
export const resolveSchemaProperty = (prop: InputSchemaProperty): InputSchemaProperty => {
|
||||
// JSON Schema represents a nullable/optional field as a type array (e.g. ["integer", "null"]) -
|
||||
// the shape Pydantic's model_json_schema() emits for Optional[int]. Collapse to the single
|
||||
// non-null type before any caller branches on it.
|
||||
if (Array.isArray(prop.type)) {
|
||||
return { ...prop, type: prop.type.find((t) => t !== "null") ?? prop.type[0] };
|
||||
}
|
||||
if (prop.type !== undefined) return prop;
|
||||
const members = (prop.anyOf ?? prop.oneOf ?? []).filter((member) => member.type !== "null");
|
||||
if (members.length !== 1 || members[0].type === undefined) return prop;
|
||||
|
|
@ -185,12 +191,17 @@ function buildDefaultValue(declared: InputSchemaProperty | undefined, overrideDe
|
|||
if (prop.type === "array") return buildArrayDefault(prop, effectiveDefault);
|
||||
if (effectiveDefault !== undefined) return effectiveDefault;
|
||||
|
||||
// A nullable numeric/boolean field (an array "type" on the ORIGINAL declaration) with no
|
||||
// explicit default starts empty rather than a synthetic 0/false: unlike a string's natural ""
|
||||
// default, that value looks user-provided, passes the non-empty submission filter in
|
||||
// buildToolCallArguments, and gets sent to the tool even when the field was never touched.
|
||||
const isNullable = Array.isArray(declared.type);
|
||||
switch (prop.type) {
|
||||
case "integer":
|
||||
case "number":
|
||||
return 0;
|
||||
return isNullable ? undefined : 0;
|
||||
case "boolean":
|
||||
return false;
|
||||
return isNullable ? undefined : false;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
@ -199,7 +210,11 @@ function buildDefaultValue(declared: InputSchemaProperty | undefined, overrideDe
|
|||
export const initialArgumentValues = (fields: readonly ToolArgumentField[]): unknown[] =>
|
||||
fields.map(({ prop }) => {
|
||||
const resolved = resolveSchemaProperty(prop);
|
||||
const defaultValue = buildDefaultValue(resolved);
|
||||
// Pass the RAW prop, not `resolved` - buildDefaultValue resolves it again internally, and
|
||||
// needs the original `type` (possibly still an array) to know the field is nullable at all.
|
||||
// Passing the pre-resolved (already-scalar) type here would make Array.isArray(declared.type)
|
||||
// always false, silently defeating the nullable-omits-default behavior above.
|
||||
const defaultValue = buildDefaultValue(prop);
|
||||
if (isJsonField(resolved)) {
|
||||
return isBlank(defaultValue) ? "" : JSON.stringify(defaultValue, null, 2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,3 +178,51 @@ describe("MCPToolArgumentsForm", () => {
|
|||
await expect(submit(ref)).resolves.toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MCPToolArgumentsForm nullable schema types", () => {
|
||||
it("renders a number widget for nullable (type-array) schema properties instead of a plain text fallback", () => {
|
||||
// Unlike ToolTestPanel, this component has a final "else" branch that falls back to a plain
|
||||
// text Input, so the pre-fix bug here was a wrong widget (text instead of number), not a
|
||||
// missing one; the field must also start empty, not a synthetic 0.
|
||||
renderForm({
|
||||
type: "object",
|
||||
properties: {
|
||||
parentSuiteId: { type: ["integer", "null"], description: "Optional parent suite id" },
|
||||
},
|
||||
required: [],
|
||||
});
|
||||
|
||||
// Native type="number" input: jest-dom reports an empty one as null, not "".
|
||||
const input = screen.getByLabelText("parentSuiteId");
|
||||
expect(input).toHaveValue(null);
|
||||
});
|
||||
|
||||
it("omits an untouched nullable numeric field from getSubmitValues instead of sending a synthetic 0", async () => {
|
||||
const ref = renderForm({
|
||||
type: "object",
|
||||
properties: {
|
||||
parentSuiteId: { type: ["integer", "null"], description: "Optional parent suite id" },
|
||||
},
|
||||
required: [],
|
||||
});
|
||||
|
||||
await expect(submit(ref)).resolves.toEqual({});
|
||||
});
|
||||
|
||||
it("coerces a nullable integer field to a number in getSubmitValues, not a string", async () => {
|
||||
const user = userEvent.setup();
|
||||
const ref = renderForm({
|
||||
type: "object",
|
||||
properties: {
|
||||
parentSuiteId: { type: ["integer", "null"], description: "Optional parent suite id" },
|
||||
},
|
||||
required: [],
|
||||
});
|
||||
|
||||
const input = screen.getByLabelText("parentSuiteId");
|
||||
await user.clear(input);
|
||||
await user.type(input, "42");
|
||||
|
||||
await expect(submit(ref)).resolves.toEqual(expect.objectContaining({ parentSuiteId: 42 }));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { MCPTool, InputSchema, InputSchemaProperty } from "./types";
|
||||
import { resolveSchemaType, getInitialValueForField } from "./mcpToolSchemaDefaults";
|
||||
|
||||
type ToolFormValues = Record<string, unknown>;
|
||||
|
||||
|
|
@ -20,14 +21,14 @@ const BOOLEAN_ITEMS = [
|
|||
|
||||
const isBlank = (value: unknown): boolean => value === undefined || value === null || value === "";
|
||||
|
||||
const jsonErrorFor = (prop: InputSchemaProperty, value: unknown): string | null => {
|
||||
const jsonErrorFor = (effectiveType: string | undefined, value: unknown): string | null => {
|
||||
try {
|
||||
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
||||
const isValidObject =
|
||||
prop.type === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
||||
const isValidArray = prop.type === "array" && Array.isArray(parsed);
|
||||
effectiveType === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
||||
const isValidArray = effectiveType === "array" && Array.isArray(parsed);
|
||||
if (isValidObject || isValidArray) return null;
|
||||
return prop.type === "object" ? "Please enter a JSON object" : "Please enter a JSON array";
|
||||
return effectiveType === "object" ? "Please enter a JSON object" : "Please enter a JSON array";
|
||||
} catch {
|
||||
return "Invalid JSON";
|
||||
}
|
||||
|
|
@ -46,9 +47,10 @@ const collectErrors = (
|
|||
if (actualSchema.required?.includes(key) && blank) {
|
||||
return [[key, { type: "required", message: requiredMessages[key] ?? `Please enter ${key}` }]];
|
||||
}
|
||||
if (prop.type !== "object" && prop.type !== "array") return [];
|
||||
const effectiveType = resolveSchemaType(prop.type);
|
||||
if (effectiveType !== "object" && effectiveType !== "array") return [];
|
||||
if (blank) return [];
|
||||
const message = jsonErrorFor(prop, value);
|
||||
const message = jsonErrorFor(effectiveType, value);
|
||||
return message === null ? [] : [[key, { type: "validate", message }]];
|
||||
});
|
||||
return Object.fromEntries(entries);
|
||||
|
|
@ -73,74 +75,6 @@ const labelFor = (key: string, prop: InputSchemaProperty, required: boolean): Re
|
|||
</span>
|
||||
);
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, any> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
function buildArrayItems(items?: InputSchemaProperty | InputSchemaProperty[]): any[] {
|
||||
if (!items) return [];
|
||||
if (Array.isArray(items)) {
|
||||
return items.map((item) => buildDefaultValue(item)).filter((value) => value !== undefined);
|
||||
}
|
||||
const itemDefault = buildDefaultValue(items);
|
||||
return itemDefault !== undefined ? [itemDefault] : [];
|
||||
}
|
||||
|
||||
function buildDefaultValue(prop?: InputSchemaProperty, overrideDefault?: any): any {
|
||||
if (!prop) return undefined;
|
||||
const effectiveDefault = overrideDefault !== undefined ? overrideDefault : prop.default;
|
||||
|
||||
if (prop.type === "object") {
|
||||
const base = isPlainObject(effectiveDefault) ? { ...effectiveDefault } : {};
|
||||
if (prop.properties) {
|
||||
Object.entries(prop.properties).forEach(([childKey, childProp]) => {
|
||||
base[childKey] = buildDefaultValue(childProp, base[childKey]);
|
||||
});
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
if (prop.type === "array") {
|
||||
if (Array.isArray(effectiveDefault)) {
|
||||
const itemSchema = prop.items;
|
||||
if (!itemSchema) return effectiveDefault;
|
||||
if (effectiveDefault.length === 0) {
|
||||
const sample = buildArrayItems(itemSchema);
|
||||
return sample.length ? sample : effectiveDefault;
|
||||
}
|
||||
if (Array.isArray(itemSchema)) {
|
||||
return effectiveDefault.map((value, index) => {
|
||||
const schema = itemSchema[index] ?? itemSchema[itemSchema.length - 1];
|
||||
return buildDefaultValue(schema, value);
|
||||
});
|
||||
}
|
||||
return effectiveDefault.map((value) => buildDefaultValue(itemSchema, value));
|
||||
}
|
||||
if (effectiveDefault !== undefined) return effectiveDefault;
|
||||
return buildArrayItems(prop.items);
|
||||
}
|
||||
|
||||
if (effectiveDefault !== undefined) return effectiveDefault;
|
||||
switch (prop.type) {
|
||||
case "integer":
|
||||
case "number":
|
||||
return 0;
|
||||
case "boolean":
|
||||
return false;
|
||||
case "string":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const getInitialValueForField = (prop: InputSchemaProperty): any => {
|
||||
const defaultValue = buildDefaultValue(prop);
|
||||
if (prop.type === "object" || prop.type === "array") {
|
||||
const fallback = prop.type === "array" ? [] : {};
|
||||
return JSON.stringify(defaultValue ?? fallback, null, 2);
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
function convertFormValues(
|
||||
values: Record<string, any>,
|
||||
actualSchema: InputSchema,
|
||||
|
|
@ -152,7 +86,8 @@ function convertFormValues(
|
|||
Object.entries(values).forEach(([key, value]) => {
|
||||
const prop = schemaToUse.properties?.[key];
|
||||
if (prop && value !== null && value !== undefined && value !== "") {
|
||||
switch (prop.type) {
|
||||
const effectiveType = resolveSchemaType(prop.type);
|
||||
switch (effectiveType) {
|
||||
case "boolean":
|
||||
convertedValues[key] = value === "true" || value === true;
|
||||
break;
|
||||
|
|
@ -161,7 +96,7 @@ function convertFormValues(
|
|||
const numericValue = Number(value);
|
||||
convertedValues[key] = Number.isNaN(numericValue)
|
||||
? value
|
||||
: prop.type === "integer"
|
||||
: effectiveType === "integer"
|
||||
? Math.trunc(numericValue)
|
||||
: numericValue;
|
||||
break;
|
||||
|
|
@ -171,9 +106,9 @@ function convertFormValues(
|
|||
try {
|
||||
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
||||
const isValidObject =
|
||||
prop.type === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
||||
const isValidArray = prop.type === "array" && Array.isArray(parsed);
|
||||
if ((prop.type === "object" && isValidObject) || (prop.type === "array" && isValidArray)) {
|
||||
effectiveType === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
||||
const isValidArray = effectiveType === "array" && Array.isArray(parsed);
|
||||
if ((effectiveType === "object" && isValidObject) || (effectiveType === "array" && isValidArray)) {
|
||||
convertedValues[key] = parsed;
|
||||
} else {
|
||||
convertedValues[key] = value;
|
||||
|
|
@ -194,7 +129,8 @@ function convertFormValues(
|
|||
}
|
||||
});
|
||||
|
||||
const isNestedParams = schema.properties?.params?.type === "object" && schema.properties.params.properties;
|
||||
const isNestedParams =
|
||||
resolveSchemaType(schema.properties?.params?.type) === "object" && schema.properties?.params?.properties;
|
||||
|
||||
return isNestedParams ? { params: convertedValues } : convertedValues;
|
||||
}
|
||||
|
|
@ -227,7 +163,7 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
}, [tool.inputSchema]);
|
||||
|
||||
const actualSchema: InputSchema = useMemo(() => {
|
||||
if (schema.properties?.params?.type === "object" && schema.properties.params.properties) {
|
||||
if (resolveSchemaType(schema.properties?.params?.type) === "object" && schema.properties?.params?.properties) {
|
||||
return {
|
||||
type: "object",
|
||||
properties: schema.properties.params.properties,
|
||||
|
|
@ -320,6 +256,7 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
<FieldGroup>
|
||||
{Object.entries(actualSchema.properties).map(([key, prop]) => {
|
||||
const required = actualSchema.required?.includes(key) ?? false;
|
||||
const effectiveType = resolveSchemaType(prop.type);
|
||||
return (
|
||||
<FormField
|
||||
key={`${tool.name}-${key}`}
|
||||
|
|
@ -328,7 +265,7 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
label={labelFor(key, prop, required)}
|
||||
>
|
||||
{(field) => {
|
||||
if (prop.type === "string" && prop.enum) {
|
||||
if (effectiveType === "string" && prop.enum) {
|
||||
return (
|
||||
<Select value={field.value ?? ""} onValueChange={field.onChange}>
|
||||
<SelectTrigger
|
||||
|
|
@ -350,7 +287,7 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
</Select>
|
||||
);
|
||||
}
|
||||
if (prop.type === "boolean") {
|
||||
if (effectiveType === "boolean") {
|
||||
return (
|
||||
<Select items={BOOLEAN_ITEMS} value={field.value ?? ""} onValueChange={field.onChange}>
|
||||
<SelectTrigger
|
||||
|
|
@ -369,28 +306,30 @@ const MCPToolArgumentsForm = forwardRef<MCPToolArgumentsFormRef, MCPToolArgument
|
|||
</Select>
|
||||
);
|
||||
}
|
||||
if (prop.type === "number" || prop.type === "integer") {
|
||||
if (effectiveType === "number" || effectiveType === "integer") {
|
||||
return (
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
step={prop.type === "integer" ? 1 : undefined}
|
||||
value={field.value as number | string}
|
||||
step={effectiveType === "integer" ? 1 : undefined}
|
||||
value={(field.value as number | string) ?? ""}
|
||||
placeholder={prop.description || `Enter ${key}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (prop.type === "object" || prop.type === "array") {
|
||||
if (effectiveType === "object" || effectiveType === "array") {
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
rows={prop.type === "object" ? 4 : 3}
|
||||
rows={effectiveType === "object" ? 4 : 3}
|
||||
value={field.value as string}
|
||||
spellCheck={false}
|
||||
className="font-mono"
|
||||
placeholder={
|
||||
prop.description ||
|
||||
(prop.type === "object" ? `Enter JSON object for ${key}` : `Enter JSON array for ${key}`)
|
||||
(effectiveType === "object"
|
||||
? `Enter JSON object for ${key}`
|
||||
: `Enter JSON array for ${key}`)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildDefaultValue, getInitialValueForField, resolveSchemaType } from "./mcpToolSchemaDefaults";
|
||||
|
||||
describe("resolveSchemaType", () => {
|
||||
it("returns a scalar type unchanged", () => {
|
||||
expect(resolveSchemaType("string")).toBe("string");
|
||||
});
|
||||
|
||||
it("returns the first non-null entry of a nullable type array", () => {
|
||||
expect(resolveSchemaType(["integer", "null"])).toBe("integer");
|
||||
expect(resolveSchemaType(["null", "boolean"])).toBe("boolean");
|
||||
});
|
||||
|
||||
it("falls back to the first entry when every entry is null", () => {
|
||||
expect(resolveSchemaType(["null"])).toBe("null");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDefaultValue", () => {
|
||||
it("defaults a required (non-nullable) integer with no explicit default to 0", () => {
|
||||
expect(buildDefaultValue({ type: "integer" })).toBe(0);
|
||||
});
|
||||
|
||||
it("defaults a required (non-nullable) boolean with no explicit default to false", () => {
|
||||
expect(buildDefaultValue({ type: "boolean" })).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults a required (non-nullable) string with no explicit default to an empty string", () => {
|
||||
expect(buildDefaultValue({ type: "string" })).toBe("");
|
||||
});
|
||||
|
||||
it("leaves a nullable integer with no explicit default undefined, not a synthetic 0", () => {
|
||||
expect(buildDefaultValue({ type: ["integer", "null"] })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves a nullable boolean with no explicit default undefined, not a synthetic false", () => {
|
||||
expect(buildDefaultValue({ type: ["boolean", "null"] })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("defaults a nullable string with no explicit default to an empty string, unaffected by nullability", () => {
|
||||
expect(buildDefaultValue({ type: ["string", "null"] })).toBe("");
|
||||
});
|
||||
|
||||
it("still honors an explicit default on a nullable numeric field", () => {
|
||||
expect(buildDefaultValue({ type: ["integer", "null"], default: 7 })).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInitialValueForField", () => {
|
||||
it("passes through undefined for a nullable numeric field with no default", () => {
|
||||
expect(getInitialValueForField({ type: ["integer", "null"] })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("JSON-stringifies an object field's computed default", () => {
|
||||
expect(getInitialValueForField({ type: "object", properties: { a: { type: "string" } } })).toBe(
|
||||
JSON.stringify({ a: "" }, null, 2),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { InputSchemaProperty } from "./types";
|
||||
|
||||
// JSON Schema allows "type" to be an array (e.g. ["string", "null"]) for a nullable field, the
|
||||
// shape Pydantic's model_json_schema() emits for Optional[str] / str | None. Every branch that
|
||||
// decides a widget or a value conversion from a property's type needs the single effective
|
||||
// (non-null) type, not the raw field verbatim.
|
||||
export function resolveSchemaType(type: InputSchemaProperty["type"] | undefined): string | undefined {
|
||||
if (Array.isArray(type)) {
|
||||
return type.find((t) => t !== "null") ?? type[0];
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, any> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
function buildArrayItems(items?: InputSchemaProperty | InputSchemaProperty[]): any[] {
|
||||
if (!items) return [];
|
||||
if (Array.isArray(items)) {
|
||||
return items.map((item) => buildDefaultValue(item)).filter((value) => value !== undefined);
|
||||
}
|
||||
const itemDefault = buildDefaultValue(items);
|
||||
return itemDefault !== undefined ? [itemDefault] : [];
|
||||
}
|
||||
|
||||
export function buildDefaultValue(prop?: InputSchemaProperty, overrideDefault?: any): any {
|
||||
if (!prop) return undefined;
|
||||
const effectiveDefault = overrideDefault !== undefined ? overrideDefault : prop.default;
|
||||
const effectiveType = resolveSchemaType(prop.type);
|
||||
|
||||
if (effectiveType === "object") {
|
||||
const base = isPlainObject(effectiveDefault) ? { ...effectiveDefault } : {};
|
||||
if (prop.properties) {
|
||||
Object.entries(prop.properties).forEach(([childKey, childProp]) => {
|
||||
base[childKey] = buildDefaultValue(childProp, base[childKey]);
|
||||
});
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
if (effectiveType === "array") {
|
||||
if (Array.isArray(effectiveDefault)) {
|
||||
const itemSchema = prop.items;
|
||||
if (!itemSchema) return effectiveDefault;
|
||||
if (effectiveDefault.length === 0) {
|
||||
const sample = buildArrayItems(itemSchema);
|
||||
return sample.length ? sample : effectiveDefault;
|
||||
}
|
||||
if (Array.isArray(itemSchema)) {
|
||||
return effectiveDefault.map((value, index) => {
|
||||
const schema = itemSchema[index] ?? itemSchema[itemSchema.length - 1];
|
||||
return buildDefaultValue(schema, value);
|
||||
});
|
||||
}
|
||||
return effectiveDefault.map((value) => buildDefaultValue(itemSchema, value));
|
||||
}
|
||||
if (effectiveDefault !== undefined) return effectiveDefault;
|
||||
return buildArrayItems(prop.items);
|
||||
}
|
||||
|
||||
if (effectiveDefault !== undefined) return effectiveDefault;
|
||||
|
||||
// A nullable numeric/boolean field (an array "type") with no explicit default starts empty
|
||||
// rather than a synthetic 0/false: unlike a string's natural "" default, that value looks
|
||||
// user-provided, passes the non-empty submission filter, and gets sent to the tool even when
|
||||
// the field was never touched.
|
||||
const isNullable = Array.isArray(prop.type);
|
||||
switch (effectiveType) {
|
||||
case "integer":
|
||||
case "number":
|
||||
return isNullable ? undefined : 0;
|
||||
case "boolean":
|
||||
return isNullable ? undefined : false;
|
||||
case "string":
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const getInitialValueForField = (prop: InputSchemaProperty): any => {
|
||||
const defaultValue = buildDefaultValue(prop);
|
||||
const effectiveType = resolveSchemaType(prop.type);
|
||||
if (effectiveType === "object" || effectiveType === "array") {
|
||||
const fallback = effectiveType === "array" ? [] : {};
|
||||
return JSON.stringify(defaultValue ?? fallback, null, 2);
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
|
@ -294,7 +294,12 @@ export const handleAuth = (authType?: string | null): string => {
|
|||
|
||||
// Define the structure for tool input schema properties
|
||||
export interface InputSchemaProperty {
|
||||
type?: string;
|
||||
// JSON Schema allows an array here (e.g. ["string", "null"]) for a nullable/optional field —
|
||||
// the shape Pydantic's model_json_schema() emits for Optional[str] / str | None. Callers must
|
||||
// resolve to a single type with resolveSchemaType (mcpToolSchemaDefaults.ts / toolCallArguments.ts)
|
||||
// before branching on it. Optional because a property may instead be described purely via
|
||||
// anyOf/oneOf, with no top-level type at all.
|
||||
type?: string | string[];
|
||||
description?: string;
|
||||
properties?: Record<string, InputSchemaProperty>; // For nested object properties
|
||||
required?: string[]; // For required fields in nested objects
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue