feat(client): show all tool annotations with explicit/implied states

Display all 4 tool annotation badges (Read-only, Destructive, Idempotent,
Open-world) for every tool, showing:
- ✓/✗ prefix to indicate true/false value
- Green badge for true, gray for false
- Solid border for explicitly set values
- Dashed border + dimmed for implied defaults
- Tooltip explaining if value is explicit or implied

Follows MCP spec defaults:
- readOnlyHint: false
- destructiveHint: true
- idempotentHint: false
- openWorldHint: true

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
olaservo 2026-01-25 06:57:56 -07:00
commit dad17c6cc2

View file

@ -65,49 +65,69 @@ const hasAnnotations = (
(tool as { annotations?: unknown }).annotations !== null; (tool as { annotations?: unknown }).annotations !== null;
// Helper to render annotation badges // Helper to render annotation badges
// Shows all 4 annotation values with their state (true/false/implied default)
const AnnotationBadges = ({ const AnnotationBadges = ({
annotations, annotations,
}: { }: {
annotations: ToolAnnotations; annotations: ToolAnnotations | undefined;
}) => { }) => {
const badges: { // Spec defaults: readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true
label: string; const getValueAndImplied = (
variant: "default" | "destructive" | "secondary" | "outline"; value: boolean | undefined,
}[] = []; defaultValue: boolean,
): { value: boolean; implied: boolean } => ({
value: value ?? defaultValue,
implied: value === undefined,
});
if (annotations.readOnlyHint === true) { const readOnly = getValueAndImplied(annotations?.readOnlyHint, false);
badges.push({ label: "Read-only", variant: "secondary" }); const destructive = getValueAndImplied(annotations?.destructiveHint, true);
} const idempotent = getValueAndImplied(annotations?.idempotentHint, false);
if (annotations.destructiveHint === true) { const openWorld = getValueAndImplied(annotations?.openWorldHint, true);
badges.push({ label: "Destructive", variant: "destructive" });
}
if (annotations.idempotentHint === true) {
badges.push({ label: "Idempotent", variant: "outline" });
}
if (annotations.openWorldHint === true) {
badges.push({ label: "Open-world", variant: "default" });
}
if (badges.length === 0) return null; 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 ( return (
<div className="flex flex-wrap gap-1 mt-2"> <div className="flex flex-wrap gap-1 mt-2">
{badges.map(({ label, variant }) => ( {badges.map(({ label, value, implied }) => (
<span <span
key={label} key={label}
title={
implied
? `${value ? "Yes" : "No"} (implied default)`
: `${value ? "Yes" : "No"} (explicitly set)`
}
className={cn( className={cn(
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium", "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border",
variant === "destructive" && implied && "border-dashed opacity-60",
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", !implied && "border-solid",
variant === "secondary" && value
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200", ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 border-green-300 dark:border-green-700"
variant === "outline" && : "bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-300 dark:border-gray-600",
"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600",
variant === "default" &&
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
)} )}
> >
{label} {value ? "✓" : "✗"} {label}
</span> </span>
))} ))}
</div> </div>
@ -271,9 +291,13 @@ const ToolsTab = ({
<p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto"> <p className="text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap max-h-48 overflow-y-auto">
{selectedTool.description} {selectedTool.description}
</p> </p>
{hasAnnotations(selectedTool) && ( <AnnotationBadges
<AnnotationBadges annotations={selectedTool.annotations} /> annotations={
)} hasAnnotations(selectedTool)
? selectedTool.annotations
: undefined
}
/>
{Object.entries(selectedTool.inputSchema.properties ?? []).map( {Object.entries(selectedTool.inputSchema.properties ?? []).map(
([key, value]) => { ([key, value]) => {
// First resolve any $ref references // First resolve any $ref references