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:
parent
3549220acc
commit
dad17c6cc2
1 changed files with 56 additions and 32 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue