diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index c581e98a6..3d4284e88 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,84 @@ 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 +// 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, @@ -217,6 +296,13 @@ const ToolsTab = ({

{selectedTool.description}

+ {Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { // First resolve any $ref references