Skip to content

Commit a8ffc70

Browse files
add support for progress flow
1 parent a75dd7b commit a8ffc70

File tree

6 files changed

+238
-39
lines changed

6 files changed

+238
-39
lines changed

client/src/App.tsx

Lines changed: 16 additions & 20 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);
@@ -148,7 +145,7 @@ const App = () => {
148145
serverCapabilities,
149146
mcpClient,
150147
requestHistory,
151-
makeRequest: makeConnectionRequest,
148+
makeRequest,
152149
sendNotification,
153150
handleCompletion,
154151
completionsSupported,
@@ -161,8 +158,7 @@ const App = () => {
161158
sseUrl,
162159
env,
163160
bearerToken,
164-
proxyServerUrl: getMCPProxyAddress(config),
165-
requestTimeout: getMCPServerRequestTimeout(config),
161+
config,
166162
onNotification: (notification) => {
167163
setNotifications((prev) => [...prev, notification as ServerNotification]);
168164
},
@@ -279,13 +275,13 @@ const App = () => {
279275
setErrors((prev) => ({ ...prev, [tabKey]: null }));
280276
};
281277

282-
const makeRequest = async <T extends z.ZodType>(
278+
const makeConnectionRequest = async <T extends z.ZodType>(
283279
request: ClientRequest,
284280
schema: T,
285281
tabKey?: keyof typeof errors,
286282
) => {
287283
try {
288-
const response = await makeConnectionRequest(request, schema);
284+
const response = await makeRequest(request, schema);
289285
if (tabKey !== undefined) {
290286
clearError(tabKey);
291287
}
@@ -303,7 +299,7 @@ const App = () => {
303299
};
304300

305301
const listResources = async () => {
306-
const response = await makeRequest(
302+
const response = await makeConnectionRequest(
307303
{
308304
method: "resources/list" as const,
309305
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
@@ -316,7 +312,7 @@ const App = () => {
316312
};
317313

318314
const listResourceTemplates = async () => {
319-
const response = await makeRequest(
315+
const response = await makeConnectionRequest(
320316
{
321317
method: "resources/templates/list" as const,
322318
params: nextResourceTemplateCursor
@@ -333,7 +329,7 @@ const App = () => {
333329
};
334330

335331
const readResource = async (uri: string) => {
336-
const response = await makeRequest(
332+
const response = await makeConnectionRequest(
337333
{
338334
method: "resources/read" as const,
339335
params: { uri },
@@ -346,7 +342,7 @@ const App = () => {
346342

347343
const subscribeToResource = async (uri: string) => {
348344
if (!resourceSubscriptions.has(uri)) {
349-
await makeRequest(
345+
await makeConnectionRequest(
350346
{
351347
method: "resources/subscribe" as const,
352348
params: { uri },
@@ -362,7 +358,7 @@ const App = () => {
362358

363359
const unsubscribeFromResource = async (uri: string) => {
364360
if (resourceSubscriptions.has(uri)) {
365-
await makeRequest(
361+
await makeConnectionRequest(
366362
{
367363
method: "resources/unsubscribe" as const,
368364
params: { uri },
@@ -377,7 +373,7 @@ const App = () => {
377373
};
378374

379375
const listPrompts = async () => {
380-
const response = await makeRequest(
376+
const response = await makeConnectionRequest(
381377
{
382378
method: "prompts/list" as const,
383379
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
@@ -390,7 +386,7 @@ const App = () => {
390386
};
391387

392388
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
393-
const response = await makeRequest(
389+
const response = await makeConnectionRequest(
394390
{
395391
method: "prompts/get" as const,
396392
params: { name, arguments: args },
@@ -402,7 +398,7 @@ const App = () => {
402398
};
403399

404400
const listTools = async () => {
405-
const response = await makeRequest(
401+
const response = await makeConnectionRequest(
406402
{
407403
method: "tools/list" as const,
408404
params: nextToolCursor ? { cursor: nextToolCursor } : {},
@@ -415,7 +411,7 @@ const App = () => {
415411
};
416412

417413
const callTool = async (name: string, params: Record<string, unknown>) => {
418-
const response = await makeRequest(
414+
const response = await makeConnectionRequest(
419415
{
420416
method: "tools/call" as const,
421417
params: {
@@ -437,7 +433,7 @@ const App = () => {
437433
};
438434

439435
const sendLogLevelRequest = async (level: LoggingLevel) => {
440-
await makeRequest(
436+
await makeConnectionRequest(
441437
{
442438
method: "logging/setLevel" as const,
443439
params: { level },
@@ -654,7 +650,7 @@ const App = () => {
654650
<ConsoleTab />
655651
<PingTab
656652
onPingClick={() => {
657-
void makeRequest(
653+
void makeConnectionRequest(
658654
{
659655
method: "ping" as const,
660656
},

client/src/lib/configurationTypes.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,21 @@ export type InspectorConfig = {
1515
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
1616
*/
1717
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
18+
19+
/**
20+
* Whether to reset the timeout on progress notifications. Useful for long-running operations that send periodic progress updates.
21+
* Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow
22+
*/
23+
MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS: ConfigItem;
24+
25+
/**
26+
* Maximum total time in milliseconds to wait for a response from the MCP server before timing out. Used in conjunction with MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS.
27+
* Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow
28+
*/
29+
MCP_SERVER_REQUEST_TIMEOUT_MAX_TOTAL_TIMEOUT: ConfigItem;
30+
31+
/**
32+
* The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577
33+
*/
1834
MCP_PROXY_FULL_ADDRESS: ConfigItem;
1935
};

client/src/lib/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
2525
description: "Timeout for requests to the MCP server (ms)",
2626
value: 10000,
2727
},
28+
MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
29+
description: "Reset timeout on progress notifications",
30+
value: true,
31+
},
32+
MCP_SERVER_REQUEST_TIMEOUT_MAX_TOTAL_TIMEOUT: {
33+
description: "Maximum total timeout for requests sent to the MCP server (ms)",
34+
value: 60000,
35+
},
2836
MCP_PROXY_FULL_ADDRESS: {
2937
description:
3038
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { renderHook, act } from "@testing-library/react";
2+
import { useConnection } from "../useConnection";
3+
import { z } from "zod";
4+
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
5+
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
6+
7+
// Mock fetch
8+
global.fetch = jest.fn().mockResolvedValue({
9+
json: () => Promise.resolve({ status: "ok" }),
10+
});
11+
12+
// Mock the SDK dependencies
13+
const mockRequest = jest.fn().mockResolvedValue({ test: "response" });
14+
const mockClient = {
15+
request: mockRequest,
16+
notification: jest.fn(),
17+
connect: jest.fn().mockResolvedValue(undefined),
18+
close: jest.fn(),
19+
getServerCapabilities: jest.fn(),
20+
setNotificationHandler: jest.fn(),
21+
setRequestHandler: jest.fn(),
22+
};
23+
24+
jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
25+
Client: jest.fn().mockImplementation(() => mockClient),
26+
}));
27+
28+
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
29+
SSEClientTransport: jest.fn(),
30+
SseError: jest.fn(),
31+
}));
32+
33+
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
34+
auth: jest.fn().mockResolvedValue("AUTHORIZED"),
35+
}));
36+
37+
// Mock the toast hook
38+
jest.mock("@/hooks/use-toast", () => ({
39+
useToast: () => ({
40+
toast: jest.fn(),
41+
}),
42+
}));
43+
44+
// Mock the auth provider
45+
jest.mock("../../auth", () => ({
46+
authProvider: {
47+
tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }),
48+
},
49+
}));
50+
51+
describe("useConnection", () => {
52+
const defaultProps = {
53+
transportType: "sse" as const,
54+
command: "",
55+
args: "",
56+
sseUrl: "http://localhost:8080",
57+
env: {},
58+
config: DEFAULT_INSPECTOR_CONFIG,
59+
};
60+
61+
describe("Request Configuration", () => {
62+
beforeEach(() => {
63+
jest.clearAllMocks();
64+
});
65+
66+
test("uses the default config values in makeRequest", async () => {
67+
const { result } = renderHook(() => useConnection(defaultProps));
68+
69+
// Connect the client
70+
await act(async () => {
71+
await result.current.connect();
72+
});
73+
74+
// Wait for state update
75+
await act(async () => {
76+
await new Promise((resolve) => setTimeout(resolve, 0));
77+
});
78+
79+
const mockRequest: ClientRequest = {
80+
method: "ping",
81+
params: {},
82+
};
83+
84+
const mockSchema = z.object({
85+
test: z.string(),
86+
});
87+
88+
await act(async () => {
89+
await result.current.makeRequest(mockRequest, mockSchema);
90+
});
91+
92+
expect(mockClient.request).toHaveBeenCalledWith(
93+
mockRequest,
94+
mockSchema,
95+
expect.objectContaining({
96+
timeout: DEFAULT_INSPECTOR_CONFIG.MCP_SERVER_REQUEST_TIMEOUT.value,
97+
maxTotalTimeout:
98+
DEFAULT_INSPECTOR_CONFIG
99+
.MCP_SERVER_REQUEST_TIMEOUT_MAX_TOTAL_TIMEOUT.value,
100+
resetTimeoutOnProgress:
101+
DEFAULT_INSPECTOR_CONFIG
102+
.MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS.value,
103+
}),
104+
);
105+
});
106+
107+
test("overrides the default config values when passed in options in makeRequest", async () => {
108+
const { result } = renderHook(() => useConnection(defaultProps));
109+
110+
// Connect the client
111+
await act(async () => {
112+
await result.current.connect();
113+
});
114+
115+
// Wait for state update
116+
await act(async () => {
117+
await new Promise((resolve) => setTimeout(resolve, 0));
118+
});
119+
120+
const mockRequest: ClientRequest = {
121+
method: "ping",
122+
params: {},
123+
};
124+
125+
const mockSchema = z.object({
126+
test: z.string(),
127+
});
128+
129+
await act(async () => {
130+
await result.current.makeRequest(mockRequest, mockSchema, {
131+
timeout: 1000,
132+
maxTotalTimeout: 2000,
133+
resetTimeoutOnProgress: false,
134+
});
135+
});
136+
137+
expect(mockClient.request).toHaveBeenCalledWith(
138+
mockRequest,
139+
mockSchema,
140+
expect.objectContaining({
141+
timeout: 1000,
142+
maxTotalTimeout: 2000,
143+
resetTimeoutOnProgress: false,
144+
}),
145+
);
146+
});
147+
});
148+
149+
test("throws error when mcpClient is not connected", async () => {
150+
const { result } = renderHook(() => useConnection(defaultProps));
151+
152+
const mockRequest: ClientRequest = {
153+
method: "ping",
154+
params: {},
155+
};
156+
157+
const mockSchema = z.object({
158+
test: z.string(),
159+
});
160+
161+
await expect(
162+
result.current.makeRequest(mockRequest, mockSchema),
163+
).rejects.toThrow("MCP client not connected");
164+
});
165+
});

0 commit comments

Comments
 (0)