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:
parent
eb5be0d853
commit
d65ed01922
2 changed files with 48 additions and 9 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue