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";
// Type guard to safely detect the optional _meta field without using `any`
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
typeof (tool as { _meta?: unknown })._meta !== "undefined";
// 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);
const badges = [
{
label: "Read-only",
value: readOnly.value,
implied: readOnly.implied,
},
{
label: "Destructive",
value: destructive.value,
implied: destructive.implied,
},
{
label: "Idempotent",
value: idempotent.value,
implied: idempotent.implied,
},
{
label: "Open-world",
value: openWorld.value,
implied: openWorld.implied,
},
];
return (
{badges.map(({ label, value, implied }) => (
{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 = () => {
const errors = Object.values(formRefs.current).some(
(ref) => ref && !ref.validateJson().isValid,
);
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));
setRunAsTask(false);
// 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);
});
return (
{
clearTools();
setSelectedTool(null);
setRunAsTask(false);
}}
setSelectedItem={setSelectedTool}
renderItem={(tool) => (
{tool.name}
{tool.description}
)}
title="Tools"
buttonText={nextCursor ? "List More Tools" : "List Tools"}
isButtonDisabled={!nextCursor && tools.length > 0}
/>
{selectedTool && (
)}
{selectedTool ? 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" ? (
);
},
)}
Tool-specific Metadata:
{metadataEntries.length === 0 ? (
No metadata pairs.
) : (
{metadataEntries.map((entry, index) => {
const trimmedKey = entry.key.trim();
const hasInvalidPrefix =
trimmedKey !== "" && !hasValidMetaPrefix(trimmedKey);
const isReservedKey =
trimmedKey !== "" && isReservedMetaKey(trimmedKey);
const hasInvalidName =
trimmedKey !== "" && !hasValidMetaName(trimmedKey);
const validationMessage = hasInvalidPrefix
? META_PREFIX_RULES_MESSAGE
: isReservedKey
? RESERVED_NAMESPACE_MESSAGE
: hasInvalidName
? META_NAME_RULES_MESSAGE
: null;
return (
{
const value = e.target.value;
setMetadataEntries((prev) =>
prev.map((m, i) =>
i === index ? { ...m, key: value } : m,
),
);
}}
className={cn(
"h-8 flex-1",
validationMessage &&
"border-red-500 focus-visible:ring-red-500 focus-visible:ring-1",
)}
aria-invalid={Boolean(validationMessage)}
/>
{
const value = e.target.value;
setMetadataEntries((prev) =>
prev.map((m, i) =>
i === index ? { ...m, value } : m,
),
);
}}
className="h-8 flex-1"
disabled={Boolean(validationMessage)}
/>
{validationMessage && (
{validationMessage}
)}
);
})}
)}
{(hasReservedMetadataEntry ||
hasInvalidMetaPrefixEntry ||
hasInvalidMetaNameEntry) && (
Remove reserved or invalid metadata keys (prefix/name)
before running the tool.
)}
{selectedTool.outputSchema && (
Output Schema:
)}
{selectedTool &&
hasMeta(selectedTool) &&
selectedTool._meta && (
Meta:
)}
setRunAsTask(checked)
}
/>
) : (
Select a tool from the list to view its details and run it
)}
);
};
export default ToolsTab;