From 815acb399df8753a8ccf9efe717850c09580d9b7 Mon Sep 17 00:00:00 2001 From: marc-antoinejean-optable Date: Thu, 18 Jun 2026 09:08:15 -0400 Subject: [PATCH] feat: support initial connection type via query param and CLI flag --- README.md | 4 +- client/bin/start.js | 11 ++++ client/src/App.tsx | 15 +++-- client/src/__tests__/App.config.test.tsx | 57 +++++++++++++++++++ .../src/utils/__tests__/configUtils.test.ts | 51 ++++++++++++++++- client/src/utils/configUtils.ts | 11 ++++ server/src/index.ts | 2 + 7 files changed, 142 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 36e9f3dee..916f7d99f 100644 --- a/README.md +++ b/README.md @@ -357,11 +357,11 @@ npx @modelcontextprotocol/inspector --config mcp.json > **Tip:** You can easily generate this configuration format using the **Server Entry** and **Servers File** buttons in the Inspector UI, as described in the Servers File Export section above. -You can also set the initial `transport` type, `serverUrl`, `serverCommand`, and `serverArgs` via query params, for example: +You can also set the initial `transport` type, `serverUrl`, `connectionType`, `serverCommand`, and `serverArgs` via query params, for example: ``` http://localhost:6274/?transport=sse&serverUrl=http://localhost:8787/sse -http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:8787/mcp +http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:8787/mcp&connectionType=direct http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2 ``` diff --git a/client/bin/start.js b/client/bin/start.js index 7e7e013af..21465ed55 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -36,6 +36,7 @@ async function startDevServer(serverOptions) { abort, transport, serverUrl, + connectionType, } = serverOptions; const serverCommand = "npx"; const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"]; @@ -51,6 +52,7 @@ async function startDevServer(serverOptions) { MCP_ENV_VARS: JSON.stringify(envVars), ...(transport ? { MCP_TRANSPORT: transport } : {}), ...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}), + ...(connectionType ? { MCP_CONNECTION_TYPE: connectionType } : {}), }, signal: abort.signal, echoOutput: true, @@ -91,6 +93,7 @@ async function startProdServer(serverOptions) { mcpServerArgs, transport, serverUrl, + connectionType, } = serverOptions; const inspectorServerPath = resolve( __dirname, @@ -110,6 +113,7 @@ async function startProdServer(serverOptions) { : []), ...(transport ? [`--transport=${transport}`] : []), ...(serverUrl ? [`--server-url=${serverUrl}`] : []), + ...(connectionType ? [`--connection-type=${connectionType}`] : []), ], { env: { @@ -233,6 +237,7 @@ async function main() { let isDev = false; let transport = null; let serverUrl = null; + let connectionType = null; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -257,6 +262,11 @@ async function main() { continue; } + if (parsingFlags && arg === "--connection-type" && i + 1 < args.length) { + connectionType = args[++i]; + continue; + } + if (parsingFlags && arg === "-e" && i + 1 < args.length) { const envVar = args[++i]; const equalsIndex = envVar.indexOf("="); @@ -310,6 +320,7 @@ async function main() { mcpServerArgs, transport, serverUrl, + connectionType, }; const result = isDev diff --git a/client/src/App.tsx b/client/src/App.tsx index 7cf6d751a..06b52baf2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -87,6 +87,7 @@ import { getInitialTransportType, getInitialCommand, getInitialArgs, + getInitialConnectionType, initializeInspectorConfig, saveInspectorConfig, getMCPTaskTtl, @@ -179,13 +180,9 @@ const App = () => { "stdio" | "sse" | "streamable-http" >(getInitialTransportType); const [connectionType, setConnectionType] = useState<"direct" | "proxy">( - () => { - return ( - (localStorage.getItem("lastConnectionType") as "direct" | "proxy") || - "proxy" - ); - }, + getInitialConnectionType, ); + const [logLevel, setLogLevel] = useState("debug"); const [notifications, setNotifications] = useState([]); const [roots, setRoots] = useState([]); @@ -739,6 +736,12 @@ const App = () => { if (data.defaultServerUrl) { setSseUrl(data.defaultServerUrl); } + if ( + data.defaultConnectionType === "direct" || + data.defaultConnectionType === "proxy" + ) { + setConnectionType(data.defaultConnectionType); + } }) .catch((error) => console.error("Error fetching default environment:", error), diff --git a/client/src/__tests__/App.config.test.tsx b/client/src/__tests__/App.config.test.tsx index 7458c2055..aedf5ef58 100644 --- a/client/src/__tests__/App.config.test.tsx +++ b/client/src/__tests__/App.config.test.tsx @@ -33,6 +33,7 @@ jest.mock("../utils/configUtils", () => ({ getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"), getInitialCommand: jest.fn(() => "mcp-server-everything"), getInitialArgs: jest.fn(() => ""), + getInitialConnectionType: jest.fn(() => "proxy"), initializeInspectorConfig: jest.fn(() => DEFAULT_INSPECTOR_CONFIG), saveInspectorConfig: jest.fn(), })); @@ -210,6 +211,62 @@ describe("App - Config Endpoint", () => { ); }); + test("applies defaultConnectionType from /config response", async () => { + const mockConfig = { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_PROXY_AUTH_TOKEN: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, + value: "test-proxy-token", + }, + }; + mockInitializeInspectorConfig.mockReturnValue(mockConfig); + + (global.fetch as jest.Mock).mockResolvedValue({ + json: () => + Promise.resolve({ + defaultEnvironment: {}, + defaultConnectionType: "direct", + }), + }); + + localStorage.setItem("lastConnectionType", "proxy"); + + render(); + + await waitFor(() => { + expect(localStorage.getItem("lastConnectionType")).toBe("direct"); + }); + }); + + test("ignores invalid defaultConnectionType values from /config", async () => { + const mockConfig = { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_PROXY_AUTH_TOKEN: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN, + value: "test-proxy-token", + }, + }; + mockInitializeInspectorConfig.mockReturnValue(mockConfig); + + (global.fetch as jest.Mock).mockResolvedValue({ + json: () => + Promise.resolve({ + defaultEnvironment: {}, + defaultConnectionType: "bogus", + }), + }); + + localStorage.setItem("lastConnectionType", "proxy"); + + render(); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalled(); + }); + + expect(localStorage.getItem("lastConnectionType")).toBe("proxy"); + }); + test("handles config endpoint errors gracefully", async () => { const mockConfig = { ...DEFAULT_INSPECTOR_CONFIG, diff --git a/client/src/utils/__tests__/configUtils.test.ts b/client/src/utils/__tests__/configUtils.test.ts index 86dccbdf1..6c996ae5b 100644 --- a/client/src/utils/__tests__/configUtils.test.ts +++ b/client/src/utils/__tests__/configUtils.test.ts @@ -1,4 +1,4 @@ -import { getMCPProxyAuthToken } from "../configUtils"; +import { getMCPProxyAuthToken, getInitialConnectionType } from "../configUtils"; import { DEFAULT_INSPECTOR_CONFIG } from "../../lib/constants"; import { InspectorConfig } from "../../lib/configurationTypes"; @@ -69,4 +69,53 @@ describe("configUtils", () => { }); }); }); + + describe("getInitialConnectionType", () => { + const originalLocation = window.location; + + const setLocation = (search: string) => { + Object.defineProperty(window, "location", { + configurable: true, + value: new URL(`http://localhost:6274/${search}`), + }); + }; + + beforeEach(() => { + localStorage.clear(); + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); + }); + + test("returns 'direct' when query param is 'direct'", () => { + setLocation("?connectionType=direct"); + expect(getInitialConnectionType()).toBe("direct"); + }); + + test("returns 'proxy' when query param is 'proxy'", () => { + setLocation("?connectionType=proxy"); + expect(getInitialConnectionType()).toBe("proxy"); + }); + + test("falls back to localStorage when query param is missing", () => { + setLocation(""); + localStorage.setItem("lastConnectionType", "direct"); + expect(getInitialConnectionType()).toBe("direct"); + }); + + test("ignores invalid query param values and falls back to localStorage", () => { + setLocation("?connectionType=bogus"); + localStorage.setItem("lastConnectionType", "direct"); + expect(getInitialConnectionType()).toBe("direct"); + }); + + test("defaults to 'proxy' when nothing is set", () => { + setLocation(""); + expect(getInitialConnectionType()).toBe("proxy"); + }); + }); }); diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index bc081b8f8..fe2e72622 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -92,6 +92,17 @@ export const getInitialArgs = (): string => { return localStorage.getItem("lastArgs") || ""; }; +export const getInitialConnectionType = (): "direct" | "proxy" => { + const param = getSearchParam("connectionType"); + if (param === "proxy" || param === "direct") { + return param; + } + return ( + (localStorage.getItem("lastConnectionType") as "direct" | "proxy") || + "proxy" + ); +}; + // Returns a map of config key -> value from query params if present export const getConfigOverridesFromQueryParams = ( defaultConfig: InspectorConfig, diff --git a/server/src/index.ts b/server/src/index.ts index bdfe49019..8189727e8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -53,6 +53,7 @@ const { values } = parseArgs({ command: { type: "string", default: "" }, transport: { type: "string", default: "" }, "server-url": { type: "string", default: "" }, + "connection-type": { type: "string", default: "" }, }, }); @@ -922,6 +923,7 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => { defaultArgs: values.args, defaultTransport: values.transport, defaultServerUrl: values["server-url"], + defaultConnectionType: values["connection-type"], }); } catch (error) { console.error("Error in /config route:", error);