Merge pull request #1072 from cliffhall/hide-run-as-task-if-not-supported

Manage display of "Run as Task" checkbox on Tools Tab
This commit is contained in:
Bob Dickinson 2026-02-23 19:06:43 -08:00 • committed by GitHub
commit c4c6e6b6b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 147 additions and 22 deletions

View file

@ -1526,6 +1526,9 @@ const App = () => {
error={errors.prompts}
/>
<ToolsTab
serverSupportsTaskRequests={
!!serverCapabilities?.tasks?.requests?.tools?.call
}
tools={tools}
listTools={() => {
clearError("tools");

View file

@ -53,9 +53,38 @@ import {
isReservedMetaKey,
} from "@/utils/metaUtils";
/**
* Extended Tool type that includes optional fields used by the inspector.
*/
export interface ExtendedTool extends Tool, WithIcons {
_meta?: Record<string, unknown>;
execution?: {
taskSupport?: "forbidden" | "required" | "optional";
};
}
// Type guard to safely detect the optional _meta field without using `any`
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
typeof (tool as { _meta?: unknown })._meta !== "undefined";
const hasMeta = (
tool: Tool,
): tool is ExtendedTool & { _meta: Record<string, unknown> } =>
typeof (tool as ExtendedTool)._meta !== "undefined";
// Returns the execution.taskSupport value for a tool, defaulting to "forbidden" per MCP spec
const getTaskSupport = (
tool: Tool | null,
): "forbidden" | "required" | "optional" => {
if (!tool) return "forbidden";
const extendedTool = tool as ExtendedTool;
const taskSupport = extendedTool.execution?.taskSupport;
if (
taskSupport === "forbidden" ||
taskSupport === "required" ||
taskSupport === "optional"
) {
return taskSupport;
}
return "forbidden";
};
// Type guard to safely detect the optional annotations field
const hasAnnotations = (
@ -148,6 +177,7 @@ const ToolsTab = ({
error,
resourceContent,
onReadResource,
serverSupportsTaskRequests,
}: {
tools: Tool[];
listTools: () => void;
@ -166,6 +196,7 @@ const ToolsTab = ({
error: string | null;
resourceContent: Record<string, string>;
onReadResource?: (uri: string) => void;
serverSupportsTaskRequests: boolean;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
const [runAsTask, setRunAsTask] = useState(false);
@ -210,14 +241,17 @@ const ToolsTab = ({
];
});
setParams(Object.fromEntries(params));
setRunAsTask(false);
const toolTaskSupport = serverSupportsTaskRequests
? getTaskSupport(selectedTool)
: "forbidden";
setRunAsTask(toolTaskSupport === "required");
// Reset validation errors when switching tools
setHasValidationErrors(false);
// Clear form refs for the previous tool
formRefs.current = {};
}, [selectedTool]);
}, [selectedTool, serverSupportsTaskRequests]);
const hasReservedMetadataEntry = metadataEntries.some(({ key }) => {
const trimmedKey = key.trim();
@ -234,6 +268,10 @@ const ToolsTab = ({
return trimmedKey !== "" && !hasValidMetaName(trimmedKey);
});
const taskSupport = serverSupportsTaskRequests
? getTaskSupport(selectedTool)
: "forbidden";
return (
<TabsContent value="tools">
<div className="grid grid-cols-2 gap-4">
@ -249,7 +287,7 @@ const ToolsTab = ({
renderItem={(tool) => (
<div className="flex items-start w-full gap-2">
<div className="flex-shrink-0 mt-1">
<IconDisplay icons={(tool as WithIcons).icons} size="sm" />
<IconDisplay icons={(tool as ExtendedTool).icons} size="sm" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="truncate">{tool.title || tool.name}</span>
@ -270,7 +308,7 @@ const ToolsTab = ({
<div className="flex items-center gap-2">
{selectedTool && (
<IconDisplay
icons={(selectedTool as WithIcons).icons}
icons={(selectedTool as ExtendedTool).icons}
size="md"
/>
)}
@ -747,21 +785,24 @@ const ToolsTab = ({
</div>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="run-as-task"
checked={runAsTask}
onCheckedChange={(checked: boolean) =>
setRunAsTask(checked)
}
/>
<Label
htmlFor="run-as-task"
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
>
Run as task
</Label>
</div>
{taskSupport !== "forbidden" && (
<div className="flex items-center space-x-2">
<Checkbox
id="run-as-task"
checked={runAsTask}
onCheckedChange={(checked: boolean) =>
setRunAsTask(checked)
}
disabled={taskSupport === "required"}
/>
<Label
htmlFor="run-as-task"
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
>
Run as task
</Label>
</div>
)}
<Button
onClick={async () => {
// Validate JSON inputs before calling tool

View file

@ -1,7 +1,7 @@
import { render, screen, fireEvent, act } from "@testing-library/react";
import "@testing-library/jest-dom";
import { describe, it, jest, beforeEach } from "@jest/globals";
import ToolsTab from "../ToolsTab";
import ToolsTab, { ExtendedTool } from "../ToolsTab";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { Tabs } from "../ui/tabs";
import { cacheToolOutputSchemas } from "../../utils/schemaUtils";
@ -73,6 +73,7 @@ describe("ToolsTab", () => {
error: null,
resourceContent: {},
onReadResource: jest.fn(),
serverSupportsTaskRequests: true,
};
const renderToolsTab = (props = {}) => {
@ -107,6 +108,86 @@ describe("ToolsTab", () => {
expect(newInput.value).toBe("");
});
it("should show/hide/disable run-as-task checkbox based on taskSupport", async () => {
const forbiddenTool: ExtendedTool = {
...mockTools[0],
name: "forbiddenTool",
execution: { taskSupport: "forbidden" },
};
const requiredTool: ExtendedTool = {
...mockTools[0],
name: "requiredTool",
execution: { taskSupport: "required" },
};
const optionalTool: ExtendedTool = {
...mockTools[0],
name: "optionalTool",
execution: { taskSupport: "optional" },
};
const { rerender } = renderToolsTab({
selectedTool: forbiddenTool,
});
expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();
rerender(
<Tabs defaultValue="tools">
<ToolsTab {...defaultProps} selectedTool={optionalTool} />
</Tabs>,
);
const optionalCheckbox = screen.getByLabelText(
/run as task/i,
) as HTMLInputElement;
expect(optionalCheckbox).toBeInTheDocument();
expect(optionalCheckbox.getAttribute("aria-checked")).toBe("false");
expect(optionalCheckbox).not.toBeDisabled();
rerender(
<Tabs defaultValue="tools">
<ToolsTab {...defaultProps} selectedTool={requiredTool} />
</Tabs>,
);
const requiredCheckbox = screen.getByLabelText(
/run as task/i,
) as HTMLInputElement;
expect(requiredCheckbox).toBeInTheDocument();
expect(requiredCheckbox.getAttribute("aria-checked")).toBe("true");
expect(requiredCheckbox).toBeDisabled();
});
it("should hide run-as-task checkbox when serverSupportsTaskRequests is false even for required/optional tools", async () => {
const requiredTool: ExtendedTool = {
...mockTools[0],
name: "requiredTool",
execution: { taskSupport: "required" },
};
const optionalTool: ExtendedTool = {
...mockTools[0],
name: "optionalTool",
execution: { taskSupport: "optional" },
};
const { rerender } = renderToolsTab({
selectedTool: requiredTool,
serverSupportsTaskRequests: false,
});
expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();
rerender(
<Tabs defaultValue="tools">
<ToolsTab
{...defaultProps}
selectedTool={optionalTool}
serverSupportsTaskRequests={false}
/>
</Tabs>,
);
expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();
});
it("should handle integer type inputs", async () => {
renderToolsTab({
selectedTool: mockTools[1], // Use the tool with integer type