diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 9c475500b15..d6d67e19233 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -715,12 +715,6 @@ } }, "src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx": { - "no-nested-ternary": { - "count": 3 - }, - "no-restricted-imports": { - "count": 1 - }, "react-hooks/set-state-in-effect": { "count": 1 } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolArgumentsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolArgumentsForm.tsx new file mode 100644 index 00000000000..0a212a75345 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolArgumentsForm.tsx @@ -0,0 +1,330 @@ +import React from "react"; +import { useForm, type Control } from "react-hook-form"; +import { CircleHelp } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { FieldGroup } from "@/components/shared/form/field"; +import { FormField, type FormFieldControlProps } from "@/components/shared/form/FormField"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { InputSchemaProperty } from "@/components/mcp_tools/types"; +import { + ToolArgumentField, + ToolArgumentsFormValues, + buildToolCallArguments, + toolArgumentsResolver, +} from "./toolCallArguments"; + +const isPlainObject = (value: unknown): value is Record => + 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); + if (itemDefault === undefined) { + return []; + } + + return [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; +}; + +const argumentLabel = (field: ToolArgumentField): React.ReactNode => ( + + {field.key} + {field.required && *} + {field.prop.description && ( + + } /> + {field.prop.description} + + )} + +); + +const BOOLEAN_ITEMS = [ + { value: true, label: "True" }, + { value: false, label: "False" }, +]; + +const booleanTitle = (value: unknown): string | undefined => { + if (value === true) return "True"; + if (value === false) return "False"; + return undefined; +}; + +const JsonArgumentControl: React.FC<{ + field: ToolArgumentField; + control: FormFieldControlProps; +}> = ({ field, control }) => { + const isObject = field.prop.type === "object"; + const fallbackPlaceholder = isObject ? `Enter JSON object for ${field.key}` : `Enter JSON array for ${field.key}`; + return ( +
+