Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed client/pr_demo.mp4
Binary file not shown.
14 changes: 13 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
import { OAuthStateMachine } from "./lib/oauth-state-machine";
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
import { cleanParams } from "./utils/paramUtils";
import type { JsonSchemaType } from "./utils/jsonUtils";
import React, {
Suspense,
useCallback,
Expand Down Expand Up @@ -777,12 +779,18 @@ const App = () => {
lastToolCallOriginTabRef.current = currentTabRef.current;

try {
// Find the tool schema to clean parameters properly
const tool = tools.find((t) => t.name === name);
const cleanedParams = tool?.inputSchema
? cleanParams(params, tool.inputSchema as JsonSchemaType)
: params;

const response = await sendMCPRequest(
{
method: "tools/call" as const,
params: {
name,
arguments: params,
arguments: cleanedParams,
_meta: {
progressToken: progressTokenRef.current++,
},
Expand All @@ -793,6 +801,8 @@ const App = () => {
);

setToolResult(response);
// Clear any validation errors since tool execution completed
setErrors((prev) => ({ ...prev, tools: null }));
} catch (e) {
const toolResult: CompatibilityCallToolResult = {
content: [
Expand All @@ -804,6 +814,8 @@ const App = () => {
isError: true,
};
setToolResult(toolResult);
// Clear validation errors - tool execution errors are shown in ToolResults
setErrors((prev) => ({ ...prev, tools: null }));
}
};

Expand Down
76 changes: 55 additions & 21 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,15 @@ const ToolsTab = ({
</h3>
</div>
<div className="p-4">
{error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedTool ? (
{selectedTool ? (
<div className="space-y-4">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<p className="text-sm text-gray-600 dark:text-gray-400">
{selectedTool.description}
</p>
Expand Down Expand Up @@ -184,16 +185,27 @@ const ToolsTab = ({
id={key}
name={key}
placeholder={prop.description}
value={(params[key] as string) ?? ""}
onChange={(e) =>
setParams({
...params,
[key]:
e.target.value === ""
? undefined
: e.target.value,
})
value={
params[key] === undefined
? ""
: String(params[key])
}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Field cleared - set to undefined
setParams({
...params,
[key]: undefined,
});
} else {
// Field has value - keep as string
setParams({
...params,
[key]: value,
});
}
}}
className="mt-1"
/>
) : prop.type === "object" || prop.type === "array" ? (
Expand Down Expand Up @@ -227,13 +239,35 @@ const ToolsTab = ({
id={key}
name={key}
placeholder={prop.description}
value={(params[key] as string) ?? ""}
value={
params[key] === undefined
? ""
: String(params[key])
}
onChange={(e) => {
const value = e.target.value;
setParams({
...params,
[key]: value === "" ? "" : Number(value),
});
if (value === "") {
// Field cleared - set to undefined
setParams({
...params,
[key]: undefined,
});
} else {
// Field has value - try to convert to number, but store input either way
const num = Number(value);
if (!isNaN(num)) {
setParams({
...params,
[key]: num,
});
} else {
// Store invalid input as string - let server validate
setParams({
...params,
[key]: value,
});
}
}
}}
className="mt-1"
/>
Expand Down
25 changes: 17 additions & 8 deletions client/src/components/__tests__/ToolsTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ describe("ToolsTab", () => {
description: "Tool with JSON parameters",
inputSchema: {
type: "object" as const,
required: ["config", "data"], // Make them required so they render as form fields
properties: {
config: {
type: "object" as const,
Expand Down Expand Up @@ -693,15 +694,16 @@ describe("ToolsTab", () => {
callTool: mockCallTool,
});

// Find JSON editor textareas
// Find JSON editor textareas (should have one for each required field: config and data)
const textareas = screen.getAllByRole("textbox");
expect(textareas.length).toBe(2);

// Enter valid JSON in the first textarea
// Enter valid JSON in each textarea
fireEvent.change(textareas[0], {
target: {
value:
'{ "config": { "setting": "value" }, "data": ["item1", "item2"] }',
},
target: { value: '{ "setting": "value" }' },
});
fireEvent.change(textareas[1], {
target: { value: '["item1", "item2"]' },
});

// Wait for debounced updates
Expand Down Expand Up @@ -799,9 +801,16 @@ describe("ToolsTab", () => {
});

const textareas = screen.getAllByRole("textbox");
expect(textareas.length).toBe(2);

// Clear both textareas (empty JSON should be valid)
fireEvent.change(textareas[0], { target: { value: "{}" } });
fireEvent.change(textareas[1], { target: { value: "[]" } });

// Clear the textarea (empty JSON should be valid)
fireEvent.change(textareas[0], { target: { value: "" } });
// Wait for debounced updates
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 350));
});

// Try to run the tool
const runButton = screen.getByRole("button", { name: /run tool/i });
Expand Down
Loading
Loading