From 3549220acc8ba301c87bb4d7085b08c25059aaca Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 25 Jan 2026 06:09:08 -0700 Subject: [PATCH 1/4] feat(client): display tool annotation badges in Tools tab Add visual badges to show tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) when a tool is selected. Badges appear below the tool description with color-coded styling: - Read-only: blue - Destructive: red - Idempotent: gray outline - Open-world: purple Co-Authored-By: Claude Opus 4.5 --- client/src/components/ToolsTab.tsx | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index d872e6299..19d8fa0cb 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -24,6 +24,7 @@ import { CompatibilityCallToolResult, ListToolsResult, Tool, + ToolAnnotations, } from "@modelcontextprotocol/sdk/types.js"; import { Loader2, @@ -56,6 +57,63 @@ import { 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 +const AnnotationBadges = ({ + annotations, +}: { + annotations: ToolAnnotations; +}) => { + const badges: { + label: string; + variant: "default" | "destructive" | "secondary" | "outline"; + }[] = []; + + if (annotations.readOnlyHint === true) { + badges.push({ label: "Read-only", variant: "secondary" }); + } + if (annotations.destructiveHint === 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; + + return ( +
+ {badges.map(({ label, variant }) => ( + + {label} + + ))} +
+ ); +}; + const ToolsTab = ({ tools, listTools, @@ -213,6 +271,9 @@ const ToolsTab = ({

{selectedTool.description}

+ {hasAnnotations(selectedTool) && ( + + )} {Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { // First resolve any $ref references From dad17c6cc246fddf6d3fb8578f5088e57b930e58 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 25 Jan 2026 06:57:56 -0700 Subject: [PATCH 2/4] feat(client): show all tool annotations with explicit/implied states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- client/src/components/ToolsTab.tsx | 88 +++++++++++++++++++----------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 19d8fa0cb..659e3180c 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -65,49 +65,69 @@ const hasAnnotations = ( (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; + annotations: ToolAnnotations | undefined; }) => { - const badges: { - label: string; - variant: "default" | "destructive" | "secondary" | "outline"; - }[] = []; + // 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, + }); - if (annotations.readOnlyHint === true) { - badges.push({ label: "Read-only", variant: "secondary" }); - } - if (annotations.destructiveHint === 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" }); - } + const readOnly = getValueAndImplied(annotations?.readOnlyHint, false); + const destructive = getValueAndImplied(annotations?.destructiveHint, true); + const idempotent = getValueAndImplied(annotations?.idempotentHint, false); + const openWorld = getValueAndImplied(annotations?.openWorldHint, true); - 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 (
- {badges.map(({ label, variant }) => ( + {badges.map(({ label, value, implied }) => ( - {label} + {value ? "✓" : "✗"} {label} ))}
@@ -271,9 +291,13 @@ const ToolsTab = ({

{selectedTool.description}

- {hasAnnotations(selectedTool) && ( - - )} + {Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { // First resolve any $ref references From 1cadda06c04bf33559ca5662eda8b5ed745dd65f Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 25 Jan 2026 07:03:21 -0700 Subject: [PATCH 3/4] feat(client): add MCP spec descriptions to annotation tooltips Enhance tooltips to include descriptions from the MCP specification: - Read-only: Tool does not modify its environment - Destructive: Tool may perform destructive updates (delete/overwrite data) - Idempotent: Calling repeatedly with same args has no additional effect - Open-world: Tool may interact with external entities beyond its local environment Co-Authored-By: Claude Opus 4.5 --- client/src/components/ToolsTab.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 659e3180c..2128d5993 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -85,39 +85,42 @@ const AnnotationBadges = ({ 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 }) => ( + {badges.map(({ label, value, implied, description }) => ( Date: Sat, 7 Feb 2026 10:36:20 -0700 Subject: [PATCH 4/4] feat(client): use consistent slate styling for annotation badges Replace value-based green/gray badge coloring with uniform slate colors, relying on checkmark/cross icons and opacity to convey state. Co-Authored-By: Claude Opus 4.6 --- client/src/components/ToolsTab.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 2128d5993..a805de0da 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -123,11 +123,9 @@ const AnnotationBadges = ({ title={`${description}\n\nValue: ${value ? "Yes" : "No"} (${implied ? "implied default" : "explicitly set"})`} className={cn( "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border", + "bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-600", implied && "border-dashed opacity-60", !implied && "border-solid", - value - ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 border-green-300 dark:border-green-700" - : "bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400 border-gray-300 dark:border-gray-600", )} > {value ? "✓" : "✗"} {label}