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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; import { generateDefaultValue, isPropertyRequired, normalizeUnionType, resolveRef, } from "@/utils/schemaUtils"; import { CompatibilityCallToolResult, ListToolsResult, Tool, ToolAnnotations, } from "@modelcontextprotocol/sdk/types.js"; import { Loader2, Send, ChevronDown, ChevronUp, ChevronRight, AlertCircle, Copy, CheckCheck, } from "lucide-react"; import { useEffect, useState, useRef } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; import ToolResults from "./ToolResults"; import { useToast } from "@/lib/hooks/useToast"; import useCopy from "@/lib/hooks/useCopy"; import IconDisplay, { WithIcons } from "./IconDisplay"; import { cn } from "@/lib/utils"; import { META_NAME_RULES_MESSAGE, META_PREFIX_RULES_MESSAGE, RESERVED_NAMESPACE_MESSAGE, hasValidMetaName, hasValidMetaPrefix, isReservedMetaKey, } from "@/utils/metaUtils"; /** * Extended Tool type that includes optional fields used by the inspector. */ export interface ExtendedTool extends Tool, WithIcons { _meta?: Record; execution?: { taskSupport?: "forbidden" | "required" | "optional"; }; } // Type guard to safely detect the optional _meta field without using `any` const hasMeta = ( tool: Tool, ): tool is ExtendedTool & { _meta: Record } => typeof (tool as ExtendedTool)._meta !== "undefined"; // Returns the execution.taskSupport value for a tool, defaulting to "forbidden" per MCP spec const getTaskSupport = ( tool: Tool | null, ): "forbidden" | "required" | "optional" => { if (!tool) return "forbidden"; const extendedTool = tool as ExtendedTool; const taskSupport = extendedTool.execution?.taskSupport; if ( taskSupport === "forbidden" || taskSupport === "required" || taskSupport === "optional" ) { return taskSupport; } return "forbidden"; }; // Type guard to safely detect the optional annotations field const hasAnnotations = ( tool: Tool, ): tool is Tool & { annotations: ToolAnnotations } => typeof (tool as { annotations?: unknown }).annotations !== "undefined" && (tool as { annotations?: unknown }).annotations !== null; // Helper to render annotation badges // Shows all 4 annotation values with their state (true/false/implied default) const AnnotationBadges = ({ annotations, }: { annotations: ToolAnnotations | undefined; }) => { // Spec defaults: readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true const getValueAndImplied = ( value: boolean | undefined, defaultValue: boolean, ): { value: boolean; implied: boolean } => ({ value: value ?? defaultValue, implied: value === undefined, }); const readOnly = getValueAndImplied(annotations?.readOnlyHint, false); const destructive = getValueAndImplied(annotations?.destructiveHint, true); const idempotent = getValueAndImplied(annotations?.idempotentHint, false); const openWorld = getValueAndImplied(annotations?.openWorldHint, true); // Descriptions from MCP spec const badges = [ { label: "Read-only", value: readOnly.value, implied: readOnly.implied, description: "Tool does not modify its environment", }, { label: "Destructive", value: destructive.value, implied: destructive.implied, description: "Tool may perform destructive updates (delete/overwrite data)", }, { label: "Idempotent", value: idempotent.value, implied: idempotent.implied, description: "Calling repeatedly with same args has no additional effect", }, { label: "Open-world", value: openWorld.value, implied: openWorld.implied, description: "Tool may interact with external entities beyond its local environment", }, ]; return (
{badges.map(({ label, value, implied, description }) => ( {value ? "✓" : "✗"} {label} ))}
); }; const ToolsTab = ({ tools, listTools, clearTools, callTool, selectedTool, setSelectedTool, toolResult, isPollingTask, nextCursor, error, resourceContent, onReadResource, }: { tools: Tool[]; listTools: () => void; clearTools: () => void; callTool: ( name: string, params: Record, metadata?: Record, runAsTask?: boolean, ) => Promise; selectedTool: Tool | null; setSelectedTool: (tool: Tool | null) => void; toolResult: CompatibilityCallToolResult | null; isPollingTask?: boolean; nextCursor: ListToolsResult["nextCursor"]; error: string | null; resourceContent: Record; onReadResource?: (uri: string) => void; }) => { const [params, setParams] = useState>({}); const [runAsTask, setRunAsTask] = useState(false); const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); const [isMetadataExpanded, setIsMetadataExpanded] = useState(false); const [metadataEntries, setMetadataEntries] = useState< { id: string; key: string; value: string }[] >([]); const [hasValidationErrors, setHasValidationErrors] = useState(false); const formRefs = useRef>({}); const { toast } = useToast(); const { copied, setCopied } = useCopy(); // Function to check if any form has validation errors const checkValidationErrors = (validateChildren: boolean = false) => { const errors = Object.values(formRefs.current).some( (ref) => ref && (validateChildren ? !ref.validateJson().isValid : ref.hasJsonError()), ); setHasValidationErrors(errors); return errors; }; useEffect(() => { const params = Object.entries( selectedTool?.inputSchema.properties ?? [], ).map(([key, value]) => { // First resolve any $ref references const resolvedValue = resolveRef( value as JsonSchemaType, selectedTool?.inputSchema as JsonSchemaType, ); return [ key, generateDefaultValue( resolvedValue, key, selectedTool?.inputSchema as JsonSchemaType, ), ]; }); setParams(Object.fromEntries(params)); const taskSupport = getTaskSupport(selectedTool); setRunAsTask(taskSupport === "required"); // Reset validation errors when switching tools setHasValidationErrors(false); // Clear form refs for the previous tool formRefs.current = {}; }, [selectedTool]); const hasReservedMetadataEntry = metadataEntries.some(({ key }) => { const trimmedKey = key.trim(); return trimmedKey !== "" && isReservedMetaKey(trimmedKey); }); const hasInvalidMetaPrefixEntry = metadataEntries.some(({ key }) => { const trimmedKey = key.trim(); return trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey); }); const hasInvalidMetaNameEntry = metadataEntries.some(({ key }) => { const trimmedKey = key.trim(); return trimmedKey !== "" && !hasValidMetaName(trimmedKey); }); const taskSupport = getTaskSupport(selectedTool); return (
{ clearTools(); setSelectedTool(null); setRunAsTask(false); }} setSelectedItem={setSelectedTool} renderItem={(tool) => (
{tool.title || tool.name} {tool.description}
)} title="Tools" buttonText={nextCursor ? "List More Tools" : "List Tools"} isButtonDisabled={!nextCursor && tools.length > 0} />
{selectedTool && ( )}

{selectedTool ? selectedTool.title || selectedTool.name : "Select a tool"}

{selectedTool ? (
{error && ( Error {error} )}

{selectedTool.description}

{Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { // First resolve any $ref references const resolvedValue = resolveRef( value as JsonSchemaType, selectedTool.inputSchema as JsonSchemaType, ); const prop = normalizeUnionType(resolvedValue); const inputSchema = selectedTool.inputSchema as JsonSchemaType; const required = isPropertyRequired(key, inputSchema); return (
{prop.nullable ? (
setParams({ ...params, [key]: checked ? null : prop.type === "array" ? undefined : prop.default !== null ? prop.default : prop.type === "boolean" ? false : prop.type === "string" ? "" : prop.type === "number" || prop.type === "integer" ? undefined : undefined, }) } />
) : null}
{prop.type === "boolean" ? (
setParams({ ...params, [key]: checked, }) } />
) : prop.type === "string" && prop.enum ? ( ) : prop.type === "string" ? (