From f514857a3401c05826d73cb94854886b4f0f1aa4 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 19 May 2025 05:31:24 -0700 Subject: [PATCH 01/36] Apply process env after defaults --- cli/src/transport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/transport.ts b/cli/src/transport.ts index e693f246..22dde73b 100644 --- a/cli/src/transport.ts +++ b/cli/src/transport.ts @@ -38,8 +38,8 @@ function createStdioTransport(options: TransportOptions): Transport { const defaultEnv = getDefaultEnvironment(); const env: Record = { - ...processEnv, ...defaultEnv, + ...processEnv, }; const { cmd: actualCommand, args: actualArgs } = findActualExecutable( From f1401b143660aac71ffa3aab577ad88e11f09f89 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 19 May 2025 10:14:26 -0700 Subject: [PATCH 02/36] Apply process env after defaults for UI mode, too --- server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index c967b60c..97dbe941 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -59,8 +59,8 @@ const createTransport = async (req: express.Request): Promise => { if (transportType === "stdio") { const command = query.command as string; const origArgs = shellParseArgs(query.args as string) as string[]; - const queryEnv = query.env ? JSON.parse(query.env as string) : {}; - const env = { ...process.env, ...defaultEnvironment, ...queryEnv }; + const queryEnv = query.env ? JSON.parse(query.env as string) : {}; + const env = { ...defaultEnvironment, ...process.env, ...queryEnv }; const { cmd, args } = findActualExecutable(command, origArgs); From 9655841399999fa51f4e16a30511901428428984 Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 27 May 2025 09:01:29 -0700 Subject: [PATCH 03/36] Fix formatting --- server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 97dbe941..c113d1ea 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -59,8 +59,8 @@ const createTransport = async (req: express.Request): Promise => { if (transportType === "stdio") { const command = query.command as string; const origArgs = shellParseArgs(query.args as string) as string[]; - const queryEnv = query.env ? JSON.parse(query.env as string) : {}; - const env = { ...defaultEnvironment, ...process.env, ...queryEnv }; + const queryEnv = query.env ? JSON.parse(query.env as string) : {}; + const env = { ...defaultEnvironment, ...process.env, ...queryEnv }; const { cmd, args } = findActualExecutable(command, origArgs); From 0194e5f62a8bc4bb3772e220db1673f1beadfaf4 Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 4 Aug 2025 14:35:33 -0400 Subject: [PATCH 04/36] Add warning for DANGEROUSLY_OMIT_AUTH usage Added warning about disabling authentication and its risks. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6a671f1c..6eaa7e4b 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,16 @@ If you need to disable authentication (NOT RECOMMENDED), you can set the `DANGER DANGEROUSLY_OMIT_AUTH=true npm start ``` +--- + +**🚨 WARNING 🚨** + +Disabling authentication with `DANGEROUSLY_OMIT_AUTH` is incredibly dangerous! Disabling auth leaves your machine open to attack not just when exposed to the public internet, but also **via your web browser**. Meaning, visiting a malicious website OR viewing a malicious advertizement could allow an attacker to remotely compromise your computer. Do not disable this feature unless you truly understand the risks. + +Read more about the risks of this vulnerability on Oligo's blog: [Critical RCE Vulnerability in Anthropic MCP Inspector - CVE-2025-49596](https://www.oligo.security/blog/critical-rce-vulnerability-in-anthropic-mcp-inspector-cve-2025-49596) + +--- + You can also set the token via the `MCP_PROXY_AUTH_TOKEN` environment variable when starting the server: ```bash From fc4aeb392fa942048870ee7fb36f7ea1b435be86 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 13 Aug 2025 14:00:08 -0700 Subject: [PATCH 05/36] Restore AuthDebuggerState.resource as URL Not doing so prevents the resource URI from getting sent to the OAuth server's token endpoint during the "Quick OAuth flow" --- client/src/components/OAuthDebugCallback.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx index 99774297..e7a566f9 100644 --- a/client/src/components/OAuthDebugCallback.tsx +++ b/client/src/components/OAuthDebugCallback.tsx @@ -46,6 +46,9 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => { if (storedState) { try { restoredState = JSON.parse(storedState); + if (typeof restoredState.resource === "string") { + restoredState.resource = new URL(restoredState.resource); + } // Clean up the stored state sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); } catch (e) { From 10b4681e3c6de348388891f5c7f461457ae2f630 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 14 Aug 2025 10:47:15 -0700 Subject: [PATCH 06/36] Also fix authorizationUrl type --- client/src/components/OAuthDebugCallback.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx index e7a566f9..67ddc926 100644 --- a/client/src/components/OAuthDebugCallback.tsx +++ b/client/src/components/OAuthDebugCallback.tsx @@ -46,9 +46,12 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => { if (storedState) { try { restoredState = JSON.parse(storedState); - if (typeof restoredState.resource === "string") { + if (restoredState && typeof restoredState.resource === "string") { restoredState.resource = new URL(restoredState.resource); } + if (restoredState && typeof restoredState.authorizationUrl === "string") { + restoredState.authorizationUrl = new URL(restoredState.authorizationUrl); + } // Clean up the stored state sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); } catch (e) { From 8d8af870ba0a9c46342a0c74f2510a5df59361c5 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 14 Aug 2025 00:14:30 -0700 Subject: [PATCH 07/36] Trim whitespace from `command` on blur --- client/src/components/Sidebar.tsx | 1 + .../src/components/__tests__/Sidebar.test.tsx | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index a41e4bfc..4691c99c 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -268,6 +268,7 @@ const Sidebar = ({ placeholder="Command" value={command} onChange={(e) => setCommand(e.target.value)} + onBlur={(e) => setCommand(e.target.value.trim())} className="font-mono" /> diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index e892a7f8..b4dcb684 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -878,4 +878,36 @@ describe("Sidebar Environment Variables", () => { expect(mockClipboardWrite).toHaveBeenCalledWith(expectedConfig); }); }); + + describe("Command and arguments", () => { + it("should trim whitespace from command input on blur", () => { + const setCommand = jest.fn(); + renderSidebar({ command: " node server.js ", setCommand }); + + const commandInput = screen.getByLabelText("Command"); + + fireEvent.blur(commandInput); + expect(setCommand).toHaveBeenLastCalledWith("node server.js"); + }); + + it("should handle whitespace-only command input on blur", () => { + const setCommand = jest.fn(); + renderSidebar({ command: " ", setCommand }); + + const commandInput = screen.getByLabelText("Command"); + + fireEvent.blur(commandInput); + expect(setCommand).toHaveBeenLastCalledWith(""); + }); + + it("should not affect command without surrounding whitespace", () => { + const setCommand = jest.fn(); + renderSidebar({ command: "node", setCommand }); + + const commandInput = screen.getByLabelText("Command"); + + fireEvent.blur(commandInput); + expect(setCommand).toHaveBeenLastCalledWith("node"); + }); + }); }); From 9a1540c79ab376fe9ec6c4a922a37990c3d64022 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 14 Aug 2025 00:15:19 -0700 Subject: [PATCH 08/36] Trim whitespace from `command` query parameter --- server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index 34a69414..7e0f151c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -181,7 +181,7 @@ const createTransport = async (req: express.Request): Promise => { const transportType = query.transportType as string; if (transportType === "stdio") { - const command = query.command as string; + const command = (query.command as string).trim(); const origArgs = shellParseArgs(query.args as string) as string[]; const queryEnv = query.env ? JSON.parse(query.env as string) : {}; const env = { ...process.env, ...defaultEnvironment, ...queryEnv }; From 399e16afa4eeee8b7b4ad7a43800f78b9c4976de Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 14 Aug 2025 10:40:32 -0700 Subject: [PATCH 09/36] Re-organize Sidebar tests - Tests were mistakenly grouped in Environment Variables block - Tests follow UI order, top-to-bottom --- .../src/components/__tests__/Sidebar.test.tsx | 1048 +++++++++-------- 1 file changed, 529 insertions(+), 519 deletions(-) diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index b4dcb684..dc737896 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -69,586 +69,325 @@ describe("Sidebar Environment Variables", () => { ); }; - const openEnvVarsSection = () => { - const button = screen.getByTestId("env-vars-button"); - fireEvent.click(button); - }; - beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); }); - describe("Basic Operations", () => { - it("should add a new environment variable", () => { - const setEnv = jest.fn(); - renderSidebar({ env: {}, setEnv }); + describe("Command and arguments", () => { + it("should trim whitespace from command input on blur", () => { + const setCommand = jest.fn(); + renderSidebar({ command: " node server.js ", setCommand }); - openEnvVarsSection(); + const commandInput = screen.getByLabelText("Command"); - const addButton = screen.getByText("Add Environment Variable"); - fireEvent.click(addButton); - - expect(setEnv).toHaveBeenCalledWith({ "": "" }); + fireEvent.blur(commandInput); + expect(setCommand).toHaveBeenLastCalledWith("node server.js"); }); - it("should remove an environment variable", () => { - const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: "test_value" }; - renderSidebar({ env: initialEnv, setEnv }); + it("should handle whitespace-only command input on blur", () => { + const setCommand = jest.fn(); + renderSidebar({ command: " ", setCommand }); - openEnvVarsSection(); + const commandInput = screen.getByLabelText("Command"); - const removeButton = screen.getByRole("button", { name: "Γ—" }); - fireEvent.click(removeButton); - - expect(setEnv).toHaveBeenCalledWith({}); + fireEvent.blur(commandInput); + expect(setCommand).toHaveBeenLastCalledWith(""); }); - it("should update environment variable value", () => { - const setEnv = jest.fn(); - const initialEnv = { TEST_KEY: "test_value" }; - renderSidebar({ env: initialEnv, setEnv }); + it("should not affect command without surrounding whitespace", () => { + const setCommand = jest.fn(); + renderSidebar({ command: "node", setCommand }); - openEnvVarsSection(); + const commandInput = screen.getByLabelText("Command"); - 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"); + fireEvent.blur(commandInput); + expect(setCommand).toHaveBeenLastCalledWith("node"); }); }); - describe("Authentication", () => { - const openAuthSection = () => { - const button = screen.getByTestId("auth-button"); + describe("Environment Variables", () => { + const openEnvVarsSection = () => { + const button = screen.getByTestId("env-vars-button"); fireEvent.click(button); }; - it("should update bearer token", () => { - const setBearerToken = jest.fn(); - renderSidebar({ - bearerToken: "", - setBearerToken, - transportType: "sse", // Set transport type to SSE + 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({ "": "" }); }); - openAuthSection(); + it("should remove an environment variable", () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: "test_value" }; + renderSidebar({ env: initialEnv, setEnv }); - const tokenInput = screen.getByTestId("bearer-token-input"); - fireEvent.change(tokenInput, { target: { value: "new_token" } }); + openEnvVarsSection(); - expect(setBearerToken).toHaveBeenCalledWith("new_token"); - }); + const removeButton = screen.getByRole("button", { name: "Γ—" }); + fireEvent.click(removeButton); - it("should update header name", () => { - const setHeaderName = jest.fn(); - renderSidebar({ - headerName: "Authorization", - setHeaderName, - transportType: "sse", + expect(setEnv).toHaveBeenCalledWith({}); }); - openAuthSection(); + it("should update environment variable value", () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: "test_value" }; + renderSidebar({ env: initialEnv, setEnv }); - const headerInput = screen.getByTestId("header-input"); - fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } }); + openEnvVarsSection(); - expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth"); - }); + const valueInput = screen.getByDisplayValue("test_value"); + fireEvent.change(valueInput, { target: { value: "new_value" } }); - it("should clear bearer token", () => { - const setBearerToken = jest.fn(); - renderSidebar({ - bearerToken: "existing_token", - setBearerToken, - transportType: "sse", // Set transport type to SSE + expect(setEnv).toHaveBeenCalledWith({ TEST_KEY: "new_value" }); }); - openAuthSection(); + it("should toggle value visibility", () => { + const initialEnv = { TEST_KEY: "test_value" }; + renderSidebar({ env: initialEnv }); - const tokenInput = screen.getByTestId("bearer-token-input"); - fireEvent.change(tokenInput, { target: { value: "" } }); + openEnvVarsSection(); - expect(setBearerToken).toHaveBeenCalledWith(""); - }); + const valueInput = screen.getByDisplayValue("test_value"); + expect(valueInput).toHaveProperty("type", "password"); - it("should properly render bearer token input", () => { - const { rerender } = renderSidebar({ - bearerToken: "existing_token", - transportType: "sse", // Set transport type to SSE - }); + const toggleButton = screen.getByRole("button", { + name: /show value/i, + }); + fireEvent.click(toggleButton); - openAuthSection(); - - // Token input should be a password field - const tokenInput = screen.getByTestId("bearer-token-input"); - expect(tokenInput).toHaveProperty("type", "password"); - - // Update the token - fireEvent.change(tokenInput, { target: { value: "new_token" } }); - - // Rerender with updated token - rerender( - - - , - ); - - // Token input should still exist after update - expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument(); - }); - - it("should maintain token visibility state after update", () => { - const { rerender } = renderSidebar({ - bearerToken: "existing_token", - transportType: "sse", // Set transport type to SSE - }); - - openAuthSection(); - - // Token input should be a password field - const tokenInput = screen.getByTestId("bearer-token-input"); - expect(tokenInput).toHaveProperty("type", "password"); - - // Update the token - fireEvent.change(tokenInput, { target: { value: "new_token" } }); - - // Rerender with updated token - rerender( - - - , - ); - - // Token input should still exist after update - expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument(); - }); - - it("should maintain header name when toggling auth section", () => { - renderSidebar({ - headerName: "X-API-Key", - transportType: "sse", - }); - - // Open auth section - openAuthSection(); - - // Verify header name is displayed - const headerInput = screen.getByTestId("header-input"); - expect(headerInput).toHaveValue("X-API-Key"); - - // Close auth section - const authButton = screen.getByTestId("auth-button"); - fireEvent.click(authButton); - - // Reopen auth section - fireEvent.click(authButton); - - // Verify header name is still preserved - expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key"); - }); - - it("should display default header name when not specified", () => { - renderSidebar({ - headerName: undefined, - transportType: "sse", - }); - - openAuthSection(); - - const headerInput = screen.getByTestId("header-input"); - expect(headerInput).toHaveAttribute("placeholder", "Authorization"); - }); - }); - - 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", + expect(valueInput).toHaveProperty("type", "text"); }); }); - 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 }); + 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(); + openEnvVarsSection(); - const middleKeyInput = screen.getByDisplayValue("SECOND_KEY"); - fireEvent.change(middleKeyInput, { target: { value: "NEW_SECOND_KEY" } }); + const firstKeyInput = screen.getByDisplayValue("FIRST_KEY"); + fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_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", - }); - }); - - 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) }, + expect(setEnv).toHaveBeenCalledWith({ + NEW_FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + THIRD_KEY: "third_value", }); }); - // 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); + 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 }); - // 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 - }); - }); + openEnvVarsSection(); - 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 }); + const middleKeyInput = screen.getByDisplayValue("SECOND_KEY"); + fireEvent.change(middleKeyInput, { + target: { value: "NEW_SECOND_KEY" }, + }); - openEnvVarsSection(); + expect(setEnv).toHaveBeenCalledWith({ + FIRST_KEY: "first_value", + NEW_SECOND_KEY: "second_value", + THIRD_KEY: "third_value", + }); + }); - // First key edit - const firstKeyInput = screen.getByDisplayValue("FIRST_KEY"); - fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_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", + }; + renderSidebar({ env: initialEnv, setEnv }); - // Get the updated env from the first setEnv call - const updatedEnv = setEnv.mock.calls[0][0] as Record; + openEnvVarsSection(); - // Rerender with the updated env - rerender( - - - , - ); + const lastKeyInput = screen.getByDisplayValue("THIRD_KEY"); + fireEvent.change(lastKeyInput, { target: { value: "NEW_THIRD_KEY" } }); - // Second key edit - const secondKeyInput = screen.getByDisplayValue("SECOND_KEY"); - fireEvent.change(secondKeyInput, { target: { value: "NEW_SECOND_KEY" } }); + expect(setEnv).toHaveBeenCalledWith({ + FIRST_KEY: "first_value", + SECOND_KEY: "second_value", + NEW_THIRD_KEY: "third_value", + }); + }); - // Verify the final state matches what we expect - expect(setEnv).toHaveBeenLastCalledWith({ - NEW_FIRST_KEY: "first_value", - NEW_SECOND_KEY: "second_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 }); }); - it("should maintain visibility state after key edit", () => { - const initialEnv = { TEST_KEY: "test_value" }; - const { rerender } = renderSidebar({ env: initialEnv }); + 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(); + openEnvVarsSection(); - // Show the value - const toggleButton = screen.getByRole("button", { name: /show value/i }); - fireEvent.click(toggleButton); + // First key edit + const firstKeyInput = screen.getByDisplayValue("FIRST_KEY"); + fireEvent.change(firstKeyInput, { target: { value: "NEW_FIRST_KEY" } }); - const valueInput = screen.getByDisplayValue("test_value"); - expect(valueInput).toHaveProperty("type", "text"); + // Get the updated env from the first setEnv call + const updatedEnv = setEnv.mock.calls[0][0] as Record; - // Edit the key - const keyInput = screen.getByDisplayValue("TEST_KEY"); - fireEvent.change(keyInput, { target: { value: "NEW_KEY" } }); + // Rerender with the updated env + rerender( + + + , + ); - // Rerender with updated env - rerender( - - - , - ); + // Second key edit + const secondKeyInput = screen.getByDisplayValue("SECOND_KEY"); + fireEvent.change(secondKeyInput, { + target: { value: "NEW_SECOND_KEY" }, + }); - // 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" }); - }); - }); - - describe("Configuration Operations", () => { - const openConfigSection = () => { - const button = screen.getByTestId("config-button"); - fireEvent.click(button); - }; - - it("should update MCP server request timeout", () => { - const setConfig = jest.fn(); - renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); - - openConfigSection(); - - const timeoutInput = screen.getByTestId( - "MCP_SERVER_REQUEST_TIMEOUT-input", - ); - fireEvent.change(timeoutInput, { target: { value: "5000" } }); - - expect(setConfig).toHaveBeenCalledWith( - expect.objectContaining({ - MCP_SERVER_REQUEST_TIMEOUT: { - label: "Request Timeout", - description: "Timeout for requests to the MCP server (ms)", - value: 5000, - is_session_item: false, - }, - }), - ); - }); - - it("should update MCP server proxy address", () => { - const setConfig = jest.fn(); - renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); - - openConfigSection(); - - const proxyAddressInput = screen.getByTestId( - "MCP_PROXY_FULL_ADDRESS-input", - ); - fireEvent.change(proxyAddressInput, { - target: { value: "http://localhost:8080" }, + // Verify the final state matches what we expect + expect(setEnv).toHaveBeenLastCalledWith({ + NEW_FIRST_KEY: "first_value", + NEW_SECOND_KEY: "second_value", + }); }); - expect(setConfig).toHaveBeenCalledWith( - expect.objectContaining({ - MCP_PROXY_FULL_ADDRESS: { - label: "Inspector Proxy Address", - description: - "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577", - value: "http://localhost:8080", - is_session_item: false, - }, - }), - ); + 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"); + }); }); - it("should update max total timeout", () => { - const setConfig = jest.fn(); - renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + describe("Edge Cases", () => { + it("should handle empty key", () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: "test_value" }; + renderSidebar({ env: initialEnv, setEnv }); - openConfigSection(); + openEnvVarsSection(); - const maxTotalTimeoutInput = screen.getByTestId( - "MCP_REQUEST_MAX_TOTAL_TIMEOUT-input", - ); - fireEvent.change(maxTotalTimeoutInput, { - target: { value: "10000" }, + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "" } }); + + expect(setEnv).toHaveBeenCalledWith({ "": "test_value" }); }); - expect(setConfig).toHaveBeenCalledWith( - expect.objectContaining({ - MCP_REQUEST_MAX_TOTAL_TIMEOUT: { - label: "Maximum Total Timeout", - description: - "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)", - value: 10000, - is_session_item: false, - }, - }), - ); - }); + it("should handle special characters in key", () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: "test_value" }; + renderSidebar({ env: initialEnv, setEnv }); - it("should handle invalid timeout values entered by user", () => { - const setConfig = jest.fn(); - renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + openEnvVarsSection(); - openConfigSection(); + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "TEST-KEY@123" } }); - const timeoutInput = screen.getByTestId( - "MCP_SERVER_REQUEST_TIMEOUT-input", - ); - fireEvent.change(timeoutInput, { target: { value: "abc1" } }); - - expect(setConfig).toHaveBeenCalledWith( - expect.objectContaining({ - MCP_SERVER_REQUEST_TIMEOUT: { - label: "Request Timeout", - description: "Timeout for requests to the MCP server (ms)", - value: 0, - is_session_item: false, - }, - }), - ); - }); - - it("should maintain configuration state after multiple updates", () => { - const setConfig = jest.fn(); - const { rerender } = renderSidebar({ - config: DEFAULT_INSPECTOR_CONFIG, - setConfig, + expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" }); }); - openConfigSection(); - // First update - const timeoutInput = screen.getByTestId( - "MCP_SERVER_REQUEST_TIMEOUT-input", - ); - fireEvent.change(timeoutInput, { target: { value: "5000" } }); + it("should handle unicode characters", () => { + const setEnv = jest.fn(); + const initialEnv = { TEST_KEY: "test_value" }; + renderSidebar({ env: initialEnv, setEnv }); - // Get the updated config from the first setConfig call - const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig; + openEnvVarsSection(); - // Rerender with the updated config - rerender( - - - , - ); + const keyInput = screen.getByDisplayValue("TEST_KEY"); + fireEvent.change(keyInput, { target: { value: "TEST_πŸ”‘" } }); - // Second update - const updatedTimeoutInput = screen.getByTestId( - "MCP_SERVER_REQUEST_TIMEOUT-input", - ); - fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } }); + expect(setEnv).toHaveBeenCalledWith({ "TEST_πŸ”‘": "test_value" }); + }); - // Verify the final state matches what we expect - expect(setConfig).toHaveBeenLastCalledWith( - expect.objectContaining({ - MCP_SERVER_REQUEST_TIMEOUT: { - label: "Request Timeout", - description: "Timeout for requests to the MCP server (ms)", - value: 3000, - is_session_item: false, - }, - }), - ); + 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" }); + }); }); }); @@ -879,35 +618,306 @@ describe("Sidebar Environment Variables", () => { }); }); - describe("Command and arguments", () => { - it("should trim whitespace from command input on blur", () => { - const setCommand = jest.fn(); - renderSidebar({ command: " node server.js ", setCommand }); + describe("Authentication", () => { + const openAuthSection = () => { + const button = screen.getByTestId("auth-button"); + fireEvent.click(button); + }; - const commandInput = screen.getByLabelText("Command"); + it("should update bearer token", () => { + const setBearerToken = jest.fn(); + renderSidebar({ + bearerToken: "", + setBearerToken, + transportType: "sse", // Set transport type to SSE + }); - fireEvent.blur(commandInput); - expect(setCommand).toHaveBeenLastCalledWith("node server.js"); + openAuthSection(); + + const tokenInput = screen.getByTestId("bearer-token-input"); + fireEvent.change(tokenInput, { target: { value: "new_token" } }); + + expect(setBearerToken).toHaveBeenCalledWith("new_token"); }); - it("should handle whitespace-only command input on blur", () => { - const setCommand = jest.fn(); - renderSidebar({ command: " ", setCommand }); + it("should update header name", () => { + const setHeaderName = jest.fn(); + renderSidebar({ + headerName: "Authorization", + setHeaderName, + transportType: "sse", + }); - const commandInput = screen.getByLabelText("Command"); + openAuthSection(); - fireEvent.blur(commandInput); - expect(setCommand).toHaveBeenLastCalledWith(""); + const headerInput = screen.getByTestId("header-input"); + fireEvent.change(headerInput, { target: { value: "X-Custom-Auth" } }); + + expect(setHeaderName).toHaveBeenCalledWith("X-Custom-Auth"); }); - it("should not affect command without surrounding whitespace", () => { - const setCommand = jest.fn(); - renderSidebar({ command: "node", setCommand }); + it("should clear bearer token", () => { + const setBearerToken = jest.fn(); + renderSidebar({ + bearerToken: "existing_token", + setBearerToken, + transportType: "sse", // Set transport type to SSE + }); - const commandInput = screen.getByLabelText("Command"); + openAuthSection(); - fireEvent.blur(commandInput); - expect(setCommand).toHaveBeenLastCalledWith("node"); + const tokenInput = screen.getByTestId("bearer-token-input"); + fireEvent.change(tokenInput, { target: { value: "" } }); + + expect(setBearerToken).toHaveBeenCalledWith(""); + }); + + it("should properly render bearer token input", () => { + const { rerender } = renderSidebar({ + bearerToken: "existing_token", + transportType: "sse", // Set transport type to SSE + }); + + openAuthSection(); + + // Token input should be a password field + const tokenInput = screen.getByTestId("bearer-token-input"); + expect(tokenInput).toHaveProperty("type", "password"); + + // Update the token + fireEvent.change(tokenInput, { target: { value: "new_token" } }); + + // Rerender with updated token + rerender( + + + , + ); + + // Token input should still exist after update + expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument(); + }); + + it("should maintain token visibility state after update", () => { + const { rerender } = renderSidebar({ + bearerToken: "existing_token", + transportType: "sse", // Set transport type to SSE + }); + + openAuthSection(); + + // Token input should be a password field + const tokenInput = screen.getByTestId("bearer-token-input"); + expect(tokenInput).toHaveProperty("type", "password"); + + // Update the token + fireEvent.change(tokenInput, { target: { value: "new_token" } }); + + // Rerender with updated token + rerender( + + + , + ); + + // Token input should still exist after update + expect(screen.getByTestId("bearer-token-input")).toBeInTheDocument(); + }); + + it("should maintain header name when toggling auth section", () => { + renderSidebar({ + headerName: "X-API-Key", + transportType: "sse", + }); + + // Open auth section + openAuthSection(); + + // Verify header name is displayed + const headerInput = screen.getByTestId("header-input"); + expect(headerInput).toHaveValue("X-API-Key"); + + // Close auth section + const authButton = screen.getByTestId("auth-button"); + fireEvent.click(authButton); + + // Reopen auth section + fireEvent.click(authButton); + + // Verify header name is still preserved + expect(screen.getByTestId("header-input")).toHaveValue("X-API-Key"); + }); + + it("should display default header name when not specified", () => { + renderSidebar({ + headerName: undefined, + transportType: "sse", + }); + + openAuthSection(); + + const headerInput = screen.getByTestId("header-input"); + expect(headerInput).toHaveAttribute("placeholder", "Authorization"); + }); + }); + + describe("Configuration Operations", () => { + const openConfigSection = () => { + const button = screen.getByTestId("config-button"); + fireEvent.click(button); + }; + + it("should update MCP server request timeout", () => { + const setConfig = jest.fn(); + renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + + openConfigSection(); + + const timeoutInput = screen.getByTestId( + "MCP_SERVER_REQUEST_TIMEOUT-input", + ); + fireEvent.change(timeoutInput, { target: { value: "5000" } }); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", + description: "Timeout for requests to the MCP server (ms)", + value: 5000, + is_session_item: false, + }, + }), + ); + }); + + it("should update MCP server proxy address", () => { + const setConfig = jest.fn(); + renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + + openConfigSection(); + + const proxyAddressInput = screen.getByTestId( + "MCP_PROXY_FULL_ADDRESS-input", + ); + fireEvent.change(proxyAddressInput, { + target: { value: "http://localhost:8080" }, + }); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_PROXY_FULL_ADDRESS: { + label: "Inspector Proxy Address", + description: + "Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577", + value: "http://localhost:8080", + is_session_item: false, + }, + }), + ); + }); + + it("should update max total timeout", () => { + const setConfig = jest.fn(); + renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + + openConfigSection(); + + const maxTotalTimeoutInput = screen.getByTestId( + "MCP_REQUEST_MAX_TOTAL_TIMEOUT-input", + ); + fireEvent.change(maxTotalTimeoutInput, { + target: { value: "10000" }, + }); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_REQUEST_MAX_TOTAL_TIMEOUT: { + label: "Maximum Total Timeout", + description: + "Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)", + value: 10000, + is_session_item: false, + }, + }), + ); + }); + + it("should handle invalid timeout values entered by user", () => { + const setConfig = jest.fn(); + renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig }); + + openConfigSection(); + + const timeoutInput = screen.getByTestId( + "MCP_SERVER_REQUEST_TIMEOUT-input", + ); + fireEvent.change(timeoutInput, { target: { value: "abc1" } }); + + expect(setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", + description: "Timeout for requests to the MCP server (ms)", + value: 0, + is_session_item: false, + }, + }), + ); + }); + + it("should maintain configuration state after multiple updates", () => { + const setConfig = jest.fn(); + const { rerender } = renderSidebar({ + config: DEFAULT_INSPECTOR_CONFIG, + setConfig, + }); + + openConfigSection(); + // First update + const timeoutInput = screen.getByTestId( + "MCP_SERVER_REQUEST_TIMEOUT-input", + ); + fireEvent.change(timeoutInput, { target: { value: "5000" } }); + + // Get the updated config from the first setConfig call + const updatedConfig = setConfig.mock.calls[0][0] as InspectorConfig; + + // Rerender with the updated config + rerender( + + + , + ); + + // Second update + const updatedTimeoutInput = screen.getByTestId( + "MCP_SERVER_REQUEST_TIMEOUT-input", + ); + fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } }); + + // Verify the final state matches what we expect + expect(setConfig).toHaveBeenLastCalledWith( + expect.objectContaining({ + MCP_SERVER_REQUEST_TIMEOUT: { + label: "Request Timeout", + description: "Timeout for requests to the MCP server (ms)", + value: 3000, + is_session_item: false, + }, + }), + ); }); }); }); From 34938b72d30d28bf0e656efa0a10a2f1dfd2537a Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 14 Aug 2025 10:49:04 -0700 Subject: [PATCH 10/36] Tidy test descriptions and titles --- client/src/components/__tests__/Sidebar.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index dc737896..cb43663a 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -31,7 +31,7 @@ Object.defineProperty(navigator, "clipboard", { // Setup fake timers jest.useFakeTimers(); -describe("Sidebar Environment Variables", () => { +describe("Sidebar", () => { const defaultProps = { connectionStatus: "disconnected" as const, transportType: "stdio" as const, @@ -362,7 +362,7 @@ describe("Sidebar Environment Variables", () => { expect(setEnv).toHaveBeenCalledWith({ "TEST-KEY@123": "test_value" }); }); - it("should handle unicode characters", () => { + it("should handle unicode characters in key", () => { const setEnv = jest.fn(); const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); @@ -375,7 +375,7 @@ describe("Sidebar Environment Variables", () => { expect(setEnv).toHaveBeenCalledWith({ "TEST_πŸ”‘": "test_value" }); }); - it("should handle very long key names", () => { + it("should handle a very long key name", () => { const setEnv = jest.fn(); const initialEnv = { TEST_KEY: "test_value" }; renderSidebar({ env: initialEnv, setEnv }); @@ -391,7 +391,7 @@ describe("Sidebar Environment Variables", () => { }); }); - describe("Copy Configuration Features", () => { + describe("Copy Server Features", () => { beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); @@ -769,7 +769,7 @@ describe("Sidebar Environment Variables", () => { }); }); - describe("Configuration Operations", () => { + describe("Configuration", () => { const openConfigSection = () => { const button = screen.getByTestId("config-button"); fireEvent.click(button); From 410f792d29af58ea77e929fe449db2d90c644f46 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 14 Aug 2025 10:50:29 -0700 Subject: [PATCH 11/36] Remove redundant mock and timer clear --- client/src/components/__tests__/Sidebar.test.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/client/src/components/__tests__/Sidebar.test.tsx b/client/src/components/__tests__/Sidebar.test.tsx index cb43663a..8f693717 100644 --- a/client/src/components/__tests__/Sidebar.test.tsx +++ b/client/src/components/__tests__/Sidebar.test.tsx @@ -392,11 +392,6 @@ describe("Sidebar", () => { }); describe("Copy Server Features", () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.clearAllTimers(); - }); - const getCopyButtons = () => { return { serverEntry: screen.getByRole("button", { name: /server entry/i }), From 0831e812bb49c7d06f5a243db4b5e80708747332 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 15 Aug 2025 14:44:51 -0400 Subject: [PATCH 12/36] Align Quick OAuth Flow with Guided OAuth Flow behavior - In oauth-state-machine.ts - in token_request transition - ensure resource is URL type, not string - this fixes Quick flow which was sending string, while guided was using proper URL type and working --- client/src/lib/oauth-state-machine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 4a2c0f47..a5ae9d40 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -177,7 +177,7 @@ export const oauthTransitions: Record = { authorizationCode: context.state.authorizationCode, codeVerifier, redirectUri: context.provider.redirectUrl, - resource: context.state.resource ?? undefined, + resource: context.state.resource ? (context.state.resource instanceof URL ? context.state.resource : new URL(context.state.resource)) : undefined, }); context.provider.saveTokens(tokens); From 94f44c7b57e5070fdf7fb9f15857685f02be2751 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 15 Aug 2025 17:19:27 -0400 Subject: [PATCH 13/36] Prettier --- client/src/lib/oauth-state-machine.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index a5ae9d40..91465853 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -177,7 +177,11 @@ export const oauthTransitions: Record = { authorizationCode: context.state.authorizationCode, codeVerifier, redirectUri: context.provider.redirectUrl, - resource: context.state.resource ? (context.state.resource instanceof URL ? context.state.resource : new URL(context.state.resource)) : undefined, + resource: context.state.resource + ? context.state.resource instanceof URL + ? context.state.resource + : new URL(context.state.resource) + : undefined, }); context.provider.saveTokens(tokens); From 5323d4d8f4aef1c68745f8491d4dd58e79ba6c7e Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 15 Aug 2025 18:40:21 -0400 Subject: [PATCH 14/36] Fix connect button behavior for Oauth by making sure that scope is included in client metadata when client is registered. - In auth.ts - in InspectorOAuthClientProvider constructor - add optional scope param - set instance var this.scope to scope param - in clientMetadata getter - add scope param set to this.scope or "" - in useConnection.ts - in handleAuthError handler - move instantiation of serverAuthProvider until after scope has been determined - pass scope to InspectorOAuthClientProvider constructor --- client/src/lib/auth.ts | 8 +++++++- client/src/lib/hooks/useConnection.ts | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 99707387..78d52a59 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -102,10 +102,15 @@ export const clearClientInformationFromSessionStorage = ({ }; export class InspectorOAuthClientProvider implements OAuthClientProvider { - constructor(protected serverUrl: string) { + constructor( + protected serverUrl: string, + scope?: string, + ) { + this.scope = scope; // Save the server URL to session storage sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl); } + scope: string | undefined; get redirectUrl() { return window.location.origin + "/oauth/callback"; @@ -119,6 +124,7 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { response_types: ["code"], client_name: "MCP Inspector", client_uri: "https://github.com/modelcontextprotocol/inspector", + scope: this.scope ?? "", }; } diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index e6008197..fc3e214f 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -319,8 +319,6 @@ export function useConnection({ const handleAuthError = async (error: unknown) => { if (is401Error(error)) { - const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); - let scope = oauthScope?.trim(); if (!scope) { // Only discover resource metadata when we need to discover scopes @@ -334,6 +332,7 @@ export function useConnection({ } scope = await discoverScopes(sseUrl, resourceMetadata); } + const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl, scope); const result = await auth(serverAuthProvider, { serverUrl: sseUrl, From 3adb1dd5c9790c1092978b813af7dabfa33f6c7f Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 15 Aug 2025 18:55:15 -0400 Subject: [PATCH 15/36] Prettier --- client/src/lib/hooks/useConnection.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index fc3e214f..d3690f31 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -332,7 +332,10 @@ export function useConnection({ } scope = await discoverScopes(sseUrl, resourceMetadata); } - const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl, scope); + const serverAuthProvider = new InspectorOAuthClientProvider( + sseUrl, + scope, + ); const result = await auth(serverAuthProvider, { serverUrl: sseUrl, From 28c0bf176ab65be71aa4dc89c4062b3e1a1e7d2d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 18 Aug 2025 11:13:42 -0400 Subject: [PATCH 16/36] Update Typescript SDK version to latest --- cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- server/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/package.json b/cli/package.json index 9458ef65..7d38186e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ }, "devDependencies": {}, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "commander": "^13.1.0", "spawn-rx": "^5.1.2" } diff --git a/client/package.json b/client/package.json index 441d2168..3780a3b0 100644 --- a/client/package.json +++ b/client/package.json @@ -25,7 +25,7 @@ "cleanup:e2e": "node e2e/global-teardown.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "@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-lock.json b/package-lock.json index 3a99a46e..a7260f8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@modelcontextprotocol/inspector-cli": "^0.16.4", "@modelcontextprotocol/inspector-client": "^0.16.4", "@modelcontextprotocol/inspector-server": "^0.16.4", - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.3", @@ -1999,9 +1999,9 @@ "link": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.2.tgz", - "integrity": "sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==", + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", + "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index c2a78331..22353c33 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@modelcontextprotocol/inspector-cli": "^0.16.4", "@modelcontextprotocol/inspector-client": "^0.16.4", "@modelcontextprotocol/inspector-server": "^0.16.4", - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.3", diff --git a/server/package.json b/server/package.json index 4047e2d8..307a8c27 100644 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", From 2f93d7036430ab76b3bec7f66b42c8507e2dd742 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 18 Aug 2025 11:37:41 -0400 Subject: [PATCH 17/36] bump version to 0.16.5 --- cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 22 +++++++++++----------- package.json | 8 ++++---- server/package.json | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cli/package.json b/cli/package.json index 7d38186e..2f0f53b6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.4", + "version": "0.16.5", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/client/package.json b/client/package.json index 3780a3b0..5d634ee1 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.4", + "version": "0.16.5", "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 a7260f8b..87cbc93c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.4", + "version": "0.16.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.16.4", + "version": "0.16.5", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.16.4", - "@modelcontextprotocol/inspector-client": "^0.16.4", - "@modelcontextprotocol/inspector-server": "^0.16.4", + "@modelcontextprotocol/inspector-cli": "^0.16.5", + "@modelcontextprotocol/inspector-client": "^0.16.5", + "@modelcontextprotocol/inspector-server": "^0.16.5", "@modelcontextprotocol/sdk": "^1.17.3", "concurrently": "^9.2.0", "open": "^10.2.0", @@ -44,10 +44,10 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.4", + "version": "0.16.5", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "commander": "^13.1.0", "spawn-rx": "^5.1.2" }, @@ -67,10 +67,10 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.4", + "version": "0.16.5", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -11059,10 +11059,10 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.4", + "version": "0.16.5", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "^1.17.3", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", diff --git a/package.json b/package.json index 22353c33..33211621 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.4", + "version": "0.16.5", "description": "Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -47,9 +47,9 @@ "check-version": "node scripts/check-version-consistency.js" }, "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.16.4", - "@modelcontextprotocol/inspector-client": "^0.16.4", - "@modelcontextprotocol/inspector-server": "^0.16.4", + "@modelcontextprotocol/inspector-cli": "^0.16.5", + "@modelcontextprotocol/inspector-client": "^0.16.5", + "@modelcontextprotocol/inspector-server": "^0.16.5", "@modelcontextprotocol/sdk": "^1.17.3", "concurrently": "^9.2.0", "open": "^10.2.0", diff --git a/server/package.json b/server/package.json index 307a8c27..fb835f63 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.4", + "version": "0.16.5", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From f7b1cd6f43a8e5f256c05a11009fdd1e8961038f Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 18 Aug 2025 08:43:09 -0700 Subject: [PATCH 18/36] Setup prettier pre-commit formatting with husky and lint-staged - Install husky and lint-staged dev dependencies - Configure pre-commit hook to run prettier on staged files - Only format changed files to improve performance - Prevents CI formatting failures by formatting locally --- .husky/pre-commit | 1 + package-lock.json | 612 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 9 +- 3 files changed, 614 insertions(+), 8 deletions(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..2312dc58 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/package-lock.json b/package-lock.json index 3a99a46e..cf7137c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,9 @@ "@types/jest": "^29.5.14", "@types/node": "^22.17.0", "@types/shell-quote": "^1.7.5", + "husky": "^9.1.7", "jest-fixed-jsdom": "^0.0.9", + "lint-staged": "^16.1.5", "prettier": "^3.6.2", "rimraf": "^6.0.1", "typescript": "^5.4.2" @@ -4649,6 +4651,93 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4724,6 +4813,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4962,9 +5058,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5245,6 +5341,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -5593,6 +5702,13 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/eventsource": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", @@ -6049,6 +6165,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6328,6 +6457,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7857,6 +8002,160 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.1.5", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.5.tgz", + "integrity": "sha512-uAeQQwByI6dfV7wpt/gVqg+jAPaSp8WwOA8kKC/dv1qw14oGpnpAisY65ibGHUGDUv0rYaZ8CAJZ/1U8hUvC2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.5.0", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^9.0.1", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", + "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7893,6 +8192,160 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8070,6 +8523,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8118,6 +8584,19 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -8496,6 +8975,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -9225,6 +9717,52 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9235,6 +9773,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", @@ -9711,6 +10256,49 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9790,6 +10378,16 @@ "node": ">= 0.8" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -10979,15 +11577,15 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index c2a78331..0e6bb653 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "prettier-fix": "prettier --write .", "prettier-check": "prettier --check .", "lint": "prettier --check . && cd client && npm run lint", - "prepare": "npm run build", + "prepare": "husky", "publish-all": "npm publish --workspaces --access public && npm publish --access public", "update-version": "node scripts/update-version.js", "check-version": "node scripts/check-version-consistency.js" @@ -63,12 +63,19 @@ "@types/jest": "^29.5.14", "@types/node": "^22.17.0", "@types/shell-quote": "^1.7.5", + "husky": "^9.1.7", "jest-fixed-jsdom": "^0.0.9", + "lint-staged": "^16.1.5", "prettier": "^3.6.2", "rimraf": "^6.0.1", "typescript": "^5.4.2" }, "engines": { "node": ">=22.7.5" + }, + "lint-staged": { + "**/*.{js,ts,jsx,tsx,json,md}": [ + "prettier --write" + ] } } From b33dabe8dc1666eb9e9a82999d8c4b1251f48ac3 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 18 Aug 2025 08:50:57 -0700 Subject: [PATCH 19/36] Fix: restore original 'prepare' script that was overwritten by husky init - Restore 'prepare': 'npm run build' for proper package installation - Husky pre-commit hooks remain functional via existing .husky directory --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d14e5d09..a5994d7c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "prettier-fix": "prettier --write .", "prettier-check": "prettier --check .", "lint": "prettier --check . && cd client && npm run lint", - "prepare": "husky", + "prepare": "npm run build", "publish-all": "npm publish --workspaces --access public && npm publish --access public", "update-version": "node scripts/update-version.js", "check-version": "node scripts/check-version-consistency.js" From de1754633e05a535e12d430b713bce894505087d Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 18 Aug 2025 08:52:33 -0700 Subject: [PATCH 20/36] Test pre-commit formatting hook with intentionally bad formatting --- test-formatting.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test-formatting.js diff --git a/test-formatting.js b/test-formatting.js new file mode 100644 index 00000000..32f7e029 --- /dev/null +++ b/test-formatting.js @@ -0,0 +1,10 @@ +// Test file with bad formatting +const badly = { + formatted: "code", + missing: "semicolons", + extra: "spaces", +}; + +function test() { + console.log("test"); +} From 420f9a7b9a6db5354f935194c4fcd30232f7c1ff Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 18 Aug 2025 08:53:49 -0700 Subject: [PATCH 21/36] Remove test formatting file --- test-formatting.js | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 test-formatting.js diff --git a/test-formatting.js b/test-formatting.js deleted file mode 100644 index 32f7e029..00000000 --- a/test-formatting.js +++ /dev/null @@ -1,10 +0,0 @@ -// Test file with bad formatting -const badly = { - formatted: "code", - missing: "semicolons", - extra: "spaces", -}; - -function test() { - console.log("test"); -} From 36a30fa3db08d5933a903fc2fee24ebd13b37993 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 18 Aug 2025 13:01:13 -0700 Subject: [PATCH 22/36] prettier --write client/src/components/OAuthDebugCallback.tsx --- client/src/components/OAuthDebugCallback.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/components/OAuthDebugCallback.tsx b/client/src/components/OAuthDebugCallback.tsx index 67ddc926..95ccc076 100644 --- a/client/src/components/OAuthDebugCallback.tsx +++ b/client/src/components/OAuthDebugCallback.tsx @@ -49,8 +49,13 @@ const OAuthDebugCallback = ({ onConnect }: OAuthCallbackProps) => { if (restoredState && typeof restoredState.resource === "string") { restoredState.resource = new URL(restoredState.resource); } - if (restoredState && typeof restoredState.authorizationUrl === "string") { - restoredState.authorizationUrl = new URL(restoredState.authorizationUrl); + if ( + restoredState && + typeof restoredState.authorizationUrl === "string" + ) { + restoredState.authorizationUrl = new URL( + restoredState.authorizationUrl, + ); } // Clean up the stored state sessionStorage.removeItem(SESSION_KEYS.AUTH_DEBUGGER_STATE); From fa9b99b5df6a1c3126e7fff99080a5f988c2da4f Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Tue, 19 Aug 2025 06:55:29 -0700 Subject: [PATCH 23/36] Update .husky/pre-commit Co-authored-by: Cliff Hall --- .husky/pre-commit | 1 + 1 file changed, 1 insertion(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index 2312dc58..0a3afdcb 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ npx lint-staged +git update-index --again From df6152ee351bb3a30c81e4fbb564f61717bb5e2f Mon Sep 17 00:00:00 2001 From: Ola Hungerford Date: Tue, 19 Aug 2025 06:57:09 -0700 Subject: [PATCH 24/36] Update npm prepare script to include husky --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a5994d7c..879c7b2c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "prettier-fix": "prettier --write .", "prettier-check": "prettier --check .", "lint": "prettier --check . && cd client && npm run lint", - "prepare": "npm run build", + "prepare": "husky && npm run build", "publish-all": "npm publish --workspaces --access public && npm publish --access public", "update-version": "node scripts/update-version.js", "check-version": "node scripts/check-version-consistency.js" From 650f3090d26344a672026b737d81586595bb1f60 Mon Sep 17 00:00:00 2001 From: Jenn Newton Date: Fri, 15 Aug 2025 13:09:49 -0400 Subject: [PATCH 25/36] fix: add validation for OAuth redirect URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create shared validateRedirectUrl utility to centralize URL validation - Only allow HTTP and HTTPS protocols in OAuth authorization URLs - Fix three vulnerable redirect paths that bypassed validation: - AuthDebugger.tsx: Direct window.location.href redirect - OAuthFlowProgress.tsx: window.open() without validation - OAuthFlowProgress.tsx: Direct anchor href attribute - Add comprehensive test coverage for URL validation - Update existing auth.ts to use shared validation utility This prevents malicious MCP servers from injecting javascript: or data: protocol URLs that would execute XSS attacks when users are redirected to the authorization endpoint. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/src/components/AuthDebugger.tsx | 17 +++ client/src/components/OAuthFlowProgress.tsx | 35 ++++- client/src/lib/auth.ts | 9 +- .../src/utils/__tests__/urlValidation.test.ts | 127 ++++++++++++++++++ client/src/utils/urlValidation.ts | 21 +++ 5 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 client/src/utils/__tests__/urlValidation.test.ts create mode 100644 client/src/utils/urlValidation.ts diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index ec09963e..c8d59c42 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -6,6 +6,7 @@ import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types"; import { OAuthFlowProgress } from "./OAuthFlowProgress"; import { OAuthStateMachine } from "../lib/oauth-state-machine"; import { SESSION_KEYS } from "../lib/constants"; +import { validateRedirectUrl } from "@/utils/urlValidation"; export interface AuthDebuggerProps { serverUrl: string; @@ -163,6 +164,22 @@ const AuthDebugger = ({ currentState.oauthStep === "authorization_code" && currentState.authorizationUrl ) { + // Validate the URL before redirecting + try { + validateRedirectUrl(currentState.authorizationUrl); + } catch (error) { + updateAuthState({ + ...currentState, + isInitiatingAuth: false, + latestError: error instanceof Error ? error : new Error(String(error)), + statusMessage: { + type: "error", + message: `Invalid authorization URL: ${error instanceof Error ? error.message : String(error)}`, + }, + }); + return; + } + // Store the current auth state before redirecting sessionStorage.setItem( SESSION_KEYS.AUTH_DEBUGGER_STATE, diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 0e260556..797d9f4d 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -4,6 +4,8 @@ import { Button } from "./ui/button"; import { DebugInspectorOAuthClientProvider } from "@/lib/auth"; import { useEffect, useMemo, useState } from "react"; import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { validateRedirectUrl } from "@/utils/urlValidation"; +import { useToast } from "@/lib/hooks/useToast"; interface OAuthStepProps { label: string; @@ -71,6 +73,7 @@ export const OAuthFlowProgress = ({ updateAuthState, proceedToNextStep, }: OAuthFlowProgressProps) => { + const { toast } = useToast(); const provider = useMemo( () => new DebugInspectorOAuthClientProvider(serverUrl), [serverUrl], @@ -239,16 +242,25 @@ export const OAuthFlowProgress = ({

{authState.authorizationUrl}

- { + try { + validateRedirectUrl(authState.authorizationUrl!); + window.open(authState.authorizationUrl!, "_blank"); + } catch (error) { + toast({ + title: "Invalid URL", + description: error instanceof Error ? error.message : "The authorization URL is not valid", + variant: "destructive", + }); + } + }} className="flex items-center text-blue-500 hover:text-blue-700" aria-label="Open authorization URL in new tab" title="Open authorization URL" > - +

Click the link to authorize in your browser. After @@ -354,7 +366,18 @@ export const OAuthFlowProgress = ({ authState.authorizationUrl && ( diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 78d52a59..c1505398 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -11,6 +11,7 @@ import { import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./constants"; import { generateOAuthState } from "@/utils/oauthUtils"; +import { validateRedirectUrl } from "@/utils/urlValidation"; /** * Discovers OAuth scopes from server metadata, with preference for resource metadata scopes @@ -182,12 +183,8 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } redirectToAuthorization(authorizationUrl: URL) { - if ( - authorizationUrl.protocol !== "http:" && - authorizationUrl.protocol !== "https:" - ) { - throw new Error("Authorization URL must be HTTP or HTTPS"); - } + // Validate the URL using the shared utility + validateRedirectUrl(authorizationUrl.href); window.location.href = authorizationUrl.href; } diff --git a/client/src/utils/__tests__/urlValidation.test.ts b/client/src/utils/__tests__/urlValidation.test.ts new file mode 100644 index 00000000..279197cb --- /dev/null +++ b/client/src/utils/__tests__/urlValidation.test.ts @@ -0,0 +1,127 @@ +import { validateRedirectUrl } from "../urlValidation"; + +describe("validateRedirectUrl", () => { + describe("valid URLs", () => { + it("should allow HTTP URLs", () => { + expect(() => validateRedirectUrl("http://example.com")).not.toThrow(); + }); + + it("should allow HTTPS URLs", () => { + expect(() => validateRedirectUrl("https://example.com")).not.toThrow(); + }); + + it("should allow URLs with ports", () => { + expect(() => validateRedirectUrl("https://example.com:8080")).not.toThrow(); + }); + + it("should allow URLs with paths", () => { + expect(() => validateRedirectUrl("https://example.com/path/to/auth")).not.toThrow(); + }); + + it("should allow URLs with query parameters", () => { + expect(() => validateRedirectUrl("https://example.com?param=value")).not.toThrow(); + }); + }); + + describe("invalid URLs - XSS vectors", () => { + it("should block javascript: protocol", () => { + expect(() => validateRedirectUrl("javascript:alert('XSS')")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + + it("should block javascript: with encoded characters", () => { + expect(() => validateRedirectUrl("javascript:alert%28%27XSS%27%29")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + + it("should block data: protocol", () => { + expect(() => validateRedirectUrl("data:text/html,")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + + it("should block vbscript: protocol", () => { + expect(() => validateRedirectUrl("vbscript:msgbox")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + + it("should block file: protocol", () => { + expect(() => validateRedirectUrl("file:///etc/passwd")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + + it("should block about: protocol", () => { + expect(() => validateRedirectUrl("about:blank")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + + it("should block custom protocols", () => { + expect(() => validateRedirectUrl("custom://example")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + }); + + describe("edge cases", () => { + it("should handle malformed URLs", () => { + expect(() => validateRedirectUrl("not a url")).toThrow( + "Invalid URL: not a url" + ); + }); + + it("should handle empty string", () => { + expect(() => validateRedirectUrl("")).toThrow( + "Invalid URL: " + ); + }); + + it("should handle URLs with unicode characters", () => { + expect(() => validateRedirectUrl("https://δΎ‹γˆ.jp")).not.toThrow(); + }); + + it("should handle URLs with case variations", () => { + expect(() => validateRedirectUrl("HTTPS://EXAMPLE.COM")).not.toThrow(); + expect(() => validateRedirectUrl("HtTpS://example.com")).not.toThrow(); + }); + + it("should handle protocol-relative URLs as invalid", () => { + expect(() => validateRedirectUrl("//example.com")).toThrow( + "Invalid URL: //example.com" + ); + }); + + it("should handle URLs with authentication", () => { + expect(() => validateRedirectUrl("https://user:pass@example.com")).not.toThrow(); + }); + }); + + describe("security considerations", () => { + it("should not be fooled by whitespace", () => { + expect(() => validateRedirectUrl(" javascript:alert('XSS')")).toThrow(); + expect(() => validateRedirectUrl("javascript:alert('XSS') ")).toThrow(); + }); + + it("should handle null bytes", () => { + expect(() => validateRedirectUrl("java\x00script:alert('XSS')")).toThrow(); + }); + + it("should handle tab characters", () => { + expect(() => validateRedirectUrl("java\tscript:alert('XSS')")).toThrow(); + }); + + it("should handle newlines", () => { + expect(() => validateRedirectUrl("java\nscript:alert('XSS')")).toThrow(); + }); + + it("should handle mixed case protocols", () => { + expect(() => validateRedirectUrl("JaVaScRiPt:alert('XSS')")).toThrow( + "Authorization URL must be HTTP or HTTPS" + ); + }); + }); +}); \ No newline at end of file diff --git a/client/src/utils/urlValidation.ts b/client/src/utils/urlValidation.ts new file mode 100644 index 00000000..fb0ef76b --- /dev/null +++ b/client/src/utils/urlValidation.ts @@ -0,0 +1,21 @@ +/** + * Validates that a URL is safe for redirection. + * Only allows HTTP and HTTPS protocols to prevent XSS attacks. + * + * @param url - The URL string to validate + * @throws Error if the URL has an unsafe protocol + */ +export function validateRedirectUrl(url: string): void { + try { + const parsedUrl = new URL(url); + if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { + throw new Error("Authorization URL must be HTTP or HTTPS"); + } + } catch (error) { + if (error instanceof Error && error.message === "Authorization URL must be HTTP or HTTPS") { + throw error; + } + // If URL parsing fails, it's also invalid + throw new Error(`Invalid URL: ${url}`); + } +} \ No newline at end of file From fa2e6b0c4089bafefaa4f6434f0f71e2abc9c92c Mon Sep 17 00:00:00 2001 From: Jenn Newton Date: Thu, 21 Aug 2025 15:31:53 +0000 Subject: [PATCH 26/36] Fix to properly open in new tab for OAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/src/components/OAuthFlowProgress.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 797d9f4d..e50b7df4 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -246,11 +246,18 @@ export const OAuthFlowProgress = ({ onClick={() => { try { validateRedirectUrl(authState.authorizationUrl!); - window.open(authState.authorizationUrl!, "_blank"); + window.open( + authState.authorizationUrl!, + "_blank", + "noopener noreferrer", + ); } catch (error) { toast({ title: "Invalid URL", - description: error instanceof Error ? error.message : "The authorization URL is not valid", + description: + error instanceof Error + ? error.message + : "The authorization URL is not valid", variant: "destructive", }); } @@ -373,7 +380,10 @@ export const OAuthFlowProgress = ({ } catch (error) { toast({ title: "Invalid URL", - description: error instanceof Error ? error.message : "The authorization URL is not valid", + description: + error instanceof Error + ? error.message + : "The authorization URL is not valid", variant: "destructive", }); } From a018a6d7587b04f02d45dd473fa03b7fdf982d97 Mon Sep 17 00:00:00 2001 From: Jenn Newton Date: Thu, 21 Aug 2025 15:41:33 +0000 Subject: [PATCH 27/36] Apply Prettier formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/src/components/AuthDebugger.tsx | 5 +- .../src/utils/__tests__/urlValidation.test.ts | 54 +++++++++++-------- client/src/utils/urlValidation.ts | 9 ++-- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx index c8d59c42..e7acbd80 100644 --- a/client/src/components/AuthDebugger.tsx +++ b/client/src/components/AuthDebugger.tsx @@ -171,7 +171,8 @@ const AuthDebugger = ({ updateAuthState({ ...currentState, isInitiatingAuth: false, - latestError: error instanceof Error ? error : new Error(String(error)), + latestError: + error instanceof Error ? error : new Error(String(error)), statusMessage: { type: "error", message: `Invalid authorization URL: ${error instanceof Error ? error.message : String(error)}`, @@ -179,7 +180,7 @@ const AuthDebugger = ({ }); return; } - + // Store the current auth state before redirecting sessionStorage.setItem( SESSION_KEYS.AUTH_DEBUGGER_STATE, diff --git a/client/src/utils/__tests__/urlValidation.test.ts b/client/src/utils/__tests__/urlValidation.test.ts index 279197cb..9ee6a454 100644 --- a/client/src/utils/__tests__/urlValidation.test.ts +++ b/client/src/utils/__tests__/urlValidation.test.ts @@ -11,58 +11,64 @@ describe("validateRedirectUrl", () => { }); it("should allow URLs with ports", () => { - expect(() => validateRedirectUrl("https://example.com:8080")).not.toThrow(); + expect(() => + validateRedirectUrl("https://example.com:8080"), + ).not.toThrow(); }); it("should allow URLs with paths", () => { - expect(() => validateRedirectUrl("https://example.com/path/to/auth")).not.toThrow(); + expect(() => + validateRedirectUrl("https://example.com/path/to/auth"), + ).not.toThrow(); }); it("should allow URLs with query parameters", () => { - expect(() => validateRedirectUrl("https://example.com?param=value")).not.toThrow(); + expect(() => + validateRedirectUrl("https://example.com?param=value"), + ).not.toThrow(); }); }); describe("invalid URLs - XSS vectors", () => { it("should block javascript: protocol", () => { expect(() => validateRedirectUrl("javascript:alert('XSS')")).toThrow( - "Authorization URL must be HTTP or HTTPS" + "Authorization URL must be HTTP or HTTPS", ); }); it("should block javascript: with encoded characters", () => { - expect(() => validateRedirectUrl("javascript:alert%28%27XSS%27%29")).toThrow( - "Authorization URL must be HTTP or HTTPS" - ); + expect(() => + validateRedirectUrl("javascript:alert%28%27XSS%27%29"), + ).toThrow("Authorization URL must be HTTP or HTTPS"); }); it("should block data: protocol", () => { - expect(() => validateRedirectUrl("data:text/html,")).toThrow( - "Authorization URL must be HTTP or HTTPS" - ); + expect(() => + validateRedirectUrl("data:text/html,"), + ).toThrow("Authorization URL must be HTTP or HTTPS"); }); it("should block vbscript: protocol", () => { expect(() => validateRedirectUrl("vbscript:msgbox")).toThrow( - "Authorization URL must be HTTP or HTTPS" + "Authorization URL must be HTTP or HTTPS", ); }); it("should block file: protocol", () => { expect(() => validateRedirectUrl("file:///etc/passwd")).toThrow( - "Authorization URL must be HTTP or HTTPS" + "Authorization URL must be HTTP or HTTPS", ); }); it("should block about: protocol", () => { expect(() => validateRedirectUrl("about:blank")).toThrow( - "Authorization URL must be HTTP or HTTPS" + "Authorization URL must be HTTP or HTTPS", ); }); it("should block custom protocols", () => { expect(() => validateRedirectUrl("custom://example")).toThrow( - "Authorization URL must be HTTP or HTTPS" + "Authorization URL must be HTTP or HTTPS", ); }); }); @@ -70,14 +76,12 @@ describe("validateRedirectUrl", () => { describe("edge cases", () => { it("should handle malformed URLs", () => { expect(() => validateRedirectUrl("not a url")).toThrow( - "Invalid URL: not a url" + "Invalid URL: not a url", ); }); it("should handle empty string", () => { - expect(() => validateRedirectUrl("")).toThrow( - "Invalid URL: " - ); + expect(() => validateRedirectUrl("")).toThrow("Invalid URL: "); }); it("should handle URLs with unicode characters", () => { @@ -91,12 +95,14 @@ describe("validateRedirectUrl", () => { it("should handle protocol-relative URLs as invalid", () => { expect(() => validateRedirectUrl("//example.com")).toThrow( - "Invalid URL: //example.com" + "Invalid URL: //example.com", ); }); it("should handle URLs with authentication", () => { - expect(() => validateRedirectUrl("https://user:pass@example.com")).not.toThrow(); + expect(() => + validateRedirectUrl("https://user:pass@example.com"), + ).not.toThrow(); }); }); @@ -107,7 +113,9 @@ describe("validateRedirectUrl", () => { }); it("should handle null bytes", () => { - expect(() => validateRedirectUrl("java\x00script:alert('XSS')")).toThrow(); + expect(() => + validateRedirectUrl("java\x00script:alert('XSS')"), + ).toThrow(); }); it("should handle tab characters", () => { @@ -120,8 +128,8 @@ describe("validateRedirectUrl", () => { it("should handle mixed case protocols", () => { expect(() => validateRedirectUrl("JaVaScRiPt:alert('XSS')")).toThrow( - "Authorization URL must be HTTP or HTTPS" + "Authorization URL must be HTTP or HTTPS", ); }); }); -}); \ No newline at end of file +}); diff --git a/client/src/utils/urlValidation.ts b/client/src/utils/urlValidation.ts index fb0ef76b..70bc458b 100644 --- a/client/src/utils/urlValidation.ts +++ b/client/src/utils/urlValidation.ts @@ -1,7 +1,7 @@ /** * Validates that a URL is safe for redirection. * Only allows HTTP and HTTPS protocols to prevent XSS attacks. - * + * * @param url - The URL string to validate * @throws Error if the URL has an unsafe protocol */ @@ -12,10 +12,13 @@ export function validateRedirectUrl(url: string): void { throw new Error("Authorization URL must be HTTP or HTTPS"); } } catch (error) { - if (error instanceof Error && error.message === "Authorization URL must be HTTP or HTTPS") { + if ( + error instanceof Error && + error.message === "Authorization URL must be HTTP or HTTPS" + ) { throw error; } // If URL parsing fails, it's also invalid throw new Error(`Invalid URL: ${url}`); } -} \ No newline at end of file +} From 9dbd5cc85bd1d7f309392ab626452a988da6fcc3 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 21 Aug 2025 18:01:29 -0400 Subject: [PATCH 28/36] * Remove handling of out-of-spec 'notification/stderr' messages. It's not a thing. See https://github.com/modelcontextprotocol/servers/pull/2469 * Inspect the stderr output of STDIO servers and attempt to assign an appropriate RFC 5424 Syslog Protocol level before sending a leveled logging message to the client --- client/src/App.tsx | 16 --------- client/src/components/Sidebar.tsx | 35 ------------------ client/src/lib/hooks/useConnection.ts | 10 +----- client/src/lib/notificationTypes.ts | 14 ++------ server/src/index.ts | 51 ++++++++++++++++++++++++--- 5 files changed, 49 insertions(+), 77 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index d6680c35..fecd9839 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -34,7 +34,6 @@ import { useDraggablePane, useDraggableSidebar, } from "./lib/hooks/useDraggablePane"; -import { StdErrNotification } from "./lib/notificationTypes"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; @@ -106,9 +105,6 @@ const App = () => { >(getInitialTransportType); const [logLevel, setLogLevel] = useState("debug"); const [notifications, setNotifications] = useState([]); - const [stdErrNotifications, setStdErrNotifications] = useState< - StdErrNotification[] - >([]); const [roots, setRoots] = useState([]); const [env, setEnv] = useState>({}); @@ -224,12 +220,6 @@ const App = () => { onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); }, - onStdErrNotification: (notification) => { - setStdErrNotifications((prev) => [ - ...prev, - notification as StdErrNotification, - ]); - }, onPendingRequest: (request, resolve, reject) => { setPendingSampleRequests((prev) => [ ...prev, @@ -757,10 +747,6 @@ const App = () => { setLogLevel(level); }; - const clearStdErrNotifications = () => { - setStdErrNotifications([]); - }; - const AuthDebuggerWrapper = () => ( { setOauthScope={setOauthScope} onConnect={connectMcpServer} onDisconnect={disconnectMcpServer} - stdErrNotifications={stdErrNotifications} logLevel={logLevel} sendLogLevelRequest={sendLogLevelRequest} loggingSupported={!!serverCapabilities?.logging || false} - clearStdErrNotifications={clearStdErrNotifications} />

void; onConnect: () => void; onDisconnect: () => void; - stdErrNotifications: StdErrNotification[]; - clearStdErrNotifications: () => void; logLevel: LoggingLevel; sendLogLevelRequest: (level: LoggingLevel) => void; loggingSupported: boolean; @@ -93,8 +90,6 @@ const Sidebar = ({ setOauthScope, onConnect, onDisconnect, - stdErrNotifications, - clearStdErrNotifications, logLevel, sendLogLevelRequest, loggingSupported, @@ -760,36 +755,6 @@ const Sidebar = ({
)} - - {stdErrNotifications.length > 0 && ( - <> -
-
-

- Error output from MCP server -

- -
-
- {stdErrNotifications.map((notification, index) => ( -
- {notification.params.content} -
- ))} -
-
- - )} diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index d3690f31..8c44d51b 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -36,7 +36,7 @@ import { useEffect, useState } from "react"; import { useToast } from "@/lib/hooks/useToast"; import { z } from "zod"; import { ConnectionStatus } from "../constants"; -import { Notification, StdErrNotificationSchema } from "../notificationTypes"; +import { Notification } from "../notificationTypes"; import { auth, discoverOAuthProtectedResourceMetadata, @@ -92,7 +92,6 @@ export function useConnection({ oauthScope, config, onNotification, - onStdErrNotification, onPendingRequest, onElicitationRequest, getRoots, @@ -505,13 +504,6 @@ export function useConnection({ }; } - if (onStdErrNotification) { - client.setNotificationHandler( - StdErrNotificationSchema, - onStdErrNotification, - ); - } - let capabilities; try { const transport = diff --git a/client/src/lib/notificationTypes.ts b/client/src/lib/notificationTypes.ts index 8627ccc6..a956452a 100644 --- a/client/src/lib/notificationTypes.ts +++ b/client/src/lib/notificationTypes.ts @@ -5,18 +5,8 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; -export const StdErrNotificationSchema = BaseNotificationSchema.extend({ - method: z.literal("notifications/stderr"), - params: z.object({ - content: z.string(), - }), -}); - export const NotificationSchema = ClientNotificationSchema.or( - StdErrNotificationSchema, -) - .or(ServerNotificationSchema) - .or(BaseNotificationSchema); + ServerNotificationSchema, +).or(BaseNotificationSchema); -export type StdErrNotification = z.infer; export type Notification = z.infer; diff --git a/server/src/index.ts b/server/src/index.ts index 0a0f7bcc..65746017 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -401,24 +401,65 @@ app.get( (serverTransport as StdioClientTransport).stderr!.on("data", (chunk) => { if (chunk.toString().includes("MODULE_NOT_FOUND")) { + // Server command not found, remove transports + const message = "Command not found, transports removed"; webAppTransport.send({ jsonrpc: "2.0", - method: "notifications/stderr", + method: "notifications/message", params: { - content: "Command not found, transports removed", + level: "alert", + data: { + error: message, + }, }, }); webAppTransport.close(); serverTransport.close(); webAppTransports.delete(webAppTransport.sessionId); serverTransports.delete(webAppTransport.sessionId); - console.error("Command not found, transports removed"); + console.error(message); } else { + // Inspect message and attempt to assign a RFC 5424 Syslog Protocol level + let level; + let message = chunk.toString(); + let ucMsg = chunk.toString().toUpperCase(); + if (ucMsg.includes("DEBUG")) { + level = "debug"; + } else if (ucMsg.includes("INFO")) { + level = "info"; + } else if (ucMsg.includes("NOTICE")) { + level = "notice"; + } else if (ucMsg.includes("WARN")) { + level = "warning"; + } else if (ucMsg.includes("ERROR")) { + level = "error"; + } else if (ucMsg.includes("CRITICAL")) { + level = "critical"; + } else if (ucMsg.includes("ALERT")) { + level = "alert"; + } else if (ucMsg.includes("EMERGENCY")) { + level = "emergency"; + } else if (ucMsg.includes("SIGINT")) { + level = "alert"; + message = "SIGINT received. Server shutdown."; + } else if (ucMsg.includes("SIGHUP")) { + level = "alert"; + message = "SIGHUP received. Server shutdown."; + } else if (ucMsg.includes("SIGTERM")) { + level = "alert"; + message = "SIGTERM received. Server shutdown."; + } else { + level = "info"; + } webAppTransport.send({ jsonrpc: "2.0", - method: "notifications/stderr", + method: "notifications/message", params: { - content: chunk.toString(), + level, + logger: "stdio", + data: { + error: message, + }, }, }); } From ec99372d925e24c46f8470e96b02c1116bef6242 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 21 Aug 2025 18:04:07 -0400 Subject: [PATCH 29/36] * Remove handling of out-of-spec 'notification/stderr' messages. It's not a thing. See https://github.com/modelcontextprotocol/servers/pull/2469 * Inspect the stderr output of STDIO servers and attempt to assign an appropriate RFC 5424 Syslog Protocol level before sending a leveled logging message to the client --- server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index 65746017..284424e5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -458,7 +458,7 @@ app.get( level, logger: "stdio", data: { - error: message, + message, }, }, }); From f472327ea0174cf198d365c3e722617bf24290f9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 21 Aug 2025 18:12:38 -0400 Subject: [PATCH 30/36] Trim message before sending to client --- server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index 284424e5..fc87aa0f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -421,7 +421,7 @@ app.get( } else { // Inspect message and attempt to assign a RFC 5424 Syslog Protocol level let level; - let message = chunk.toString(); + let message = chunk.toString().trim(); let ucMsg = chunk.toString().toUpperCase(); if (ucMsg.includes("DEBUG")) { level = "debug"; From c54215252e949b5b2ec3cc878c06b8b49de21708 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 21 Aug 2025 18:40:19 -0400 Subject: [PATCH 31/36] Trim message before sending to client --- server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index fc87aa0f..dc3351d7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -409,7 +409,7 @@ app.get( params: { level: "alert", data: { - error: message, + message, }, }, }); From fb0b56505aa985146dcee6c8c8753fd9b40512d7 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 21 Aug 2025 18:55:58 -0400 Subject: [PATCH 32/36] Trim message before sending to client --- server/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/index.ts b/server/src/index.ts index dc3351d7..11e36060 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -408,6 +408,7 @@ app.get( method: "notifications/message", params: { level: "alert", + logger: "proxy", data: { message, }, From 4cf7aeb9edf7320bf98f3777abdb0ace36c2c12d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 21 Aug 2025 19:08:51 -0400 Subject: [PATCH 33/36] proper levels for server shutdown messages --- server/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 11e36060..1f517d57 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -407,7 +407,7 @@ app.get( jsonrpc: "2.0", method: "notifications/message", params: { - level: "alert", + level: "emergency", logger: "proxy", data: { message, @@ -441,14 +441,14 @@ app.get( } else if (ucMsg.includes("EMERGENCY")) { level = "emergency"; } else if (ucMsg.includes("SIGINT")) { - level = "alert"; message = "SIGINT received. Server shutdown."; + level = "emergency"; } else if (ucMsg.includes("SIGHUP")) { - level = "alert"; message = "SIGHUP received. Server shutdown."; + level = "emergency"; } else if (ucMsg.includes("SIGTERM")) { - level = "alert"; message = "SIGTERM received. Server shutdown."; + level = "emergency"; } else { level = "info"; } From c2e117a4a91e4b3d0d03818298661d905c1557a1 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 25 Aug 2025 10:13:32 -0400 Subject: [PATCH 34/36] Add Docker container startup command to README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 1f6cb822..777f6eeb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,14 @@ npx @modelcontextprotocol/inspector The server will start up and the UI will be accessible at `http://localhost:6274`. +### Docker Container + +You can also start it in a Docker container with the following command: + +```bash +docker run --rm --network host -p 6274:6274 -p 6277:6277 ghcr.io/modelcontextprotocol/inspector:latest +``` + ### From an MCP server repository To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`: From 6853dcf42d4abfa8785fb1c7ee550bf626ee7db6 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 25 Aug 2025 11:31:54 -0400 Subject: [PATCH 35/36] In Dockerfile - use current-alpine3.22 AS builder - The node:24-slim has a high vulnerability CVE as yet unfixed which allows running commands on the host as root --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f36fb8bb..c87e4665 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM node:24-slim AS builder +FROM current-alpine3.22 AS builder # Set working directory WORKDIR /app @@ -49,4 +49,4 @@ ENV SERVER_PORT=6277 EXPOSE ${CLIENT_PORT} ${SERVER_PORT} # Use ENTRYPOINT with CMD for arguments -ENTRYPOINT ["npm", "start"] \ No newline at end of file +ENTRYPOINT ["npm", "start"] From 98becdf5e081ee631af172b3c8079105128557e3 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 25 Aug 2025 11:41:44 -0400 Subject: [PATCH 36/36] In Dockerfile - use current-alpine3.22 AS builder - The node:24-slim has a high vulnerability CVE as yet unfixed which allows running commands on the host as root --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c87e4665..d66091d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM current-alpine3.22 AS builder +FROM node:current-alpine3.22 AS builder # Set working directory WORKDIR /app