From fb667fd4d08e7bb118b60f2b40018baebc09ad48 Mon Sep 17 00:00:00 2001 From: Max Gerber <89937743+max-stytch@users.noreply.github.com> Date: Fri, 14 Mar 2025 16:16:31 -0700 Subject: [PATCH 01/37] fix: Prefer 127.0.0.1 over localhost --- bin/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/cli.js b/bin/cli.js index 94348fb..115e118 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -96,7 +96,7 @@ async function main() { 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://localhost:${CLIENT_PORT}${portParam} šŸš€`, + `\nšŸ” MCP Inspector is up and running at http://127.0.0.1:${CLIENT_PORT}${portParam} šŸš€`, ); try { From cda3905e5a64ebd0c8c3072c852fc777b5d0fd80 Mon Sep 17 00:00:00 2001 From: Maxwell Gerber Date: Fri, 14 Mar 2025 16:33:39 -0700 Subject: [PATCH 02/37] fix: Update URL in CONTRIBUTING.md too --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ad5699..b225713 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://localhost:5173 to interact with the inspector +4. Use the web UI at http://127.0.0.1:5173 to interact with the inspector ## Development Process & Pull Requests From dd460bd877eb9df356ce02a35b9e8cc2ef5222ef Mon Sep 17 00:00:00 2001 From: Nathan Arseneau Date: Mon, 17 Mar 2025 02:44:59 -0400 Subject: [PATCH 03/37] Refactor notification handling to include all notifications --- client/src/lib/hooks/useConnection.ts | 23 ++++++----------------- client/src/lib/notificationTypes.ts | 4 +++- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 19b0e9d..d432c28 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -8,9 +8,6 @@ import { ClientRequest, CreateMessageRequestSchema, ListRootsRequestSchema, - ProgressNotificationSchema, - ResourceUpdatedNotificationSchema, - LoggingMessageNotificationSchema, Request, Result, ServerCapabilities, @@ -250,20 +247,12 @@ export function useConnection({ }); if (onNotification) { - client.setNotificationHandler( - ProgressNotificationSchema, - onNotification, - ); - - client.setNotificationHandler( - ResourceUpdatedNotificationSchema, - onNotification, - ); - - client.setNotificationHandler( - LoggingMessageNotificationSchema, - onNotification, - ); + client.fallbackNotificationHandler = ( + notification: Notification, + ): Promise => { + onNotification(notification); + return Promise.resolve(); + }; } if (onStdErrNotification) { diff --git a/client/src/lib/notificationTypes.ts b/client/src/lib/notificationTypes.ts index 82c1fd8..8627ccc 100644 --- a/client/src/lib/notificationTypes.ts +++ b/client/src/lib/notificationTypes.ts @@ -14,7 +14,9 @@ export const StdErrNotificationSchema = BaseNotificationSchema.extend({ export const NotificationSchema = ClientNotificationSchema.or( StdErrNotificationSchema, -).or(ServerNotificationSchema); +) + .or(ServerNotificationSchema) + .or(BaseNotificationSchema); export type StdErrNotification = z.infer; export type Notification = z.infer; From 536b7e0a99170b402fec55946b157642bb6f5e74 Mon Sep 17 00:00:00 2001 From: Maxwell Gerber Date: Tue, 18 Mar 2025 09:29:48 -0700 Subject: [PATCH 04/37] fix: Update vite host --- client/vite.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/vite.config.ts b/client/vite.config.ts index b3d0f45..c971d58 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -5,7 +5,9 @@ import { defineConfig } from "vite"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - server: {}, + server: { + host: "127.0.0.1", + }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), From 27b54104c1523f06e6fa3a9ad54a249a74f9f6b2 Mon Sep 17 00:00:00 2001 From: Nathan Arseneau Date: Wed, 19 Mar 2025 19:51:01 -0400 Subject: [PATCH 05/37] Add support for additional notification schemas in useConnection hook --- client/src/lib/hooks/useConnection.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index d432c28..f0379eb 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -8,6 +8,9 @@ import { ClientRequest, CreateMessageRequestSchema, ListRootsRequestSchema, + ProgressNotificationSchema, + ResourceUpdatedNotificationSchema, + LoggingMessageNotificationSchema, Request, Result, ServerCapabilities, @@ -16,6 +19,10 @@ import { McpError, CompleteResultSchema, ErrorCode, + CancelledNotificationSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, + PromptListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { useState } from "react"; import { toast } from "react-toastify"; @@ -247,6 +254,18 @@ export function useConnection({ }); if (onNotification) { + [ + CancelledNotificationSchema, + ProgressNotificationSchema, + LoggingMessageNotificationSchema, + ResourceUpdatedNotificationSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, + PromptListChangedNotificationSchema, + ].forEach((notificationSchema) => { + client.setNotificationHandler(notificationSchema, onNotification); + }); + client.fallbackNotificationHandler = ( notification: Notification, ): Promise => { From ce81fb976be3d34187d01d2fac190376af42e52d Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Thu, 20 Mar 2025 22:18:44 +0900 Subject: [PATCH 06/37] Restructure link buttons in sidebar to respect theme --- client/src/components/Sidebar.tsx | 51 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 48c6ff2..74b0fac 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -331,36 +331,37 @@ const Sidebar = ({ From dcbd1dad410dc8e6abb6e98cda17688a6c64ce4f Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Fri, 21 Mar 2025 06:54:46 -0700 Subject: [PATCH 07/37] Bump prismjs from 1.29.0 to 1.30.0 to address --- client/package.json | 2 +- package-lock.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/package.json b/client/package.json index 0471765..700bbee 100644 --- a/client/package.json +++ b/client/package.json @@ -38,7 +38,7 @@ "cmdk": "^1.0.4", "lucide-react": "^0.447.0", "pkce-challenge": "^4.1.0", - "prismjs": "^1.29.0", + "prismjs": "^1.30.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-simple-code-editor": "^0.14.1", diff --git a/package-lock.json b/package-lock.json index 21c9c30..1db7ef2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "cmdk": "^1.0.4", "lucide-react": "^0.447.0", "pkce-challenge": "^4.1.0", - "prismjs": "^1.29.0", + "prismjs": "^1.30.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-simple-code-editor": "^0.14.1", @@ -8841,9 +8841,9 @@ } }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { "node": ">=6" From 3ac00598ff76a13290584aed8c78f8e5a8db41ac Mon Sep 17 00:00:00 2001 From: yusheng chen Date: Sun, 23 Mar 2025 21:29:46 +0800 Subject: [PATCH 08/37] perf: add `useMemo` to the return value of `useTheme` --- client/src/lib/useTheme.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/client/src/lib/useTheme.ts b/client/src/lib/useTheme.ts index c73159b..8e598e1 100644 --- a/client/src/lib/useTheme.ts +++ b/client/src/lib/useTheme.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; type Theme = "light" | "dark" | "system"; @@ -35,17 +35,18 @@ const useTheme = (): [Theme, (mode: Theme) => void] => { darkModeMediaQuery.removeEventListener("change", handleDarkModeChange); }; }, [theme]); - - return [ + + const setThemeWithSideEffect = useCallback((newTheme: Theme) => { + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + if (newTheme !== "system") { + document.documentElement.classList.toggle("dark", newTheme === "dark"); + } + }, []); + return useMemo(() => [ theme, - useCallback((newTheme: Theme) => { - setTheme(newTheme); - localStorage.setItem("theme", newTheme); - if (newTheme !== "system") { - document.documentElement.classList.toggle("dark", newTheme === "dark"); - } - }, []), - ]; + setThemeWithSideEffect, + ], [theme]); }; export default useTheme; From 9b0da1f892fe60429b7f72cb1239a0859d68f561 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 08:19:52 -0700 Subject: [PATCH 09/37] Add note on security considerations for proxy server --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a6ab6d4..4eb2467 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ For more details on ways to use the inspector, see the [Inspector section of the The inspector supports bearer token authentication for SSE connections. Enter your token in the UI when connecting to an MCP server, and it will be sent in the Authorization header. +### Security Considerations + +The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. + ### From this repository If you're working on the inspector itself: From ec738314875c0b984dca92df88c2be73c7ef4df2 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 08:30:06 -0700 Subject: [PATCH 10/37] Fix formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4eb2467..f1bd97c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ The inspector supports bearer token authentication for SSE connections. Enter yo ### Security Considerations -The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. +The MCP Inspector includes a proxy server that can run and communicate with local MCP processes. The proxy server should not be exposed to untrusted networks as it has permissions to spawn local processes and can connect to any specified MCP server. ### From this repository From 210975e38531b68be7af1eae72dee7aa0c0c5ab0 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 12:44:30 -0700 Subject: [PATCH 11/37] Add test dependencies --- client/package.json | 4 +- package-lock.json | 272 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 0471765..c3e4ed3 100644 --- a/client/package.json +++ b/client/package.json @@ -24,8 +24,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", - "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.3", @@ -50,6 +50,8 @@ }, "devDependencies": { "@eslint/js": "^9.11.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", "@types/react": "^18.3.10", diff --git a/package-lock.json b/package-lock.json index 68929c5..23bc2a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,8 @@ }, "devDependencies": { "@eslint/js": "^9.11.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", "@types/react": "^18.3.10", @@ -87,6 +89,13 @@ "vite": "^5.4.8" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -576,6 +585,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -3786,6 +3808,148 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz", + "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3820,6 +3984,14 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4585,6 +4757,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -5331,6 +5513,13 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5467,6 +5656,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5524,6 +5723,14 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -6796,6 +7003,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7984,6 +8201,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8137,6 +8365,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -9151,6 +9389,27 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9837,6 +10096,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", From fc76a7c7d49cf2a05c70846387c8da6daabcb454 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 12:46:30 -0700 Subject: [PATCH 12/37] Add setup file and remove old testing mock that no longer exists from moduleNameMapper --- client/jest.config.cjs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 3830e79..73e0d16 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,12 +1,11 @@ module.exports = { preset: "ts-jest", testEnvironment: "jsdom", + setupFilesAfterEnv: [ + '/src/setupTests.ts' + ], moduleNameMapper: { - "^@/(.*)$": "/src/$1", - "^../components/DynamicJsonForm$": - "/src/utils/__mocks__/DynamicJsonForm.ts", - "^../../components/DynamicJsonForm$": - "/src/utils/__mocks__/DynamicJsonForm.ts", + "^@/(.*)$": "/src/$1" }, transform: { "^.+\\.tsx?$": [ @@ -14,9 +13,9 @@ module.exports = { { useESM: true, jsx: "react-jsx", - tsconfig: "tsconfig.jest.json", - }, - ], + tsconfig: "tsconfig.jest.json" + } + ] }, extensionsToTreatAsEsm: [".ts", ".tsx"], testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", @@ -25,13 +24,13 @@ module.exports = { "/node_modules/", "/dist/", "/bin/", - "\\.config\\.(js|ts|cjs|mjs)$", + "\\.config\\.(js|ts|cjs|mjs)$" ], // Exclude the same patterns from coverage reports coveragePathIgnorePatterns: [ "/node_modules/", "/dist/", "/bin/", - "\\.config\\.(js|ts|cjs|mjs)$", - ], + "\\.config\\.(js|ts|cjs|mjs)$" + ] }; From 85f0e216791d5c0e6e610bb1b30743de8b4b15a7 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 13:22:13 -0700 Subject: [PATCH 13/37] Use commonjs for jest --- client/jest.config.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 73e0d16..6a48e7d 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -11,7 +11,6 @@ module.exports = { "^.+\\.tsx?$": [ "ts-jest", { - useESM: true, jsx: "react-jsx", tsconfig: "tsconfig.jest.json" } From 668cc915e4028132dd13254b406919a74b76bfd8 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 13:22:31 -0700 Subject: [PATCH 14/37] Add jest-dom types --- client/tsconfig.app.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 980c215..e6620f7 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -24,7 +24,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "types": ["jest", "@testing-library/jest-dom", "node"] }, "include": ["src"] } From 451704471c6324007cd263acf557a66ac943f95d Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 13:25:06 -0700 Subject: [PATCH 15/37] Remove setup --- client/jest.config.cjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 6a48e7d..949df20 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,9 +1,6 @@ module.exports = { preset: "ts-jest", testEnvironment: "jsdom", - setupFilesAfterEnv: [ - '/src/setupTests.ts' - ], moduleNameMapper: { "^@/(.*)$": "/src/$1" }, From 61e229a5529d343134c8333e2fb1700a43b4a79b Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Sun, 23 Mar 2025 13:35:24 -0700 Subject: [PATCH 16/37] Add sidebar tests --- .../src/components/__tests__/Sidebar.test.tsx | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 client/src/components/__tests__/Sidebar.test.tsx diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx new file mode 100644 index 0000000..765de2f --- /dev/null +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -0,0 +1,278 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, beforeEach, jest } from '@jest/globals'; +import Sidebar from '../Sidebar'; + +// Mock theme hook +jest.mock('../../lib/useTheme', () => ({ + __esModule: true, + default: () => ['light', jest.fn()], +})); + +describe('Sidebar Environment Variables', () => { + const defaultProps = { + connectionStatus: 'disconnected' as const, + transportType: 'stdio' as const, + setTransportType: jest.fn(), + command: '', + setCommand: jest.fn(), + args: '', + setArgs: jest.fn(), + sseUrl: '', + setSseUrl: jest.fn(), + env: {}, + setEnv: jest.fn(), + bearerToken: '', + setBearerToken: jest.fn(), + onConnect: jest.fn(), + stdErrNotifications: [], + logLevel: 'info' as const, + sendLogLevelRequest: jest.fn(), + loggingSupported: true, + }; + + const renderSidebar = (props = {}) => { + return render(); + }; + + const openEnvVarsSection = () => { + const button = screen.getByText('Environment Variables'); + fireEvent.click(button); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Basic Operations', () => { + it('should add a new environment variable', () => { + const setEnv = jest.fn(); + renderSidebar({ env: {}, setEnv }); + + openEnvVarsSection(); + + const addButton = screen.getByText('Add Environment Variable'); + fireEvent.click(addButton); + + expect(setEnv).toHaveBeenCalledWith({ '': '' }); + }); + + it('should remove an environment variable', () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const removeButton = screen.getByRole('button', { name: 'Ɨ' }); + fireEvent.click(removeButton); + + expect(setEnv).toHaveBeenCalledWith({}); + }); + + it('should update environment variable value', () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const valueInput = screen.getByDisplayValue('test_value'); + fireEvent.change(valueInput, { target: { value: 'new_value' } }); + + expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: 'new_value' }); + }); + + it('should toggle value visibility', () => { + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv }); + + openEnvVarsSection(); + + const valueInput = screen.getByDisplayValue('test_value'); + expect(valueInput).toHaveProperty('type', 'password'); + + const toggleButton = screen.getByRole('button', { name: /show value/i }); + fireEvent.click(toggleButton); + + expect(valueInput).toHaveProperty('type', 'text'); + }); + }); + + describe('Key Editing', () => { + it('should maintain order when editing first key', () => { + const setEnv = jest.fn(); + const initialEnv = { + FIRST_KEY: 'first_value', + SECOND_KEY: 'second_value', + THIRD_KEY: 'third_value', + }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const firstKeyInput = screen.getByDisplayValue('FIRST_KEY'); + fireEvent.change(firstKeyInput, { target: { value: 'NEW_FIRST_KEY' } }); + + expect(setEnv).toHaveBeenCalledWith({ + NEW_FIRST_KEY: 'first_value', + SECOND_KEY: 'second_value', + THIRD_KEY: 'third_value', + }); + }); + + it('should maintain order when editing middle key', () => { + const setEnv = jest.fn(); + const initialEnv = { + FIRST_KEY: 'first_value', + SECOND_KEY: 'second_value', + THIRD_KEY: 'third_value', + }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const middleKeyInput = screen.getByDisplayValue('SECOND_KEY'); + fireEvent.change(middleKeyInput, { target: { value: 'NEW_SECOND_KEY' } }); + + expect(setEnv).toHaveBeenCalledWith({ + FIRST_KEY: 'first_value', + NEW_SECOND_KEY: 'second_value', + THIRD_KEY: 'third_value', + }); + }); + + it('should maintain order when editing last key', () => { + const setEnv = jest.fn(); + const initialEnv = { + FIRST_KEY: 'first_value', + SECOND_KEY: 'second_value', + THIRD_KEY: 'third_value', + }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const lastKeyInput = screen.getByDisplayValue('THIRD_KEY'); + fireEvent.change(lastKeyInput, { target: { value: 'NEW_THIRD_KEY' } }); + + expect(setEnv).toHaveBeenCalledWith({ + FIRST_KEY: 'first_value', + SECOND_KEY: 'second_value', + NEW_THIRD_KEY: 'third_value', + }); + }); + }); + + describe('Multiple Operations', () => { + it('should maintain state after multiple key edits', () => { + const setEnv = jest.fn(); + const initialEnv = { + FIRST_KEY: 'first_value', + SECOND_KEY: 'second_value', + }; + const { rerender } = renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + // First key edit + const firstKeyInput = screen.getByDisplayValue('FIRST_KEY'); + fireEvent.change(firstKeyInput, { target: { value: 'NEW_FIRST_KEY' } }); + + // Get the updated env from the first setEnv call + const updatedEnv = (setEnv.mock.calls[0][0] as Record); + + // Rerender with the updated env + rerender(); + + // Second key edit + const secondKeyInput = screen.getByDisplayValue('SECOND_KEY'); + fireEvent.change(secondKeyInput, { target: { value: 'NEW_SECOND_KEY' } }); + + // Verify the final state matches what we expect + expect(setEnv).toHaveBeenLastCalledWith({ + NEW_FIRST_KEY: 'first_value', + NEW_SECOND_KEY: 'second_value', + }); + }); + + it('should maintain visibility state after key edit', () => { + const initialEnv = { TEST_KEY: 'test_value' }; + const { rerender } = renderSidebar({ env: initialEnv }); + + openEnvVarsSection(); + + // Show the value + const toggleButton = screen.getByRole('button', { name: /show value/i }); + fireEvent.click(toggleButton); + + const valueInput = screen.getByDisplayValue('test_value'); + expect(valueInput).toHaveProperty('type', 'text'); + + // Edit the key + const keyInput = screen.getByDisplayValue('TEST_KEY'); + fireEvent.change(keyInput, { target: { value: 'NEW_KEY' } }); + + // Rerender with updated env + rerender(); + + // Value should still be visible + const updatedValueInput = screen.getByDisplayValue('test_value'); + expect(updatedValueInput).toHaveProperty('type', 'text'); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty key', () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const keyInput = screen.getByDisplayValue('TEST_KEY'); + fireEvent.change(keyInput, { target: { value: '' } }); + + expect(setEnv).toHaveBeenCalledWith({ '': 'test_value' }); + }); + + it('should handle special characters in key', () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const keyInput = screen.getByDisplayValue('TEST_KEY'); + fireEvent.change(keyInput, { target: { value: 'TEST-KEY@123' } }); + + expect(setEnv).toHaveBeenCalledWith({ 'TEST-KEY@123': 'test_value' }); + }); + + it('should handle unicode characters', () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const keyInput = screen.getByDisplayValue('TEST_KEY'); + fireEvent.change(keyInput, { target: { value: 'TEST_šŸ”‘' } }); + + expect(setEnv).toHaveBeenCalledWith({ 'TEST_šŸ”‘': 'test_value' }); + }); + + it('should handle very long key names', () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: 'test_value' }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + const keyInput = screen.getByDisplayValue('TEST_KEY'); + const longKey = 'A'.repeat(100); + fireEvent.change(keyInput, { target: { value: longKey } }); + + expect(setEnv).toHaveBeenCalledWith({ [longKey]: 'test_value' }); + }); + }); +}); From cab1ed3dd849a85b37255eb2ec20ee7764565a4c Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 08:28:28 -0700 Subject: [PATCH 17/37] Add some json form tests and handle css in ui tests --- client/jest.config.cjs | 3 +- client/src/__mocks__/styleMock.js | 1 + .../__tests__/DynamicJsonForm.test.tsx | 61 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 client/src/__mocks__/styleMock.js create mode 100644 client/src/components/__tests__/DynamicJsonForm.test.tsx diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 949df20..b87383e 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -2,7 +2,8 @@ module.exports = { preset: "ts-jest", testEnvironment: "jsdom", moduleNameMapper: { - "^@/(.*)$": "/src/$1" + "^@/(.*)$": "/src/$1", + "\\.css$": "/src/__mocks__/styleMock.js" }, transform: { "^.+\\.tsx?$": [ diff --git a/client/src/__mocks__/styleMock.js b/client/src/__mocks__/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/client/src/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx new file mode 100644 index 0000000..d140663 --- /dev/null +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -0,0 +1,61 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, jest } from '@jest/globals'; +import DynamicJsonForm from '../DynamicJsonForm'; +import type { JsonSchemaType } from '../DynamicJsonForm'; + +describe('DynamicJsonForm Integer Fields', () => { + const renderForm = (props = {}) => { + const defaultProps = { + schema: { + type: "integer" as const, + description: "Test integer field" + } satisfies JsonSchemaType, + value: undefined, + onChange: jest.fn() + }; + return render(); + }; + + describe('Basic Operations', () => { + it('should render number input with step=1', () => { + renderForm(); + const input = screen.getByRole('spinbutton'); + expect(input).toHaveProperty('type', 'number'); + expect(input).toHaveProperty('step', '1'); + }); + + it('should pass integer values to onChange', () => { + const onChange = jest.fn(); + renderForm({ onChange }); + + const input = screen.getByRole('spinbutton'); + fireEvent.change(input, { target: { value: '42' } }); + + expect(onChange).toHaveBeenCalledWith(42); + // Verify the value is a number, not a string + expect(typeof onChange.mock.calls[0][0]).toBe('number'); + }); + + it('should not pass string values to onChange', () => { + const onChange = jest.fn(); + renderForm({ onChange }); + + const input = screen.getByRole('spinbutton'); + fireEvent.change(input, { target: { value: 'abc' } }); + + expect(onChange).not.toHaveBeenCalled(); + }); + }); + + describe('Edge Cases', () => { + it('should handle non-numeric input by not calling onChange', () => { + const onChange = jest.fn(); + renderForm({ onChange }); + + const input = screen.getByRole('spinbutton'); + fireEvent.change(input, { target: { value: 'abc' } }); + + expect(onChange).not.toHaveBeenCalled(); + }); + }); +}); From 5735f2347a4d1465033806e4648583a5a35a4e0f Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 08:52:45 -0700 Subject: [PATCH 18/37] Add tests related to issues/187 to confirm fixed --- .../__tests__/DynamicJsonForm.test.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index d140663..f18c15f 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -3,6 +3,40 @@ import { describe, it, expect, jest } from '@jest/globals'; import DynamicJsonForm from '../DynamicJsonForm'; import type { JsonSchemaType } from '../DynamicJsonForm'; +describe('DynamicJsonForm String Fields', () => { + const renderForm = (props = {}) => { + const defaultProps = { + schema: { + type: "string" as const, + description: "Test string field" + } satisfies JsonSchemaType, + value: undefined, + onChange: jest.fn() + }; + return render(); + }; + + describe('Type Validation', () => { + it('should handle numeric input as string type', () => { + const onChange = jest.fn(); + renderForm({ onChange }); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: '123321' } }); + + expect(onChange).toHaveBeenCalledWith('123321'); + // Verify the value is a string, not a number + expect(typeof onChange.mock.calls[0][0]).toBe('string'); + }); + + it('should render as text input, not number input', () => { + renderForm(); + const input = screen.getByRole('textbox'); + expect(input).toHaveProperty('type', 'text'); + }); + }); +}); + describe('DynamicJsonForm Integer Fields', () => { const renderForm = (props = {}) => { const defaultProps = { From fa3e2867c97e27c2bb434be7eaacd6698dc8fb4b Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 09:07:53 -0700 Subject: [PATCH 19/37] Fix formatting --- client/jest.config.cjs | 14 +- .../__tests__/DynamicJsonForm.test.tsx | 86 ++--- .../src/components/__tests__/Sidebar.test.tsx | 296 +++++++++--------- 3 files changed, 198 insertions(+), 198 deletions(-) diff --git a/client/jest.config.cjs b/client/jest.config.cjs index b87383e..c360e72 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -3,16 +3,16 @@ module.exports = { testEnvironment: "jsdom", moduleNameMapper: { "^@/(.*)$": "/src/$1", - "\\.css$": "/src/__mocks__/styleMock.js" + "\\.css$": "/src/__mocks__/styleMock.js", }, transform: { "^.+\\.tsx?$": [ "ts-jest", { jsx: "react-jsx", - tsconfig: "tsconfig.jest.json" - } - ] + tsconfig: "tsconfig.jest.json", + }, + ], }, extensionsToTreatAsEsm: [".ts", ".tsx"], testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", @@ -21,13 +21,13 @@ module.exports = { "/node_modules/", "/dist/", "/bin/", - "\\.config\\.(js|ts|cjs|mjs)$" + "\\.config\\.(js|ts|cjs|mjs)$", ], // Exclude the same patterns from coverage reports coveragePathIgnorePatterns: [ "/node_modules/", "/dist/", "/bin/", - "\\.config\\.(js|ts|cjs|mjs)$" - ] + "\\.config\\.(js|ts|cjs|mjs)$", + ], }; diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index f18c15f..fce6014 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -1,94 +1,94 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, jest } from '@jest/globals'; -import DynamicJsonForm from '../DynamicJsonForm'; -import type { JsonSchemaType } from '../DynamicJsonForm'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, jest } from "@jest/globals"; +import DynamicJsonForm from "../DynamicJsonForm"; +import type { JsonSchemaType } from "../DynamicJsonForm"; -describe('DynamicJsonForm String Fields', () => { +describe("DynamicJsonForm String Fields", () => { const renderForm = (props = {}) => { const defaultProps = { schema: { type: "string" as const, - description: "Test string field" + description: "Test string field", } satisfies JsonSchemaType, value: undefined, - onChange: jest.fn() + onChange: jest.fn(), }; return render(); }; - describe('Type Validation', () => { - it('should handle numeric input as string type', () => { + describe("Type Validation", () => { + it("should handle numeric input as string type", () => { const onChange = jest.fn(); renderForm({ onChange }); - - const input = screen.getByRole('textbox'); - fireEvent.change(input, { target: { value: '123321' } }); - - expect(onChange).toHaveBeenCalledWith('123321'); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: "123321" } }); + + expect(onChange).toHaveBeenCalledWith("123321"); // Verify the value is a string, not a number - expect(typeof onChange.mock.calls[0][0]).toBe('string'); + expect(typeof onChange.mock.calls[0][0]).toBe("string"); }); - it('should render as text input, not number input', () => { + it("should render as text input, not number input", () => { renderForm(); - const input = screen.getByRole('textbox'); - expect(input).toHaveProperty('type', 'text'); + const input = screen.getByRole("textbox"); + expect(input).toHaveProperty("type", "text"); }); }); }); -describe('DynamicJsonForm Integer Fields', () => { +describe("DynamicJsonForm Integer Fields", () => { const renderForm = (props = {}) => { const defaultProps = { schema: { type: "integer" as const, - description: "Test integer field" + description: "Test integer field", } satisfies JsonSchemaType, value: undefined, - onChange: jest.fn() + onChange: jest.fn(), }; return render(); }; - describe('Basic Operations', () => { - it('should render number input with step=1', () => { + describe("Basic Operations", () => { + it("should render number input with step=1", () => { renderForm(); - const input = screen.getByRole('spinbutton'); - expect(input).toHaveProperty('type', 'number'); - expect(input).toHaveProperty('step', '1'); + const input = screen.getByRole("spinbutton"); + expect(input).toHaveProperty("type", "number"); + expect(input).toHaveProperty("step", "1"); }); - it('should pass integer values to onChange', () => { + it("should pass integer values to onChange", () => { const onChange = jest.fn(); renderForm({ onChange }); - - const input = screen.getByRole('spinbutton'); - fireEvent.change(input, { target: { value: '42' } }); - + + const input = screen.getByRole("spinbutton"); + fireEvent.change(input, { target: { value: "42" } }); + expect(onChange).toHaveBeenCalledWith(42); // Verify the value is a number, not a string - expect(typeof onChange.mock.calls[0][0]).toBe('number'); + expect(typeof onChange.mock.calls[0][0]).toBe("number"); }); - it('should not pass string values to onChange', () => { + it("should not pass string values to onChange", () => { const onChange = jest.fn(); renderForm({ onChange }); - - const input = screen.getByRole('spinbutton'); - fireEvent.change(input, { target: { value: 'abc' } }); - + + const input = screen.getByRole("spinbutton"); + fireEvent.change(input, { target: { value: "abc" } }); + expect(onChange).not.toHaveBeenCalled(); }); }); - describe('Edge Cases', () => { - it('should handle non-numeric input by not calling onChange', () => { + describe("Edge Cases", () => { + it("should handle non-numeric input by not calling onChange", () => { const onChange = jest.fn(); renderForm({ onChange }); - - const input = screen.getByRole('spinbutton'); - fireEvent.change(input, { target: { value: 'abc' } }); - + + const input = screen.getByRole("spinbutton"); + fireEvent.change(input, { target: { value: "abc" } }); + expect(onChange).not.toHaveBeenCalled(); }); }); diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index 765de2f..8c0b313 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -1,31 +1,31 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, beforeEach, jest } from '@jest/globals'; -import Sidebar from '../Sidebar'; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, beforeEach, jest } from "@jest/globals"; +import Sidebar from "../Sidebar"; // Mock theme hook -jest.mock('../../lib/useTheme', () => ({ +jest.mock("../../lib/useTheme", () => ({ __esModule: true, - default: () => ['light', jest.fn()], + default: () => ["light", jest.fn()], })); -describe('Sidebar Environment Variables', () => { +describe("Sidebar Environment Variables", () => { const defaultProps = { - connectionStatus: 'disconnected' as const, - transportType: 'stdio' as const, + connectionStatus: "disconnected" as const, + transportType: "stdio" as const, setTransportType: jest.fn(), - command: '', + command: "", setCommand: jest.fn(), - args: '', + args: "", setArgs: jest.fn(), - sseUrl: '', + sseUrl: "", setSseUrl: jest.fn(), env: {}, setEnv: jest.fn(), - bearerToken: '', + bearerToken: "", setBearerToken: jest.fn(), onConnect: jest.fn(), stdErrNotifications: [], - logLevel: 'info' as const, + logLevel: "info" as const, sendLogLevelRequest: jest.fn(), loggingSupported: true, }; @@ -35,7 +35,7 @@ describe('Sidebar Environment Variables', () => { }; const openEnvVarsSection = () => { - const button = screen.getByText('Environment Variables'); + const button = screen.getByText("Environment Variables"); fireEvent.click(button); }; @@ -43,236 +43,236 @@ describe('Sidebar Environment Variables', () => { jest.clearAllMocks(); }); - describe('Basic Operations', () => { - it('should add a new environment variable', () => { + describe("Basic Operations", () => { + it("should add a new environment variable", () => { const setEnv = jest.fn(); renderSidebar({ env: {}, setEnv }); - + openEnvVarsSection(); - - const addButton = screen.getByText('Add Environment Variable'); + + const addButton = screen.getByText("Add Environment Variable"); fireEvent.click(addButton); - - expect(setEnv).toHaveBeenCalledWith({ '': '' }); + + expect(setEnv).toHaveBeenCalledWith({ "": "" }); }); - it('should remove an environment variable', () => { + it("should remove an environment variable", () => { const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: 'test_value' }; + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const removeButton = screen.getByRole('button', { name: 'Ɨ' }); + + const removeButton = screen.getByRole("button", { name: "Ɨ" }); fireEvent.click(removeButton); - + expect(setEnv).toHaveBeenCalledWith({}); }); - it('should update environment variable value', () => { + it("should update environment variable value", () => { const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: 'test_value' }; + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const valueInput = screen.getByDisplayValue('test_value'); - fireEvent.change(valueInput, { target: { value: 'new_value' } }); - - expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: 'new_value' }); + + const valueInput = screen.getByDisplayValue("test_value"); + fireEvent.change(valueInput, { target: { value: "new_value" } }); + + expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: "new_value" }); }); - it('should toggle value visibility', () => { - const initialEnv = { TEST_KEY: 'test_value' }; + it("should toggle value visibility", () => { + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv }); - + openEnvVarsSection(); - - const valueInput = screen.getByDisplayValue('test_value'); - expect(valueInput).toHaveProperty('type', 'password'); - - const toggleButton = screen.getByRole('button', { name: /show value/i }); + + const valueInput = screen.getByDisplayValue("test_value"); + expect(valueInput).toHaveProperty("type", "password"); + + const toggleButton = screen.getByRole("button", { name: /show value/i }); fireEvent.click(toggleButton); - - expect(valueInput).toHaveProperty('type', 'text'); + + expect(valueInput).toHaveProperty("type", "text"); }); }); - describe('Key Editing', () => { - it('should maintain order when editing first key', () => { + describe("Key Editing", () => { + it("should maintain order when editing first key", () => { const setEnv = jest.fn(); const initialEnv = { - FIRST_KEY: 'first_value', - SECOND_KEY: 'second_value', - THIRD_KEY: 'third_value', + FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + THIRD_KEY: "third_value", }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const firstKeyInput = screen.getByDisplayValue('FIRST_KEY'); - fireEvent.change(firstKeyInput, { target: { value: 'NEW_FIRST_KEY' } }); - + + const firstKeyInput = screen.getByDisplayValue("FIRST_KEY"); + fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } }); + expect(setEnv).toHaveBeenCalledWith({ - NEW_FIRST_KEY: 'first_value', - SECOND_KEY: 'second_value', - THIRD_KEY: 'third_value', + NEW_FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + THIRD_KEY: "third_value", }); }); - it('should maintain order when editing middle key', () => { + it("should maintain order when editing middle key", () => { const setEnv = jest.fn(); const initialEnv = { - FIRST_KEY: 'first_value', - SECOND_KEY: 'second_value', - THIRD_KEY: 'third_value', + FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + THIRD_KEY: "third_value", }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const middleKeyInput = screen.getByDisplayValue('SECOND_KEY'); - fireEvent.change(middleKeyInput, { target: { value: 'NEW_SECOND_KEY' } }); - + + const middleKeyInput = screen.getByDisplayValue("SECOND_KEY"); + fireEvent.change(middleKeyInput, { target: { value: "NEW_SECOND_KEY" } }); + expect(setEnv).toHaveBeenCalledWith({ - FIRST_KEY: 'first_value', - NEW_SECOND_KEY: 'second_value', - THIRD_KEY: 'third_value', + FIRST_KEY: "first_value", + NEW_SECOND_KEY: "second_value", + THIRD_KEY: "third_value", }); }); - it('should maintain order when editing last key', () => { + it("should maintain order when editing last key", () => { const setEnv = jest.fn(); const initialEnv = { - FIRST_KEY: 'first_value', - SECOND_KEY: 'second_value', - THIRD_KEY: 'third_value', + FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + THIRD_KEY: "third_value", }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const lastKeyInput = screen.getByDisplayValue('THIRD_KEY'); - fireEvent.change(lastKeyInput, { target: { value: 'NEW_THIRD_KEY' } }); - + + const lastKeyInput = screen.getByDisplayValue("THIRD_KEY"); + fireEvent.change(lastKeyInput, { target: { value: "NEW_THIRD_KEY" } }); + expect(setEnv).toHaveBeenCalledWith({ - FIRST_KEY: 'first_value', - SECOND_KEY: 'second_value', - NEW_THIRD_KEY: 'third_value', + FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + NEW_THIRD_KEY: "third_value", }); }); }); - describe('Multiple Operations', () => { - it('should maintain state after multiple key edits', () => { + describe("Multiple Operations", () => { + it("should maintain state after multiple key edits", () => { const setEnv = jest.fn(); const initialEnv = { - FIRST_KEY: 'first_value', - SECOND_KEY: 'second_value', + FIRST_KEY: "first_value", + SECOND_KEY: "second_value", }; const { rerender } = renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - + // First key edit - const firstKeyInput = screen.getByDisplayValue('FIRST_KEY'); - fireEvent.change(firstKeyInput, { target: { value: 'NEW_FIRST_KEY' } }); - + const firstKeyInput = screen.getByDisplayValue("FIRST_KEY"); + fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } }); + // Get the updated env from the first setEnv call - const updatedEnv = (setEnv.mock.calls[0][0] as Record); - + const updatedEnv = setEnv.mock.calls[0][0] as Record; + // Rerender with the updated env rerender(); - + // Second key edit - const secondKeyInput = screen.getByDisplayValue('SECOND_KEY'); - fireEvent.change(secondKeyInput, { target: { value: 'NEW_SECOND_KEY' } }); - + const secondKeyInput = screen.getByDisplayValue("SECOND_KEY"); + fireEvent.change(secondKeyInput, { target: { value: "NEW_SECOND_KEY" } }); + // Verify the final state matches what we expect expect(setEnv).toHaveBeenLastCalledWith({ - NEW_FIRST_KEY: 'first_value', - NEW_SECOND_KEY: 'second_value', + NEW_FIRST_KEY: "first_value", + NEW_SECOND_KEY: "second_value", }); }); - it('should maintain visibility state after key edit', () => { - const initialEnv = { TEST_KEY: 'test_value' }; + it("should maintain visibility state after key edit", () => { + const initialEnv = { TEST_KEY: "test_value" }; const { rerender } = renderSidebar({ env: initialEnv }); - + openEnvVarsSection(); - + // Show the value - const toggleButton = screen.getByRole('button', { name: /show value/i }); + const toggleButton = screen.getByRole("button", { name: /show value/i }); fireEvent.click(toggleButton); - - const valueInput = screen.getByDisplayValue('test_value'); - expect(valueInput).toHaveProperty('type', 'text'); - + + const valueInput = screen.getByDisplayValue("test_value"); + expect(valueInput).toHaveProperty("type", "text"); + // Edit the key - const keyInput = screen.getByDisplayValue('TEST_KEY'); - fireEvent.change(keyInput, { target: { value: 'NEW_KEY' } }); - + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "NEW_KEY" } }); + // Rerender with updated env - rerender(); - + rerender(); + // Value should still be visible - const updatedValueInput = screen.getByDisplayValue('test_value'); - expect(updatedValueInput).toHaveProperty('type', 'text'); + const updatedValueInput = screen.getByDisplayValue("test_value"); + expect(updatedValueInput).toHaveProperty("type", "text"); }); }); - describe('Edge Cases', () => { - it('should handle empty key', () => { + describe("Edge Cases", () => { + it("should handle empty key", () => { const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: 'test_value' }; + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const keyInput = screen.getByDisplayValue('TEST_KEY'); - fireEvent.change(keyInput, { target: { value: '' } }); - - expect(setEnv).toHaveBeenCalledWith({ '': 'test_value' }); + + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "" } }); + + expect(setEnv).toHaveBeenCalledWith({ "": "test_value" }); }); - it('should handle special characters in key', () => { + it("should handle special characters in key", () => { const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: 'test_value' }; + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const keyInput = screen.getByDisplayValue('TEST_KEY'); - fireEvent.change(keyInput, { target: { value: 'TEST-KEY@123' } }); - - expect(setEnv).toHaveBeenCalledWith({ 'TEST-KEY@123': 'test_value' }); + + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "TEST-KEY@123" } }); + + expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" }); }); - it('should handle unicode characters', () => { + it("should handle unicode characters", () => { const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: 'test_value' }; + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const keyInput = screen.getByDisplayValue('TEST_KEY'); - fireEvent.change(keyInput, { target: { value: 'TEST_šŸ”‘' } }); - - expect(setEnv).toHaveBeenCalledWith({ 'TEST_šŸ”‘': 'test_value' }); + + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "TEST_šŸ”‘" } }); + + expect(setEnv).toHaveBeenCalledWith({ "TEST_šŸ”‘": "test_value" }); }); - it('should handle very long key names', () => { + it("should handle very long key names", () => { const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: 'test_value' }; + const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); - + openEnvVarsSection(); - - const keyInput = screen.getByDisplayValue('TEST_KEY'); - const longKey = 'A'.repeat(100); + + const keyInput = screen.getByDisplayValue("TEST_KEY"); + const longKey = "A".repeat(100); fireEvent.change(keyInput, { target: { value: longKey } }); - - expect(setEnv).toHaveBeenCalledWith({ [longKey]: 'test_value' }); + + expect(setEnv).toHaveBeenCalledWith({ [longKey]: "test_value" }); }); }); }); From a7f25153c445769add41e89dfaf15b7f5c60971f Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 09:18:58 -0700 Subject: [PATCH 20/37] Add failing ToolsTab test that should get fixed with pull/198 --- .../components/__tests__/ToolsTab.test.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 client/src/components/__tests__/ToolsTab.test.tsx diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx new file mode 100644 index 0000000..2688b52 --- /dev/null +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -0,0 +1,72 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, jest } from "@jest/globals"; +import ToolsTab from "../ToolsTab"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { Tabs } from "@/components/ui/tabs"; + +describe("ToolsTab", () => { + const mockTools: Tool[] = [ + { + name: "tool1", + description: "First tool", + inputSchema: { + type: "object" as const, + properties: { + num: { type: "number" as const } + } + } + }, + { + name: "tool2", + description: "Second tool", + inputSchema: { + type: "object" as const, + properties: { + num: { type: "number" as const } + } + } + } + ]; + + const defaultProps = { + tools: mockTools, + listTools: jest.fn(), + clearTools: jest.fn(), + callTool: jest.fn(), + selectedTool: null, + setSelectedTool: jest.fn(), + toolResult: null, + nextCursor: "", + error: null + }; + + const renderToolsTab = (props = {}) => { + return render( + + + + ); + }; + + it("should reset input values when switching tools", () => { + const { rerender } = renderToolsTab({ + selectedTool: mockTools[0] + }); + + // Enter a value in the first tool's input + const input = screen.getByRole("spinbutton") as HTMLInputElement; + fireEvent.change(input, { target: { value: "42" } }); + expect(input.value).toBe("42"); + + // Switch to second tool + rerender( + + + + ); + + // Verify input is reset + const newInput = screen.getByRole("spinbutton") as HTMLInputElement; + expect(newInput.value).toBe(""); + }); +}); From b7fa23676a6255176dad029dd1e79252ccba9cb4 Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 09:27:51 -0700 Subject: [PATCH 21/37] Fix formatting --- .../components/__tests__/ToolsTab.test.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 2688b52..2a45065 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -12,9 +12,9 @@ describe("ToolsTab", () => { inputSchema: { type: "object" as const, properties: { - num: { type: "number" as const } - } - } + num: { type: "number" as const }, + }, + }, }, { name: "tool2", @@ -22,10 +22,10 @@ describe("ToolsTab", () => { inputSchema: { type: "object" as const, properties: { - num: { type: "number" as const } - } - } - } + num: { type: "number" as const }, + }, + }, + }, ]; const defaultProps = { @@ -37,20 +37,20 @@ describe("ToolsTab", () => { setSelectedTool: jest.fn(), toolResult: null, nextCursor: "", - error: null + error: null, }; const renderToolsTab = (props = {}) => { return render( - + , ); }; it("should reset input values when switching tools", () => { const { rerender } = renderToolsTab({ - selectedTool: mockTools[0] + selectedTool: mockTools[0], }); // Enter a value in the first tool's input @@ -62,7 +62,7 @@ describe("ToolsTab", () => { rerender( - + , ); // Verify input is reset From 379486b5ea4d4ab2e2faae5a96c79276e374234f Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 09:43:48 -0700 Subject: [PATCH 22/37] Add failing test for pull/206 --- .../src/components/__tests__/Sidebar.test.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index 8c0b313..fc8a5f4 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -161,6 +161,31 @@ describe("Sidebar Environment Variables", () => { NEW_THIRD_KEY: "third_value", }); }); + + it("should maintain order during key editing", () => { + const setEnv = jest.fn(); + const initialEnv = { + KEY1: "value1", + KEY2: "value2" + }; + renderSidebar({ env: initialEnv, setEnv }); + + openEnvVarsSection(); + + // Type "NEW_" one character at a time + const key1Input = screen.getByDisplayValue("KEY1"); + "NEW_".split("").forEach((char) => { + fireEvent.change(key1Input, { target: { value: char + "KEY1".slice(1) } }); + }); + + // Verify the last setEnv call maintains the order + const lastCall = setEnv.mock.calls[setEnv.mock.calls.length - 1][0] as Record; + const entries = Object.entries(lastCall); + + // The values should stay with their original keys + expect(entries[0][1]).toBe("value1"); // First entry should still have value1 + expect(entries[1][1]).toBe("value2"); // Second entry should still have value2 + }); }); describe("Multiple Operations", () => { From 65a0d46816aca50e87643e325d80160a4743b33b Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 09:47:34 -0700 Subject: [PATCH 23/37] Fix formatting --- client/src/components/__tests__/Sidebar.test.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index fc8a5f4..710c80d 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -166,7 +166,7 @@ describe("Sidebar Environment Variables", () => { const setEnv = jest.fn(); const initialEnv = { KEY1: "value1", - KEY2: "value2" + KEY2: "value2", }; renderSidebar({ env: initialEnv, setEnv }); @@ -175,13 +175,17 @@ describe("Sidebar Environment Variables", () => { // Type "NEW_" one character at a time const key1Input = screen.getByDisplayValue("KEY1"); "NEW_".split("").forEach((char) => { - fireEvent.change(key1Input, { target: { value: char + "KEY1".slice(1) } }); + fireEvent.change(key1Input, { + target: { value: char + "KEY1".slice(1) }, + }); }); // Verify the last setEnv call maintains the order - const lastCall = setEnv.mock.calls[setEnv.mock.calls.length - 1][0] as Record; + const lastCall = setEnv.mock.calls[ + setEnv.mock.calls.length - 1 + ][0] as Record; const entries = Object.entries(lastCall); - + // The values should stay with their original keys expect(entries[0][1]).toBe("value1"); // First entry should still have value1 expect(entries[1][1]).toBe("value2"); // Second entry should still have value2 From f0b28d476040f4fa14fdb7bc07cdecd8faaa2d07 Mon Sep 17 00:00:00 2001 From: cgoing Date: Tue, 25 Mar 2025 01:48:29 +0900 Subject: [PATCH 24/37] feat: json view component --- client/src/components/History.tsx | 13 +- client/src/components/JsonView.tsx | 210 +++++++++++++++++++++++++ client/src/components/ResourcesTab.tsx | 3 +- client/src/components/SamplingTab.tsx | 3 +- client/src/components/ToolsTab.tsx | 11 +- 5 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 client/src/components/JsonView.tsx diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index 532d3b1..7ce7dcf 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -1,6 +1,7 @@ import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; import { Copy } from "lucide-react"; import { useState } from "react"; +import JsonView from "./JsonView"; const HistoryAndNotifications = ({ requestHistory, @@ -75,7 +76,7 @@ const HistoryAndNotifications = ({
-                          {JSON.stringify(JSON.parse(request.request), null, 2)}
+                          
                         
{request.response && ( @@ -92,11 +93,7 @@ const HistoryAndNotifications = ({
-                            {JSON.stringify(
-                              JSON.parse(request.response),
-                              null,
-                              2,
-                            )}
+                            
                           
)} @@ -147,7 +144,9 @@ const HistoryAndNotifications = ({
-                        {JSON.stringify(notification, null, 2)}
+                        
                       
)} diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx new file mode 100644 index 0000000..21d65d8 --- /dev/null +++ b/client/src/components/JsonView.tsx @@ -0,0 +1,210 @@ +import { useState, memo } from "react"; +import { JsonValue } from "./DynamicJsonForm"; + +interface JsonViewProps { + data: JsonValue; + name?: string; + initialExpandDepth?: number; +} + +function tryParseJson(str: string): { success: boolean; data: JsonValue } { + const trimmed = str.trim(); + if ( + !(trimmed.startsWith("{") && trimmed.endsWith("}")) && + !(trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + return { success: false, data: str }; + } + try { + return { success: true, data: JSON.parse(str) }; + } catch { + return { success: false, data: str }; + } +} + +const JsonView = memo( + ({ data, name, initialExpandDepth = 2 }: JsonViewProps) => { + const normalizedData = + typeof data === "string" + ? tryParseJson(data).success + ? tryParseJson(data).data + : data + : data; + + return ( +
+ +
+ ); + }, +); + +JsonView.displayName = "JsonView"; + +interface JsonNodeProps { + data: JsonValue; + name?: string; + depth: number; + initialExpandDepth: number; +} + +const JsonNode = memo( + ({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => { + const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); + + const getDataType = (value: JsonValue): string => { + if (Array.isArray(value)) return "array"; + if (value === null) return "null"; + return typeof value; + }; + + const dataType = getDataType(data); + + const typeStyleMap: Record = { + number: "text-blue-600", + boolean: "text-amber-600", + null: "text-purple-600", + undefined: "text-gray-600", + string: "text-green-600", + default: "text-gray-700", + }; + + const renderCollapsible = (isArray: boolean) => { + const items = isArray + ? (data as JsonValue[]) + : Object.entries(data as Record); + const itemCount = items.length; + const isEmpty = itemCount === 0; + + const symbolMap = { + open: isArray ? "[" : "{", + close: isArray ? "]" : "}", + collapsed: isArray ? "[ ... ]" : "{ ... }", + empty: isArray ? "[]" : "{}", + }; + + if (isEmpty) { + return ( +
+ {name && {name}:} + {symbolMap.empty} +
+ ); + } + + return ( +
+
setIsExpanded(!isExpanded)} + > + {name && ( + + {name}: + + )} + {isExpanded ? ( + + {symbolMap.open} + + ) : ( + <> + + {symbolMap.collapsed} + + + {itemCount} {itemCount === 1 ? "item" : "items"} + + + )} +
+ {isExpanded && ( + <> +
+ {isArray + ? (items as JsonValue[]).map((item, index) => ( +
+ +
+ )) + : (items as [string, JsonValue][]).map(([key, value]) => ( +
+ +
+ ))} +
+
{symbolMap.close}
+ + )} +
+ ); + }; + + const renderString = (value: string) => { + const maxLength = 100; + const isTooLong = value.length > maxLength; + + if (!isTooLong) { + return ( +
+ {name && {name}:} + "{value}" +
+ ); + } + + return ( +
+ {name && ( + + {name}: + + )} + setIsExpanded(!isExpanded)} + title={isExpanded ? "ķ“ė¦­ķ•˜ģ—¬ ģ¶•ģ†Œ" : "ķ“ė¦­ķ•˜ģ—¬ 전첓 볓기"} + > + {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`} + +
+ ); + }; + + switch (dataType) { + case "object": + case "array": + return renderCollapsible(dataType === "array"); + case "string": + return renderString(data as string); + default: + return ( +
+ {name && {name}:} + + {data === null ? "null" : String(data)} + +
+ ); + } + }, +); + +JsonNode.displayName = "JsonNode"; + +export default JsonView; diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index f000840..bc3e39a 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -15,6 +15,7 @@ import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react"; import ListPane from "./ListPane"; import { useEffect, useState } from "react"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; +import JsonView from "./JsonView"; const ResourcesTab = ({ resources, @@ -215,7 +216,7 @@ const ResourcesTab = ({ ) : selectedResource ? (
-              {resourceContent}
+              
             
) : selectedTemplate ? (
diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index 5c45400..d14d4c8 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -5,6 +5,7 @@ import { CreateMessageRequest, CreateMessageResult, } from "@modelcontextprotocol/sdk/types.js"; +import JsonView from "./JsonView"; export type PendingRequest = { id: number; @@ -44,7 +45,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => { {pendingRequests.map((request) => (
-              {JSON.stringify(request.request, null, 2)}
+              
             
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 82ebdb0..345f7bd 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -17,6 +17,7 @@ import { AlertCircle, Send } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { escapeUnicode } from "@/utils/escapeUnicode"; +import JsonView from "./JsonView"; const ToolsTab = ({ tools, @@ -54,7 +55,7 @@ const ToolsTab = ({ <>

Invalid Tool Result:

-              {escapeUnicode(toolResult)}
+              
             

Errors:

{parsedResult.error.errors.map((error, idx) => ( @@ -62,7 +63,7 @@ const ToolsTab = ({ key={idx} className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64" > - {escapeUnicode(error)} + ))} @@ -80,7 +81,7 @@ const ToolsTab = ({
{item.type === "text" && (
-                  {item.text}
+                  
                 
)} {item.type === "image" && ( @@ -101,7 +102,7 @@ const ToolsTab = ({ ) : (
-                    {escapeUnicode(item.resource)}
+                    
                   
))}
@@ -113,7 +114,7 @@ const ToolsTab = ({ <>

Tool Result (Legacy):

-            {escapeUnicode(toolResult.toolResult)}
+            
           
); From d204dd6e7e9b5b8a72232b14a350eccb1e8ceb2a Mon Sep 17 00:00:00 2001 From: cgoing Date: Tue, 25 Mar 2025 01:56:53 +0900 Subject: [PATCH 25/37] feat: json view component - dark color --- client/src/components/JsonView.tsx | 36 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 21d65d8..7d3c586 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -91,7 +91,11 @@ const JsonNode = memo( if (isEmpty) { return (
- {name && {name}:} + {name && ( + + {name}: + + )} {symbolMap.empty}
); @@ -100,24 +104,24 @@ const JsonNode = memo( return (
setIsExpanded(!isExpanded)} > {name && ( - + {name}: )} {isExpanded ? ( - + {symbolMap.open} ) : ( <> - + {symbolMap.collapsed} - + {itemCount} {itemCount === 1 ? "item" : "items"} @@ -125,7 +129,7 @@ const JsonNode = memo(
{isExpanded && ( <> -
+
{isArray ? (items as JsonValue[]).map((item, index) => (
@@ -148,7 +152,9 @@ const JsonNode = memo(
))}
-
{symbolMap.close}
+
+ {symbolMap.close} +
)}
@@ -162,7 +168,11 @@ const JsonNode = memo( if (!isTooLong) { return (
- {name && {name}:} + {name && ( + + {name}: + + )} "{value}"
); @@ -171,7 +181,7 @@ const JsonNode = memo( return (
{name && ( - + {name}: )} @@ -195,7 +205,11 @@ const JsonNode = memo( default: return (
- {name && {name}:} + {name && ( + + {name}: + + )} {data === null ? "null" : String(data)} From 16b38071e7554eb5556491f2f04339e58c5a073c Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Mon, 24 Mar 2025 12:36:17 -0700 Subject: [PATCH 26/37] Bump version to 0.7.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 0471765..30cf37f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.6.0", + "version": "0.7.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 68929c5..eec3c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "workspaces": [ "client", "server" ], "dependencies": { - "@modelcontextprotocol/inspector-client": "^0.6.0", - "@modelcontextprotocol/inspector-server": "^0.6.0", + "@modelcontextprotocol/inspector-client": "^0.7.0", + "@modelcontextprotocol/inspector-server": "^0.7.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.6.0", + "version": "0.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", @@ -11299,7 +11299,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", diff --git a/package.json b/package.json index e3bce92..66cc1a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.6.0", + "version": "0.7.0", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -34,8 +34,8 @@ "publish-all": "npm publish --workspaces --access public && npm publish --access public" }, "dependencies": { - "@modelcontextprotocol/inspector-client": "^0.6.0", - "@modelcontextprotocol/inspector-server": "^0.6.0", + "@modelcontextprotocol/inspector-client": "^0.7.0", + "@modelcontextprotocol/inspector-server": "^0.7.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 5d8839f..732993f 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.6.0", + "version": "0.7.0", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 2588f3aeb3c7518d7a3e87e2529692e4bc86841b Mon Sep 17 00:00:00 2001 From: cgoing Date: Wed, 26 Mar 2025 00:02:10 +0900 Subject: [PATCH 27/37] Change tooltip title from Korean to English --- 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 7d3c586..9add405 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -188,7 +188,7 @@ const JsonNode = memo( setIsExpanded(!isExpanded)} - title={isExpanded ? "ķ“ė¦­ķ•˜ģ—¬ ģ¶•ģ†Œ" : "ķ“ė¦­ķ•˜ģ—¬ 전첓 볓기"} + title={isExpanded ? "Click to collapse" : "Click to expand"} > {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`} From 03c1ba3092d748f60beeb1ef9b5e78be35b98c84 Mon Sep 17 00:00:00 2001 From: cgoing Date: Wed, 26 Mar 2025 00:11:07 +0900 Subject: [PATCH 28/37] Change JsonView default initialExpandDepth from 2 to 3 --- 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 9add405..6c348fe 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -23,7 +23,7 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } { } const JsonView = memo( - ({ data, name, initialExpandDepth = 2 }: JsonViewProps) => { + ({ data, name, initialExpandDepth = 3 }: JsonViewProps) => { const normalizedData = typeof data === "string" ? tryParseJson(data).success From e6f5da838301d7dcc6a263aa97f586ccae56d068 Mon Sep 17 00:00:00 2001 From: cgoing Date: Thu, 27 Mar 2025 10:49:37 +0900 Subject: [PATCH 29/37] Improve JsonView component styling and change to use JsonView in PromptsTab --- client/src/components/JsonView.tsx | 10 +++++++--- client/src/components/PromptsTab.tsx | 11 +++++------ client/src/components/ToolsTab.tsx | 23 ++++++++++------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 6c348fe..e20dd5c 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,5 +1,6 @@ import { useState, memo } from "react"; import { JsonValue } from "./DynamicJsonForm"; +import clsx from "clsx"; interface JsonViewProps { data: JsonValue; @@ -70,7 +71,7 @@ const JsonNode = memo( boolean: "text-amber-600", null: "text-purple-600", undefined: "text-gray-600", - string: "text-green-600", + string: "text-green-600 break-all", default: "text-gray-700", }; @@ -173,7 +174,7 @@ const JsonNode = memo( {name}: )} - "{value}" +

"{value}"

); } @@ -186,7 +187,10 @@ const JsonNode = memo(
)} setIsExpanded(!isExpanded)} title={isExpanded ? "Click to collapse" : "Click to expand"} > diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index f88b16f..b42cf77 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Combobox } from "@/components/ui/combobox"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; + import { ListPromptsResult, PromptReference, @@ -13,6 +13,7 @@ import { AlertCircle } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { useCompletionState } from "@/lib/hooks/useCompletionState"; +import JsonView from "./JsonView"; export type Prompt = { name: string; @@ -151,11 +152,9 @@ const PromptsTab = ({ Get Prompt {promptContent && ( -