From d65ed01922b64afff5790460046d2ef09c238563 Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Thu, 19 Jun 2025 02:32:40 +1000 Subject: [PATCH 1/3] Add JSON validation to tool execution Prevents execution of tools with invalid JSON parameters by validating JSON syntax before tool execution and showing error UI for invalid input. - Add validateJson method to DynamicJsonForm via forwardRef - Add validation check in ToolsTab before tool execution - Reuse existing JsonEditor error display for consistent UX --- client/src/components/DynamicJsonForm.tsx | 31 ++++++++++++++++++++--- client/src/components/ToolsTab.tsx | 26 +++++++++++++++---- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 6a5993c3..fe2cfd2d 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import JsonEditor from "./JsonEditor"; @@ -13,6 +13,10 @@ interface DynamicJsonFormProps { maxDepth?: number; } +export interface DynamicJsonFormRef { + validateJson: () => { isValid: boolean; error: string | null }; +} + const isSimpleObject = (schema: JsonSchemaType): boolean => { const supportedTypes = ["string", "number", "integer", "boolean", "null"]; if (supportedTypes.includes(schema.type)) return true; @@ -22,12 +26,12 @@ const isSimpleObject = (schema: JsonSchemaType): boolean => { ); }; -const DynamicJsonForm = ({ +const DynamicJsonForm = forwardRef(({ schema, value, onChange, maxDepth = 3, -}: DynamicJsonFormProps) => { +}, ref) => { const isOnlyJSON = !isSimpleObject(schema); const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [jsonError, setJsonError] = useState(); @@ -108,6 +112,25 @@ const DynamicJsonForm = ({ } }; + const validateJson = () => { + if (!isJsonMode) return { isValid: true, error: null }; + try { + const jsonStr = rawJsonValue.trim(); + if (!jsonStr) return { isValid: true, error: null }; + JSON.parse(jsonStr); + setJsonError(undefined); + return { isValid: true, error: null }; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Invalid JSON"; + setJsonError(errorMessage); + return { isValid: false, error: errorMessage }; + } + }; + + useImperativeHandle(ref, () => ({ + validateJson, + })); + const renderFormFields = ( propSchema: JsonSchemaType, currentValue: JsonValue, @@ -303,6 +326,6 @@ const DynamicJsonForm = ({ )} ); -}; +}); export default DynamicJsonForm; diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 8a7f6578..1ce034fb 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -1,11 +1,11 @@ -import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; -import DynamicJsonForm from "./DynamicJsonForm"; +import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; import { generateDefaultValue } from "@/utils/schemaUtils"; import { @@ -13,8 +13,8 @@ import { ListToolsResult, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react"; -import { useEffect, useState } from "react"; +import { Loader2, Send, ChevronDown, ChevronUp, AlertCircle } from "lucide-react"; +import { useEffect, useState, useRef } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; import ToolResults from "./ToolResults"; @@ -28,6 +28,7 @@ const ToolsTab = ({ setSelectedTool, toolResult, nextCursor, + error, }: { tools: Tool[]; listTools: () => void; @@ -42,6 +43,7 @@ const ToolsTab = ({ const [params, setParams] = useState>({}); const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); + const formRefs = useRef>({}); useEffect(() => { const params = Object.entries( @@ -84,7 +86,13 @@ const ToolsTab = ({
- {selectedTool ? ( + {error ? ( + + + Error + {error} + + ) : selectedTool ? (

{selectedTool.description} @@ -137,6 +145,7 @@ const ToolsTab = ({ ) : prop.type === "object" || prop.type === "array" ? (

(formRefs.current[key] = ref)} schema={{ type: prop.type, properties: prop.properties, @@ -174,6 +183,7 @@ const ToolsTab = ({ ) : (
(formRefs.current[key] = ref)} schema={{ type: prop.type, properties: prop.properties, @@ -232,6 +242,12 @@ const ToolsTab = ({ )} +
+ ))} + +
+
+ ); + } + + // For complex arrays, fall back to JSON editor return ( { try { const parsed = JSON.parse(newValue); @@ -286,199 +398,98 @@ const DynamicJsonForm = forwardRef(({ /> ); } + default: + return null; + } + }; - return ( -
- {Object.entries(propSchema.properties).map(([key, subSchema]) => ( -
- - {renderFormFields( - subSchema as JsonSchemaType, - (currentValue as Record)?.[key], - [...path, key], - depth + 1, - propSchema, - key, - )} -
- ))} -
- ); - case "array": { - const arrayValue = Array.isArray(currentValue) ? currentValue : []; - if (!propSchema.items) return null; + const handleFieldChange = (path: string[], fieldValue: JsonValue) => { + if (path.length === 0) { + onChange(fieldValue); + return; + } - // If the array items are simple, render as form fields, otherwise use JSON editor - if (isSimpleObject(propSchema.items)) { - return ( -
- {propSchema.description && ( -

- {propSchema.description} -

- )} + try { + const newValue = updateValueAtPath(value, path, fieldValue); + onChange(newValue); + } catch (error) { + console.error("Failed to update form value:", error); + onChange(value); + } + }; - {propSchema.items?.description && ( -

- Items: {propSchema.items.description} -

- )} + const shouldUseJsonMode = + schema.type === "object" && + (!schema.properties || Object.keys(schema.properties).length === 0); -
- {arrayValue.map((item, index) => ( -
- {renderFormFields( - propSchema.items as JsonSchemaType, - item, - [...path, index.toString()], - depth + 1, - )} - -
- ))} - -
-
- ); - } + useEffect(() => { + if (shouldUseJsonMode && !isJsonMode) { + setIsJsonMode(true); + } + }, [shouldUseJsonMode, isJsonMode]); - // For complex arrays, fall back to JSON editor - return ( + return ( +
+
+ {isJsonMode && ( + + )} + {!isOnlyJSON && ( + + )} +
+ + {isJsonMode ? ( { - try { - const parsed = JSON.parse(newValue); - handleFieldChange(path, parsed); - setJsonError(undefined); - } catch (err) { - setJsonError( - err instanceof Error ? err.message : "Invalid JSON", - ); - } + // Always update local state + setRawJsonValue(newValue); + + // Use the debounced function to attempt parsing and updating parent + debouncedUpdateParent(newValue); }} error={jsonError} /> - ); - } - default: - return null; - } - }; - - const handleFieldChange = (path: string[], fieldValue: JsonValue) => { - if (path.length === 0) { - onChange(fieldValue); - return; - } - - try { - const newValue = updateValueAtPath(value, path, fieldValue); - onChange(newValue); - } catch (error) { - console.error("Failed to update form value:", error); - onChange(value); - } - }; - - const shouldUseJsonMode = - schema.type === "object" && - (!schema.properties || Object.keys(schema.properties).length === 0); - - useEffect(() => { - if (shouldUseJsonMode && !isJsonMode) { - setIsJsonMode(true); - } - }, [shouldUseJsonMode, isJsonMode]); - - return ( -
-
- {isJsonMode && ( - - )} - {!isOnlyJSON && ( - + ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data, + // render a simple representation of the JSON data + schema.type === "object" && + (typeof value !== "object" || + value === null || + Object.keys(value).length === 0) && + rawJsonValue && + rawJsonValue !== "{}" ? ( +
+

+ Form view not available for this JSON structure. Using simplified + view: +

+
+              {rawJsonValue}
+            
+

+ Use JSON mode for full editing capabilities. +

+
+ ) : ( + renderFormFields(schema, value) )}
- - {isJsonMode ? ( - { - // Always update local state - setRawJsonValue(newValue); - - // Use the debounced function to attempt parsing and updating parent - debouncedUpdateParent(newValue); - }} - error={jsonError} - /> - ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data, - // render a simple representation of the JSON data - schema.type === "object" && - (typeof value !== "object" || - value === null || - Object.keys(value).length === 0) && - rawJsonValue && - rawJsonValue !== "{}" ? ( -
-

- Form view not available for this JSON structure. Using simplified - view: -

-
-            {rawJsonValue}
-          
-

- Use JSON mode for full editing capabilities. -

-
- ) : ( - renderFormFields(schema, value) - )} -
- ); -}); + ); + }, +); export default DynamicJsonForm; diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 9f0d2008..7258eaa8 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -13,7 +13,13 @@ import { ListToolsResult, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { Loader2, Send, ChevronDown, ChevronUp, AlertCircle } from "lucide-react"; +import { + Loader2, + Send, + ChevronDown, + ChevronUp, + AlertCircle, +} from "lucide-react"; import { useEffect, useState, useRef } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; @@ -254,9 +260,9 @@ const ToolsTab = ({ + <> + + + )} {!isOnlyJSON && (