From 3c5c38462b449487f6d5b3b12449c19b28d460f3 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 5 May 2025 11:40:14 -0600 Subject: [PATCH 1/2] feat(client): initialize via search params --- README.md | 10 ++++++++ client/src/App.tsx | 29 +++++++++------------- client/src/utils/configUtils.ts | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b312f71..21b51a7 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,16 @@ Example server configuration file: } ``` +You can also set the initial `transport` type, `serverUrl`, `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=stdio&serverCommand=npx&serverArgs=arg1%20arg2 +``` + +Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence. + ### From this repository If you're working on the inspector itself: diff --git a/client/src/App.tsx b/client/src/App.tsx index d990db5..7e6a986 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -51,7 +51,13 @@ import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; -import { getMCPProxyAddress } from "./utils/configUtils"; +import { + getMCPProxyAddress, + getInitialSseUrl, + getInitialTransportType, + getInitialCommand, + getInitialArgs, +} from "./utils/configUtils"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -71,26 +77,13 @@ const App = () => { prompts: null, tools: null, }); - const [command, setCommand] = useState(() => { - return localStorage.getItem("lastCommand") || "mcp-server-everything"; - }); - const [args, setArgs] = useState(() => { - return localStorage.getItem("lastArgs") || ""; - }); + const [command, setCommand] = useState(getInitialCommand); + const [args, setArgs] = useState(getInitialArgs); - const [sseUrl, setSseUrl] = useState(() => { - return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse"; - }); + const [sseUrl, setSseUrl] = useState(getInitialSseUrl); const [transportType, setTransportType] = useState< "stdio" | "sse" | "streamable-http" - >(() => { - return ( - (localStorage.getItem("lastTransportType") as - | "stdio" - | "sse" - | "streamable-http") || "stdio" - ); - }); + >(getInitialTransportType); const [logLevel, setLogLevel] = useState("debug"); const [notifications, setNotifications] = useState([]); const [stdErrNotifications, setStdErrNotifications] = useState< diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index 86e1643..ef9bf38 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -24,3 +24,46 @@ export const getMCPServerRequestMaxTotalTimeout = ( ): number => { return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number; }; + +const getSearchParam = (key: string): string | null => { + try { + const url = new URL(window.location.href); + return url.searchParams.get(key); + } catch { + return null; + } +}; + +export const getInitialTransportType = (): + | "stdio" + | "sse" + | "streamable-http" => { + const param = getSearchParam("transport"); + if (param === "stdio" || param === "sse" || param === "streamable-http") { + return param; + } + return ( + (localStorage.getItem("lastTransportType") as + | "stdio" + | "sse" + | "streamable-http") || "stdio" + ); +}; + +export const getInitialSseUrl = (): string => { + const param = getSearchParam("serverUrl"); + if (param) return param; + return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse"; +}; + +export const getInitialCommand = (): string => { + const param = getSearchParam("serverCommand"); + if (param) return param; + return localStorage.getItem("lastCommand") || "mcp-server-everything"; +}; + +export const getInitialArgs = (): string => { + const param = getSearchParam("serverArgs"); + if (param) return param; + return localStorage.getItem("lastArgs") || ""; +}; From 2915cccd852ce7a764e191f4c949fa95abf6770d Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 5 May 2025 17:44:33 -0600 Subject: [PATCH 2/2] feat(client): make all config initialize-able via search params --- README.md | 6 ++++ client/src/App.tsx | 26 +++------------ client/src/utils/configUtils.ts | 59 ++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 21b51a7..6239e31 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,12 @@ http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:8787 http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2 ``` +You can also set initial config settings via query params, for example: + +``` +http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=10000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577 +``` + Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence. ### From this repository diff --git a/client/src/App.tsx b/client/src/App.tsx index 7e6a986..32bdcf3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -49,7 +49,6 @@ import RootsTab from "./components/RootsTab"; import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; -import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; import { getMCPProxyAddress, @@ -57,6 +56,7 @@ import { getInitialTransportType, getInitialCommand, getInitialArgs, + initializeInspectorConfig, } from "./utils/configUtils"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; @@ -92,27 +92,9 @@ const App = () => { const [roots, setRoots] = useState([]); const [env, setEnv] = useState>({}); - const [config, setConfig] = useState(() => { - const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); - if (savedConfig) { - // merge default config with saved config - const mergedConfig = { - ...DEFAULT_INSPECTOR_CONFIG, - ...JSON.parse(savedConfig), - } as InspectorConfig; - - // update description of keys to match the new description (in case of any updates to the default config description) - Object.entries(mergedConfig).forEach(([key, value]) => { - mergedConfig[key as keyof InspectorConfig] = { - ...value, - label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, - }; - }); - - return mergedConfig; - } - return DEFAULT_INSPECTOR_CONFIG; - }); + const [config, setConfig] = useState(() => + initializeInspectorConfig(CONFIG_LOCAL_STORAGE_KEY), + ); const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; }); diff --git a/client/src/utils/configUtils.ts b/client/src/utils/configUtils.ts index ef9bf38..ae404d6 100644 --- a/client/src/utils/configUtils.ts +++ b/client/src/utils/configUtils.ts @@ -1,5 +1,8 @@ import { InspectorConfig } from "@/lib/configurationTypes"; -import { DEFAULT_MCP_PROXY_LISTEN_PORT } from "@/lib/constants"; +import { + DEFAULT_MCP_PROXY_LISTEN_PORT, + DEFAULT_INSPECTOR_CONFIG, +} from "@/lib/constants"; export const getMCPProxyAddress = (config: InspectorConfig): string => { const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string; @@ -67,3 +70,57 @@ export const getInitialArgs = (): string => { if (param) return param; return localStorage.getItem("lastArgs") || ""; }; + +// Returns a map of config key -> value from query params if present +export const getConfigOverridesFromQueryParams = ( + defaultConfig: InspectorConfig, +): Partial => { + const url = new URL(window.location.href); + const overrides: Partial = {}; + for (const key of Object.keys(defaultConfig)) { + const param = url.searchParams.get(key); + if (param !== null) { + // Try to coerce to correct type based on default value + const defaultValue = defaultConfig[key as keyof InspectorConfig].value; + let value: string | number | boolean = param; + if (typeof defaultValue === "number") { + value = Number(param); + } else if (typeof defaultValue === "boolean") { + value = param === "true"; + } + overrides[key as keyof InspectorConfig] = { + ...defaultConfig[key as keyof InspectorConfig], + value, + }; + } + } + return overrides; +}; + +export const initializeInspectorConfig = ( + localStorageKey: string, +): InspectorConfig => { + const savedConfig = localStorage.getItem(localStorageKey); + let baseConfig: InspectorConfig; + if (savedConfig) { + // merge default config with saved config + const mergedConfig = { + ...DEFAULT_INSPECTOR_CONFIG, + ...JSON.parse(savedConfig), + } as InspectorConfig; + + // update description of keys to match the new description (in case of any updates to the default config description) + for (const [key, value] of Object.entries(mergedConfig)) { + mergedConfig[key as keyof InspectorConfig] = { + ...value, + label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, + }; + } + baseConfig = mergedConfig; + } else { + baseConfig = DEFAULT_INSPECTOR_CONFIG; + } + // Apply query param overrides + const overrides = getConfigOverridesFromQueryParams(DEFAULT_INSPECTOR_CONFIG); + return { ...baseConfig, ...overrides }; +};