Skip to content

Commit da4e2fa

Browse files
authored
Merge pull request #271 from pulkitsharma07/progress_flow_support
feat: Progress Support for Long Running Tool Calls ⏳
2 parents 6659d54 + 9c6663c commit da4e2fa

File tree

11 files changed

+442
-77
lines changed

11 files changed

+442
-77
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,16 @@ The MCP Inspector includes a proxy server that can run and communicate with loca
4848

4949
### Configuration
5050

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

53-
| Name | Purpose | Default Value |
54-
| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
55-
| MCP_SERVER_REQUEST_TIMEOUT | Maximum time in milliseconds to wait for a response from the MCP server before timing out | 10000 |
56-
| MCP_PROXY_FULL_ADDRESS | The full URL of the MCP Inspector proxy server (e.g. `http://10.2.1.14:2277`) | `null` |
53+
| Setting | Description | Default |
54+
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------- |
55+
| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 |
56+
| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true |
57+
| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 |
58+
| `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 | "" |
59+
60+
These settings can be adjusted in real-time through the UI and will persist across sessions.
5761

5862
### From this repository
5963

client/src/App.tsx

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@ import Sidebar from "./components/Sidebar";
4545
import ToolsTab from "./components/ToolsTab";
4646
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
4747
import { InspectorConfig } from "./lib/configurationTypes";
48-
import {
49-
getMCPProxyAddress,
50-
getMCPServerRequestTimeout,
51-
} from "./utils/configUtils";
48+
import { getMCPProxyAddress } from "./utils/configUtils";
5249
import { useToast } from "@/hooks/use-toast";
5350

5451
const params = new URLSearchParams(window.location.search);
@@ -98,10 +95,21 @@ const App = () => {
9895
const [config, setConfig] = useState<InspectorConfig>(() => {
9996
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
10097
if (savedConfig) {
101-
return {
98+
// merge default config with saved config
99+
const mergedConfig = {
102100
...DEFAULT_INSPECTOR_CONFIG,
103101
...JSON.parse(savedConfig),
104102
} as InspectorConfig;
103+
104+
// update description of keys to match the new description (in case of any updates to the default config description)
105+
Object.entries(mergedConfig).forEach(([key, value]) => {
106+
mergedConfig[key as keyof InspectorConfig] = {
107+
...value,
108+
label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label,
109+
};
110+
});
111+
112+
return mergedConfig;
105113
}
106114
return DEFAULT_INSPECTOR_CONFIG;
107115
});
@@ -148,7 +156,7 @@ const App = () => {
148156
serverCapabilities,
149157
mcpClient,
150158
requestHistory,
151-
makeRequest: makeConnectionRequest,
159+
makeRequest,
152160
sendNotification,
153161
handleCompletion,
154162
completionsSupported,
@@ -161,8 +169,7 @@ const App = () => {
161169
sseUrl,
162170
env,
163171
bearerToken,
164-
proxyServerUrl: getMCPProxyAddress(config),
165-
requestTimeout: getMCPServerRequestTimeout(config),
172+
config,
166173
onNotification: (notification) => {
167174
setNotifications((prev) => [...prev, notification as ServerNotification]);
168175
},
@@ -279,13 +286,13 @@ const App = () => {
279286
setErrors((prev) => ({ ...prev, [tabKey]: null }));
280287
};
281288

282-
const makeRequest = async <T extends z.ZodType>(
289+
const sendMCPRequest = async <T extends z.ZodType>(
283290
request: ClientRequest,
284291
schema: T,
285292
tabKey?: keyof typeof errors,
286293
) => {
287294
try {
288-
const response = await makeConnectionRequest(request, schema);
295+
const response = await makeRequest(request, schema);
289296
if (tabKey !== undefined) {
290297
clearError(tabKey);
291298
}
@@ -303,7 +310,7 @@ const App = () => {
303310
};
304311

305312
const listResources = async () => {
306-
const response = await makeRequest(
313+
const response = await sendMCPRequest(
307314
{
308315
method: "resources/list" as const,
309316
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
@@ -316,7 +323,7 @@ const App = () => {
316323
};
317324

318325
const listResourceTemplates = async () => {
319-
const response = await makeRequest(
326+
const response = await sendMCPRequest(
320327
{
321328
method: "resources/templates/list" as const,
322329
params: nextResourceTemplateCursor
@@ -333,7 +340,7 @@ const App = () => {
333340
};
334341

335342
const readResource = async (uri: string) => {
336-
const response = await makeRequest(
343+
const response = await sendMCPRequest(
337344
{
338345
method: "resources/read" as const,
339346
params: { uri },
@@ -346,7 +353,7 @@ const App = () => {
346353

347354
const subscribeToResource = async (uri: string) => {
348355
if (!resourceSubscriptions.has(uri)) {
349-
await makeRequest(
356+
await sendMCPRequest(
350357
{
351358
method: "resources/subscribe" as const,
352359
params: { uri },
@@ -362,7 +369,7 @@ const App = () => {
362369

363370
const unsubscribeFromResource = async (uri: string) => {
364371
if (resourceSubscriptions.has(uri)) {
365-
await makeRequest(
372+
await sendMCPRequest(
366373
{
367374
method: "resources/unsubscribe" as const,
368375
params: { uri },
@@ -377,7 +384,7 @@ const App = () => {
377384
};
378385

379386
const listPrompts = async () => {
380-
const response = await makeRequest(
387+
const response = await sendMCPRequest(
381388
{
382389
method: "prompts/list" as const,
383390
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
@@ -390,7 +397,7 @@ const App = () => {
390397
};
391398

392399
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
393-
const response = await makeRequest(
400+
const response = await sendMCPRequest(
394401
{
395402
method: "prompts/get" as const,
396403
params: { name, arguments: args },
@@ -402,7 +409,7 @@ const App = () => {
402409
};
403410

404411
const listTools = async () => {
405-
const response = await makeRequest(
412+
const response = await sendMCPRequest(
406413
{
407414
method: "tools/list" as const,
408415
params: nextToolCursor ? { cursor: nextToolCursor } : {},
@@ -415,29 +422,42 @@ const App = () => {
415422
};
416423

417424
const callTool = async (name: string, params: Record<string, unknown>) => {
418-
const response = await makeRequest(
419-
{
420-
method: "tools/call" as const,
421-
params: {
422-
name,
423-
arguments: params,
424-
_meta: {
425-
progressToken: progressTokenRef.current++,
425+
try {
426+
const response = await sendMCPRequest(
427+
{
428+
method: "tools/call" as const,
429+
params: {
430+
name,
431+
arguments: params,
432+
_meta: {
433+
progressToken: progressTokenRef.current++,
434+
},
426435
},
427436
},
428-
},
429-
CompatibilityCallToolResultSchema,
430-
"tools",
431-
);
432-
setToolResult(response);
437+
CompatibilityCallToolResultSchema,
438+
"tools",
439+
);
440+
setToolResult(response);
441+
} catch (e) {
442+
const toolResult: CompatibilityCallToolResult = {
443+
content: [
444+
{
445+
type: "text",
446+
text: (e as Error).message ?? String(e),
447+
},
448+
],
449+
isError: true,
450+
};
451+
setToolResult(toolResult);
452+
}
433453
};
434454

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

439459
const sendLogLevelRequest = async (level: LoggingLevel) => {
440-
await makeRequest(
460+
await sendMCPRequest(
441461
{
442462
method: "logging/setLevel" as const,
443463
params: { level },
@@ -637,9 +657,10 @@ const App = () => {
637657
setTools([]);
638658
setNextToolCursor(undefined);
639659
}}
640-
callTool={(name, params) => {
660+
callTool={async (name, params) => {
641661
clearError("tools");
642-
callTool(name, params);
662+
setToolResult(null);
663+
await callTool(name, params);
643664
}}
644665
selectedTool={selectedTool}
645666
setSelectedTool={(tool) => {
@@ -654,7 +675,7 @@ const App = () => {
654675
<ConsoleTab />
655676
<PingTab
656677
onPingClick={() => {
657-
void makeRequest(
678+
void sendMCPRequest(
658679
{
659680
method: "ping" as const,
660681
},

client/src/components/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,8 @@ const Sidebar = ({
325325
return (
326326
<div key={key} className="space-y-2">
327327
<div className="flex items-center gap-1">
328-
<label className="text-sm font-medium text-green-600">
329-
{configKey}
328+
<label className="text-sm font-medium text-green-600 break-all">
329+
{configItem.label}
330330
</label>
331331
<Tooltip>
332332
<TooltipTrigger asChild>

client/src/components/ToolsTab.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
ListToolsResult,
1414
Tool,
1515
} from "@modelcontextprotocol/sdk/types.js";
16-
import { Send } from "lucide-react";
16+
import { Loader2, Send } from "lucide-react";
1717
import { useEffect, useState } from "react";
1818
import ListPane from "./ListPane";
1919
import JsonView from "./JsonView";
@@ -31,14 +31,16 @@ const ToolsTab = ({
3131
tools: Tool[];
3232
listTools: () => void;
3333
clearTools: () => void;
34-
callTool: (name: string, params: Record<string, unknown>) => void;
34+
callTool: (name: string, params: Record<string, unknown>) => Promise<void>;
3535
selectedTool: Tool | null;
3636
setSelectedTool: (tool: Tool | null) => void;
3737
toolResult: CompatibilityCallToolResult | null;
3838
nextCursor: ListToolsResult["nextCursor"];
3939
error: string | null;
4040
}) => {
4141
const [params, setParams] = useState<Record<string, unknown>>({});
42+
const [isToolRunning, setIsToolRunning] = useState(false);
43+
4244
useEffect(() => {
4345
setParams({});
4446
}, [selectedTool]);
@@ -235,9 +237,28 @@ const ToolsTab = ({
235237
);
236238
},
237239
)}
238-
<Button onClick={() => callTool(selectedTool.name, params)}>
239-
<Send className="w-4 h-4 mr-2" />
240-
Run Tool
240+
<Button
241+
onClick={async () => {
242+
try {
243+
setIsToolRunning(true);
244+
await callTool(selectedTool.name, params);
245+
} finally {
246+
setIsToolRunning(false);
247+
}
248+
}}
249+
disabled={isToolRunning}
250+
>
251+
{isToolRunning ? (
252+
<>
253+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
254+
Running...
255+
</>
256+
) : (
257+
<>
258+
<Send className="w-4 h-4 mr-2" />
259+
Run Tool
260+
</>
261+
)}
241262
</Button>
242263
{toolResult && renderToolResult()}
243264
</div>

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,64 @@ describe("Sidebar Environment Variables", () => {
343343
expect(setConfig).toHaveBeenCalledWith(
344344
expect.objectContaining({
345345
MCP_SERVER_REQUEST_TIMEOUT: {
346+
label: "Request Timeout",
346347
description: "Timeout for requests to the MCP server (ms)",
347348
value: 5000,
348349
},
349350
}),
350351
);
351352
});
352353

354+
it("should update MCP server proxy address", () => {
355+
const setConfig = jest.fn();
356+
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
357+
358+
openConfigSection();
359+
360+
const proxyAddressInput = screen.getByTestId(
361+
"MCP_PROXY_FULL_ADDRESS-input",
362+
);
363+
fireEvent.change(proxyAddressInput, {
364+
target: { value: "http://localhost:8080" },
365+
});
366+
367+
expect(setConfig).toHaveBeenCalledWith(
368+
expect.objectContaining({
369+
MCP_PROXY_FULL_ADDRESS: {
370+
label: "Inspector Proxy Address",
371+
description:
372+
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
373+
value: "http://localhost:8080",
374+
},
375+
}),
376+
);
377+
});
378+
379+
it("should update max total timeout", () => {
380+
const setConfig = jest.fn();
381+
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
382+
383+
openConfigSection();
384+
385+
const maxTotalTimeoutInput = screen.getByTestId(
386+
"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
387+
);
388+
fireEvent.change(maxTotalTimeoutInput, {
389+
target: { value: "10000" },
390+
});
391+
392+
expect(setConfig).toHaveBeenCalledWith(
393+
expect.objectContaining({
394+
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
395+
label: "Maximum Total Timeout",
396+
description:
397+
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
398+
value: 10000,
399+
},
400+
}),
401+
);
402+
});
403+
353404
it("should handle invalid timeout values entered by user", () => {
354405
const setConfig = jest.fn();
355406
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
@@ -364,6 +415,7 @@ describe("Sidebar Environment Variables", () => {
364415
expect(setConfig).toHaveBeenCalledWith(
365416
expect.objectContaining({
366417
MCP_SERVER_REQUEST_TIMEOUT: {
418+
label: "Request Timeout",
367419
description: "Timeout for requests to the MCP server (ms)",
368420
value: 0,
369421
},
@@ -409,6 +461,7 @@ describe("Sidebar Environment Variables", () => {
409461
expect(setConfig).toHaveBeenLastCalledWith(
410462
expect.objectContaining({
411463
MCP_SERVER_REQUEST_TIMEOUT: {
464+
label: "Request Timeout",
412465
description: "Timeout for requests to the MCP server (ms)",
413466
value: 3000,
414467
},

0 commit comments

Comments
 (0)