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 { 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<DynamicJsonFormRef, DynamicJsonFormProps>(({
schema,
value,
onChange,
maxDepth = 3,
}: DynamicJsonFormProps) => {
}, ref) => {
const isOnlyJSON = !isSimpleObject(schema);
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
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 = (
propSchema: JsonSchemaType,
currentValue: JsonValue,
@ -303,6 +326,6 @@ const DynamicJsonForm = ({
)}
</div>
);
};
});
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 { 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<Record<string, unknown>>({});
const [isToolRunning, setIsToolRunning] = useState(false);
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});
useEffect(() => {
const params = Object.entries(
@ -84,7 +86,13 @@ const ToolsTab = ({
</h3>
</div>
<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">
<p className="text-sm text-gray-600 dark:text-gray-400">
{selectedTool.description}
@ -137,6 +145,7 @@ const ToolsTab = ({
) : prop.type === "object" || prop.type === "array" ? (
<div className="mt-1">
<DynamicJsonForm
ref={(ref) => (formRefs.current[key] = ref)}
schema={{
type: prop.type,
properties: prop.properties,
@ -174,6 +183,7 @@ const ToolsTab = ({
) : (
<div className="mt-1">
<DynamicJsonForm
ref={(ref) => (formRefs.current[key] = ref)}
schema={{
type: prop.type,
properties: prop.properties,
@ -232,6 +242,12 @@ const ToolsTab = ({
)}
<Button
onClick={async () => {
// Validate JSON inputs before calling tool
const hasValidationErrors = Object.values(formRefs.current).some(
(ref) => ref && !ref.validateJson().isValid
);
if (hasValidationErrors) return;
try {
setIsToolRunning(true);
await callTool(selectedTool.name, params);