From 353d6b549bfbb0c64f995350a95674b288c42075 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 27 Mar 2025 09:52:35 +0100 Subject: [PATCH 01/61] fix: clean up previous transport processes --- client/src/components/Sidebar.tsx | 14 ++++++++++++-- server/src/index.ts | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 4f60a77..6c9f137 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -8,6 +8,7 @@ import { Github, Eye, EyeOff, + RotateCcw, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -286,8 +287,17 @@ const Sidebar = ({
diff --git a/server/src/index.ts b/server/src/index.ts index 2a4fe65..d4bfc6a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -12,6 +12,7 @@ import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; import { findActualExecutable } from "spawn-rx"; @@ -98,12 +99,14 @@ const createTransport = async (req: express.Request) => { } }; +let backingServerTransport: Transport | undefined; + app.get("/sse", async (req, res) => { try { console.log("New SSE connection"); - let backingServerTransport; try { + await backingServerTransport?.close(); backingServerTransport = await createTransport(req); } catch (error) { if (error instanceof SseError && error.code === 401) { From da0c855ef5eb88996adb09d25b244a1824c12f9c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 28 Mar 2025 09:45:17 +0100 Subject: [PATCH 02/61] adapt title --- client/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 6c9f137..6e59084 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -290,7 +290,7 @@ const Sidebar = ({ {connectionStatus === "connected" ? ( <> - Restart + {transportType === 'stdio' ? 'Restart' : 'Reconnect'} ) : ( <> From 1b754f52ca2df7b7459fc35aeab08aceb0b814a9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 28 Mar 2025 11:59:21 -0400 Subject: [PATCH 03/61] This fixes #214 where when the inspector is started, it can report that the inspector is up, when in fact it isn't because the address is already in use. It catches this condition, as well as the condition where the proxy server port is in use, reports it, and exits. * In bin/cli.js - in the delay function, have the setTimeout return a true value. - try the server's spawnPromise call and within the try block, use Promise.race to get the return value of the first promise to resolve. If the server is up, it will not resolve and the 2 second delay will resolve with true, telling us the server is ok. Any error will have been reported by the server startup process, so we will not pile on with more output in the catch block. - If the server started ok, then we will await the spawnPromise call for starting the client. If the client fails to start it will report and exit, otherwise we are done and both servers are running and have reported as much. If an error is caught and it isn't SIGINT or if process.env.DEBUG is true then the error will be thrown before exiting. * In client/bin/cli.js - add a "listening" handler to the server, logging that the MCP inspector is up at the given port - Add an "error" handler to the server that reports that the client port is in use if the error message includes "EADDRINUSE", otherwise throw the error so the entire contents can be seen. * In server/src/index.ts - add a "listening" handler to the server, logging that the Proxy server is up at the given port - Add an "error" handler to the server that reports that the server port is in use if the error message includes "EADDRINUSE", otherwise throw the error so the entire contents can be seen. * In package.json - in preview script - add --port 5173 to start the client on the proper port. This was useful in getting an instance of the client running before trying the start script. otherwise it starts on 4173 --- bin/cli.js | 75 +++++++++++++++++++++++---------------------- client/bin/cli.js | 16 +++++++++- client/package.json | 2 +- server/src/index.ts | 25 ++++++++------- 4 files changed, 69 insertions(+), 49 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 1b744ce..d559688 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -7,7 +7,7 @@ import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms, true)); } async function main() { @@ -73,42 +73,45 @@ async function main() { cancelled = true; abort.abort(); }); - - const server = spawnPromise( - "node", - [ - inspectorServerPath, - ...(command ? [`--env`, command] : []), - ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), - ], - { - env: { - ...process.env, - PORT: SERVER_PORT, - MCP_ENV_VARS: JSON.stringify(envVars), - }, - signal: abort.signal, - echoOutput: true, - }, - ); - - const client = spawnPromise("node", [inspectorClientPath], { - env: { ...process.env, PORT: CLIENT_PORT }, - signal: abort.signal, - echoOutput: true, - }); - - // Make sure our server/client didn't immediately fail - await Promise.any([server, client, delay(2 * 1000)]); - const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`; - console.log( - `\nšŸ” MCP Inspector is up and running at http://127.0.0.1:${CLIENT_PORT}${portParam} šŸš€`, - ); - + let server, serverOk; try { - await Promise.any([server, client]); - } catch (e) { - if (!cancelled || process.env.DEBUG) throw e; + + server = spawnPromise( + "node", + [ + inspectorServerPath, + ...(command ? [`--env`, command] : []), + ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), + ], + { + env: { + ...process.env, + PORT: SERVER_PORT, + MCP_ENV_VARS: JSON.stringify(envVars), + }, + signal: abort.signal, + echoOutput: true, + }, + ) + + // Make sure server started before starting client + serverOk = await Promise.race([server, delay(2 * 1000)]); + + } catch(error) {} + + if (serverOk) { + try { + + await spawnPromise("node", [inspectorClientPath], { + env: { ...process.env, PORT: CLIENT_PORT }, + signal: abort.signal, + echoOutput: true, + }); + + } catch (e) { + if (!cancelled || process.env.DEBUG) throw e; + } + } return 0; diff --git a/client/bin/cli.js b/client/bin/cli.js index 7dc93ea..9dbe796 100755 --- a/client/bin/cli.js +++ b/client/bin/cli.js @@ -16,4 +16,18 @@ const server = http.createServer((request, response) => { }); const port = process.env.PORT || 5173; -server.listen(port, () => {}); +server.on("listening", () => { + console.log( + `šŸ” MCP Inspector is up and running at http://127.0.0.1:${port} šŸš€`, + ); +}) +server.on("error", (err) => { + if (err.message.includes(`EADDRINUSE`)) { + console.error( + `āŒ MCP Inspector PORT IS IN USE at http://127.0.0.1:${port} āŒ `, + ); + } else { + throw err; + } +}) +server.listen(port); diff --git a/client/package.json b/client/package.json index 9eff88c..c268c33 100644 --- a/client/package.json +++ b/client/package.json @@ -18,7 +18,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview", + "preview": "vite preview --port 5173", "test": "jest --config jest.config.cjs", "test:watch": "jest --config jest.config.cjs --watch" }, diff --git a/server/src/index.ts b/server/src/index.ts index 2a4fe65..7e45de4 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -184,15 +184,18 @@ app.get("/config", (req, res) => { const PORT = process.env.PORT || 3000; -try { - const server = app.listen(PORT); - - server.on("listening", () => { - const addr = server.address(); - const port = typeof addr === "string" ? addr : addr?.port; - console.log(`Proxy server listening on port ${port}`); - }); -} catch (error) { - console.error("Failed to start server:", error); +const server = app.listen(PORT); +server.on("listening", () => { + console.log(`āš™ļø Proxy server listening on port ${PORT}`); +}); +server.on("error", (err) => { + if (err.message.includes(`EADDRINUSE`)) { + console.error( + `āŒ Proxy Server PORT IS IN USE at port ${PORT} āŒ `, + ); + } else { + console.error(err.message); + } process.exit(1); -} +}) + From 054741be03bbade7ef0bd09012bd2e8c3f3b2628 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 29 Mar 2025 12:36:03 -0400 Subject: [PATCH 04/61] Run prettier. --- bin/cli.js | 9 ++------- client/bin/cli.js | 4 ++-- server/src/index.ts | 7 ++----- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index d559688..8c5e526 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -75,7 +75,6 @@ async function main() { }); let server, serverOk; try { - server = spawnPromise( "node", [ @@ -92,26 +91,22 @@ async function main() { signal: abort.signal, echoOutput: true, }, - ) + ); // Make sure server started before starting client serverOk = await Promise.race([server, delay(2 * 1000)]); - - } catch(error) {} + } catch (error) {} if (serverOk) { try { - await spawnPromise("node", [inspectorClientPath], { env: { ...process.env, PORT: CLIENT_PORT }, signal: abort.signal, echoOutput: true, }); - } catch (e) { if (!cancelled || process.env.DEBUG) throw e; } - } return 0; diff --git a/client/bin/cli.js b/client/bin/cli.js index 9dbe796..95b6429 100755 --- a/client/bin/cli.js +++ b/client/bin/cli.js @@ -20,7 +20,7 @@ server.on("listening", () => { console.log( `šŸ” MCP Inspector is up and running at http://127.0.0.1:${port} šŸš€`, ); -}) +}); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { console.error( @@ -29,5 +29,5 @@ server.on("error", (err) => { } else { throw err; } -}) +}); server.listen(port); diff --git a/server/src/index.ts b/server/src/index.ts index 7e45de4..6c66de0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -190,12 +190,9 @@ server.on("listening", () => { }); server.on("error", (err) => { if (err.message.includes(`EADDRINUSE`)) { - console.error( - `āŒ Proxy Server PORT IS IN USE at port ${PORT} āŒ `, - ); + console.error(`āŒ Proxy Server PORT IS IN USE at port ${PORT} āŒ `); } else { console.error(err.message); } process.exit(1); -}) - +}); From da9dd097655086180588cbfa0e79cba2dcf1b866 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Mon, 31 Mar 2025 00:31:28 +0000 Subject: [PATCH 05/61] refactor: Update default ports for MCPI client and MPCP server Changes the default ports used by the MCP Inspector client UI and the MCP Proxy server to avoid conflicts with common development ports and provide a memorable mnemonic based on T9 mapping. --- CONTRIBUTING.md | 2 +- README.md | 2 +- bin/cli.js | 6 +++--- client/bin/cli.js | 2 +- client/src/App.tsx | 2 +- server/src/index.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed28e9d..72502f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Thanks for your interest in contributing! This guide explains how to get involve 1. Fork the repository and clone it locally 2. Install dependencies with `npm install` 3. Run `npm run dev` to start both client and server in development mode -4. Use the web UI at http://127.0.0.1:5173 to interact with the inspector +4. Use the web UI at http://127.0.0.1:6274 to interact with the inspector ## Development Process & Pull Requests diff --git a/README.md b/README.md index 5fe3693..0192038 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/inde npx @modelcontextprotocol/inspector -e KEY=$VALUE -- node build/index.js -e server-flag ``` -The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed: +The inspector runs both an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MPCP) server (default port 6727). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MPCP respectively, as a mnemonic). You can customize the ports if needed: ```bash CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js diff --git a/bin/cli.js b/bin/cli.js index 1b744ce..b495f7b 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -61,8 +61,8 @@ async function main() { "cli.js", ); - const CLIENT_PORT = process.env.CLIENT_PORT ?? "5173"; - const SERVER_PORT = process.env.SERVER_PORT ?? "3000"; + const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274"; + const SERVER_PORT = process.env.SERVER_PORT ?? "6727"; console.log("Starting MCP inspector..."); @@ -100,7 +100,7 @@ async function main() { // Make sure our server/client didn't immediately fail await Promise.any([server, client, delay(2 * 1000)]); - const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`; + const portParam = SERVER_PORT === "6727" ? "" : `?proxyPort=${SERVER_PORT}`; console.log( `\nšŸ” MCP Inspector is up and running at http://127.0.0.1:${CLIENT_PORT}${portParam} šŸš€`, ); diff --git a/client/bin/cli.js b/client/bin/cli.js index 7dc93ea..81b7bd1 100755 --- a/client/bin/cli.js +++ b/client/bin/cli.js @@ -15,5 +15,5 @@ const server = http.createServer((request, response) => { }); }); -const port = process.env.PORT || 5173; +const port = process.env.PORT || 6274; server.listen(port, () => {}); diff --git a/client/src/App.tsx b/client/src/App.tsx index c29ef71..f65bdc2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -49,7 +49,7 @@ import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; const params = new URLSearchParams(window.location.search); -const PROXY_PORT = params.get("proxyPort") ?? "3000"; +const PROXY_PORT = params.get("proxyPort") ?? "6727"; const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; diff --git a/server/src/index.ts b/server/src/index.ts index 2a4fe65..7a527fc 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -182,7 +182,7 @@ app.get("/config", (req, res) => { } }); -const PORT = process.env.PORT || 3000; +const PORT = process.env.PORT || 6727; try { const server = app.listen(PORT); From 7b055b6b9a80e8dc2eda4b1b0a73f48353eb97b2 Mon Sep 17 00:00:00 2001 From: katopz Date: Mon, 31 Mar 2025 18:29:09 +0900 Subject: [PATCH 06/61] fix: prop.type not accept integer --- client/src/components/ToolsTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index eb91c98..26f5b3b 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -226,7 +226,7 @@ const ToolsTab = ({
) : ( Date: Mon, 31 Mar 2025 07:32:55 -0700 Subject: [PATCH 07/61] Add failing test for integer input --- .../components/__tests__/ToolsTab.test.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 2a45065..f75c8f9 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -16,6 +16,16 @@ describe("ToolsTab", () => { }, }, }, + { + name: "tool3", + description: "Integer tool", + inputSchema: { + type: "object" as const, + properties: { + count: { type: "integer" as const }, + }, + }, + }, { name: "tool2", description: "Second tool", @@ -69,4 +79,28 @@ describe("ToolsTab", () => { const newInput = screen.getByRole("spinbutton") as HTMLInputElement; expect(newInput.value).toBe(""); }); + + it("should handle integer type inputs", () => { + renderToolsTab({ + selectedTool: mockTools[2], + }); + + // Verify input is rendered as a number input + const input = screen.getByRole("spinbutton") as HTMLInputElement; + expect(input.type).toBe("text"); // This will fail - should be "number" + + // Enter an integer value + fireEvent.change(input, { target: { value: "42" } }); + expect(input.value).toBe("42"); + + // Verify the callTool function receives the value as a number + const submitButton = screen.getByRole("button", { name: /submit/i }); + fireEvent.click(submitButton); + + expect(defaultProps.callTool).toHaveBeenCalledWith( + mockTools[2].name, + { count: 42 }, // Should be number 42, not string "42" + expect.any(Function) + ); + }); }); From 7ac1e40c9dcd1866445baede30a8fe459d9104b9 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 31 Mar 2025 07:51:28 -0700 Subject: [PATCH 08/61] Update tests --- client/src/components/__tests__/ToolsTab.test.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index f75c8f9..b36085e 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -71,7 +71,7 @@ describe("ToolsTab", () => { // Switch to second tool rerender( - + , ); @@ -82,25 +82,24 @@ describe("ToolsTab", () => { it("should handle integer type inputs", () => { renderToolsTab({ - selectedTool: mockTools[2], + selectedTool: mockTools[1], // Use the tool with integer type }); // Verify input is rendered as a number input const input = screen.getByRole("spinbutton") as HTMLInputElement; - expect(input.type).toBe("text"); // This will fail - should be "number" + expect(input.type).toBe("number"); // Integer type should be treated as number // Enter an integer value fireEvent.change(input, { target: { value: "42" } }); expect(input.value).toBe("42"); // Verify the callTool function receives the value as a number - const submitButton = screen.getByRole("button", { name: /submit/i }); + const submitButton = screen.getByRole("button", { name: /run tool/i }); fireEvent.click(submitButton); expect(defaultProps.callTool).toHaveBeenCalledWith( - mockTools[2].name, - { count: 42 }, // Should be number 42, not string "42" - expect.any(Function) + mockTools[1].name, + { count: 42 } ); }); }); From 7753b275e57987b62b4fe1b791b49e8848f2986b Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 31 Mar 2025 08:04:53 -0700 Subject: [PATCH 09/61] Update test to fail more explicitly --- client/src/components/__tests__/ToolsTab.test.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index b36085e..624a72e 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -85,15 +85,11 @@ describe("ToolsTab", () => { selectedTool: mockTools[1], // Use the tool with integer type }); - // Verify input is rendered as a number input - const input = screen.getByRole("spinbutton") as HTMLInputElement; - expect(input.type).toBe("number"); // Integer type should be treated as number - - // Enter an integer value + const input = screen.getByRole("textbox", { name: /count/i }) as HTMLInputElement; + expect(input).toHaveProperty("type", "number"); fireEvent.change(input, { target: { value: "42" } }); expect(input.value).toBe("42"); - // Verify the callTool function receives the value as a number const submitButton = screen.getByRole("button", { name: /run tool/i }); fireEvent.click(submitButton); From 538fc97289829804bdb9e0d0889de1e4f1f49b0c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Mon, 31 Mar 2025 18:10:16 +0200 Subject: [PATCH 10/61] fix prettier --- client/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 90471d5..33e88a7 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -379,7 +379,7 @@ const Sidebar = ({ {connectionStatus === "connected" ? ( <> - {transportType === 'stdio' ? 'Restart' : 'Reconnect'} + {transportType === "stdio" ? "Restart" : "Reconnect"} ) : ( <> From 180760c4db647b5c4ad8dc53af5f95d297b5c1f9 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 31 Mar 2025 09:16:25 -0700 Subject: [PATCH 11/61] Fix formatting --- client/src/components/__tests__/ToolsTab.test.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 624a72e..27e273c 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -85,17 +85,18 @@ describe("ToolsTab", () => { selectedTool: mockTools[1], // Use the tool with integer type }); - const input = screen.getByRole("textbox", { name: /count/i }) as HTMLInputElement; + const input = screen.getByRole("textbox", { + name: /count/i, + }) as HTMLInputElement; expect(input).toHaveProperty("type", "number"); fireEvent.change(input, { target: { value: "42" } }); expect(input.value).toBe("42"); const submitButton = screen.getByRole("button", { name: /run tool/i }); fireEvent.click(submitButton); - - expect(defaultProps.callTool).toHaveBeenCalledWith( - mockTools[1].name, - { count: 42 } - ); + + expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, { + count: 42, + }); }); }); From 83ceefca790fe27cc1f173ab0e30a0f4364d5a23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:30:35 +0000 Subject: [PATCH 12/61] Bump vite in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 5.4.12 to 5.4.15 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.15/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.15/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 448edd6..c891d5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10777,9 +10777,9 @@ } }, "node_modules/vite": { - "version": "5.4.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz", - "integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==", + "version": "5.4.15", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", + "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", "dev": true, "license": "MIT", "dependencies": { From 80f5ab11369cdabfc4533f016b2625e6cb9420a3 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Mon, 31 Mar 2025 18:28:40 +0000 Subject: [PATCH 13/61] fix: typo --- README.md | 2 +- bin/cli.js | 4 ++-- client/src/App.tsx | 2 +- server/src/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0192038..b0e18de 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/inde npx @modelcontextprotocol/inspector -e KEY=$VALUE -- node build/index.js -e server-flag ``` -The inspector runs both an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MPCP) server (default port 6727). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MPCP respectively, as a mnemonic). You can customize the ports if needed: +The inspector runs both an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MCPP) server (default port 6277). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MCPP respectively, as a mnemonic). You can customize the ports if needed: ```bash CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js diff --git a/bin/cli.js b/bin/cli.js index b495f7b..460ce58 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -62,7 +62,7 @@ async function main() { ); const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274"; - const SERVER_PORT = process.env.SERVER_PORT ?? "6727"; + const SERVER_PORT = process.env.SERVER_PORT ?? "6277"; console.log("Starting MCP inspector..."); @@ -100,7 +100,7 @@ async function main() { // Make sure our server/client didn't immediately fail await Promise.any([server, client, delay(2 * 1000)]); - const portParam = SERVER_PORT === "6727" ? "" : `?proxyPort=${SERVER_PORT}`; + const portParam = SERVER_PORT === "6277" ? "" : `?proxyPort=${SERVER_PORT}`; console.log( `\nšŸ” MCP Inspector is up and running at http://127.0.0.1:${CLIENT_PORT}${portParam} šŸš€`, ); diff --git a/client/src/App.tsx b/client/src/App.tsx index f65bdc2..23c508f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -49,7 +49,7 @@ import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; const params = new URLSearchParams(window.location.search); -const PROXY_PORT = params.get("proxyPort") ?? "6727"; +const PROXY_PORT = params.get("proxyPort") ?? "6277"; const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; diff --git a/server/src/index.ts b/server/src/index.ts index 7a527fc..fcb8295 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -182,7 +182,7 @@ app.get("/config", (req, res) => { } }); -const PORT = process.env.PORT || 6727; +const PORT = process.env.PORT || 6277; try { const server = app.listen(PORT); From b99cf276ae91db8ee135a443c04b673e608a3b65 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Mon, 31 Mar 2025 18:34:12 +0000 Subject: [PATCH 14/61] fix: linting --- bin/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/cli.js b/bin/cli.js index ca1958a..35edf0e 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -117,4 +117,4 @@ main() .catch((e) => { console.error(e); process.exit(1); - }); \ No newline at end of file + }); From d1746f53a4d5bc5d9f9bba0ae248e33512dd4660 Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Mon, 31 Mar 2025 20:11:40 +0000 Subject: [PATCH 15/61] Update package.json --- client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index c268c33..eb0091f 100644 --- a/client/package.json +++ b/client/package.json @@ -18,7 +18,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview --port 5173", + "preview": "vite preview --port 6274", "test": "jest --config jest.config.cjs", "test:watch": "jest --config jest.config.cjs --watch" }, From 7dc2c6fb58ccd2cd12bc1e7f8cc96b164032642f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 20:38:18 +0000 Subject: [PATCH 16/61] Bump vite in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 5.4.15 to 5.4.16 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.16/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.16/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c891d5c..feb893f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10777,9 +10777,9 @@ } }, "node_modules/vite": { - "version": "5.4.15", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", - "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", + "version": "5.4.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz", + "integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==", "dev": true, "license": "MIT", "dependencies": { From 2ee0a53e360ec25c487dd2d952c7d98ed44429a6 Mon Sep 17 00:00:00 2001 From: katopz Date: Tue, 1 Apr 2025 09:27:24 +0900 Subject: [PATCH 17/61] fix: prettier --- client/src/components/ToolsTab.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 26f5b3b..d9074c3 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -226,7 +226,11 @@ const ToolsTab = ({
) : ( Date: Mon, 31 Mar 2025 18:54:44 -0700 Subject: [PATCH 18/61] feat: Add lightweight Disconnect button --- client/src/App.tsx | 2 ++ client/src/components/Sidebar.tsx | 30 ++++++++++++------- .../src/components/__tests__/Sidebar.test.tsx | 1 + client/src/lib/hooks/useConnection.ts | 9 ++++++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 23c508f..e560d55 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -145,6 +145,7 @@ const App = () => { handleCompletion, completionsSupported, connect: connectMcpServer, + disconnect: disconnectMcpServer, } = useConnection({ transportType, command, @@ -458,6 +459,7 @@ const App = () => { bearerToken={bearerToken} setBearerToken={setBearerToken} onConnect={connectMcpServer} + onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} logLevel={logLevel} sendLogLevelRequest={sendLogLevelRequest} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 33e88a7..568518d 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -10,6 +10,7 @@ import { EyeOff, RotateCcw, Settings, + RefreshCwOff, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -45,6 +46,7 @@ interface SidebarProps { bearerToken: string; setBearerToken: (token: string) => void; onConnect: () => void; + onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; logLevel: LoggingLevel; sendLogLevelRequest: (level: LoggingLevel) => void; @@ -68,6 +70,7 @@ const Sidebar = ({ bearerToken, setBearerToken, onConnect, + onDisconnect, stdErrNotifications, logLevel, sendLogLevelRequest, @@ -375,19 +378,24 @@ const Sidebar = ({
- + + +
+ )} + {connectionStatus !== "connected" && ( + + )}
{ bearerToken: "", setBearerToken: jest.fn(), onConnect: jest.fn(), + onDisconnect: jest.fn(), stdErrNotifications: [], logLevel: "info" as const, sendLogLevelRequest: jest.fn(), diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 180240f..25e5584 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -321,6 +321,14 @@ export function useConnection({ } }; + const disconnect = async () => { + await mcpClient?.close(); + setMcpClient(null); + setConnectionStatus("disconnected"); + setCompletionsSupported(false); + setServerCapabilities(null); + }; + return { connectionStatus, serverCapabilities, @@ -331,5 +339,6 @@ export function useConnection({ handleCompletion, completionsSupported, connect, + disconnect, }; } From b82c74458357ada6f927ecf1d7385f61cd6e3be0 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Tue, 1 Apr 2025 06:50:23 -0700 Subject: [PATCH 19/61] Use actual rendered element spinbutton in test --- client/src/components/__tests__/ToolsTab.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 27e273c..07aa52d 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -85,9 +85,7 @@ describe("ToolsTab", () => { selectedTool: mockTools[1], // Use the tool with integer type }); - const input = screen.getByRole("textbox", { - name: /count/i, - }) as HTMLInputElement; + const input = screen.getByRole("spinbutton", { name: /count/i }) as HTMLInputElement; expect(input).toHaveProperty("type", "number"); fireEvent.change(input, { target: { value: "42" } }); expect(input.value).toBe("42"); From 51c7eda6a6397ef50b9c5aa4ef59e696df3f3584 Mon Sep 17 00:00:00 2001 From: Pulkit Sharma Date: Tue, 1 Apr 2025 17:35:25 +0530 Subject: [PATCH 20/61] Add MCP proxy address config support, better error messages --- client/package.json | 1 + client/src/App.tsx | 22 +- client/src/components/Sidebar.tsx | 70 +++-- .../src/components/__tests__/Sidebar.test.tsx | 82 +++--- client/src/components/ui/tooltip.tsx | 30 +++ client/src/lib/configurationTypes.ts | 1 + client/src/lib/constants.ts | 17 ++ client/src/lib/hooks/useConnection.ts | 78 ++++-- client/src/main.tsx | 7 +- client/src/utils/configUtils.ts | 14 + package-lock.json | 239 ++++++++++++++++++ server/src/index.ts | 9 +- 12 files changed, 483 insertions(+), 87 deletions(-) create mode 100644 client/src/components/ui/tooltip.tsx create mode 100644 client/src/utils/configUtils.ts diff --git a/client/package.json b/client/package.json index eb0091f..3a2f51c 100644 --- a/client/package.json +++ b/client/package.json @@ -32,6 +32,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-tooltip": "^1.1.8", "@types/prismjs": "^1.26.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/client/src/App.tsx b/client/src/App.tsx index 23c508f..e652263 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,7 +20,6 @@ import { import React, { Suspense, useEffect, useRef, useState } from "react"; import { useConnection } from "./lib/hooks/useConnection"; import { useDraggablePane } from "./lib/hooks/useDraggablePane"; - import { StdErrNotification } from "./lib/notificationTypes"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -47,10 +46,12 @@ 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"; const params = new URLSearchParams(window.location.search); -const PROXY_PORT = params.get("proxyPort") ?? "6277"; -const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; const App = () => { @@ -95,7 +96,13 @@ const App = () => { const [config, setConfig] = useState(() => { const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); - return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG; + if (savedConfig) { + return { + ...DEFAULT_INSPECTOR_CONFIG, + ...JSON.parse(savedConfig), + } as InspectorConfig; + } + return DEFAULT_INSPECTOR_CONFIG; }); const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; @@ -152,8 +159,8 @@ const App = () => { sseUrl, env, bearerToken, - proxyServerUrl: PROXY_SERVER_URL, - requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number, + proxyServerUrl: getMCPProxyAddress(config), + requestTimeout: getMCPServerRequestTimeout(config), onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); }, @@ -214,7 +221,7 @@ const App = () => { }, [connectMcpServer]); useEffect(() => { - fetch(`${PROXY_SERVER_URL}/config`) + fetch(`${getMCPProxyAddress(config)}/config`) .then((response) => response.json()) .then((data) => { setEnv(data.defaultEnvironment); @@ -228,6 +235,7 @@ const App = () => { .catch((error) => console.error("Error fetching default environment:", error), ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 33e88a7..69aec99 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -10,6 +10,7 @@ import { EyeOff, RotateCcw, Settings, + HelpCircle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -26,12 +27,17 @@ import { LoggingLevelSchema, } from "@modelcontextprotocol/sdk/types.js"; import { InspectorConfig } from "@/lib/configurationTypes"; - +import { ConnectionStatus } from "@/lib/constants"; import useTheme from "../lib/useTheme"; import { version } from "../../../package.json"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip"; interface SidebarProps { - connectionStatus: "disconnected" | "connected" | "error"; + connectionStatus: ConnectionStatus; transportType: "stdio" | "sse"; setTransportType: (type: "stdio" | "sse") => void; command: string; @@ -177,6 +183,7 @@ const Sidebar = ({ variant="outline" onClick={() => setShowEnvVars(!showEnvVars)} className="flex items-center w-full" + data-testid="env-vars-button" > {showEnvVars ? ( @@ -298,6 +305,7 @@ const Sidebar = ({ variant="outline" onClick={() => setShowConfig(!showConfig)} className="flex items-center w-full" + data-testid="config-button" > {showConfig ? ( @@ -313,9 +321,19 @@ const Sidebar = ({ const configKey = key as keyof InspectorConfig; return (
- +
+ + + + + + + {configItem.description} + + +
{typeof configItem.value === "number" ? (
- {toolResult && renderToolResult()} + {error && ( + + + Error + {error} + + )}
) : ( From 93c9c74dc95b859f18360f3efcaae82a08e647ea Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Tue, 1 Apr 2025 06:59:50 -0700 Subject: [PATCH 22/61] Fix formatting --- client/src/components/__tests__/ToolsTab.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 07aa52d..89ad603 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -85,7 +85,9 @@ describe("ToolsTab", () => { selectedTool: mockTools[1], // Use the tool with integer type }); - const input = screen.getByRole("spinbutton", { name: /count/i }) as HTMLInputElement; + const input = screen.getByRole("spinbutton", { + name: /count/i, + }) as HTMLInputElement; expect(input).toHaveProperty("type", "number"); fireEvent.change(input, { target: { value: "42" } }); expect(input.value).toBe("42"); From d2db697d894706500a4eaf562e168c1060d420fd Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 11:00:13 -0300 Subject: [PATCH 23/61] fix: prettier. --- client/src/components/ToolsTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 3bd3da9..6a6f9f5 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -149,9 +149,9 @@ const ToolsTab = ({
{selectedTool ? (
-

- {selectedTool.description} -

+

+ {selectedTool.description} +

{Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { const prop = value as JsonSchemaType; From 51f2f726779253a07928bf1a323628a8d1166ac5 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 11:07:43 -0300 Subject: [PATCH 24/61] fix: add tests --- client/src/components/__tests__/ToolsTab.test.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 2a45065..41501db 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -1,5 +1,6 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, jest } from "@jest/globals"; +import "@testing-library/jest-dom"; import ToolsTab from "../ToolsTab"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Tabs } from "@/components/ui/tabs"; @@ -69,4 +70,16 @@ describe("ToolsTab", () => { const newInput = screen.getByRole("spinbutton") as HTMLInputElement; expect(newInput.value).toBe(""); }); + + it("should display error message when error prop is provided", () => { + const errorMessage = "Test error message"; + renderToolsTab({ + selectedTool: mockTools[0], + error: errorMessage, + }); + + // Verify error message is displayed + expect(screen.getByText("Error")).toBeTruthy(); + expect(screen.getByText(errorMessage)).toBeTruthy(); + }); }); From 65f38a482722ba658fd9a1c223bc8d98aab56235 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Tue, 1 Apr 2025 07:07:57 -0700 Subject: [PATCH 25/61] Bump version to 0.8.0 --- client/package.json | 2 +- package-lock.json | 12 ++++++------ package.json | 6 +++--- server/package.json | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/package.json b/client/package.json index eb0091f..8dc5a90 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.7.0", + "version": "0.8.0", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/package-lock.json b/package-lock.json index feb893f..e9254db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "workspaces": [ "client", "server" ], "dependencies": { - "@modelcontextprotocol/inspector-client": "^0.7.0", - "@modelcontextprotocol/inspector-server": "^0.7.0", + "@modelcontextprotocol/inspector-client": "^0.8.0", + "@modelcontextprotocol/inspector-server": "^0.8.0", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", @@ -32,7 +32,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", @@ -11571,7 +11571,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", diff --git a/package.json b/package.json index 28b93d8..d323dda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.7.0", + "version": "0.8.0", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -36,8 +36,8 @@ "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { - "@modelcontextprotocol/inspector-client": "^0.7.0", - "@modelcontextprotocol/inspector-server": "^0.7.0", + "@modelcontextprotocol/inspector-client": "^0.8.0", + "@modelcontextprotocol/inspector-server": "^0.8.0", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", diff --git a/server/package.json b/server/package.json index 732993f..9072f51 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.7.0", + "version": "0.8.0", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 0dcd10c1dd12565d6b1020bea367de6f398c8c60 Mon Sep 17 00:00:00 2001 From: Pulkit Sharma Date: Tue, 1 Apr 2025 19:46:55 +0530 Subject: [PATCH 26/61] Update readme --- README.md | 1 + client/src/lib/hooks/useConnection.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b0e18de..c43f943 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The MCP Inspector supports the following configuration settings. To change them | 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` | ### From this repository diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 0bb8124..404cd98 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -233,14 +233,13 @@ export function useConnection({ }, ); - const mcpProxyServerUrl = new URL(`${proxyServerUrl}/sse`); try { await checkProxyHealth(); } catch { setConnectionStatus("error-connecting-to-proxy"); return; } - + const mcpProxyServerUrl = new URL(`${proxyServerUrl}/sse`); mcpProxyServerUrl.searchParams.append("transportType", transportType); if (transportType === "stdio") { mcpProxyServerUrl.searchParams.append("command", command); From c48670f426cd7443c85a01c90fd782140958e5e6 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 16:43:23 -0300 Subject: [PATCH 27/61] Add copy button to JSON & fix button styles override. --- client/src/components/JsonView.tsx | 2 +- client/src/components/ToolsTab.tsx | 31 ++++++++++++++++--- .../components/__tests__/ToolsTab.test.tsx | 1 - client/src/index.css | 26 ---------------- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index e2922f0..3b9ec25 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -33,7 +33,7 @@ const JsonView = memo( : data; return ( -
+
{ + try { + navigator.clipboard.writeText(JSON.stringify(toolResult)) + setCopied(true) + setTimeout(() => { + setCopied(false) + }, 500) + } catch (error) { + toast.error(`There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`) + } + }, [toolResult]) + const renderToolResult = () => { if (!toolResult) return null; @@ -53,7 +68,8 @@ const ToolsTab = ({ return ( <>

Invalid Tool Result:

-
+
+

Errors:

@@ -76,7 +92,12 @@ const ToolsTab = ({ {structuredResult.content.map((item, index) => (
{item.type === "text" && ( -
+
+
)} @@ -234,7 +255,7 @@ const ToolsTab = ({ ...params, [key]: prop.type === "number" || - prop.type === "integer" + prop.type === "integer" ? Number(e.target.value) : e.target.value, }) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index e1b306d..1c24a6c 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -81,7 +81,6 @@ describe("ToolsTab", () => { expect(newInput.value).toBe(""); }); - it("should display error message when error prop is provided", () => { const errorMessage = "Test error message"; renderToolsTab({ diff --git a/client/src/index.css b/client/src/index.css index 1795f58..11c6f23 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -38,29 +38,6 @@ h1 { line-height: 1.1; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -button[role="checkbox"] { - padding: 0; -} - @media (prefers-color-scheme: light) { :root { color: #213547; @@ -69,9 +46,6 @@ button[role="checkbox"] { a:hover { color: #747bff; } - button { - background-color: #f9f9f9; - } } @layer base { From d1e155f984b44699378d309d651b07e68a46bab4 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 16:48:01 -0300 Subject: [PATCH 28/61] Revert "Add copy button to JSON & fix button styles override." This reverts commit c48670f426cd7443c85a01c90fd782140958e5e6. --- client/src/components/JsonView.tsx | 2 +- client/src/components/ToolsTab.tsx | 31 +++---------------- .../components/__tests__/ToolsTab.test.tsx | 1 + client/src/index.css | 26 ++++++++++++++++ 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 3b9ec25..e2922f0 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -33,7 +33,7 @@ const JsonView = memo( : data; return ( -
+
{ - try { - navigator.clipboard.writeText(JSON.stringify(toolResult)) - setCopied(true) - setTimeout(() => { - setCopied(false) - }, 500) - } catch (error) { - toast.error(`There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`) - } - }, [toolResult]) - const renderToolResult = () => { if (!toolResult) return null; @@ -68,8 +53,7 @@ const ToolsTab = ({ return ( <>

Invalid Tool Result:

-
- +

Errors:

@@ -92,12 +76,7 @@ const ToolsTab = ({ {structuredResult.content.map((item, index) => (
{item.type === "text" && ( -
- +
)} @@ -255,7 +234,7 @@ const ToolsTab = ({ ...params, [key]: prop.type === "number" || - prop.type === "integer" + prop.type === "integer" ? Number(e.target.value) : e.target.value, }) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 1c24a6c..e1b306d 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -81,6 +81,7 @@ describe("ToolsTab", () => { expect(newInput.value).toBe(""); }); + it("should display error message when error prop is provided", () => { const errorMessage = "Test error message"; renderToolsTab({ diff --git a/client/src/index.css b/client/src/index.css index 11c6f23..1795f58 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -38,6 +38,29 @@ h1 { line-height: 1.1; } +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +button[role="checkbox"] { + padding: 0; +} + @media (prefers-color-scheme: light) { :root { color: #213547; @@ -46,6 +69,9 @@ h1 { a:hover { color: #747bff; } + button { + background-color: #f9f9f9; + } } @layer base { From 1504d1307e2dc30752d38f2c90aa3c319ee22027 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 16:49:10 -0300 Subject: [PATCH 29/61] Remove error --- client/src/components/ToolsTab.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 7958664..6e85531 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -1,4 +1,4 @@ -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; @@ -13,7 +13,7 @@ import { ListToolsResult, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { AlertCircle, Send } from "lucide-react"; +import { Send } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; @@ -27,7 +27,6 @@ const ToolsTab = ({ setSelectedTool, toolResult, nextCursor, - error, }: { tools: Tool[]; listTools: () => void; @@ -251,13 +250,6 @@ const ToolsTab = ({ Run Tool {toolResult && renderToolResult()} - {error && ( - - - Error - {error} - - )}
) : ( From 80f2986fd6dbc0803249d7cd08d08a782f107c66 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 16:58:32 -0300 Subject: [PATCH 30/61] remove unneeded test --- client/src/components/__tests__/ToolsTab.test.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index e1b306d..349977a 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -80,20 +80,6 @@ describe("ToolsTab", () => { const newInput = screen.getByRole("spinbutton") as HTMLInputElement; expect(newInput.value).toBe(""); }); - - - it("should display error message when error prop is provided", () => { - const errorMessage = "Test error message"; - renderToolsTab({ - selectedTool: mockTools[0], - error: errorMessage, - }); - - // Verify error message is displayed - expect(screen.getByText("Error")).toBeTruthy(); - expect(screen.getByText(errorMessage)).toBeTruthy(); - }); - it("should handle integer type inputs", () => { renderToolsTab({ selectedTool: mockTools[1], // Use the tool with integer type From 539de0fd851612ab9392b0dd81edb3ac252249be Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 17:18:40 -0300 Subject: [PATCH 31/61] add copy button --- client/src/components/JsonView.tsx | 2 +- client/src/components/ToolsTab.tsx | 38 ++++++++++++++++++++++++++---- client/src/index.css | 26 -------------------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index e2922f0..3b9ec25 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -33,7 +33,7 @@ const JsonView = memo( : data; return ( -
+
{ + try { + navigator.clipboard.writeText(JSON.stringify(toolResult)); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 500); + } catch (error) { + toast.error( + `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, [toolResult]); + const renderToolResult = () => { if (!toolResult) return null; @@ -52,7 +69,8 @@ const ToolsTab = ({ return ( <>

Invalid Tool Result:

-
+
+

Errors:

@@ -75,7 +93,19 @@ const ToolsTab = ({ {structuredResult.content.map((item, index) => (
{item.type === "text" && ( -
+
+
)} diff --git a/client/src/index.css b/client/src/index.css index 1795f58..11c6f23 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -38,29 +38,6 @@ h1 { line-height: 1.1; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -button[role="checkbox"] { - padding: 0; -} - @media (prefers-color-scheme: light) { :root { color: #213547; @@ -69,9 +46,6 @@ button[role="checkbox"] { a:hover { color: #747bff; } - button { - background-color: #f9f9f9; - } } @layer base { From 8586d63e6d1dd586095f88eb4d1f4be54d8672ee Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 17:38:25 -0300 Subject: [PATCH 32/61] Add shadcn alert to match project design --- client/package.json | 2 +- client/src/App.tsx | 11 +- client/src/components/ToolsTab.tsx | 13 +- client/src/components/ui/toast.tsx | 126 ++++++++++++++ client/src/components/ui/toaster.tsx | 33 ++++ client/src/hooks/use-toast.ts | 191 +++++++++++++++++++++ client/src/lib/hooks/useConnection.ts | 27 ++- client/src/main.tsx | 5 +- package-lock.json | 235 ++++++++++++++++++++++++-- 9 files changed, 611 insertions(+), 32 deletions(-) create mode 100644 client/src/components/ui/toast.tsx create mode 100644 client/src/components/ui/toaster.tsx create mode 100644 client/src/hooks/use-toast.ts diff --git a/client/package.json b/client/package.json index 8dc5a90..a415b18 100644 --- a/client/package.json +++ b/client/package.json @@ -32,6 +32,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.6", "@types/prismjs": "^1.26.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -42,7 +43,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-simple-code-editor": "^0.14.1", - "react-toastify": "^10.0.6", "serve-handler": "^6.1.6", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", diff --git a/client/src/App.tsx b/client/src/App.tsx index e560d55..5c09e8e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -33,7 +33,6 @@ import { MessageSquare, } from "lucide-react"; -import { toast } from "react-toastify"; import { z } from "zod"; import "./App.css"; import ConsoleTab from "./components/ConsoleTab"; @@ -47,13 +46,14 @@ import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; - +import { useToast } from "@/hooks/use-toast"; const params = new URLSearchParams(window.location.search); const PROXY_PORT = params.get("proxyPort") ?? "6277"; const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; const App = () => { + const { toast } = useToast(); // Handle OAuth callback route const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< @@ -208,11 +208,14 @@ const App = () => { newUrl.searchParams.delete("serverUrl"); window.history.replaceState({}, "", newUrl.toString()); // Show success toast for OAuth - toast.success("Successfully authenticated with OAuth"); + toast({ + title: "Success", + description: "Successfully authenticated with OAuth", + }); // Connect to the server connectMcpServer(); } - }, [connectMcpServer]); + }, [connectMcpServer, toast]); useEffect(() => { fetch(`${PROXY_SERVER_URL}/config`) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 694377e..bd30f96 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -17,7 +17,7 @@ import { Copy, Send, CheckCheck } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; -import { toast } from "react-toastify"; +import { useToast } from "@/hooks/use-toast"; const ToolsTab = ({ tools, @@ -39,6 +39,7 @@ const ToolsTab = ({ nextCursor: ListToolsResult["nextCursor"]; error: string | null; }) => { + const { toast } = useToast(); const [params, setParams] = useState>({}); useEffect(() => { setParams({}); @@ -54,11 +55,13 @@ const ToolsTab = ({ setCopied(false); }, 500); } catch (error) { - toast.error( - `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, - ); + toast({ + title: "Error", + description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, + variant: "destructive", + }); } - }, [toolResult]); + }, [toast, toolResult]); const renderToolResult = () => { if (!toolResult) return null; diff --git a/client/src/components/ui/toast.tsx b/client/src/components/ui/toast.tsx new file mode 100644 index 0000000..9cea397 --- /dev/null +++ b/client/src/components/ui/toast.tsx @@ -0,0 +1,126 @@ +import * as React from "react"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; +import { Cross2Icon } from "@radix-ui/react-icons"; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +}; diff --git a/client/src/components/ui/toaster.tsx b/client/src/components/ui/toaster.tsx new file mode 100644 index 0000000..5887f08 --- /dev/null +++ b/client/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { useToast } from "@/hooks/use-toast"; +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast"; + +export function Toaster() { + const { toasts } = useToast(); + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/client/src/hooks/use-toast.ts b/client/src/hooks/use-toast.ts new file mode 100644 index 0000000..6555e79 --- /dev/null +++ b/client/src/hooks/use-toast.ts @@ -0,0 +1,191 @@ +"use client"; + +// Inspired by react-hot-toast library +import * as React from "react"; + +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; + +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; + +type ToasterToast = ToastProps & { + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const; + +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); +} + +type ActionType = typeof actionTypes; + +type Action = + | { + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; + } + | { + type: ActionType["UPDATE_TOAST"]; + toast: Partial; + } + | { + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; + } + | { + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }); + }, TOAST_REMOVE_DELAY); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t, + ), + }; + + case "DISMISS_TOAST": { + const { toastId } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId); + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +} + +type Toast = Omit; + +function toast({ ...props }: Toast) { + const id = genId(); + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +} + +function useToast() { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; +} + +export { useToast, toast }; diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 25e5584..56c91d0 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -25,7 +25,7 @@ import { PromptListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { useState } from "react"; -import { toast } from "react-toastify"; +import { useToast } from "@/hooks/use-toast"; import { z } from "zod"; import { SESSION_KEYS } from "../constants"; import { Notification, StdErrNotificationSchema } from "../notificationTypes"; @@ -70,6 +70,7 @@ export function useConnection({ onPendingRequest, getRoots, }: UseConnectionOptions) { + const { toast } = useToast(); const [connectionStatus, setConnectionStatus] = useState< "disconnected" | "connected" | "error" >("disconnected"); @@ -125,7 +126,11 @@ export function useConnection({ } catch (e: unknown) { if (!options?.suppressToast) { const errorString = (e as Error).message ?? String(e); - toast.error(errorString); + toast({ + title: "Error", + description: errorString, + variant: "destructive", + }); } throw e; } @@ -167,7 +172,11 @@ export function useConnection({ } // Unexpected errors - show toast and rethrow - toast.error(e instanceof Error ? e.message : String(e)); + toast({ + title: "Error", + description: e instanceof Error ? e.message : String(e), + variant: "destructive", + }); throw e; } }; @@ -175,7 +184,11 @@ export function useConnection({ const sendNotification = async (notification: ClientNotification) => { if (!mcpClient) { const error = new Error("MCP client not connected"); - toast.error(error.message); + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); throw error; } @@ -188,7 +201,11 @@ export function useConnection({ // Log MCP protocol errors pushHistory(notification, { error: e.message }); } - toast.error(e instanceof Error ? e.message : String(e)); + toast({ + title: "Error", + description: e instanceof Error ? e.message : String(e), + variant: "destructive", + }); throw e; } }; diff --git a/client/src/main.tsx b/client/src/main.tsx index 450213d..7379a14 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,13 +1,12 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { ToastContainer } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; +import { Toaster } from "@/components/ui/toaster.tsx"; import App from "./App.tsx"; import "./index.css"; createRoot(document.getElementById("root")!).render( - + , ); diff --git a/package-lock.json b/package-lock.json index e9254db..b018e16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.6", "@types/prismjs": "^1.26.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -54,7 +55,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-simple-code-editor": "^0.14.1", - "react-toastify": "^10.0.6", "serve-handler": "^6.1.6", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", @@ -3383,6 +3383,226 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz", + "integrity": "sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -9355,19 +9575,6 @@ } } }, - "node_modules/react-toastify": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", - "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", From cc70fbd0f51e11b2d2f28631e3aff65e79e99cf3 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 19:14:32 -0300 Subject: [PATCH 33/61] add ring color --- client/src/index.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/index.css b/client/src/index.css index 11c6f23..14c9470 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -26,6 +26,11 @@ a:hover { color: #535bf2; } +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + body { margin: 0; place-items: center; From c9ee22b7815f3f4edcfe54301100f5c791fe24fd Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Tue, 1 Apr 2025 19:23:57 -0300 Subject: [PATCH 34/61] fix(Select): add missing style. --- client/src/components/ui/select.tsx | 2 +- client/src/index.css | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/client/src/components/ui/select.tsx b/client/src/components/ui/select.tsx index 939dce1..9d99d92 100644 --- a/client/src/components/ui/select.tsx +++ b/client/src/components/ui/select.tsx @@ -22,7 +22,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 hover:border-[#646cff] hover:border-1", className, )} {...props} diff --git a/client/src/index.css b/client/src/index.css index 14c9470..11c6f23 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -26,11 +26,6 @@ a:hover { color: #535bf2; } -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - body { margin: 0; place-items: center; From e69bfc58bc949ffb004803c364afd29a904402af Mon Sep 17 00:00:00 2001 From: Pulkit Sharma Date: Wed, 2 Apr 2025 13:45:09 +0530 Subject: [PATCH 35/61] prettier-fix --- client/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index c0369ec..19f8020 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -398,7 +398,7 @@ const Sidebar = ({
{connectionStatus === "connected" && (
- From c964ff5cfe464f6980c73bb02cf10f289f3684af Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Wed, 2 Apr 2025 10:39:39 -0300 Subject: [PATCH 36/61] Use copy button insde JSON view component --- client/src/components/History.tsx | 49 +++++----------- client/src/components/JsonView.tsx | 80 ++++++++++++++++++++++---- client/src/components/PromptsTab.tsx | 4 +- client/src/components/ResourcesTab.tsx | 7 ++- client/src/components/SamplingTab.tsx | 8 ++- client/src/components/ToolsTab.tsx | 60 +++---------------- 6 files changed, 100 insertions(+), 108 deletions(-) diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index b03d1f4..0b05b55 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -1,5 +1,4 @@ import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; -import { Copy } from "lucide-react"; import { useState } from "react"; import JsonView from "./JsonView"; @@ -25,10 +24,6 @@ const HistoryAndNotifications = ({ setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] })); }; - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - }; - return (
@@ -68,16 +63,12 @@ const HistoryAndNotifications = ({ Request: - -
-
-
+ +
{request.response && (
@@ -85,16 +76,11 @@ const HistoryAndNotifications = ({ Response: - -
-
-
+
)} @@ -134,20 +120,11 @@ const HistoryAndNotifications = ({ Details: - -
-
-
+
)} diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 3b9ec25..a98a11c 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,11 +1,16 @@ -import { useState, memo } from "react"; +import { useState, memo, useMemo, useCallback, useEffect } from "react"; import { JsonValue } from "./DynamicJsonForm"; import clsx from "clsx"; +import { Copy, CheckCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; interface JsonViewProps { data: unknown; name?: string; initialExpandDepth?: number; + className?: string; + withCopyButton?: boolean; } function tryParseJson(str: string): { success: boolean; data: JsonValue } { @@ -24,22 +29,75 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } { } const JsonView = memo( - ({ data, name, initialExpandDepth = 3 }: JsonViewProps) => { - const normalizedData = - typeof data === "string" + ({ + data, + name, + initialExpandDepth = 3, + className, + withCopyButton = true, + }: JsonViewProps) => { + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (copied) { + timeoutId = setTimeout(() => { + setCopied(false); + }, 500); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [copied]); + + const normalizedData = useMemo(() => { + return typeof data === "string" ? tryParseJson(data).success ? tryParseJson(data).data : data : data; + }, [data]); + + const handleCopy = useCallback(() => { + try { + navigator.clipboard.writeText(JSON.stringify(normalizedData, null, 2)); + setCopied(true); + } catch (error) { + toast({ + title: "Error", + description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, + variant: "destructive", + }); + } + }, [toast, normalizedData]); return ( -
- +
+ {withCopyButton && ( + + )} +
+ +
); }, diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index b42cf77..48c847d 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -152,9 +152,7 @@ const PromptsTab = ({ Get Prompt {promptContent && ( -
- -
+ )}
) : ( diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 443a902..2a10824 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -215,9 +215,10 @@ const ResourcesTab = ({ {error} ) : selectedResource ? ( -
- -
+ ) : selectedTemplate ? (

diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index 21fc7dd..a72ea7d 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -44,9 +44,11 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {

Recent Requests

{pendingRequests.map((request) => (
-
- -
+ +
- -
- )} + {item.type === "text" && } {item.type === "image" && ( Your browser does not support audio playback

) : ( -
- -
+ ))}
))} @@ -141,9 +98,8 @@ const ToolsTab = ({ return ( <>

Tool Result (Legacy):

-
- -
+ + ); } From 8b31f495ba8eafdb16ac48766e10d045f2abb733 Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Wed, 2 Apr 2025 10:41:33 -0300 Subject: [PATCH 37/61] fix unkown type. --- client/src/components/JsonView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index a98a11c..56796be 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -63,7 +63,7 @@ const JsonView = memo( const handleCopy = useCallback(() => { try { - navigator.clipboard.writeText(JSON.stringify(normalizedData, null, 2)); + navigator.clipboard.writeText(typeof normalizedData === "string" ? normalizedData : JSON.stringify(normalizedData, null, 2)); setCopied(true); } catch (error) { toast({ From 5db5fc26c79d45f5190b0d4dbfbce26e117271fa Mon Sep 17 00:00:00 2001 From: NicolasMontone Date: Wed, 2 Apr 2025 10:42:52 -0300 Subject: [PATCH 38/61] fix prettier --- client/src/components/JsonView.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 56796be..b59f3cc 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -63,7 +63,11 @@ const JsonView = memo( const handleCopy = useCallback(() => { try { - navigator.clipboard.writeText(typeof normalizedData === "string" ? normalizedData : JSON.stringify(normalizedData, null, 2)); + navigator.clipboard.writeText( + typeof normalizedData === "string" + ? normalizedData + : JSON.stringify(normalizedData, null, 2), + ); setCopied(true); } catch (error) { toast({ From 0bd51fa84a2ba166469b06bccd058e20a029eafc Mon Sep 17 00:00:00 2001 From: Maxwell Gerber Date: Wed, 2 Apr 2025 15:18:33 -0700 Subject: [PATCH 39/61] fix: Do not reconnect on rerender --- client/src/App.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/App.tsx b/client/src/App.tsx index 5c09e8e..c756903 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -47,6 +47,7 @@ import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; import { useToast } from "@/hooks/use-toast"; + const params = new URLSearchParams(window.location.search); const PROXY_PORT = params.get("proxyPort") ?? "6277"; const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`; @@ -197,8 +198,13 @@ const App = () => { localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); }, [config]); + const hasProcessedRef = useRef(false); // Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback) useEffect(() => { + if (hasProcessedRef.current) { + // Only try to connect once + return; + } const serverUrl = params.get("serverUrl"); if (serverUrl) { setSseUrl(serverUrl); @@ -212,6 +218,7 @@ const App = () => { title: "Success", description: "Successfully authenticated with OAuth", }); + hasProcessedRef.current = true; // Connect to the server connectMcpServer(); } From a4140333543c854efb7be24939238a37d5903d3b Mon Sep 17 00:00:00 2001 From: Maxwell Gerber Date: Tue, 1 Apr 2025 20:09:36 -0700 Subject: [PATCH 40/61] fix: Use same protocol for proxy server URL --- client/src/utils/configUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index a6f2dd2..dcd4c97 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -6,7 +6,7 @@ export const getMCPProxyAddress = (config: InspectorConfig): string => { if (proxyFullAddress) { return proxyFullAddress; } - return `http://${window.location.hostname}:${DEFAULT_MCP_PROXY_LISTEN_PORT}`; + return `${window.location.protocol}//${window.location.hostname}:${DEFAULT_MCP_PROXY_LISTEN_PORT}`; }; export const getMCPServerRequestTimeout = (config: InspectorConfig): number => { From 9092c780f7b85b61b6e68f6d3ea51c7d0e9a1d48 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 3 Apr 2025 14:30:59 -0400 Subject: [PATCH 41/61] Bump version to 0.8.1 * In client/package.json - version bumped to 0.8.1 - typescript SDK version 0.8.0 (latest) * In server/package.json - version bumped to 0.8.1 - typescript SDK version 0.8.0 (latest) * In client/package.json - version bumped to 0.8.1 - inspector-client bumped to 0.8.1 - inspector-server bumped to 0.8.1 --- client/package.json | 4 ++-- package.json | 6 +++--- server/package.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/package.json b/client/package.json index d4789a3..f43ab23 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.8.0", + "version": "0.8.1", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -23,7 +23,7 @@ "test:watch": "jest --config jest.config.cjs --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.8.0", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", diff --git a/package.json b/package.json index d323dda..453fa71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.8.0", + "version": "0.8.1", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -36,8 +36,8 @@ "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { - "@modelcontextprotocol/inspector-client": "^0.8.0", - "@modelcontextprotocol/inspector-server": "^0.8.0", + "@modelcontextprotocol/inspector-client": "^0.8.1", + "@modelcontextprotocol/inspector-server": "^0.8.1", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", diff --git a/server/package.json b/server/package.json index 9072f51..9da3ca8 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.8.0", + "version": "0.8.1", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -27,7 +27,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.8.0", "cors": "^2.8.5", "express": "^4.21.0", "ws": "^8.18.0", From a8ffc704f05cd1f8e0050cebd6f83a337fbddffc Mon Sep 17 00:00:00 2001 From: Pulkit Sharma Date: Fri, 4 Apr 2025 01:44:30 +0530 Subject: [PATCH 42/61] add support for progress flow --- client/src/App.tsx | 36 ++-- client/src/lib/configurationTypes.ts | 16 ++ client/src/lib/constants.ts | 8 + .../hooks/__tests__/useConnection.test.tsx | 165 ++++++++++++++++++ client/src/lib/hooks/useConnection.ts | 40 +++-- client/src/utils/configUtils.ts | 12 ++ 6 files changed, 238 insertions(+), 39 deletions(-) create mode 100644 client/src/lib/hooks/__tests__/useConnection.test.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 7564544..61dcad7 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); @@ -148,7 +145,7 @@ const App = () => { serverCapabilities, mcpClient, requestHistory, - makeRequest: makeConnectionRequest, + makeRequest, sendNotification, handleCompletion, completionsSupported, @@ -161,8 +158,7 @@ const App = () => { sseUrl, env, bearerToken, - proxyServerUrl: getMCPProxyAddress(config), - requestTimeout: getMCPServerRequestTimeout(config), + config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); }, @@ -279,13 +275,13 @@ const App = () => { setErrors((prev) => ({ ...prev, [tabKey]: null })); }; - const makeRequest = async ( + const makeConnectionRequest = 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 +299,7 @@ const App = () => { }; const listResources = async () => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "resources/list" as const, params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, @@ -316,7 +312,7 @@ const App = () => { }; const listResourceTemplates = async () => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "resources/templates/list" as const, params: nextResourceTemplateCursor @@ -333,7 +329,7 @@ const App = () => { }; const readResource = async (uri: string) => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "resources/read" as const, params: { uri }, @@ -346,7 +342,7 @@ const App = () => { const subscribeToResource = async (uri: string) => { if (!resourceSubscriptions.has(uri)) { - await makeRequest( + await makeConnectionRequest( { method: "resources/subscribe" as const, params: { uri }, @@ -362,7 +358,7 @@ const App = () => { const unsubscribeFromResource = async (uri: string) => { if (resourceSubscriptions.has(uri)) { - await makeRequest( + await makeConnectionRequest( { method: "resources/unsubscribe" as const, params: { uri }, @@ -377,7 +373,7 @@ const App = () => { }; const listPrompts = async () => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "prompts/list" as const, params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, @@ -390,7 +386,7 @@ const App = () => { }; const getPrompt = async (name: string, args: Record = {}) => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "prompts/get" as const, params: { name, arguments: args }, @@ -402,7 +398,7 @@ const App = () => { }; const listTools = async () => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "tools/list" as const, params: nextToolCursor ? { cursor: nextToolCursor } : {}, @@ -415,7 +411,7 @@ const App = () => { }; const callTool = async (name: string, params: Record) => { - const response = await makeRequest( + const response = await makeConnectionRequest( { method: "tools/call" as const, params: { @@ -437,7 +433,7 @@ const App = () => { }; const sendLogLevelRequest = async (level: LoggingLevel) => { - await makeRequest( + await makeConnectionRequest( { method: "logging/setLevel" as const, params: { level }, @@ -654,7 +650,7 @@ const App = () => { { - void makeRequest( + void makeConnectionRequest( { method: "ping" as const, }, diff --git a/client/src/lib/configurationTypes.ts b/client/src/lib/configurationTypes.ts index df9eb29..d0c1263 100644 --- a/client/src/lib/configurationTypes.ts +++ b/client/src/lib/configurationTypes.ts @@ -15,5 +15,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_SERVER_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_SERVER_REQUEST_TIMEOUT_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..9caf4bc 100644 --- a/client/src/lib/constants.ts +++ b/client/src/lib/constants.ts @@ -25,6 +25,14 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = { description: "Timeout for requests to the MCP server (ms)", value: 10000, }, + MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS: { + description: "Reset timeout on progress notifications", + value: true, + }, + MCP_SERVER_REQUEST_TIMEOUT_MAX_TOTAL_TIMEOUT: { + description: "Maximum total timeout for requests sent to the MCP server (ms)", + value: 60000, + }, 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", 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..7a96802 --- /dev/null +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -0,0 +1,165 @@ +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_SERVER_REQUEST_TIMEOUT_MAX_TOTAL_TIMEOUT.value, + resetTimeoutOnProgress: + DEFAULT_INSPECTOR_CONFIG + .MCP_SERVER_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..d67c623 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -24,6 +24,7 @@ import { ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, } 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,7 +94,7 @@ 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"); @@ -102,23 +102,25 @@ export function useConnection({ try { const abortController = new AbortController(); - const timeoutId = setTimeout(() => { - abortController.abort("Request timed out"); - }, options?.timeout ?? requestTimeout); - let response; try { response = await mcpClient.request(request, schema, { signal: options?.signal ?? abortController.signal, + resetTimeoutOnProgress: + options?.resetTimeoutOnProgress ?? + resetRequestTimeoutOnProgress(config), + timeout: options?.timeout ?? getMCPServerRequestTimeout(config), + maxTotalTimeout: + options?.maxTotalTimeout ?? + getMCPServerRequestMaxTotalTimeout(config), }); + 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 +213,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 +258,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); diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index a6f2dd2..3295f7d 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_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS.value as boolean; +}; + +export const getMCPServerRequestMaxTotalTimeout = ( + config: InspectorConfig, +): number => { + return config.MCP_SERVER_REQUEST_TIMEOUT_MAX_TOTAL_TIMEOUT.value as number; +}; From 3f73ec83a2fa5bec6fa478944374a358f0e342a6 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 3 Apr 2025 16:32:59 -0400 Subject: [PATCH 43/61] Bump version to 0.8.1 * package-lock.json --- package-lock.json | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index e626474..42fec87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.8.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "workspaces": [ "client", "server" ], "dependencies": { - "@modelcontextprotocol/inspector-client": "^0.8.0", - "@modelcontextprotocol/inspector-server": "^0.8.0", + "@modelcontextprotocol/inspector-client": "^0.8.1", + "@modelcontextprotocol/inspector-server": "^0.8.1", "concurrently": "^9.0.1", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", @@ -32,10 +32,10 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.8.0", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -1329,11 +1329,14 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.6.1", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz", + "integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", @@ -9483,10 +9486,10 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.8.0", "cors": "^2.8.5", "express": "^4.21.0", "ws": "^8.18.0", From b8ab30fdf3ae0fc7ce20565ebb5ee5ac954fbd5a Mon Sep 17 00:00:00 2001 From: yusheng chen Date: Fri, 4 Apr 2025 19:26:11 +0800 Subject: [PATCH 44/61] chore: move `@types/prismjs` to `devDependencies` --- client/package.json | 4 ++-- package-lock.json | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/package.json b/client/package.json index f43ab23..d111210 100644 --- a/client/package.json +++ b/client/package.json @@ -32,9 +32,8 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", - "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-toast": "^1.2.6", - "@types/prismjs": "^1.26.5", + "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -55,6 +54,7 @@ "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", + "@types/prismjs": "^1.26.5", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", diff --git a/package-lock.json b/package-lock.json index 42fec87..1dcc8d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,6 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", - "@types/prismjs": "^1.26.5", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -70,6 +69,7 @@ "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", + "@types/prismjs": "^1.26.5", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", @@ -3566,6 +3566,9 @@ }, "node_modules/@types/prismjs": { "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/prop-types": { From 0fd2e12c7b93ea7526d98aa1e6f5b301591af771 Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Fri, 4 Apr 2025 22:39:43 +0900 Subject: [PATCH 45/61] Fix inconsistent gap between TabsList and TabsContent --- client/src/components/PingTab.tsx | 18 +- client/src/components/PromptsTab.tsx | 158 ++++++------- client/src/components/ResourcesTab.tsx | 293 +++++++++++++------------ client/src/components/RootsTab.tsx | 64 +++--- client/src/components/SamplingTab.tsx | 54 ++--- client/src/components/ToolsTab.tsx | 260 +++++++++++----------- 6 files changed, 432 insertions(+), 415 deletions(-) diff --git a/client/src/components/PingTab.tsx b/client/src/components/PingTab.tsx index 287356c..6546901 100644 --- a/client/src/components/PingTab.tsx +++ b/client/src/components/PingTab.tsx @@ -3,14 +3,16 @@ import { Button } from "@/components/ui/button"; const PingTab = ({ onPingClick }: { onPingClick: () => void }) => { return ( - -
- + +
+
+ +
); diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index 48c847d..80e5fe6 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -84,84 +84,88 @@ const PromptsTab = ({ }; return ( - - { - setSelectedPrompt(prompt); - setPromptArgs({}); - }} - renderItem={(prompt) => ( - <> - {prompt.name} - {prompt.description} - - )} - title="Prompts" - buttonText={nextCursor ? "List More Prompts" : "List Prompts"} - isButtonDisabled={!nextCursor && prompts.length > 0} - /> - -
-
-

- {selectedPrompt ? selectedPrompt.name : "Select a prompt"} -

-
-
- {error ? ( - - - Error - {error} - - ) : selectedPrompt ? ( -
- {selectedPrompt.description && ( -

- {selectedPrompt.description} -

- )} - {selectedPrompt.arguments?.map((arg) => ( -
- - handleInputChange(arg.name, value)} - onInputChange={(value) => - handleInputChange(arg.name, value) - } - options={completions[arg.name] || []} - /> - - {arg.description && ( -

- {arg.description} - {arg.required && ( - (Required) - )} -

- )} -
- ))} - - {promptContent && ( - - )} -
- ) : ( - - - Select a prompt from the list to view and use it - - + +
+ { + setSelectedPrompt(prompt); + setPromptArgs({}); + }} + renderItem={(prompt) => ( + <> + {prompt.name} + + {prompt.description} + + )} + title="Prompts" + buttonText={nextCursor ? "List More Prompts" : "List Prompts"} + isButtonDisabled={!nextCursor && prompts.length > 0} + /> + +
+
+

+ {selectedPrompt ? selectedPrompt.name : "Select a prompt"} +

+
+
+ {error ? ( + + + Error + {error} + + ) : selectedPrompt ? ( +
+ {selectedPrompt.description && ( +

+ {selectedPrompt.description} +

+ )} + {selectedPrompt.arguments?.map((arg) => ( +
+ + handleInputChange(arg.name, value)} + onInputChange={(value) => + handleInputChange(arg.name, value) + } + options={completions[arg.name] || []} + /> + + {arg.description && ( +

+ {arg.description} + {arg.required && ( + (Required) + )} +

+ )} +
+ ))} + + {promptContent && ( + + )} +
+ ) : ( + + + Select a prompt from the list to view and use it + + + )} +
diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 2a10824..23cfbe7 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -111,155 +111,158 @@ const ResourcesTab = ({ }; return ( - - { - setSelectedResource(resource); - readResource(resource.uri); - setSelectedTemplate(null); - }} - renderItem={(resource) => ( -
- - - {resource.name} - - -
- )} - title="Resources" - buttonText={nextCursor ? "List More Resources" : "List Resources"} - isButtonDisabled={!nextCursor && resources.length > 0} - /> - - { - setSelectedTemplate(template); - setSelectedResource(null); - setTemplateValues({}); - }} - renderItem={(template) => ( -
- - - {template.name} - - -
- )} - title="Resource Templates" - buttonText={ - nextTemplateCursor ? "List More Templates" : "List Templates" - } - isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} - /> - -
-
-

- {selectedResource - ? selectedResource.name - : selectedTemplate - ? selectedTemplate.name - : "Select a resource or template"} -

- {selectedResource && ( -
- {resourceSubscriptionsSupported && - !resourceSubscriptions.has(selectedResource.uri) && ( - - )} - {resourceSubscriptionsSupported && - resourceSubscriptions.has(selectedResource.uri) && ( - - )} - + +
+ { + setSelectedResource(resource); + readResource(resource.uri); + setSelectedTemplate(null); + }} + renderItem={(resource) => ( +
+ + + {resource.name} + +
)} -
-
- {error ? ( - - - Error - {error} - - ) : selectedResource ? ( - - ) : selectedTemplate ? ( -
-

- {selectedTemplate.description} -

- {selectedTemplate.uriTemplate - .match(/{([^}]+)}/g) - ?.map((param) => { - const key = param.slice(1, -1); - return ( -
- - - handleTemplateValueChange(key, value) - } - onInputChange={(value) => - handleTemplateValueChange(key, value) - } - options={completions[key] || []} - /> -
- ); - })} - + title="Resources" + buttonText={nextCursor ? "List More Resources" : "List Resources"} + isButtonDisabled={!nextCursor && resources.length > 0} + /> + + { + setSelectedTemplate(template); + setSelectedResource(null); + setTemplateValues({}); + }} + renderItem={(template) => ( +
+ + + {template.name} + +
- ) : ( - - - Select a resource or template from the list to view its contents - - )} + title="Resource Templates" + buttonText={ + nextTemplateCursor ? "List More Templates" : "List Templates" + } + isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} + /> + +
+
+

+ {selectedResource + ? selectedResource.name + : selectedTemplate + ? selectedTemplate.name + : "Select a resource or template"} +

+ {selectedResource && ( +
+ {resourceSubscriptionsSupported && + !resourceSubscriptions.has(selectedResource.uri) && ( + + )} + {resourceSubscriptionsSupported && + resourceSubscriptions.has(selectedResource.uri) && ( + + )} + +
+ )} +
+
+ {error ? ( + + + Error + {error} + + ) : selectedResource ? ( + + ) : selectedTemplate ? ( +
+

+ {selectedTemplate.description} +

+ {selectedTemplate.uriTemplate + .match(/{([^}]+)}/g) + ?.map((param) => { + const key = param.slice(1, -1); + return ( +
+ + + handleTemplateValueChange(key, value) + } + onInputChange={(value) => + handleTemplateValueChange(key, value) + } + options={completions[key] || []} + /> +
+ ); + })} + +
+ ) : ( + + + Select a resource or template from the list to view its + contents + + + )} +
diff --git a/client/src/components/RootsTab.tsx b/client/src/components/RootsTab.tsx index 33f60d5..308b88b 100644 --- a/client/src/components/RootsTab.tsx +++ b/client/src/components/RootsTab.tsx @@ -35,40 +35,42 @@ const RootsTab = ({ }; return ( - - - - Configure the root directories that the server can access - - + +
+ + + Configure the root directories that the server can access + + - {roots.map((root, index) => ( -
- updateRoot(index, "uri", e.target.value)} - className="flex-1" - /> - +
+ ))} + +
+ +
- ))} - -
- -
); diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index a72ea7d..d7d0212 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -33,33 +33,37 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => { }; return ( - - - - When the server requests LLM sampling, requests will appear here for - approval. - - -
-

Recent Requests

- {pendingRequests.map((request) => ( -
- + +
+ + + When the server requests LLM sampling, requests will appear here for + approval. + + +
+

Recent Requests

+ {pendingRequests.map((request) => ( +
+ -
- - +
+ + +
-
- ))} - {pendingRequests.length === 0 && ( -

No pending requests

- )} + ))} + {pendingRequests.length === 0 && ( +

No pending requests

+ )} +
); diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index a9e9bf8..0316c88 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -106,147 +106,149 @@ const ToolsTab = ({ }; return ( - - { - clearTools(); - setSelectedTool(null); - }} - setSelectedItem={setSelectedTool} - renderItem={(tool) => ( - <> - {tool.name} - - {tool.description} - - - )} - title="Tools" - buttonText={nextCursor ? "List More Tools" : "List Tools"} - isButtonDisabled={!nextCursor && tools.length > 0} - /> + +
+ { + clearTools(); + setSelectedTool(null); + }} + setSelectedItem={setSelectedTool} + renderItem={(tool) => ( + <> + {tool.name} + + {tool.description} + + + )} + title="Tools" + buttonText={nextCursor ? "List More Tools" : "List Tools"} + isButtonDisabled={!nextCursor && tools.length > 0} + /> -
-
-

- {selectedTool ? selectedTool.name : "Select a tool"} -

-
-
- {selectedTool ? ( -
-

- {selectedTool.description} -

- {Object.entries(selectedTool.inputSchema.properties ?? []).map( - ([key, value]) => { - const prop = value as JsonSchemaType; - return ( -
- - {prop.type === "boolean" ? ( -
- +
+

+ {selectedTool ? selectedTool.name : "Select a tool"} +

+
+
+ {selectedTool ? ( +
+

+ {selectedTool.description} +

+ {Object.entries(selectedTool.inputSchema.properties ?? []).map( + ([key, value]) => { + const prop = value as JsonSchemaType; + return ( +
+ + {prop.type === "boolean" ? ( +
+ + setParams({ + ...params, + [key]: checked, + }) + } + /> + +
+ ) : prop.type === "string" ? ( +