Skip to content

Commit d65ed01

Browse files
committed
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
1 parent eb5be0d commit d65ed01

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback, useRef } from "react";
1+
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from "react";
22
import { Button } from "@/components/ui/button";
33
import { Input } from "@/components/ui/input";
44
import JsonEditor from "./JsonEditor";
@@ -13,6 +13,10 @@ interface DynamicJsonFormProps {
1313
maxDepth?: number;
1414
}
1515

16+
export interface DynamicJsonFormRef {
17+
validateJson: () => { isValid: boolean; error: string | null };
18+
}
19+
1620
const isSimpleObject = (schema: JsonSchemaType): boolean => {
1721
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
1822
if (supportedTypes.includes(schema.type)) return true;
@@ -22,12 +26,12 @@ const isSimpleObject = (schema: JsonSchemaType): boolean => {
2226
);
2327
};
2428

25-
const DynamicJsonForm = ({
29+
const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(({
2630
schema,
2731
value,
2832
onChange,
2933
maxDepth = 3,
30-
}: DynamicJsonFormProps) => {
34+
}, ref) => {
3135
const isOnlyJSON = !isSimpleObject(schema);
3236
const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);
3337
const [jsonError, setJsonError] = useState<string>();
@@ -108,6 +112,25 @@ const DynamicJsonForm = ({
108112
}
109113
};
110114

115+
const validateJson = () => {
116+
if (!isJsonMode) return { isValid: true, error: null };
117+
try {
118+
const jsonStr = rawJsonValue.trim();
119+
if (!jsonStr) return { isValid: true, error: null };
120+
JSON.parse(jsonStr);
121+
setJsonError(undefined);
122+
return { isValid: true, error: null };
123+
} catch (err) {
124+
const errorMessage = err instanceof Error ? err.message : "Invalid JSON";
125+
setJsonError(errorMessage);
126+
return { isValid: false, error: errorMessage };
127+
}
128+
};
129+
130+
useImperativeHandle(ref, () => ({
131+
validateJson,
132+
}));
133+
111134
const renderFormFields = (
112135
propSchema: JsonSchemaType,
113136
currentValue: JsonValue,
@@ -303,6 +326,6 @@ const DynamicJsonForm = ({
303326
)}
304327
</div>
305328
);
306-
};
329+
});
307330

308331
export default DynamicJsonForm;

client/src/components/ToolsTab.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { Alert, AlertDescription } from "@/components/ui/alert";
1+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
22
import { Button } from "@/components/ui/button";
33
import { Checkbox } from "@/components/ui/checkbox";
44
import { Input } from "@/components/ui/input";
55
import { Label } from "@/components/ui/label";
66
import { TabsContent } from "@/components/ui/tabs";
77
import { Textarea } from "@/components/ui/textarea";
8-
import DynamicJsonForm from "./DynamicJsonForm";
8+
import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm";
99
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
1010
import { generateDefaultValue } from "@/utils/schemaUtils";
1111
import {
1212
CompatibilityCallToolResult,
1313
ListToolsResult,
1414
Tool,
1515
} from "@modelcontextprotocol/sdk/types.js";
16-
import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react";
17-
import { useEffect, useState } from "react";
16+
import { Loader2, Send, ChevronDown, ChevronUp, AlertCircle } from "lucide-react";
17+
import { useEffect, useState, useRef } from "react";
1818
import ListPane from "./ListPane";
1919
import JsonView from "./JsonView";
2020
import ToolResults from "./ToolResults";
@@ -28,6 +28,7 @@ const ToolsTab = ({
2828
setSelectedTool,
2929
toolResult,
3030
nextCursor,
31+
error,
3132
}: {
3233
tools: Tool[];
3334
listTools: () => void;
@@ -42,6 +43,7 @@ const ToolsTab = ({
4243
const [params, setParams] = useState<Record<string, unknown>>({});
4344
const [isToolRunning, setIsToolRunning] = useState(false);
4445
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
46+
const formRefs = useRef<Record<string, DynamicJsonFormRef | null>>({});
4547

4648
useEffect(() => {
4749
const params = Object.entries(
@@ -84,7 +86,13 @@ const ToolsTab = ({
8486
</h3>
8587
</div>
8688
<div className="p-4">
87-
{selectedTool ? (
89+
{error ? (
90+
<Alert variant="destructive">
91+
<AlertCircle className="h-4 w-4" />
92+
<AlertTitle>Error</AlertTitle>
93+
<AlertDescription>{error}</AlertDescription>
94+
</Alert>
95+
) : selectedTool ? (
8896
<div className="space-y-4">
8997
<p className="text-sm text-gray-600 dark:text-gray-400">
9098
{selectedTool.description}
@@ -137,6 +145,7 @@ const ToolsTab = ({
137145
) : prop.type === "object" || prop.type === "array" ? (
138146
<div className="mt-1">
139147
<DynamicJsonForm
148+
ref={(ref) => (formRefs.current[key] = ref)}
140149
schema={{
141150
type: prop.type,
142151
properties: prop.properties,
@@ -174,6 +183,7 @@ const ToolsTab = ({
174183
) : (
175184
<div className="mt-1">
176185
<DynamicJsonForm
186+
ref={(ref) => (formRefs.current[key] = ref)}
177187
schema={{
178188
type: prop.type,
179189
properties: prop.properties,
@@ -232,6 +242,12 @@ const ToolsTab = ({
232242
)}
233243
<Button
234244
onClick={async () => {
245+
// Validate JSON inputs before calling tool
246+
const hasValidationErrors = Object.values(formRefs.current).some(
247+
(ref) => ref && !ref.validateJson().isValid
248+
);
249+
if (hasValidationErrors) return;
250+
235251
try {
236252
setIsToolRunning(true);
237253
await callTool(selectedTool.name, params);

0 commit comments

Comments
 (0)