Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Progress Support for Long Running Tool Calls ⏳ #271

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,16 @@ The MCP Inspector includes a proxy server that can run and communicate with loca

### Configuration

The MCP Inspector supports the following configuration settings. To change them click on the `Configuration` button in the MCP Inspector UI :
The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI:

| Name | Purpose | Default Value |
| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
| MCP_SERVER_REQUEST_TIMEOUT | Maximum time in milliseconds to wait for a response from the MCP server before timing out | 10000 |
| MCP_PROXY_FULL_ADDRESS | The full URL of the MCP Inspector proxy server (e.g. `http://10.2.1.14:2277`) | `null` |
| Setting | Description | Default |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------- |
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" |

These settings can be adjusted in real-time through the UI and will persist across sessions.

### From this repository

Expand Down
78 changes: 44 additions & 34 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
import { InspectorConfig } from "./lib/configurationTypes";
import {
getMCPProxyAddress,
getMCPServerRequestTimeout,
} from "./utils/configUtils";
import { getMCPProxyAddress } from "./utils/configUtils";
import { useToast } from "@/hooks/use-toast";

const params = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -148,7 +145,7 @@ const App = () => {
serverCapabilities,
mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
makeRequest,
sendNotification,
handleCompletion,
completionsSupported,
Expand All @@ -161,8 +158,7 @@ const App = () => {
sseUrl,
env,
bearerToken,
proxyServerUrl: getMCPProxyAddress(config),
requestTimeout: getMCPServerRequestTimeout(config),
config,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
Expand Down Expand Up @@ -279,13 +275,13 @@ const App = () => {
setErrors((prev) => ({ ...prev, [tabKey]: null }));
};

const makeRequest = async <T extends z.ZodType>(
const sendMCPRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T,
tabKey?: keyof typeof errors,
) => {
try {
const response = await makeConnectionRequest(request, schema);
const response = await makeRequest(request, schema);
if (tabKey !== undefined) {
clearError(tabKey);
}
Expand All @@ -303,7 +299,7 @@ const App = () => {
};

const listResources = async () => {
const response = await makeRequest(
const response = await sendMCPRequest(
{
method: "resources/list" as const,
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
Expand All @@ -316,7 +312,7 @@ const App = () => {
};

const listResourceTemplates = async () => {
const response = await makeRequest(
const response = await sendMCPRequest(
{
method: "resources/templates/list" as const,
params: nextResourceTemplateCursor
Expand All @@ -333,7 +329,7 @@ const App = () => {
};

const readResource = async (uri: string) => {
const response = await makeRequest(
const response = await sendMCPRequest(
{
method: "resources/read" as const,
params: { uri },
Expand All @@ -346,7 +342,7 @@ const App = () => {

const subscribeToResource = async (uri: string) => {
if (!resourceSubscriptions.has(uri)) {
await makeRequest(
await sendMCPRequest(
{
method: "resources/subscribe" as const,
params: { uri },
Expand All @@ -362,7 +358,7 @@ const App = () => {

const unsubscribeFromResource = async (uri: string) => {
if (resourceSubscriptions.has(uri)) {
await makeRequest(
await sendMCPRequest(
{
method: "resources/unsubscribe" as const,
params: { uri },
Expand All @@ -377,7 +373,7 @@ const App = () => {
};

const listPrompts = async () => {
const response = await makeRequest(
const response = await sendMCPRequest(
{
method: "prompts/list" as const,
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
Expand All @@ -390,7 +386,7 @@ const App = () => {
};

const getPrompt = async (name: string, args: Record<string, string> = {}) => {
const response = await makeRequest(
const response = await sendMCPRequest(
{
method: "prompts/get" as const,
params: { name, arguments: args },
Expand All @@ -402,7 +398,7 @@ const App = () => {
};

const listTools = async () => {
const response = await makeRequest(
const response = await sendMCPRequest(
{
method: "tools/list" as const,
params: nextToolCursor ? { cursor: nextToolCursor } : {},
Expand All @@ -415,29 +411,42 @@ const App = () => {
};

const callTool = async (name: string, params: Record<string, unknown>) => {
const response = await makeRequest(
{
method: "tools/call" as const,
params: {
name,
arguments: params,
_meta: {
progressToken: progressTokenRef.current++,
try {
const response = await sendMCPRequest(
{
method: "tools/call" as const,
params: {
name,
arguments: params,
_meta: {
progressToken: progressTokenRef.current++,
},
},
},
},
CompatibilityCallToolResultSchema,
"tools",
);
setToolResult(response);
CompatibilityCallToolResultSchema,
"tools",
);
setToolResult(response);
} catch (e) {
const toolResult: CompatibilityCallToolResult = {
content: [
{
type: "text",
text: (e as Error).message ?? String(e),
},
],
isError: true,
};
setToolResult(toolResult);
}
};

const handleRootsChange = async () => {
await sendNotification({ method: "notifications/roots/list_changed" });
};

const sendLogLevelRequest = async (level: LoggingLevel) => {
await makeRequest(
await sendMCPRequest(
{
method: "logging/setLevel" as const,
params: { level },
Expand Down Expand Up @@ -637,9 +646,10 @@ const App = () => {
setTools([]);
setNextToolCursor(undefined);
}}
callTool={(name, params) => {
callTool={async (name, params) => {
clearError("tools");
callTool(name, params);
setToolResult(null);
await callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
Expand All @@ -654,7 +664,7 @@ const App = () => {
<ConsoleTab />
<PingTab
onPingClick={() => {
void makeRequest(
void sendMCPRequest(
{
method: "ping" as const,
},
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ const Sidebar = ({
return (
<div key={key} className="space-y-2">
<div className="flex items-center gap-1">
<label className="text-sm font-medium text-green-600">
<label className="text-sm font-medium text-green-600 break-all">
{configKey}
</label>
<Tooltip>
Expand Down
31 changes: 26 additions & 5 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ListToolsResult,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { Send } from "lucide-react";
import { Loader2, Send } from "lucide-react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import JsonView from "./JsonView";
Expand All @@ -31,14 +31,16 @@ const ToolsTab = ({
tools: Tool[];
listTools: () => void;
clearTools: () => void;
callTool: (name: string, params: Record<string, unknown>) => void;
callTool: (name: string, params: Record<string, unknown>) => Promise<void>;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool | null) => void;
toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
const [isToolRunning, setIsToolRunning] = useState(false);

useEffect(() => {
setParams({});
}, [selectedTool]);
Expand Down Expand Up @@ -234,9 +236,28 @@ const ToolsTab = ({
);
},
)}
<Button onClick={() => callTool(selectedTool.name, params)}>
<Send className="w-4 h-4 mr-2" />
Run Tool
<Button
onClick={async () => {
try {
setIsToolRunning(true);
await callTool(selectedTool.name, params);
} finally {
setIsToolRunning(false);
}
}}
disabled={isToolRunning}
>
{isToolRunning ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Running...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Run Tool
</>
)}
</Button>
{toolResult && renderToolResult()}
</div>
Expand Down
48 changes: 48 additions & 0 deletions client/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,54 @@ describe("Sidebar Environment Variables", () => {
);
});

it("should update MCP server proxy address", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });

openConfigSection();

const proxyAddressInput = screen.getByTestId(
"MCP_PROXY_FULL_ADDRESS-input",
);
fireEvent.change(proxyAddressInput, {
target: { value: "http://localhost:8080" },
});

expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_PROXY_FULL_ADDRESS: {
description:
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
value: "http://localhost:8080",
},
}),
);
});

it("should update max total timeout", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });

openConfigSection();

const maxTotalTimeoutInput = screen.getByTestId(
"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
);
fireEvent.change(maxTotalTimeoutInput, {
target: { value: "10000" },
});

expect(setConfig).toHaveBeenCalledWith(
expect.objectContaining({
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
description:
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
value: 10000,
},
}),
);
});

it("should handle invalid timeout values entered by user", () => {
const setConfig = jest.fn();
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
Expand Down
Loading