diff --git a/README.md b/README.md index d307b01..0db92bf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/client/src/App.tsx b/client/src/App.tsx index 7564544..4f99ffd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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); @@ -98,10 +95,21 @@ const App = () => { const [config, setConfig] = useState(() => { const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); if (savedConfig) { - return { + // merge default config with saved config + const mergedConfig = { ...DEFAULT_INSPECTOR_CONFIG, ...JSON.parse(savedConfig), } as InspectorConfig; + + // update description of keys to match the new description (in case of any updates to the default config description) + Object.entries(mergedConfig).forEach(([key, value]) => { + mergedConfig[key as keyof InspectorConfig] = { + ...value, + label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, + }; + }); + + return mergedConfig; } return DEFAULT_INSPECTOR_CONFIG; }); @@ -148,7 +156,7 @@ const App = () => { serverCapabilities, mcpClient, requestHistory, - makeRequest: makeConnectionRequest, + makeRequest, sendNotification, handleCompletion, completionsSupported, @@ -161,8 +169,7 @@ const App = () => { sseUrl, env, bearerToken, - proxyServerUrl: getMCPProxyAddress(config), - requestTimeout: getMCPServerRequestTimeout(config), + config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); }, @@ -279,13 +286,13 @@ const App = () => { setErrors((prev) => ({ ...prev, [tabKey]: null })); }; - const makeRequest = async ( + const sendMCPRequest = async ( 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); } @@ -303,7 +310,7 @@ const App = () => { }; const listResources = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/list" as const, params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, @@ -316,7 +323,7 @@ const App = () => { }; const listResourceTemplates = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/templates/list" as const, params: nextResourceTemplateCursor @@ -333,7 +340,7 @@ const App = () => { }; const readResource = async (uri: string) => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/read" as const, params: { uri }, @@ -346,7 +353,7 @@ const App = () => { const subscribeToResource = async (uri: string) => { if (!resourceSubscriptions.has(uri)) { - await makeRequest( + await sendMCPRequest( { method: "resources/subscribe" as const, params: { uri }, @@ -362,7 +369,7 @@ const App = () => { const unsubscribeFromResource = async (uri: string) => { if (resourceSubscriptions.has(uri)) { - await makeRequest( + await sendMCPRequest( { method: "resources/unsubscribe" as const, params: { uri }, @@ -377,7 +384,7 @@ const App = () => { }; const listPrompts = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "prompts/list" as const, params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, @@ -390,7 +397,7 @@ const App = () => { }; const getPrompt = async (name: string, args: Record = {}) => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "prompts/get" as const, params: { name, arguments: args }, @@ -402,7 +409,7 @@ const App = () => { }; const listTools = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "tools/list" as const, params: nextToolCursor ? { cursor: nextToolCursor } : {}, @@ -415,21 +422,34 @@ const App = () => { }; const callTool = async (name: string, params: Record) => { - 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 () => { @@ -437,7 +457,7 @@ const App = () => { }; const sendLogLevelRequest = async (level: LoggingLevel) => { - await makeRequest( + await sendMCPRequest( { method: "logging/setLevel" as const, params: { level }, @@ -637,9 +657,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) => { @@ -654,7 +675,7 @@ const App = () => { { - void makeRequest( + void sendMCPRequest( { method: "ping" as const, }, diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 19f8020..4633d64 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -325,8 +325,8 @@ const Sidebar = ({ return (
-
diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index e527f6d..8e72b00 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -343,6 +343,7 @@ describe("Sidebar Environment Variables", () => { expect(setConfig).toHaveBeenCalledWith( expect.objectContaining({ MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", description: "Timeout for requests to the MCP server (ms)", value: 5000, }, @@ -350,6 +351,56 @@ 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: { + label: "Inspector Proxy 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: { + label: "Maximum 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 }); @@ -364,6 +415,7 @@ describe("Sidebar Environment Variables", () => { expect(setConfig).toHaveBeenCalledWith( expect.objectContaining({ MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", description: "Timeout for requests to the MCP server (ms)", value: 0, }, @@ -409,6 +461,7 @@ describe("Sidebar Environment Variables", () => { expect(setConfig).toHaveBeenLastCalledWith( expect.objectContaining({ MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", description: "Timeout for requests to the MCP server (ms)", value: 3000, }, diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 349977a..c46a32f 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, act } from "@testing-library/react"; import { describe, it, expect, jest } from "@jest/globals"; import "@testing-library/jest-dom"; import ToolsTab from "../ToolsTab"; @@ -43,7 +43,7 @@ describe("ToolsTab", () => { tools: mockTools, listTools: jest.fn(), clearTools: jest.fn(), - callTool: jest.fn(), + callTool: jest.fn(async () => {}), selectedTool: null, setSelectedTool: jest.fn(), toolResult: null, @@ -59,14 +59,16 @@ describe("ToolsTab", () => { ); }; - it("should reset input values when switching tools", () => { + it("should reset input values when switching tools", async () => { const { rerender } = renderToolsTab({ selectedTool: mockTools[0], }); // Enter a value in the first tool's input const input = screen.getByRole("spinbutton") as HTMLInputElement; - fireEvent.change(input, { target: { value: "42" } }); + await act(async () => { + fireEvent.change(input, { target: { value: "42" } }); + }); expect(input.value).toBe("42"); // Switch to second tool @@ -80,7 +82,8 @@ describe("ToolsTab", () => { const newInput = screen.getByRole("spinbutton") as HTMLInputElement; expect(newInput.value).toBe(""); }); - it("should handle integer type inputs", () => { + + it("should handle integer type inputs", async () => { renderToolsTab({ selectedTool: mockTools[1], // Use the tool with integer type }); @@ -93,10 +96,49 @@ describe("ToolsTab", () => { expect(input.value).toBe("42"); const submitButton = screen.getByRole("button", { name: /run tool/i }); - fireEvent.click(submitButton); + await act(async () => { + fireEvent.click(submitButton); + }); expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, { count: 42, }); }); + + it("should disable button and change text while tool is running", async () => { + // Create a promise that we can resolve later + let resolvePromise: ((value: unknown) => void) | undefined; + const mockPromise = new Promise((resolve) => { + resolvePromise = resolve; + }); + + // Mock callTool to return our promise + const mockCallTool = jest.fn().mockReturnValue(mockPromise); + + renderToolsTab({ + selectedTool: mockTools[0], + callTool: mockCallTool, + }); + + const submitButton = screen.getByRole("button", { name: /run tool/i }); + expect(submitButton.getAttribute("disabled")).toBeNull(); + + // Click the button and verify immediate state changes + await act(async () => { + fireEvent.click(submitButton); + }); + + // Verify button is disabled and text changed + expect(submitButton.getAttribute("disabled")).not.toBeNull(); + expect(submitButton.textContent).toBe("Running..."); + + // Resolve the promise to simulate tool completion + await act(async () => { + if (resolvePromise) { + await resolvePromise({}); + } + }); + + expect(submitButton.getAttribute("disabled")).toBeNull(); + }); }); diff --git a/client/src/lib/configurationTypes.ts b/client/src/lib/configurationTypes.ts index df9eb29..7c74bb7 100644 --- a/client/src/lib/configurationTypes.ts +++ b/client/src/lib/configurationTypes.ts @@ -1,4 +1,5 @@ export type ConfigItem = { + label: string; description: string; value: string | number | boolean; }; @@ -15,5 +16,21 @@ export type InspectorConfig = { * Maximum time in milliseconds to wait for a response from the MCP server before timing out. */ MCP_SERVER_REQUEST_TIMEOUT: ConfigItem; + + /** + * Whether to reset the timeout on progress notifications. Useful for long-running operations that send periodic progress updates. + * Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow + */ + MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: ConfigItem; + + /** + * 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. + * Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow + */ + MCP_REQUEST_MAX_TOTAL_TIMEOUT: ConfigItem; + + /** + * 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 + */ MCP_PROXY_FULL_ADDRESS: ConfigItem; }; diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts index c370b34..e7fa14c 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -22,10 +22,23 @@ export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277"; **/ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = { MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", description: "Timeout for requests to the MCP server (ms)", value: 10000, }, + MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: { + label: "Reset Timeout on Progress", + description: "Reset timeout on progress notifications", + value: true, + }, + MCP_REQUEST_MAX_TOTAL_TIMEOUT: { + label: "Maximum Total Timeout", + description: + "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)", + value: 60000, + }, MCP_PROXY_FULL_ADDRESS: { + label: "Inspector Proxy 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: "", diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx new file mode 100644 index 0000000..1671b70 --- /dev/null +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -0,0 +1,164 @@ +import { renderHook, act } from "@testing-library/react"; +import { useConnection } from "../useConnection"; +import { z } from "zod"; +import { ClientRequest } from "@modelcontextprotocol/sdk/types.js"; +import { DEFAULT_INSPECTOR_CONFIG } from "../../constants"; + +// Mock fetch +global.fetch = jest.fn().mockResolvedValue({ + json: () => Promise.resolve({ status: "ok" }), +}); + +// Mock the SDK dependencies +const mockRequest = jest.fn().mockResolvedValue({ test: "response" }); +const mockClient = { + request: mockRequest, + notification: jest.fn(), + connect: jest.fn().mockResolvedValue(undefined), + close: jest.fn(), + getServerCapabilities: jest.fn(), + setNotificationHandler: jest.fn(), + setRequestHandler: jest.fn(), +}; + +jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ + Client: jest.fn().mockImplementation(() => mockClient), +})); + +jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ + SSEClientTransport: jest.fn(), + SseError: jest.fn(), +})); + +jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ + auth: jest.fn().mockResolvedValue("AUTHORIZED"), +})); + +// Mock the toast hook +jest.mock("@/hooks/use-toast", () => ({ + useToast: () => ({ + toast: jest.fn(), + }), +})); + +// Mock the auth provider +jest.mock("../../auth", () => ({ + authProvider: { + tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }), + }, +})); + +describe("useConnection", () => { + const defaultProps = { + transportType: "sse" as const, + command: "", + args: "", + sseUrl: "http://localhost:8080", + env: {}, + config: DEFAULT_INSPECTOR_CONFIG, + }; + + describe("Request Configuration", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("uses the default config values in makeRequest", async () => { + const { result } = renderHook(() => useConnection(defaultProps)); + + // Connect the client + await act(async () => { + await result.current.connect(); + }); + + // Wait for state update + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + const mockRequest: ClientRequest = { + method: "ping", + params: {}, + }; + + const mockSchema = z.object({ + test: z.string(), + }); + + await act(async () => { + await result.current.makeRequest(mockRequest, mockSchema); + }); + + expect(mockClient.request).toHaveBeenCalledWith( + mockRequest, + mockSchema, + expect.objectContaining({ + timeout: DEFAULT_INSPECTOR_CONFIG.MCP_SERVER_REQUEST_TIMEOUT.value, + maxTotalTimeout: + DEFAULT_INSPECTOR_CONFIG.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value, + resetTimeoutOnProgress: + DEFAULT_INSPECTOR_CONFIG.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS + .value, + }), + ); + }); + + test("overrides the default config values when passed in options in makeRequest", async () => { + const { result } = renderHook(() => useConnection(defaultProps)); + + // Connect the client + await act(async () => { + await result.current.connect(); + }); + + // Wait for state update + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + const mockRequest: ClientRequest = { + method: "ping", + params: {}, + }; + + const mockSchema = z.object({ + test: z.string(), + }); + + await act(async () => { + await result.current.makeRequest(mockRequest, mockSchema, { + timeout: 1000, + maxTotalTimeout: 2000, + resetTimeoutOnProgress: false, + }); + }); + + expect(mockClient.request).toHaveBeenCalledWith( + mockRequest, + mockSchema, + expect.objectContaining({ + timeout: 1000, + maxTotalTimeout: 2000, + resetTimeoutOnProgress: false, + }), + ); + }); + }); + + test("throws error when mcpClient is not connected", async () => { + const { result } = renderHook(() => useConnection(defaultProps)); + + const mockRequest: ClientRequest = { + method: "ping", + params: {}, + }; + + const mockSchema = z.object({ + test: z.string(), + }); + + await expect( + result.current.makeRequest(mockRequest, mockSchema), + ).rejects.toThrow("MCP client not connected"); + }); +}); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index bff01ce..ac4605c 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -8,7 +8,6 @@ import { ClientRequest, CreateMessageRequestSchema, ListRootsRequestSchema, - ProgressNotificationSchema, ResourceUpdatedNotificationSchema, LoggingMessageNotificationSchema, Request, @@ -23,7 +22,9 @@ import { ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, + Progress, } from "@modelcontextprotocol/sdk/types.js"; +import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { useState } from "react"; import { useToast } from "@/hooks/use-toast"; import { z } from "zod"; @@ -32,6 +33,13 @@ import { Notification, StdErrNotificationSchema } from "../notificationTypes"; import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; import { authProvider } from "../auth"; import packageJson from "../../../package.json"; +import { + getMCPProxyAddress, + getMCPServerRequestMaxTotalTimeout, + resetRequestTimeoutOnProgress, +} from "@/utils/configUtils"; +import { getMCPServerRequestTimeout } from "@/utils/configUtils"; +import { InspectorConfig } from "../configurationTypes"; interface UseConnectionOptions { transportType: "stdio" | "sse"; @@ -39,9 +47,8 @@ interface UseConnectionOptions { args: string; sseUrl: string; env: Record; - proxyServerUrl: string; bearerToken?: string; - requestTimeout?: number; + config: InspectorConfig; onNotification?: (notification: Notification) => void; onStdErrNotification?: (notification: Notification) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -50,21 +57,14 @@ interface UseConnectionOptions { getRoots?: () => any[]; } -interface RequestOptions { - signal?: AbortSignal; - timeout?: number; - suppressToast?: boolean; -} - export function useConnection({ transportType, command, args, sseUrl, env, - proxyServerUrl, bearerToken, - requestTimeout, + config, onNotification, onStdErrNotification, onPendingRequest, @@ -94,31 +94,50 @@ export function useConnection({ const makeRequest = async ( request: ClientRequest, schema: T, - options?: RequestOptions, + options?: RequestOptions & { suppressToast?: boolean }, ): Promise> => { if (!mcpClient) { throw new Error("MCP client not connected"); } - try { const abortController = new AbortController(); - const timeoutId = setTimeout(() => { - abortController.abort("Request timed out"); - }, options?.timeout ?? requestTimeout); + + // prepare MCP Client request options + const mcpRequestOptions: RequestOptions = { + signal: options?.signal ?? abortController.signal, + resetTimeoutOnProgress: + options?.resetTimeoutOnProgress ?? + resetRequestTimeoutOnProgress(config), + timeout: options?.timeout ?? getMCPServerRequestTimeout(config), + maxTotalTimeout: + options?.maxTotalTimeout ?? + getMCPServerRequestMaxTotalTimeout(config), + }; + + // If progress notifications are enabled, add an onprogress hook to the MCP Client request options + // This is required by SDK to reset the timeout on progress notifications + if (mcpRequestOptions.resetTimeoutOnProgress) { + mcpRequestOptions.onprogress = (params: Progress) => { + // Add progress notification to `Server Notification` window in the UI + if (onNotification) { + onNotification({ + method: "notification/progress", + params, + }); + } + }; + } let response; try { - response = await mcpClient.request(request, schema, { - signal: options?.signal ?? abortController.signal, - }); + response = await mcpClient.request(request, schema, mcpRequestOptions); + pushHistory(request, response); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); pushHistory(request, { error: errorMessage }); throw error; - } finally { - clearTimeout(timeoutId); } return response; @@ -211,7 +230,7 @@ export function useConnection({ const checkProxyHealth = async () => { try { - const proxyHealthUrl = new URL(`${proxyServerUrl}/health`); + const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`); const proxyHealthResponse = await fetch(proxyHealthUrl); const proxyHealth = await proxyHealthResponse.json(); if (proxyHealth?.status !== "ok") { @@ -256,7 +275,7 @@ export function useConnection({ setConnectionStatus("error-connecting-to-proxy"); return; } - const mcpProxyServerUrl = new URL(`${proxyServerUrl}/sse`); + const mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`); mcpProxyServerUrl.searchParams.append("transportType", transportType); if (transportType === "stdio") { mcpProxyServerUrl.searchParams.append("command", command); @@ -289,7 +308,6 @@ export function useConnection({ if (onNotification) { [ CancelledNotificationSchema, - ProgressNotificationSchema, LoggingMessageNotificationSchema, ResourceUpdatedNotificationSchema, ResourceListChangedNotificationSchema, diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index dcd4c97..86e1643 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -12,3 +12,15 @@ export const getMCPProxyAddress = (config: InspectorConfig): string => { export const getMCPServerRequestTimeout = (config: InspectorConfig): number => { return config.MCP_SERVER_REQUEST_TIMEOUT.value as number; }; + +export const resetRequestTimeoutOnProgress = ( + config: InspectorConfig, +): boolean => { + return config.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS.value as boolean; +}; + +export const getMCPServerRequestMaxTotalTimeout = ( + config: InspectorConfig, +): number => { + return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number; +};