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
This commit is contained in:
Nandha Reddy 2025-06-19 02:32:40 +10:00
commit d65ed01922
2 changed files with 48 additions and 9 deletions

View file

@ -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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import JsonEditor from "./JsonEditor"; import JsonEditor from "./JsonEditor";
@ -13,6 +13,10 @@ interface DynamicJsonFormProps {
maxDepth?: number; maxDepth?: number;
} }
export interface DynamicJsonFormRef {
validateJson: () => { isValid: boolean; error: string | null };
}
const isSimpleObject = (schema: JsonSchemaType): boolean => { const isSimpleObject = (schema: JsonSchemaType): boolean => {
const supportedTypes = ["string", "number", "integer", "boolean", "null"]; const supportedTypes = ["string", "number", "integer", "boolean", "null"];
if (supportedTypes.includes(schema.type)) return true; if (supportedTypes.includes(schema.type)) return true;
@ -22,12 +26,12 @@ const isSimpleObject = (schema: JsonSchemaType): boolean => {
); );
}; };
const DynamicJsonForm = ({ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(({
schema, schema,
value, value,
onChange, onChange,
maxDepth = 3, maxDepth = 3,
}: DynamicJsonFormProps) => { }, ref) => {
const isOnlyJSON = !isSimpleObject(schema); const isOnlyJSON = !isSimpleObject(schema);
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON); const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
const [jsonError, setJsonError] = useState<string>(); const [jsonError, setJsonError] = useState<string>();
@ -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 = ( const renderFormFields = (
propSchema: JsonSchemaType, propSchema: JsonSchemaType,
currentValue: JsonValue, currentValue: JsonValue,
@ -303,6 +326,6 @@ const DynamicJsonForm = ({
)} )}
</div> </div>
); );
}; });
export default DynamicJsonForm; export default DynamicJsonForm;

View file

@ -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 { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs"; import { TabsContent } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import DynamicJsonForm from "./DynamicJsonForm"; import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm";
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
import { generateDefaultValue } from "@/utils/schemaUtils"; import { generateDefaultValue } from "@/utils/schemaUtils";
import { import {
@ -13,8 +13,8 @@ import {
ListToolsResult, ListToolsResult,
Tool, Tool,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react"; import { Loader2, Send, ChevronDown, ChevronUp, AlertCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import JsonView from "./JsonView"; import JsonView from "./JsonView";
import ToolResults from "./ToolResults"; import ToolResults from "./ToolResults";
@ -28,6 +28,7 @@ const ToolsTab = ({
setSelectedTool, setSelectedTool,
toolResult, toolResult,
nextCursor, nextCursor,
error,
}: { }: {
tools: Tool[]; tools: Tool[];
listTools: () => void; listTools: () => void;
@ -42,6 +43,7 @@ const ToolsTab = ({
const [params, setParams] = useState<Record<string, unknown>>({}); const [params, setParams] = useState<Record<string, unknown>>({});
const [isToolRunning, setIsToolRunning] = useState(false); const [isToolRunning, setIsToolRunning] = useState(false);
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});
useEffect(() => { useEffect(() => {
const params = Object.entries( const params = Object.entries(
@ -84,7 +86,13 @@ const ToolsTab = ({
</h3> </h3>
</div> </div>
<div className="p-4"> <div className="p-4">
{selectedTool ? ( {error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedTool ? (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
{selectedTool.description} {selectedTool.description}
@ -137,6 +145,7 @@ const ToolsTab = ({
) : prop.type === "object" || prop.type === "array" ? ( ) : prop.type === "object" || prop.type === "array" ? (
<div className="mt-1"> <div className="mt-1">
<DynamicJsonForm <DynamicJsonForm
ref={(ref) => (formRefs.current[key] = ref)}
schema={{ schema={{
type: prop.type, type: prop.type,
properties: prop.properties, properties: prop.properties,
@ -174,6 +183,7 @@ const ToolsTab = ({
) : ( ) : (
<div className="mt-1"> <div className="mt-1">
<DynamicJsonForm <DynamicJsonForm
ref={(ref) => (formRefs.current[key] = ref)}
schema={{ schema={{
type: prop.type, type: prop.type,
properties: prop.properties, properties: prop.properties,
@ -232,6 +242,12 @@ const ToolsTab = ({
)} )}
<Button <Button
onClick={async () => { onClick={async () => {
// Validate JSON inputs before calling tool
const hasValidationErrors = Object.values(formRefs.current).some(
(ref) => ref && !ref.validateJson().isValid
);
if (hasValidationErrors) return;
try { try {
setIsToolRunning(true); setIsToolRunning(true);
await callTool(selectedTool.name, params); await callTool(selectedTool.name, params);