-
+
{snippet}
@@ -436,14 +509,19 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
const handleCreate = async (name: string, description: string | undefined, tools: MCPToolsetTool[]) => {
if (!accessToken) return;
await createMCPToolset(accessToken, { toolset_name: name, description, tools });
- message.success("Toolset created");
+ MessageManager.success("Toolset created");
queryClient.invalidateQueries({ queryKey: ["mcpToolsets"] });
};
const handleUpdate = async (name: string, description: string | undefined, tools: MCPToolsetTool[]) => {
if (!accessToken || !editToolset) return;
- await updateMCPToolset(accessToken, { toolset_id: editToolset.toolset_id, toolset_name: name, description, tools });
- message.success("Toolset updated");
+ await updateMCPToolset(accessToken, {
+ toolset_id: editToolset.toolset_id,
+ toolset_name: name,
+ description,
+ tools,
+ });
+ MessageManager.success("Toolset updated");
queryClient.invalidateQueries({ queryKey: ["mcpToolsets"] });
setEditToolset(null);
};
@@ -453,7 +531,7 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
setDeleting(true);
try {
await deleteMCPToolset(accessToken, deleteId);
- message.success("Toolset deleted");
+ MessageManager.success("Toolset deleted");
queryClient.invalidateQueries({ queryKey: ["mcpToolsets"] });
setDeleteId(null);
} finally {
@@ -468,13 +546,15 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
-
MCP Toolsets
-
- Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via the MCP permissions dropdown.
-
+
MCP Toolsets
+
+ Curated collections of tools from one or more MCP servers. Assign toolsets to keys and teams via
+ the MCP permissions dropdown.
+
{isAdmin && (
-
);
}
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx b/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx
index f94584d1964..5fbad48ab5a 100644
--- a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx
+++ b/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx
@@ -1,9 +1,24 @@
import React from "react";
-// eslint-disable-next-line litellm-ui/no-banned-ui-imports
-import { Button, TextInput } from "@tremor/react";
+import { useForm, Controller } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Info as InfoCircleOutlined, X, Copy, Check, AlertCircle, Zap } from "lucide-react";
import { MCPTool, InputSchema, InputSchemaProperty } from "./types";
-import { Form, Select, Tooltip } from "antd";
-import { Info as InfoCircleOutlined } from "lucide-react";
import NotificationsManager from "../molecules/notifications_manager";
const isPlainObject = (value: unknown): value is Record
=>
@@ -101,6 +116,38 @@ const getInitialValueForField = (prop: InputSchemaProperty): any => {
return defaultValue;
};
+function validateField(
+ key: string,
+ prop: InputSchemaProperty,
+ required: boolean,
+ value: any,
+): string | true {
+ if (required && (value === undefined || value === null || value === "")) {
+ return `Please enter ${key}`;
+ }
+ if (prop.type === "object" || prop.type === "array") {
+ if ((value === undefined || value === null || value === "") && !required) {
+ return true;
+ }
+ 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)) {
+ return true;
+ }
+ return prop.type === "object" ? "Please enter a JSON object" : "Please enter a JSON array";
+ } catch {
+ return "Invalid JSON";
+ }
+ }
+ return true;
+}
+
export function ToolTestPanel({
tool,
onSubmit,
@@ -116,15 +163,12 @@ export function ToolTestPanel({
error: Error | null;
onClose: () => void;
}) {
- const [form] = Form.useForm();
const [viewMode, setViewMode] = React.useState<"formatted" | "json">("formatted");
const [startTime, setStartTime] = React.useState(null);
const [duration, setDuration] = React.useState(null);
- // Create a placeholder schema if we only have the "tool_input_schema" string
const schema: InputSchema = React.useMemo(() => {
if (typeof tool.inputSchema === "string") {
- // Default schema with a single text field
return {
type: "object",
properties: {
@@ -139,7 +183,6 @@ export function ToolTestPanel({
return tool.inputSchema as InputSchema;
}, [tool.inputSchema]);
- // Check if this is a nested params structure and extract the actual parameters
const actualSchema: InputSchema = React.useMemo(() => {
if (
schema.properties &&
@@ -147,7 +190,6 @@ export function ToolTestPanel({
schema.properties.params.type === "object" &&
schema.properties.params.properties
) {
- // This is a nested params structure, extract the actual parameters
return {
type: "object",
properties: schema.properties.params.properties,
@@ -157,27 +199,31 @@ export function ToolTestPanel({
return schema;
}, [schema]);
- React.useEffect(() => {
- form.resetFields();
-
- if (!actualSchema.properties) {
- return;
+ const defaultValues = React.useMemo(() => {
+ const values: Record = {};
+ if (actualSchema.properties) {
+ Object.entries(actualSchema.properties).forEach(([key, prop]) => {
+ values[key] = getInitialValueForField(prop);
+ });
}
+ return values;
+ }, [actualSchema]);
- const initialValues: Record = {};
- Object.entries(actualSchema.properties).forEach(([key, prop]) => {
- initialValues[key] = getInitialValueForField(prop);
- });
+ const form = useForm>({
+ defaultValues,
+ mode: "onSubmit",
+ });
+ const { control, handleSubmit, reset } = form;
- form.setFieldsValue(initialValues);
- }, [form, actualSchema, tool]);
+ React.useEffect(() => {
+ reset(defaultValues);
+ }, [defaultValues, reset, tool]);
- const handleSubmit = (values: Record) => {
+ const handleFormSubmit = (values: Record) => {
const start = Date.now();
setStartTime(start);
setDuration(null);
- // Convert form values to proper types based on schema
const convertedValues: Record = {};
const schemaToUse = actualSchema;
@@ -226,7 +272,6 @@ export function ToolTestPanel({
}
});
- // If this was a nested params structure, wrap the values back in params
const submitValues =
schema.properties &&
schema.properties.params &&
@@ -238,7 +283,6 @@ export function ToolTestPanel({
onSubmit(submitValues);
};
- // Track when result changes to calculate duration
React.useEffect(() => {
if (startTime && (result || error)) {
const endTime = Date.now();
@@ -248,12 +292,10 @@ export function ToolTestPanel({
const copyToClipboard = async (text: string) => {
try {
- // Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
} else {
- // Fallback for non-secure contexts (like 0.0.0.0)
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
@@ -297,7 +339,7 @@ export function ToolTestPanel({
return (
{/* Compact Header */}
-
+
{tool.mcp_info.logo_url && (
// eslint-disable-next-line @next/next/no-img-element
@@ -309,300 +351,311 @@ export function ToolTestPanel({
)}
-
Test Tool:
+
Test Tool:
-
{tool.name}
-
+
{tool.name}
+
-
{tool.description}
-
Provider: {tool.mcp_info.server_name}
+
{tool.description}
+
Provider: {tool.mcp_info.server_name}
-
-
+
+
- {/* Two Column Layout - Always Side by Side */}
+ {/* Two Column Layout */}
{/* Left Column - Input Parameters */}
-
-
+
+
-
Input Parameters
-
-
-
+ Input Parameters
+
+
+
+
+
+ Configure the input parameters for this tool call
+
+