Skip to content

Commit c4c6e6b

Browse files
authored
Merge pull request #1072 from cliffhall/hide-run-as-task-if-not-supported
Manage display of "Run as Task" checkbox on Tools Tab
2 parents 6ce441a + 92b6c89 commit c4c6e6b

File tree

3 files changed

+147
-22
lines changed

3 files changed

+147
-22
lines changed

client/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,9 @@ const App = () => {
15261526
error={errors.prompts}
15271527
/>
15281528
<ToolsTab
1529+
serverSupportsTaskRequests={
1530+
!!serverCapabilities?.tasks?.requests?.tools?.call
1531+
}
15291532
tools={tools}
15301533
listTools={() => {
15311534
clearError("tools");

client/src/components/ToolsTab.tsx

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,38 @@ import {
5353
isReservedMetaKey,
5454
} from "@/utils/metaUtils";
5555

56+
/**
57+
* Extended Tool type that includes optional fields used by the inspector.
58+
*/
59+
export interface ExtendedTool extends Tool, WithIcons {
60+
_meta?: Record<string, unknown>;
61+
execution?: {
62+
taskSupport?: "forbidden" | "required" | "optional";
63+
};
64+
}
65+
5666
// Type guard to safely detect the optional _meta field without using `any`
57-
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
58-
typeof (tool as { _meta?: unknown })._meta !== "undefined";
67+
const hasMeta = (
68+
tool: Tool,
69+
): tool is ExtendedTool & { _meta: Record<string, unknown> } =>
70+
typeof (tool as ExtendedTool)._meta !== "undefined";
71+
72+
// Returns the execution.taskSupport value for a tool, defaulting to "forbidden" per MCP spec
73+
const getTaskSupport = (
74+
tool: Tool | null,
75+
): "forbidden" | "required" | "optional" => {
76+
if (!tool) return "forbidden";
77+
const extendedTool = tool as ExtendedTool;
78+
const taskSupport = extendedTool.execution?.taskSupport;
79+
if (
80+
taskSupport === "forbidden" ||
81+
taskSupport === "required" ||
82+
taskSupport === "optional"
83+
) {
84+
return taskSupport;
85+
}
86+
return "forbidden";
87+
};
5988

6089
// Type guard to safely detect the optional annotations field
6190
const hasAnnotations = (
@@ -148,6 +177,7 @@ const ToolsTab = ({
148177
error,
149178
resourceContent,
150179
onReadResource,
180+
serverSupportsTaskRequests,
151181
}: {
152182
tools: Tool[];
153183
listTools: () => void;
@@ -166,6 +196,7 @@ const ToolsTab = ({
166196
error: string | null;
167197
resourceContent: Record<string, string>;
168198
onReadResource?: (uri: string) => void;
199+
serverSupportsTaskRequests: boolean;
169200
}) => {
170201
const [params, setParams] = useState<Record<string, unknown>>({});
171202
const [runAsTask, setRunAsTask] = useState(false);
@@ -210,14 +241,17 @@ const ToolsTab = ({
210241
];
211242
});
212243
setParams(Object.fromEntries(params));
213-
setRunAsTask(false);
244+
const toolTaskSupport = serverSupportsTaskRequests
245+
? getTaskSupport(selectedTool)
246+
: "forbidden";
247+
setRunAsTask(toolTaskSupport === "required");
214248

215249
// Reset validation errors when switching tools
216250
setHasValidationErrors(false);
217251

218252
// Clear form refs for the previous tool
219253
formRefs.current = {};
220-
}, [selectedTool]);
254+
}, [selectedTool, serverSupportsTaskRequests]);
221255

222256
const hasReservedMetadataEntry = metadataEntries.some(({ key }) => {
223257
const trimmedKey = key.trim();
@@ -234,6 +268,10 @@ const ToolsTab = ({
234268
return trimmedKey !== "" && !hasValidMetaName(trimmedKey);
235269
});
236270

271+
const taskSupport = serverSupportsTaskRequests
272+
? getTaskSupport(selectedTool)
273+
: "forbidden";
274+
237275
return (
238276
<TabsContent value="tools">
239277
<div className="grid grid-cols-2 gap-4">
@@ -249,7 +287,7 @@ const ToolsTab = ({
249287
renderItem={(tool) => (
250288
<div className="flex items-start w-full gap-2">
251289
<div className="flex-shrink-0 mt-1">
252-
<IconDisplay icons={(tool as WithIcons).icons} size="sm" />
290+
<IconDisplay icons={(tool as ExtendedTool).icons} size="sm" />
253291
</div>
254292
<div className="flex flex-col flex-1 min-w-0">
255293
<span className="truncate">{tool.title || tool.name}</span>
@@ -270,7 +308,7 @@ const ToolsTab = ({
270308
<div className="flex items-center gap-2">
271309
{selectedTool && (
272310
<IconDisplay
273-
icons={(selectedTool as WithIcons).icons}
311+
icons={(selectedTool as ExtendedTool).icons}
274312
size="md"
275313
/>
276314
)}
@@ -747,21 +785,24 @@ const ToolsTab = ({
747785
</div>
748786
</div>
749787
)}
750-
<div className="flex items-center space-x-2">
751-
<Checkbox
752-
id="run-as-task"
753-
checked={runAsTask}
754-
onCheckedChange={(checked: boolean) =>
755-
setRunAsTask(checked)
756-
}
757-
/>
758-
<Label
759-
htmlFor="run-as-task"
760-
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
761-
>
762-
Run as task
763-
</Label>
764-
</div>
788+
{taskSupport !== "forbidden" && (
789+
<div className="flex items-center space-x-2">
790+
<Checkbox
791+
id="run-as-task"
792+
checked={runAsTask}
793+
onCheckedChange={(checked: boolean) =>
794+
setRunAsTask(checked)
795+
}
796+
disabled={taskSupport === "required"}
797+
/>
798+
<Label
799+
htmlFor="run-as-task"
800+
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
801+
>
802+
Run as task
803+
</Label>
804+
</div>
805+
)}
765806
<Button
766807
onClick={async () => {
767808
// Validate JSON inputs before calling tool

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render, screen, fireEvent, act } from "@testing-library/react";
22
import "@testing-library/jest-dom";
33
import { describe, it, jest, beforeEach } from "@jest/globals";
4-
import ToolsTab from "../ToolsTab";
4+
import ToolsTab, { ExtendedTool } from "../ToolsTab";
55
import { Tool } from "@modelcontextprotocol/sdk/types.js";
66
import { Tabs } from "../ui/tabs";
77
import { cacheToolOutputSchemas } from "../../utils/schemaUtils";
@@ -73,6 +73,7 @@ describe("ToolsTab", () => {
7373
error: null,
7474
resourceContent: {},
7575
onReadResource: jest.fn(),
76+
serverSupportsTaskRequests: true,
7677
};
7778

7879
const renderToolsTab = (props = {}) => {
@@ -107,6 +108,86 @@ describe("ToolsTab", () => {
107108
expect(newInput.value).toBe("");
108109
});
109110

111+
it("should show/hide/disable run-as-task checkbox based on taskSupport", async () => {
112+
const forbiddenTool: ExtendedTool = {
113+
...mockTools[0],
114+
name: "forbiddenTool",
115+
execution: { taskSupport: "forbidden" },
116+
};
117+
const requiredTool: ExtendedTool = {
118+
...mockTools[0],
119+
name: "requiredTool",
120+
execution: { taskSupport: "required" },
121+
};
122+
const optionalTool: ExtendedTool = {
123+
...mockTools[0],
124+
name: "optionalTool",
125+
execution: { taskSupport: "optional" },
126+
};
127+
128+
const { rerender } = renderToolsTab({
129+
selectedTool: forbiddenTool,
130+
});
131+
132+
expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();
133+
134+
rerender(
135+
<Tabs defaultValue="tools">
136+
<ToolsTab {...defaultProps} selectedTool={optionalTool} />
137+
</Tabs>,
138+
);
139+
const optionalCheckbox = screen.getByLabelText(
140+
/run as task/i,
141+
) as HTMLInputElement;
142+
expect(optionalCheckbox).toBeInTheDocument();
143+
expect(optionalCheckbox.getAttribute("aria-checked")).toBe("false");
144+
expect(optionalCheckbox).not.toBeDisabled();
145+
146+
rerender(
147+
<Tabs defaultValue="tools">
148+
<ToolsTab {...defaultProps} selectedTool={requiredTool} />
149+
</Tabs>,
150+
);
151+
const requiredCheckbox = screen.getByLabelText(
152+
/run as task/i,
153+
) as HTMLInputElement;
154+
expect(requiredCheckbox).toBeInTheDocument();
155+
expect(requiredCheckbox.getAttribute("aria-checked")).toBe("true");
156+
expect(requiredCheckbox).toBeDisabled();
157+
});
158+
159+
it("should hide run-as-task checkbox when serverSupportsTaskRequests is false even for required/optional tools", async () => {
160+
const requiredTool: ExtendedTool = {
161+
...mockTools[0],
162+
name: "requiredTool",
163+
execution: { taskSupport: "required" },
164+
};
165+
const optionalTool: ExtendedTool = {
166+
...mockTools[0],
167+
name: "optionalTool",
168+
execution: { taskSupport: "optional" },
169+
};
170+
171+
const { rerender } = renderToolsTab({
172+
selectedTool: requiredTool,
173+
serverSupportsTaskRequests: false,
174+
});
175+
176+
expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();
177+
178+
rerender(
179+
<Tabs defaultValue="tools">
180+
<ToolsTab
181+
{...defaultProps}
182+
selectedTool={optionalTool}
183+
serverSupportsTaskRequests={false}
184+
/>
185+
</Tabs>,
186+
);
187+
188+
expect(screen.queryByLabelText(/run as task/i)).not.toBeInTheDocument();
189+
});
190+
110191
it("should handle integer type inputs", async () => {
111192
renderToolsTab({
112193
selectedTool: mockTools[1], // Use the tool with integer type

0 commit comments

Comments
 (0)