import { ActionIcon, Button, Code, Collapse, Divider, Group, Image, ScrollArea, Stack, Switch, Text, } from "@mantine/core"; import { useId, useState } from "react"; import { RiArrowDownSLine, RiArrowRightSLine } from "react-icons/ri"; import type { ProgressNotification, Tool, ToolAnnotations, } from "@modelcontextprotocol/client"; import { resolveDisplayLabel } from "../../../utils/toolUtils"; import { toFormSchema } from "../../../utils/jsonUtils"; import { getMirroredHeaderParams } from "@inspector/core/json/xMcpHeader.js"; import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge"; import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay"; import { SchemaForm } from "../SchemaForm/SchemaForm"; export type ToolProgress = Pick< ProgressNotification["params"], "progress" | "total" | "message" >; export interface ToolDetailPanelProps { tool: Tool; formValues: Record; isExecuting: boolean; progress?: ToolProgress; /** Whether the connected server advertises task-augmented tool calls. */ serverSupportsTaskToolCalls: boolean; /** * Modern (2026-07-28) connection with the `io.modelcontextprotocol/tasks` * extension negotiated (SEP-2663). Task creation is server-directed there, so * "Run as task" is offered for ANY tool (not just ones declaring per-tool * `taskSupport`, which is the legacy mechanism). Defaults to false (legacy). */ modernTasks?: boolean; /** User's "Run as task" preference (meaningful for `optional` tools and, on * modern connections, any tool). */ runAsTask: boolean; onRunAsTaskChange: (value: boolean) => void; onFormChange: (values: Record) => void; /** Receives the effective run-as-task decision for this execution. */ onExecute: (runAsTask: boolean) => void; onCancel: () => void; } // Outer column: title/annotations pin at top, the Execute footer pins at the // bottom, and the middle (description + form + progress) scrolls when the // enclosing card hits its `mah`. `mih: 0` lets the flex children shrink. const PanelStack = Stack.withProps({ gap: "md", miw: 0, mih: 0, }); const PinnedHeader = Stack.withProps({ gap: "md", flex: "0 0 auto", }); // `0 1 auto` + `mih: 0`: shrinks to the available space and scrolls; a short // form doesn't reserve extra height, keeping Execute snug below it. const BodyScroll = ScrollArea.withProps({ flex: "0 1 auto", miw: 0, mih: 0, type: "auto", scrollbars: "y", offsetScrollbars: true, }); const BodyStack = Stack.withProps({ gap: "md", }); // Left-aligned (Execute first, Cancel after) so the primary action sits closest // to the sidebar controls / the form fields above — shortest pointer travel. const FooterRow = Group.withProps({ justify: "flex-start", flex: "0 0 auto", }); const TitleRow = Group.withProps({ gap: "sm", wrap: "nowrap", align: "center", miw: 0, }); const ToolIcon = Image.withProps({ w: 24, h: 24, fit: "contain", }); // `flex: 1` lets the title absorb the row's slack so the chevron toggle pins // to the right edge of the (nowrap) TitleRow. const ToolTitle = Text.withProps({ fw: 700, size: "lg", truncate: "end", flex: 1, }); // Chevron toggle for the collapsible description, pinned to the right of the // title row. `aria-label` is set per-render since it reflects the open state. const DescriptionToggle = ActionIcon.withProps({ variant: "subtle", color: "gray", size: "sm", }); const DescriptionText = Text.withProps({ size: "sm", c: "dimmed", }); const CancelButton = Button.withProps({ variant: "subtle", color: "red", }); // Left-aligned row hosting the "Run as task" toggle, above the execute footer. const TaskToggleRow = Group.withProps({ justify: "flex-start", flex: "0 0 auto", }); const RunAsTaskSwitch = Switch.withProps({ size: "sm", label: "Run as task", }); // Header-mirroring section (SEP-2243): lists which args mirror their value into // an `Mcp-Param-{Name}` header on `tools/call`. `Stack` (not `Box`) so the // constant can carry props; the heading + note pin above the mapping rows. const HeaderParamsSection = Stack.withProps({ gap: "xs", }); const HeaderParamsTitle = Text.withProps({ size: "sm", fw: 600, }); const HeaderParamsNote = Text.withProps({ size: "xs", c: "var(--inspector-text-secondary)", }); // One `arg → Mcp-Param-{Name}` mapping row. const HeaderParamRow = Group.withProps({ gap: "xs", wrap: "nowrap", }); const HeaderParamArrow = Text.withProps({ size: "sm", c: "var(--inspector-text-secondary)", }); // A tool's per-tool task support, defaulting to "forbidden" (the SDK default // when `execution` is absent) so tools that say nothing can't be run as tasks. type TaskSupport = "forbidden" | "optional" | "required"; function getTaskSupport(tool: Tool): TaskSupport { return tool.execution?.taskSupport ?? "forbidden"; } function hasAnyAnnotation(annotations?: ToolAnnotations): boolean { return !!( annotations && (annotations.readOnlyHint || annotations.destructiveHint || annotations.idempotentHint || annotations.openWorldHint) ); } export function ToolDetailPanel({ tool, formValues, isExecuting, progress, serverSupportsTaskToolCalls, modernTasks = false, runAsTask, onRunAsTaskChange, onFormChange, onExecute, onCancel, }: ToolDetailPanelProps) { const { name, title, description, icons, annotations, inputSchema } = tool; // Narrow the SDK protocol schema to the form renderer's schema type. const formSchema = toFormSchema(inputSchema) ?? {}; const iconSrc = icons?.[0]?.src; // SEP-2243: args this tool declares as `x-mcp-header` — their values mirror // into `Mcp-Param-{Name}` headers on a `tools/call` (#1632). const mirroredParams = getMirroredHeaderParams(tool); // Descriptions are shown by default (most are short); the chevron lets the // user hide a long one to keep the form and Execute footer in view. Reset to // shown when switching tools (React's adjust-state-during-render pattern) so // a prior tool's hidden state doesn't carry over — mirrors how ToolsScreen // clears formValues on change. const [descriptionOpen, setDescriptionOpen] = useState(true); const [prevToolName, setPrevToolName] = useState(name); if (name !== prevToolName) { setPrevToolName(name); setDescriptionOpen(true); } // Ties the toggle to the Collapse region so assistive tech announces it as a // single expandable control (aria-expanded + aria-controls). const descriptionRegionId = useId(); // Show the toggle when the server supports task tool calls and either the // connection is modern (task creation is server-directed there, so any tool // may become a task) or the tool doesn't forbid per-tool task support // (legacy). `required` tools are forced on (checked + disabled); `optional` // and (on modern) any tool follow the user's `runAsTask` choice. // // NOTE: on modern, a per-tool `taskSupport: "forbidden"` is DELIBERATELY // ignored. Under SEP-2663 task creation is decided by the server per request, // not declared per tool, so `taskSupport` (a legacy 2025-11-25 concept) does // not gate the affordance — the server may return a task for any call. The // toggle just declares intent to poll a returned handle. const taskSupport = getTaskSupport(tool); const showRunAsTask = serverSupportsTaskToolCalls && (modernTasks || taskSupport !== "forbidden"); // Gate the effective decision on `showRunAsTask`: a stale `runAsTask`/`required` // value must not route through callToolStream when the toggle is hidden. On // legacy, a tool's taskSupport is only considered when the server advertises // `tasks.requests.tools.call`; on modern, the user's choice governs any tool. const effectiveRunAsTask = showRunAsTask && (taskSupport === "required" || ((taskSupport === "optional" || modernTasks) && runAsTask)); return ( {iconSrc && } {resolveDisplayLabel(name, title)} {description && ( setDescriptionOpen((open) => !open)} > {descriptionOpen ? : } )} {hasAnyAnnotation(annotations) && annotations && ( {annotations.readOnlyHint && ( )} {annotations.destructiveHint && ( )} {annotations.idempotentHint && ( )} {annotations.openWorldHint && ( )} )} {description && ( {description} )} {mirroredParams.length > 0 && ( Mirrored request headers (SEP-2243) {mirroredParams.map((param) => ( {param.path} → {param.header} ))} These argument values are mirrored into HTTP headers on the call. In the web client the Mcp-Param-* headers are applied by the Node backend that issues the upstream request. )} {progress && } {showRunAsTask && ( onRunAsTaskChange(event.currentTarget.checked)} /> )} {isExecuting && Cancel} ); }