Merge pull request #1066 from olaservo/feature/tool-annotations-display
feat(client): display tool annotations in Tools tab
This commit is contained in:
commit
5e63f732e2
1 changed files with 86 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ import {
|
||||||
CompatibilityCallToolResult,
|
CompatibilityCallToolResult,
|
||||||
ListToolsResult,
|
ListToolsResult,
|
||||||
Tool,
|
Tool,
|
||||||
|
ToolAnnotations,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import {
|
import {
|
||||||
Loader2,
|
Loader2,
|
||||||
|
|
@ -56,6 +57,84 @@ import {
|
||||||
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
|
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
|
||||||
typeof (tool as { _meta?: unknown })._meta !== "undefined";
|
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 (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{badges.map(({ label, value, implied, description }) => (
|
||||||
|
<span
|
||||||
|
key={label}
|
||||||
|
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 ? "✓" : "✗"} {label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ToolsTab = ({
|
const ToolsTab = ({
|
||||||
tools,
|
tools,
|
||||||
listTools,
|
listTools,
|
||||||
|
|
@ -217,6 +296,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>
|
||||||
|
<AnnotationBadges
|
||||||
|
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