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:
commit
c4c6e6b6b1
3 changed files with 147 additions and 22 deletions
|
|
@ -1526,6 +1526,9 @@ const App = () => {
|
|||
error={errors.prompts}
|
||||
/>
|
||||
<ToolsTab
|
||||
serverSupportsTaskRequests={
|
||||
!!serverCapabilities?.tasks?.requests?.tools?.call
|
||||
}
|
||||
tools={tools}
|
||||
listTools={() => {
|
||||
clearError("tools");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue