From f514857a3401c05826d73cb94854886b4f0f1aa4 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 19 May 2025 05:31:24 -0700 Subject: [PATCH 001/120] 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 002/120] 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 003/120] 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 925d11a91dc7fd61c95d45006a2226ee59497788 Mon Sep 17 00:00:00 2001 From: oleina Date: Fri, 11 Jul 2025 11:18:01 -0700 Subject: [PATCH 004/120] initalize logging for UI --- client/src/App.tsx | 1 + client/src/lib/hooks/useConnection.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/client/src/App.tsx b/client/src/App.tsx index cff51b4f..bef4ec4e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -204,6 +204,7 @@ const App = () => { ]); }, getRoots: () => rootsRef.current, + defaultLoggingLevel: "debug" }); useEffect(() => { diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9009e698..1ef55154 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -28,6 +28,7 @@ import { ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, Progress, + LoggingLevel, } from "@modelcontextprotocol/sdk/types.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { useState } from "react"; @@ -63,6 +64,7 @@ interface UseConnectionOptions { onPendingRequest?: (request: any, resolve: any, reject: any) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any getRoots?: () => any[]; + defaultLoggingLevel?: LoggingLevel, } export function useConnection({ @@ -78,6 +80,7 @@ export function useConnection({ onStdErrNotification, onPendingRequest, getRoots, + defaultLoggingLevel, }: UseConnectionOptions) { const [connectionStatus, setConnectionStatus] = useState("disconnected"); @@ -521,6 +524,10 @@ export function useConnection({ }); } + if (capabilities?.logging && defaultLoggingLevel) { + await client.setLoggingLevel(defaultLoggingLevel); + } + setMcpClient(client); setConnectionStatus("connected"); } catch (e) { From f4d9bf6d5ee9cb00fa4a04f12a6e48230b7dc58f Mon Sep 17 00:00:00 2001 From: oleina Date: Fri, 11 Jul 2025 11:18:01 -0700 Subject: [PATCH 005/120] initalize logging for cli mode --- cli/src/client/connection.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/src/client/connection.ts b/cli/src/client/connection.ts index 931f803d..dcbe8e51 100644 --- a/cli/src/client/connection.ts +++ b/cli/src/client/connection.ts @@ -18,6 +18,12 @@ export async function connect( ): Promise { try { await client.connect(transport); + + if (client.getServerCapabilities()?.logging) { + // default logging level is undefined in the spec, but the user of the + // inspector most likely wants debug. + await client.setLoggingLevel("debug"); + } } catch (error) { throw new Error( `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`, From 1c8bf36ea2b35ee4d76472e4c883fbf31c522cb2 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 13 Jul 2025 08:42:46 +0300 Subject: [PATCH 006/120] generate random state parameter in oauth state machine --- client/src/lib/oauth-state-machine.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index d87b3ecd..b3307af9 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -113,6 +113,13 @@ export const oauthTransitions: Record = { scope = metadata.scopes_supported.join(" "); } + // Generate a random state + const array = new Uint8Array(32); + crypto.getRandomValues(array); + const state = Array.from(array, (byte) => + byte.toString(16).padStart(2, '0'), + ).join(''); + const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, { @@ -120,6 +127,7 @@ export const oauthTransitions: Record = { clientInformation, redirectUrl: context.provider.redirectUrl, scope, + state: state, resource: context.state.resource ?? undefined, }, ); From 4ef85f744c5ee96ddefb97620f36ddc79d1de640 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sun, 13 Jul 2025 08:44:57 +0300 Subject: [PATCH 007/120] generate random state parameter in oauth state machine --- client/src/lib/oauth-state-machine.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index b3307af9..5078cfe4 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -117,8 +117,8 @@ export const oauthTransitions: Record = { const array = new Uint8Array(32); crypto.getRandomValues(array); const state = Array.from(array, (byte) => - byte.toString(16).padStart(2, '0'), - ).join(''); + byte.toString(16).padStart(2, "0"), + ).join(""); const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, From d810680b93fc6b64feb6ef652ddd0df8277152b6 Mon Sep 17 00:00:00 2001 From: oleina Date: Mon, 14 Jul 2025 07:57:58 -0700 Subject: [PATCH 008/120] use default logging level from useState --- client/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index bef4ec4e..59464035 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -204,7 +204,7 @@ const App = () => { ]); }, getRoots: () => rootsRef.current, - defaultLoggingLevel: "debug" + defaultLoggingLevel: logLevel }); useEffect(() => { From 7ce0938e2713ef2d4513fa6f4224a8482e5b996d Mon Sep 17 00:00:00 2001 From: oleina Date: Mon, 14 Jul 2025 08:00:13 -0700 Subject: [PATCH 009/120] run linter --- client/src/App.tsx | 2 +- client/src/lib/hooks/useConnection.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 59464035..c80cfd35 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -204,7 +204,7 @@ const App = () => { ]); }, getRoots: () => rootsRef.current, - defaultLoggingLevel: logLevel + defaultLoggingLevel: logLevel, }); useEffect(() => { diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 1ef55154..b32552f4 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -64,7 +64,7 @@ interface UseConnectionOptions { onPendingRequest?: (request: any, resolve: any, reject: any) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any getRoots?: () => any[]; - defaultLoggingLevel?: LoggingLevel, + defaultLoggingLevel?: LoggingLevel; } export function useConnection({ From 99b962d4452ca5fd49e519e528c0d4a2f3406daa Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 26 Jul 2025 09:15:27 -0700 Subject: [PATCH 010/120] Parse tool args from CLI as JsonValue instead of just strings --- cli/src/client/prompts.ts | 28 ++++++++++++++++++++++++++-- cli/src/client/tools.ts | 37 +++++++++++++++++++++++++++++-------- cli/src/index.ts | 29 ++++++++++++++++++++++++----- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts index 0b237496..79bc4d29 100644 --- a/cli/src/client/prompts.ts +++ b/cli/src/client/prompts.ts @@ -1,6 +1,16 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { McpResponse } from "./types.js"; +// JSON value type matching the client utils +type JsonValue = + | string + | number + | boolean + | null + | undefined + | JsonValue[] + | { [key: string]: JsonValue }; + // List available prompts export async function listPrompts(client: Client): Promise { try { @@ -17,12 +27,26 @@ export async function listPrompts(client: Client): Promise { export async function getPrompt( client: Client, name: string, - args?: Record, + args?: Record, ): Promise { try { + // Convert all arguments to strings for prompt arguments + const stringArgs: Record = {}; + if (args) { + for (const [key, value] of Object.entries(args)) { + if (typeof value === 'string') { + stringArgs[key] = value; + } else if (value === null || value === undefined) { + stringArgs[key] = String(value); + } else { + stringArgs[key] = JSON.stringify(value); + } + } + } + const response = await client.getPrompt({ name, - arguments: args || {}, + arguments: stringArgs, }); return response; diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts index acdb4871..6d886234 100644 --- a/cli/src/client/tools.ts +++ b/cli/src/client/tools.ts @@ -2,6 +2,16 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { McpResponse } from "./types.js"; +// JSON value type matching the client utils +type JsonValue = + | string + | number + | boolean + | null + | undefined + | JsonValue[] + | { [key: string]: JsonValue }; + type JsonSchemaType = { type: "string" | "number" | "integer" | "boolean" | "array" | "object"; description?: string; @@ -20,7 +30,7 @@ export async function listTools(client: Client): Promise { } } -function convertParameterValue(value: string, schema: JsonSchemaType): unknown { +function convertParameterValue(value: string, schema: JsonSchemaType): JsonValue { if (!value) { return value; } @@ -35,7 +45,7 @@ function convertParameterValue(value: string, schema: JsonSchemaType): unknown { if (schema.type === "object" || schema.type === "array") { try { - return JSON.parse(value); + return JSON.parse(value) as JsonValue; } catch (error) { return value; } @@ -47,8 +57,8 @@ function convertParameterValue(value: string, schema: JsonSchemaType): unknown { function convertParameters( tool: Tool, params: Record, -): Record { - const result: Record = {}; +): Record { + const result: Record = {}; const properties = tool.inputSchema.properties || {}; for (const [key, value] of Object.entries(params)) { @@ -68,18 +78,29 @@ function convertParameters( export async function callTool( client: Client, name: string, - args: Record, + args: Record, ): Promise { try { const toolsResponse = await listTools(client); const tools = toolsResponse.tools as Tool[]; const tool = tools.find((t) => t.name === name); - let convertedArgs: Record = args; + let convertedArgs: Record = args; if (tool) { - // Convert parameters based on the tool's schema - convertedArgs = convertParameters(tool, args); + // Convert parameters based on the tool's schema, but only for string values + // since we now accept pre-parsed values from the CLI + const stringArgs: Record = {}; + for (const [key, value] of Object.entries(args)) { + if (typeof value === 'string') { + stringArgs[key] = value; + } + } + + if (Object.keys(stringArgs).length > 0) { + const convertedStringArgs = convertParameters(tool, stringArgs); + convertedArgs = { ...args, ...convertedStringArgs }; + } } const response = await client.callTool({ diff --git a/cli/src/index.ts b/cli/src/index.ts index 2b0c4f53..563160bc 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -20,15 +20,25 @@ import { import { handleError } from "./error-handler.js"; import { createTransport, TransportOptions } from "./transport.js"; +// JSON value type for CLI arguments +type JsonValue = + | string + | number + | boolean + | null + | undefined + | JsonValue[] + | { [key: string]: JsonValue }; + type Args = { target: string[]; method?: string; promptName?: string; - promptArgs?: Record; + promptArgs?: Record; uri?: string; logLevel?: LogLevel; toolName?: string; - toolArg?: Record; + toolArg?: Record; transport?: "sse" | "stdio" | "http"; }; @@ -162,8 +172,8 @@ async function callMethod(args: Args): Promise { function parseKeyValuePair( value: string, - previous: Record = {}, -): Record { + previous: Record = {}, +): Record { const parts = value.split("="); const key = parts[0]; const val = parts.slice(1).join("="); @@ -174,7 +184,16 @@ function parseKeyValuePair( ); } - return { ...previous, [key as string]: val }; + // Try to parse as JSON first + let parsedValue: JsonValue; + try { + parsedValue = JSON.parse(val) as JsonValue; + } catch { + // If JSON parsing fails, keep as string + parsedValue = val; + } + + return { ...previous, [key as string]: parsedValue }; } function parseArgs(): Args { From 6ce4a0b56343c0251866e747032733c2a8b553a0 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 27 Jul 2025 07:19:48 -0700 Subject: [PATCH 011/120] Fix formatting --- cli/src/client/prompts.ts | 2 +- cli/src/client/tools.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts index 79bc4d29..87014617 100644 --- a/cli/src/client/prompts.ts +++ b/cli/src/client/prompts.ts @@ -34,7 +34,7 @@ export async function getPrompt( const stringArgs: Record = {}; if (args) { for (const [key, value] of Object.entries(args)) { - if (typeof value === 'string') { + if (typeof value === "string") { stringArgs[key] = value; } else if (value === null || value === undefined) { stringArgs[key] = String(value); diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts index 6d886234..9da0b4d9 100644 --- a/cli/src/client/tools.ts +++ b/cli/src/client/tools.ts @@ -30,7 +30,10 @@ export async function listTools(client: Client): Promise { } } -function convertParameterValue(value: string, schema: JsonSchemaType): JsonValue { +function convertParameterValue( + value: string, + schema: JsonSchemaType, +): JsonValue { if (!value) { return value; } @@ -92,11 +95,11 @@ export async function callTool( // since we now accept pre-parsed values from the CLI const stringArgs: Record = {}; for (const [key, value] of Object.entries(args)) { - if (typeof value === 'string') { + if (typeof value === "string") { stringArgs[key] = value; } } - + if (Object.keys(stringArgs).length > 0) { const convertedStringArgs = convertParameters(tool, stringArgs); convertedArgs = { ...args, ...convertedStringArgs }; From 491a8dcadda70a89ab95789d857be7ca34e9ac87 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 27 Jul 2025 08:16:19 -0700 Subject: [PATCH 012/120] Add separate tool tests --- .gitignore | 1 + cli/scripts/cli-tests.js | 61 ---- cli/scripts/cli-tool-tests.js | 611 ++++++++++++++++++++++++++++++++++ 3 files changed, 612 insertions(+), 61 deletions(-) create mode 100644 cli/scripts/cli-tool-tests.js diff --git a/.gitignore b/.gitignore index eeee17fe..349c2139 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ client/tsconfig.app.tsbuildinfo client/tsconfig.node.tsbuildinfo cli/build test-output +tool-test-output # symlinked by `npm run link:sdk`: sdk client/playwright-report/ diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index 68ce3885..c2b8b2cb 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -37,9 +37,6 @@ console.log(`${colors.BLUE}- Environment variables (-e)${colors.NC}`); console.log(`${colors.BLUE}- Config file (--config)${colors.NC}`); console.log(`${colors.BLUE}- Server selection (--server)${colors.NC}`); console.log(`${colors.BLUE}- Method selection (--method)${colors.NC}`); -console.log( - `${colors.BLUE}- Tool-related options (--tool-name, --tool-arg)${colors.NC}`, -); console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`); console.log( `${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`, @@ -454,64 +451,6 @@ async function runTests() { "tools/list", ); - console.log( - `\n${colors.YELLOW}=== Running Tool-Related Tests ===${colors.NC}`, - ); - - // Test 12: CLI mode with tool call - await runBasicTest( - "tool_call", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "message=Hello", - ); - - // Test 13: CLI mode with tool call but missing tool name (should fail) - await runErrorTest( - "missing_tool_name", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-arg", - "message=Hello", - ); - - // Test 14: CLI mode with tool call but invalid tool args format (should fail) - await runErrorTest( - "invalid_tool_args", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "echo", - "--tool-arg", - "invalid_format", - ); - - // Test 15: CLI mode with multiple tool args - await runBasicTest( - "multiple_tool_args", - TEST_CMD, - ...TEST_ARGS, - "--cli", - "--method", - "tools/call", - "--tool-name", - "add", - "--tool-arg", - "a=1", - "b=2", - ); console.log( `\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`, diff --git a/cli/scripts/cli-tool-tests.js b/cli/scripts/cli-tool-tests.js new file mode 100644 index 00000000..41e0a787 --- /dev/null +++ b/cli/scripts/cli-tool-tests.js @@ -0,0 +1,611 @@ +#!/usr/bin/env node + +// Colors for output +const colors = { + GREEN: "\x1b[32m", + YELLOW: "\x1b[33m", + RED: "\x1b[31m", + BLUE: "\x1b[34m", + ORANGE: "\x1b[33m", + NC: "\x1b[0m", // No Color +}; + +import fs from "fs"; +import path from "path"; +import { spawn } from "child_process"; +import os from "os"; +import { fileURLToPath } from "url"; + +// Get directory paths with ESM compatibility +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Track test results +let PASSED_TESTS = 0; +let FAILED_TESTS = 0; +let SKIPPED_TESTS = 0; +let TOTAL_TESTS = 0; + +console.log( + `${colors.YELLOW}=== MCP Inspector CLI Tool Tests ===${colors.NC}`, +); +console.log( + `${colors.BLUE}This script tests the MCP Inspector CLI's tool-related functionality:${colors.NC}`, +); +console.log(`${colors.BLUE}- Tool discovery and listing${colors.NC}`); +console.log(`${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`); +console.log(`${colors.BLUE}- Tool schema validation${colors.NC}`); +console.log(`${colors.BLUE}- Tool execution with various argument types${colors.NC}`); +console.log(`${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`); +console.log(`\n`); + +// Get directory paths +const SCRIPTS_DIR = __dirname; +const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../"); +const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build"); + +// Define the test server command using npx +const TEST_CMD = "npx"; +const TEST_ARGS = ["@modelcontextprotocol/server-everything"]; + +// Create output directory for test results +const OUTPUT_DIR = path.join(SCRIPTS_DIR, "tool-test-output"); +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +// Create a temporary directory for test files +const TEMP_DIR = path.join(os.tmpdir(), "mcp-inspector-tool-tests"); +fs.mkdirSync(TEMP_DIR, { recursive: true }); + +// Track servers for cleanup +let runningServers = []; + +process.on("exit", () => { + try { + fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + } catch (err) { + console.error( + `${colors.RED}Failed to remove temp directory: ${err.message}${colors.NC}`, + ); + } + + runningServers.forEach((server) => { + try { + process.kill(-server.pid); + } catch (e) {} + }); +}); + +process.on("SIGINT", () => { + runningServers.forEach((server) => { + try { + process.kill(-server.pid); + } catch (e) {} + }); + process.exit(1); +}); + +// Function to run a basic test +async function runBasicTest(testName, ...args) { + const outputFile = path.join( + OUTPUT_DIR, + `${testName.replace(/\//g, "_")}.log`, + ); + + console.log(`\n${colors.YELLOW}Testing: ${testName}${colors.NC}`); + TOTAL_TESTS++; + + // Run the command and capture output + console.log( + `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, + ); + + try { + // Create a write stream for the output file + const outputStream = fs.createWriteStream(outputFile); + + // Spawn the process + return new Promise((resolve) => { + const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { + stdio: ["ignore", "pipe", "pipe"], + }); + + const timeout = setTimeout(() => { + console.log(`${colors.YELLOW}Test timed out: ${testName}${colors.NC}`); + child.kill(); + }, 10000); + + // Pipe stdout and stderr to the output file + child.stdout.pipe(outputStream); + child.stderr.pipe(outputStream); + + // Also capture output for display + let output = ""; + child.stdout.on("data", (data) => { + output += data.toString(); + }); + child.stderr.on("data", (data) => { + output += data.toString(); + }); + + child.on("close", (code) => { + clearTimeout(timeout); + outputStream.end(); + + if (code === 0) { + console.log(`${colors.GREEN}βœ“ Test passed: ${testName}${colors.NC}`); + console.log(`${colors.BLUE}First few lines of output:${colors.NC}`); + const firstFewLines = output + .split("\n") + .slice(0, 5) + .map((line) => ` ${line}`) + .join("\n"); + console.log(firstFewLines); + PASSED_TESTS++; + resolve(true); + } else { + console.log(`${colors.RED}βœ— Test failed: ${testName}${colors.NC}`); + console.log(`${colors.RED}Error output:${colors.NC}`); + console.log( + output + .split("\n") + .map((line) => ` ${line}`) + .join("\n"), + ); + FAILED_TESTS++; + + // Stop after any error is encountered + console.log( + `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, + ); + process.exit(1); + } + }); + }); + } catch (error) { + console.error( + `${colors.RED}Error running test: ${error.message}${colors.NC}`, + ); + FAILED_TESTS++; + process.exit(1); + } +} + +// Function to run an error test (expected to fail) +async function runErrorTest(testName, ...args) { + const outputFile = path.join( + OUTPUT_DIR, + `${testName.replace(/\//g, "_")}.log`, + ); + + console.log(`\n${colors.YELLOW}Testing error case: ${testName}${colors.NC}`); + TOTAL_TESTS++; + + // Run the command and capture output + console.log( + `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`, + ); + + try { + // Create a write stream for the output file + const outputStream = fs.createWriteStream(outputFile); + + // Spawn the process + return new Promise((resolve) => { + const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], { + stdio: ["ignore", "pipe", "pipe"], + }); + + const timeout = setTimeout(() => { + console.log( + `${colors.YELLOW}Error test timed out: ${testName}${colors.NC}`, + ); + child.kill(); + }, 10000); + + // Pipe stdout and stderr to the output file + child.stdout.pipe(outputStream); + child.stderr.pipe(outputStream); + + // Also capture output for display + let output = ""; + child.stdout.on("data", (data) => { + output += data.toString(); + }); + child.stderr.on("data", (data) => { + output += data.toString(); + }); + + child.on("close", (code) => { + clearTimeout(timeout); + outputStream.end(); + + // For error tests, we expect a non-zero exit code + if (code !== 0) { + console.log( + `${colors.GREEN}βœ“ Error test passed: ${testName}${colors.NC}`, + ); + console.log(`${colors.BLUE}Error output (expected):${colors.NC}`); + const firstFewLines = output + .split("\n") + .slice(0, 5) + .map((line) => ` ${line}`) + .join("\n"); + console.log(firstFewLines); + PASSED_TESTS++; + resolve(true); + } else { + console.log( + `${colors.RED}βœ— Error test failed: ${testName} (expected error but got success)${colors.NC}`, + ); + console.log(`${colors.RED}Output:${colors.NC}`); + console.log( + output + .split("\n") + .map((line) => ` ${line}`) + .join("\n"), + ); + FAILED_TESTS++; + + // Stop after any error is encountered + console.log( + `${colors.YELLOW}Stopping tests due to error. Please validate and fix before continuing.${colors.NC}`, + ); + process.exit(1); + } + }); + }); + } catch (error) { + console.error( + `${colors.RED}Error running test: ${error.message}${colors.NC}`, + ); + FAILED_TESTS++; + process.exit(1); + } +} + +// Run all tests +async function runTests() { + console.log( + `\n${colors.YELLOW}=== Running Tool Discovery Tests ===${colors.NC}`, + ); + + // Test 1: List available tools + await runBasicTest( + "tool_discovery_list", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/list", + ); + + console.log( + `\n${colors.YELLOW}=== Running JSON Argument Parsing Tests ===${colors.NC}`, + ); + + // Test 2: String arguments (backward compatibility) + await runBasicTest( + "json_args_string", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + "message=hello world", + ); + + // Test 3: Number arguments + await runBasicTest( + "json_args_number_integer", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "add", + "--tool-arg", + "a=42", + "b=58", + ); + + // Test 4: Number arguments with decimals (using add tool with decimal numbers) + await runBasicTest( + "json_args_number_decimal", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "add", + "--tool-arg", + "a=19.99", + "b=20.01", + ); + + // Test 5: Boolean arguments - true + await runBasicTest( + "json_args_boolean_true", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "annotatedMessage", + "--tool-arg", + "messageType=success", + "includeImage=true", + ); + + // Test 6: Boolean arguments - false + await runBasicTest( + "json_args_boolean_false", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "annotatedMessage", + "--tool-arg", + "messageType=error", + "includeImage=false", + ); + + // Test 7: Null arguments (using echo with string "null") + await runBasicTest( + "json_args_null", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + 'message="null"', + ); + + + // Test 14: Multiple arguments with mixed types (using add tool) + await runBasicTest( + "json_args_multiple_mixed", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "add", + "--tool-arg", + "a=42.5", + "b=57.5", + ); + + console.log( + `\n${colors.YELLOW}=== Running JSON Parsing Edge Cases ===${colors.NC}`, + ); + + // Test 15: Invalid JSON should fall back to string + await runBasicTest( + "json_args_invalid_fallback", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + "message={invalid json}", + ); + + // Test 16: Empty string value + await runBasicTest( + "json_args_empty_value", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + 'message=""', + ); + + // Test 17: Special characters in strings + await runBasicTest( + "json_args_special_chars", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + 'message="C:\\\\Users\\\\test"', + ); + + // Test 18: Unicode characters + await runBasicTest( + "json_args_unicode", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + 'message="πŸš€πŸŽ‰βœ¨"', + ); + + // Test 19: Arguments with equals signs in values + await runBasicTest( + "json_args_equals_in_value", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + "message=2+2=4", + ); + + // Test 20: Base64-like strings + await runBasicTest( + "json_args_base64_like", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + "message=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=", + ); + + console.log( + `\n${colors.YELLOW}=== Running Tool Error Handling Tests ===${colors.NC}`, + ); + + // Test 21: Non-existent tool + await runErrorTest( + "tool_error_nonexistent", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "nonexistent_tool", + "--tool-arg", + "message=test", + ); + + // Test 22: Missing tool name + await runErrorTest( + "tool_error_missing_name", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-arg", + "message=test", + ); + + // Test 23: Invalid tool argument format + await runErrorTest( + "tool_error_invalid_arg_format", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + "invalid_format_no_equals", + ); + + console.log( + `\n${colors.YELLOW}=== Running Prompt JSON Argument Tests ===${colors.NC}`, + ); + + // Test 24: Prompt with JSON arguments + await runBasicTest( + "prompt_json_args_mixed", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "prompts/get", + "--prompt-name", + "complex_prompt", + "--prompt-args", + "temperature=0.7", + 'style="concise"', + 'options={"format":"json","max_tokens":100}', + ); + + // Test 25: Prompt with simple arguments + await runBasicTest( + "prompt_json_args_simple", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "prompts/get", + "--prompt-name", + "simple_prompt", + "--prompt-args", + "name=test", + "count=5", + ); + + console.log( + `\n${colors.YELLOW}=== Running Backward Compatibility Tests ===${colors.NC}`, + ); + + // Test 26: Ensure existing string-only usage still works + await runBasicTest( + "backward_compatibility_strings", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "echo", + "--tool-arg", + "message=hello", + ); + + // Test 27: Multiple string arguments (existing pattern) - using add tool + await runBasicTest( + "backward_compatibility_multiple_strings", + TEST_CMD, + ...TEST_ARGS, + "--cli", + "--method", + "tools/call", + "--tool-name", + "add", + "--tool-arg", + "a=10", + "b=20", + ); + + // Print test summary + console.log(`\n${colors.YELLOW}=== Test Summary ===${colors.NC}`); + console.log(`${colors.GREEN}Passed: ${PASSED_TESTS}${colors.NC}`); + console.log(`${colors.RED}Failed: ${FAILED_TESTS}${colors.NC}`); + console.log(`${colors.ORANGE}Skipped: ${SKIPPED_TESTS}${colors.NC}`); + console.log(`Total: ${TOTAL_TESTS}`); + console.log( + `${colors.BLUE}Detailed logs saved to: ${OUTPUT_DIR}${colors.NC}`, + ); + + console.log(`\n${colors.GREEN}All tool tests completed!${colors.NC}`); +} + +// Run all tests +runTests().catch((error) => { + console.error( + `${colors.RED}Tests failed with error: ${error.message}${colors.NC}`, + ); + process.exit(1); +}); From 6a2dcb0ec337715ecda84105168f19673cbcdbce Mon Sep 17 00:00:00 2001 From: olaservo Date: Sun, 27 Jul 2025 19:20:21 -0700 Subject: [PATCH 013/120] Add CLI tool tests in CI --- cli/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/package.json b/cli/package.json index bf344733..336e34e1 100644 --- a/cli/package.json +++ b/cli/package.json @@ -17,7 +17,9 @@ "scripts": { "build": "tsc", "postbuild": "node scripts/make-executable.js", - "test": "node scripts/cli-tests.js" + "test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js", + "test:cli": "node scripts/cli-tests.js", + "test:cli-tools": "node scripts/cli-tool-tests.js" }, "devDependencies": {}, "dependencies": { From 243cbf0caca6ea3d223a30623a1d70bcd50e89a9 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Wed, 25 Jun 2025 12:58:14 -0700 Subject: [PATCH 014/120] Update prettier --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53313497..fabc68fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@types/node": "^22.7.5", "@types/shell-quote": "^1.7.5", "jest-fixed-jsdom": "^0.0.9", - "prettier": "3.3.3", + "prettier": "^3.6.2", "rimraf": "^6.0.1", "typescript": "^5.4.2" }, @@ -8746,9 +8746,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index df2861c8..afc2c63b 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@types/node": "^22.7.5", "@types/shell-quote": "^1.7.5", "jest-fixed-jsdom": "^0.0.9", - "prettier": "3.3.3", + "prettier": "^3.6.2", "rimraf": "^6.0.1", "typescript": "^5.4.2" }, From 4d3dcc5fd18717b5575d320ab20f5cde2723ff1e Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:08:47 -0700 Subject: [PATCH 015/120] Update open --- package-lock.json | 25 ++++++++++++++++++++----- package.json | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index fabc68fb..fc200c09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@modelcontextprotocol/inspector-server": "^0.16.2", "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.0.1", - "open": "^10.1.0", + "open": "^10.2.0", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", @@ -8226,15 +8226,15 @@ } }, "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -10879,6 +10879,21 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index afc2c63b..a9cc7b44 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@modelcontextprotocol/inspector-server": "^0.16.2", "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.0.1", - "open": "^10.1.0", + "open": "^10.2.0", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", From 89928978b331e9c1962594f619ed353312e8581a Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:10:33 -0700 Subject: [PATCH 016/120] Update concurrently --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc200c09..9734b797 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@modelcontextprotocol/inspector-client": "^0.16.2", "@modelcontextprotocol/inspector-server": "^0.16.2", "@modelcontextprotocol/sdk": "^1.17.0", - "concurrently": "^9.0.1", + "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", @@ -4706,9 +4706,9 @@ "license": "MIT" }, "node_modules/concurrently": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", - "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", diff --git a/package.json b/package.json index a9cc7b44..b2eef3f6 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@modelcontextprotocol/inspector-client": "^0.16.2", "@modelcontextprotocol/inspector-server": "^0.16.2", "@modelcontextprotocol/sdk": "^1.17.0", - "concurrently": "^9.0.1", + "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.2", "spawn-rx": "^5.1.2", From 15bf4701735326f11ffd7e60d16d3908754dd37b Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:11:22 -0700 Subject: [PATCH 017/120] Update shell-quote --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9734b797..4bb3568b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.2.0", "open": "^10.2.0", - "shell-quote": "^1.8.2", + "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", "zod": "^3.23.8" @@ -9557,9 +9557,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index b2eef3f6..5ab78b4c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.2.0", "open": "^10.2.0", - "shell-quote": "^1.8.2", + "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", "zod": "^3.23.8" From 700b856140fb26b6c62ff65be8d709aa842b7c37 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:12:12 -0700 Subject: [PATCH 018/120] Update postcss --- client/package.json | 2 +- package-lock.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package.json b/client/package.json index 2e725a60..68634105 100644 --- a/client/package.json +++ b/client/package.json @@ -70,7 +70,7 @@ "globals": "^15.9.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "postcss": "^8.4.47", + "postcss": "^8.5.6", "tailwindcss": "^3.4.13", "ts-jest": "^29.2.6", "typescript": "^5.5.3", diff --git a/package-lock.json b/package-lock.json index 4bb3568b..8e57d136 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "globals": "^15.9.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "postcss": "^8.4.47", + "postcss": "^8.5.6", "tailwindcss": "^3.4.13", "ts-jest": "^29.2.6", "typescript": "^5.5.3", @@ -8593,9 +8593,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -8612,7 +8612,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, From 0d71c448218d1e6aff277c42743d2c01732ae8d4 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:13:55 -0700 Subject: [PATCH 019/120] Update vite --- client/package.json | 2 +- package-lock.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/package.json b/client/package.json index 68634105..bee46cc9 100644 --- a/client/package.json +++ b/client/package.json @@ -75,6 +75,6 @@ "ts-jest": "^29.2.6", "typescript": "^5.5.3", "typescript-eslint": "^8.7.0", - "vite": "^6.3.0" + "vite": "^6.3.5" } } diff --git a/package-lock.json b/package-lock.json index 8e57d136..d0a3de3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -123,7 +123,7 @@ "ts-jest": "^29.2.6", "typescript": "^5.5.3", "typescript-eslint": "^8.7.0", - "vite": "^6.3.0" + "vite": "^6.3.5" } }, "client/node_modules/pkce-challenge": { @@ -10606,9 +10606,9 @@ } }, "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", "dependencies": { From e8b9860ed837243f875df28a452f0a5d723daadd Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 11:22:43 -0700 Subject: [PATCH 020/120] Update @vitejs/plugin-react --- client/package.json | 2 +- package-lock.json | 179 +++++++++++++++++++++----------------------- 2 files changed, 88 insertions(+), 93 deletions(-) diff --git a/client/package.json b/client/package.json index bee46cc9..5d4a19f8 100644 --- a/client/package.json +++ b/client/package.json @@ -61,7 +61,7 @@ "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.20", "co": "^4.6.0", "eslint": "^9.11.1", diff --git a/package-lock.json b/package-lock.json index d0a3de3c..656e6524 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,7 +109,7 @@ "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", - "@vitejs/plugin-react": "^4.3.2", + "@vitejs/plugin-react": "^4.7.0", "autoprefixer": "^10.4.20", "co": "^4.6.0", "eslint": "^9.11.1", @@ -184,9 +184,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -194,22 +194,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -225,16 +225,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -242,13 +242,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.1", + "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -258,6 +258,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -273,15 +283,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -331,27 +341,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -642,14 +652,14 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -657,38 +667,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1952,17 +1952,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1974,15 +1970,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1990,9 +1977,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2879,6 +2866,13 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", @@ -3903,15 +3897,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -3919,7 +3914,7 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/abab": { From 8927d15e112a584eb69cd51167a0351a78d37cbb Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:20:04 -0700 Subject: [PATCH 021/120] Update typescript-eslint --- client/package.json | 2 +- package-lock.json | 173 +++++++++++++++++++++++++++++--------------- 2 files changed, 114 insertions(+), 61 deletions(-) diff --git a/client/package.json b/client/package.json index 5d4a19f8..e0fa8265 100644 --- a/client/package.json +++ b/client/package.json @@ -74,7 +74,7 @@ "tailwindcss": "^3.4.13", "ts-jest": "^29.2.6", "typescript": "^5.5.3", - "typescript-eslint": "^8.7.0", + "typescript-eslint": "^8.38.0", "vite": "^6.3.5" } } diff --git a/package-lock.json b/package-lock.json index 656e6524..441ae6fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,7 +122,7 @@ "tailwindcss": "^3.4.13", "ts-jest": "^29.2.6", "typescript": "^5.5.3", - "typescript-eslint": "^8.7.0", + "typescript-eslint": "^8.38.0", "vite": "^6.3.5" } }, @@ -3678,21 +3678,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", - "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3702,22 +3702,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", - "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "engines": { @@ -3732,15 +3742,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", - "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3750,17 +3782,35 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", - "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3775,9 +3825,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", - "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", "dev": true, "license": "MIT", "engines": { @@ -3789,20 +3839,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", - "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3842,9 +3894,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -3855,16 +3907,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", - "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3879,14 +3931,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", - "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10423,15 +10475,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.1.tgz", - "integrity": "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==", + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.31.1", - "@typescript-eslint/parser": "8.31.1", - "@typescript-eslint/utils": "8.31.1" + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" From 98a3ee0ef3dcf2d3b60c036122b1ad68ceb6563e Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:21:17 -0700 Subject: [PATCH 022/120] Update @types/express --- package-lock.json | 8 ++++---- server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 441ae6fb..c584e24b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3397,9 +3397,9 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11070,7 +11070,7 @@ }, "devDependencies": { "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", + "@types/express": "^4.17.23", "@types/ws": "^8.5.12", "tsx": "^4.19.0", "typescript": "^5.6.2" diff --git a/server/package.json b/server/package.json index 040fe71f..19ab4ce4 100644 --- a/server/package.json +++ b/server/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", + "@types/express": "^4.17.23", "@types/ws": "^8.5.12", "tsx": "^4.19.0", "typescript": "^5.6.2" From cd8b600ae3104368681b3965e2ebed3c6b44a58d Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:21:39 -0700 Subject: [PATCH 023/120] Update @types/cors --- package-lock.json | 8 ++++---- server/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c584e24b..bc748e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3380,9 +3380,9 @@ } }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, "license": "MIT", "dependencies": { @@ -11069,7 +11069,7 @@ "mcp-inspector-server": "build/index.js" }, "devDependencies": { - "@types/cors": "^2.8.17", + "@types/cors": "^2.8.19", "@types/express": "^4.17.23", "@types/ws": "^8.5.12", "tsx": "^4.19.0", diff --git a/server/package.json b/server/package.json index 19ab4ce4..e1617089 100644 --- a/server/package.json +++ b/server/package.json @@ -20,7 +20,7 @@ "dev:windows": "tsx watch --clear-screen=false src/index.ts < NUL" }, "devDependencies": { - "@types/cors": "^2.8.17", + "@types/cors": "^2.8.19", "@types/express": "^4.17.23", "@types/ws": "^8.5.12", "tsx": "^4.19.0", From 29d67af285a460044c1c1d6dc10cbbdc8c7dc800 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:22:16 -0700 Subject: [PATCH 024/120] Update @types/node --- client/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/package.json b/client/package.json index e0fa8265..4e5e3aa4 100644 --- a/client/package.json +++ b/client/package.json @@ -56,7 +56,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", - "@types/node": "^22.7.5", + "@types/node": "^22.17.0", "@types/prismjs": "^1.26.5", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", diff --git a/package-lock.json b/package-lock.json index bc748e4f..a50a71ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "devDependencies": { "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", - "@types/node": "^22.7.5", + "@types/node": "^22.17.0", "@types/shell-quote": "^1.7.5", "jest-fixed-jsdom": "^0.0.9", "prettier": "^3.6.2", @@ -104,7 +104,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", - "@types/node": "^22.7.5", + "@types/node": "^22.17.0", "@types/prismjs": "^1.26.5", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", @@ -3539,9 +3539,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "version": "22.17.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz", + "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" diff --git a/package.json b/package.json index 5ab78b4c..22f33c8e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "devDependencies": { "@playwright/test": "^1.52.0", "@types/jest": "^29.5.14", - "@types/node": "^22.7.5", + "@types/node": "^22.17.0", "@types/shell-quote": "^1.7.5", "jest-fixed-jsdom": "^0.0.9", "prettier": "^3.6.2", From 14a77ef73d4b25166b9ffe3ced6e0ff3f58364df Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:22:52 -0700 Subject: [PATCH 025/120] Update @types/react --- client/package.json | 2 +- package-lock.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/package.json b/client/package.json index 4e5e3aa4..223241ef 100644 --- a/client/package.json +++ b/client/package.json @@ -58,7 +58,7 @@ "@types/jest": "^29.5.14", "@types/node": "^22.17.0", "@types/prismjs": "^1.26.5", - "@types/react": "^18.3.10", + "@types/react": "^18.3.23", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", "@vitejs/plugin-react": "^4.7.0", diff --git a/package-lock.json b/package-lock.json index a50a71ab..f0a971e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,7 +106,7 @@ "@types/jest": "^29.5.14", "@types/node": "^22.17.0", "@types/prismjs": "^1.26.5", - "@types/react": "^18.3.10", + "@types/react": "^18.3.23", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", "@vitejs/plugin-react": "^4.7.0", @@ -3576,9 +3576,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", - "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "devOptional": true, "license": "MIT", "dependencies": { From 395853469cf41cf91d4aa550d4ea10208847ed59 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 01:25:57 -0700 Subject: [PATCH 026/120] Update playwright, @playwright/test --- package-lock.json | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0a971e8..68b20336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "mcp-inspector": "cli/build/cli.js" }, "devDependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.54.1", "@types/jest": "^29.5.14", "@types/node": "^22.17.0", "@types/shell-quote": "^1.7.5", @@ -2067,13 +2067,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.54.1" }, "bin": { "playwright": "cli.js" @@ -8593,13 +8593,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.54.1" }, "bin": { "playwright": "cli.js" @@ -8612,9 +8612,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 22f33c8e..13592366 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@playwright/test": "^1.52.0", + "@playwright/test": "^1.54.1", "@types/jest": "^29.5.14", "@types/node": "^22.17.0", "@types/shell-quote": "^1.7.5", From 3a1cf7c54b495987ae64ddc7a69e8130f19bdd13 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 11:29:36 -0700 Subject: [PATCH 027/120] Update ts-jest --- client/package.json | 2 +- package-lock.json | 37 ++++++++++++++++++++----------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/client/package.json b/client/package.json index 223241ef..3402ad36 100644 --- a/client/package.json +++ b/client/package.json @@ -72,7 +72,7 @@ "jest-environment-jsdom": "^29.7.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.13", - "ts-jest": "^29.2.6", + "ts-jest": "^29.4.0", "typescript": "^5.5.3", "typescript-eslint": "^8.38.0", "vite": "^6.3.5" diff --git a/package-lock.json b/package-lock.json index 68b20336..29504b88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,7 @@ "jest-environment-jsdom": "^29.7.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.13", - "ts-jest": "^29.2.6", + "ts-jest": "^29.4.0", "typescript": "^5.5.3", "typescript-eslint": "^8.38.0", "vite": "^6.3.5" @@ -10261,21 +10261,20 @@ "license": "Apache-2.0" }, "node_modules/ts-jest": { - "version": "29.3.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz", - "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==", + "version": "29.4.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", + "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.1", - "type-fest": "^4.39.1", + "semver": "^7.7.2", + "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -10286,10 +10285,11 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { @@ -10307,13 +10307,16 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -10324,9 +10327,9 @@ } }, "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.40.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz", - "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { From d019376c1de0a194c1c3e24d203d3befbfcf4974 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 26 Jul 2025 11:29:49 -0700 Subject: [PATCH 028/120] Update zod --- client/package.json | 2 +- package-lock.json | 12 ++++++------ package.json | 2 +- server/package.json | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/package.json b/client/package.json index 3402ad36..cbb7d8b6 100644 --- a/client/package.json +++ b/client/package.json @@ -49,7 +49,7 @@ "serve-handler": "^6.1.6", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.11.1", diff --git a/package-lock.json b/package-lock.json index 29504b88..89c27a17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "bin": { "mcp-inspector": "cli/build/cli.js" @@ -94,7 +94,7 @@ "serve-handler": "^6.1.6", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "bin": { "mcp-inspector-client": "bin/start.js" @@ -11040,9 +11040,9 @@ } }, "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -11066,7 +11066,7 @@ "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "bin": { "mcp-inspector-server": "build/index.js" diff --git a/package.json b/package.json index 13592366..e614ba21 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "shell-quote": "^1.8.3", "spawn-rx": "^5.1.2", "ts-node": "^10.9.2", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "devDependencies": { "@playwright/test": "^1.54.1", diff --git a/server/package.json b/server/package.json index e1617089..51fa6e42 100644 --- a/server/package.json +++ b/server/package.json @@ -31,6 +31,6 @@ "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", - "zod": "^3.23.8" + "zod": "^3.25.76" } } From eec20ff1fcfb63391ad452a89a4e4677541e1335 Mon Sep 17 00:00:00 2001 From: oleina Date: Thu, 31 Jul 2025 10:20:47 -0700 Subject: [PATCH 029/120] add missing close bracket --- client/src/lib/hooks/useConnection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 268ba8ff..78dfa6df 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -565,6 +565,7 @@ export function useConnection({ if (capabilities?.logging && defaultLoggingLevel) { await client.setLoggingLevel(defaultLoggingLevel); + } if (onElicitationRequest) { client.setRequestHandler(ElicitRequestSchema, async (request) => { From dc5fa6a0f52517638dc9be6cdb5ddd1520b7e145 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Wed, 30 Jul 2025 23:53:17 -0700 Subject: [PATCH 030/120] fix: Argument notation to allow parseArgs to parse leading dash-options The server was invoked as: `server/build/index.js --command comm --args arg1 arg2 arg3` (`arg1 arg2 arg3` is a string in ARGV; the single value of `--args`.) `util:parseArgs()` cannot handle an argument value with a leading dash with this usage. e.g., `server/build/index.js --command npm --args --silent --prefix path/to/servers/src/everything run start` The above will throw, and indicate correct usage: ``` TypeError [ERR_PARSE_ARGS_INVALID_OPTION_VALUE]: Option '--args' argument is ambiguous. Did you forget to specify the option argument for '--args'? To specify an option argument starting with a dash use '--args=-XYZ'. ``` However, since users do not control the invocation of the server, they cannot adjust the notation for their use-case. `arguments`, and `command`, are unknown user input, so adjust the argument form to the more lenient and robust notation. Although a `command` beginning with `-` seems unlikely, consistent handling of both improves code clarity and simplicity. Running the server directly (e.g., on the command line in development) may continue to use: `--args arg1 arg2 arg3` if desired, so this is a non-breaking change. --- client/bin/start.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/bin/start.js b/client/bin/start.js index ae6e9259..0b5e84e2 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -91,10 +91,10 @@ async function startProdServer(serverOptions) { "node", [ inspectorServerPath, - ...(command ? [`--command`, command] : []), - ...(mcpServerArgs && mcpServerArgs.length > 0 - ? [`--args`, mcpServerArgs.join(" ")] - : []), + command ? `--command=${command}` : "", + mcpServerArgs && mcpServerArgs.length > 0 + ? `--args=${mcpServerArgs.join(" ")}` + : "", ], { env: { From dbacec64bd962f78d576b9c17e147ff913bd08ba Mon Sep 17 00:00:00 2001 From: olaservo Date: Fri, 1 Aug 2025 20:40:28 -0700 Subject: [PATCH 031/120] Fix formatting --- cli/scripts/cli-tests.js | 1 - cli/scripts/cli-tool-tests.js | 17 ++++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index c2b8b2cb..24a99b37 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -451,7 +451,6 @@ async function runTests() { "tools/list", ); - console.log( `\n${colors.YELLOW}=== Running Resource-Related Tests ===${colors.NC}`, ); diff --git a/cli/scripts/cli-tool-tests.js b/cli/scripts/cli-tool-tests.js index 41e0a787..b06aea94 100644 --- a/cli/scripts/cli-tool-tests.js +++ b/cli/scripts/cli-tool-tests.js @@ -26,17 +26,21 @@ let FAILED_TESTS = 0; let SKIPPED_TESTS = 0; let TOTAL_TESTS = 0; -console.log( - `${colors.YELLOW}=== MCP Inspector CLI Tool Tests ===${colors.NC}`, -); +console.log(`${colors.YELLOW}=== MCP Inspector CLI Tool Tests ===${colors.NC}`); console.log( `${colors.BLUE}This script tests the MCP Inspector CLI's tool-related functionality:${colors.NC}`, ); console.log(`${colors.BLUE}- Tool discovery and listing${colors.NC}`); -console.log(`${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`); +console.log( + `${colors.BLUE}- JSON argument parsing (strings, numbers, booleans, objects, arrays)${colors.NC}`, +); console.log(`${colors.BLUE}- Tool schema validation${colors.NC}`); -console.log(`${colors.BLUE}- Tool execution with various argument types${colors.NC}`); -console.log(`${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`); +console.log( + `${colors.BLUE}- Tool execution with various argument types${colors.NC}`, +); +console.log( + `${colors.BLUE}- Error handling for invalid tools and arguments${colors.NC}`, +); console.log(`\n`); // Get directory paths @@ -373,7 +377,6 @@ async function runTests() { 'message="null"', ); - // Test 14: Multiple arguments with mixed types (using add tool) await runBasicTest( "json_args_multiple_mixed", From f73d8e6f8a380c69d848c23728333b15e8a5591b Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:59:45 +0200 Subject: [PATCH 032/120] fix: support FastMCP union types in tool parameter forms --- client/src/components/ToolsTab.test.tsx | 720 ++++++++++++++++++++++++ client/src/components/ToolsTab.tsx | 13 +- client/src/utils/jsonUtils.ts | 11 +- client/src/utils/schemaUtils.ts | 27 + 4 files changed, 767 insertions(+), 4 deletions(-) create mode 100644 client/src/components/ToolsTab.test.tsx diff --git a/client/src/components/ToolsTab.test.tsx b/client/src/components/ToolsTab.test.tsx new file mode 100644 index 00000000..cb662d92 --- /dev/null +++ b/client/src/components/ToolsTab.test.tsx @@ -0,0 +1,720 @@ +import { render, screen, fireEvent, act } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { describe, it, jest, beforeEach } from "@jest/globals"; +import ToolsTab from "../ToolsTab"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { Tabs } from "@/components/ui/tabs"; +import { cacheToolOutputSchemas } from "@/utils/schemaUtils"; +import { within } from "@testing-library/react"; + +describe("ToolsTab", () => { + beforeEach(() => { + // Clear the output schema cache before each test + cacheToolOutputSchemas([]); + }); + + const mockTools: Tool[] = [ + { + name: "tool1", + description: "First tool", + inputSchema: { + type: "object" as const, + properties: { + num: { type: "number" as const }, + }, + }, + }, + { + name: "tool3", + description: "Integer tool", + inputSchema: { + type: "object" as const, + properties: { + count: { type: "integer" as const }, + }, + }, + }, + { + name: "tool2", + description: "Second tool", + inputSchema: { + type: "object" as const, + properties: { + num: { type: "number" as const }, + }, + }, + }, + ]; + + const defaultProps = { + tools: mockTools, + listTools: jest.fn(), + clearTools: jest.fn(), + callTool: jest.fn(async () => {}), + selectedTool: null, + setSelectedTool: jest.fn(), + toolResult: null, + nextCursor: "", + error: null, + resourceContent: {}, + onReadResource: jest.fn(), + }; + + const renderToolsTab = (props = {}) => { + return render( + + + , + ); + }; + + it("should reset input values when switching tools", async () => { + const { rerender } = renderToolsTab({ + selectedTool: mockTools[0], + }); + + // Enter a value in the first tool's input + const input = screen.getByRole("spinbutton") as HTMLInputElement; + await act(async () => { + fireEvent.change(input, { target: { value: "42" } }); + }); + expect(input.value).toBe("42"); + + // Switch to second tool + rerender( + + + , + ); + + // Verify input is reset + const newInput = screen.getByRole("spinbutton") as HTMLInputElement; + expect(newInput.value).toBe(""); + }); + + it("should handle integer type inputs", async () => { + renderToolsTab({ + selectedTool: mockTools[1], // Use the tool with integer type + }); + + const input = screen.getByRole("spinbutton", { + name: /count/i, + }) as HTMLInputElement; + expect(input).toHaveProperty("type", "number"); + fireEvent.change(input, { target: { value: "42" } }); + expect(input.value).toBe("42"); + + const submitButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(submitButton); + }); + + expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, { + count: 42, + }); + }); + + it("should handle union string|null type inputs (like FastMCP optional parameters)", async () => { + const unionTool = { + name: "searchTool", + description: "Search with optional category", + inputSchema: { + type: "object" as const, + properties: { + category: { + anyOf: [{ type: "string" as const }, { type: "null" as const }], + description: "Optional category parameter", + }, + }, + required: [], + }, + }; + + renderToolsTab({ + selectedTool: unionTool, + }); + + const input = screen.getByRole("textbox", { + name: /category/i, + }) as HTMLTextAreaElement; + + // Initially should be empty string + expect(input.value).toBe(""); + + // User types "hello" + fireEvent.change(input, { target: { value: "hello" } }); + expect(input.value).toBe("hello"); + + const submitButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(submitButton); + }); + + // Should call with the string value, not null + expect(defaultProps.callTool).toHaveBeenCalledWith(unionTool.name, { + category: "hello", + }); + }); + + it("should omit undefined optional parameters from tool calls", async () => { + const unionTool = { + name: "searchTool", + description: "Search with optional category", + inputSchema: { + type: "object" as const, + properties: { + category: { + anyOf: [{ type: "string" as const }, { type: "null" as const }], + description: "Optional category parameter", + }, + }, + required: [], + }, + }; + + renderToolsTab({ + selectedTool: unionTool, + }); + + const input = screen.getByRole("textbox", { + name: /category/i, + }) as HTMLTextAreaElement; + + // User types something then deletes it, leaving empty + fireEvent.change(input, { target: { value: "hello" } }); + fireEvent.change(input, { target: { value: "" } }); + expect(input.value).toBe(""); + + const submitButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(submitButton); + }); + + // Should omit the empty parameter entirely + expect(defaultProps.callTool).toHaveBeenCalledWith(unionTool.name, {}); + }); + + it("should allow typing negative numbers", async () => { + renderToolsTab({ + selectedTool: mockTools[0], + }); + + const input = screen.getByRole("spinbutton") as HTMLInputElement; + + // Complete the negative number + fireEvent.change(input, { target: { value: "-42" } }); + expect(input.value).toBe("-42"); + + const submitButton = screen.getByRole("button", { name: /run tool/i }); + await act(async () => { + fireEvent.click(submitButton); + }); + + expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[0].name, { + num: -42, + }); + }); + + it("should disable button and change text while tool is running", async () => { + // Create a promise that we can resolve later + let resolvePromise: ((value: unknown) => void) | undefined; + const mockPromise = new Promise((resolve) => { + resolvePromise = resolve; + }); + + // Mock callTool to return our promise + const mockCallTool = jest.fn().mockReturnValue(mockPromise); + + renderToolsTab({ + selectedTool: mockTools[0], + callTool: mockCallTool, + }); + + const submitButton = screen.getByRole("button", { name: /run tool/i }); + expect(submitButton.getAttribute("disabled")).toBeNull(); + + // Click the button and verify immediate state changes + await act(async () => { + fireEvent.click(submitButton); + }); + + // Verify button is disabled and text changed + expect(submitButton.getAttribute("disabled")).not.toBeNull(); + expect(submitButton.textContent).toBe("Running..."); + + // Resolve the promise to simulate tool completion + await act(async () => { + if (resolvePromise) { + await resolvePromise({}); + } + }); + + expect(submitButton.getAttribute("disabled")).toBeNull(); + }); + + describe("Output Schema Display", () => { + const toolWithOutputSchema: Tool = { + name: "weatherTool", + description: "Get weather", + inputSchema: { + type: "object" as const, + properties: { + city: { type: "string" as const }, + }, + }, + outputSchema: { + type: "object" as const, + properties: { + temperature: { type: "number" as const }, + humidity: { type: "number" as const }, + }, + required: ["temperature", "humidity"], + }, + }; + + it("should display output schema when tool has one", () => { + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + }); + + expect(screen.getByText("Output Schema:")).toBeInTheDocument(); + // Check for expand/collapse button + expect( + screen.getByRole("button", { name: /expand/i }), + ).toBeInTheDocument(); + }); + + it("should not display output schema section when tool doesn't have one", () => { + renderToolsTab({ + selectedTool: mockTools[0], // Tool without outputSchema + }); + + expect(screen.queryByText("Output Schema:")).not.toBeInTheDocument(); + }); + + it("should toggle output schema expansion", () => { + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + }); + + const toggleButton = screen.getByRole("button", { name: /expand/i }); + + // Click to expand + fireEvent.click(toggleButton); + expect( + screen.getByRole("button", { name: /collapse/i }), + ).toBeInTheDocument(); + + // Click to collapse + fireEvent.click(toggleButton); + expect( + screen.getByRole("button", { name: /expand/i }), + ).toBeInTheDocument(); + }); + }); + + describe("Structured Output Results", () => { + const toolWithOutputSchema: Tool = { + name: "weatherTool", + description: "Get weather", + inputSchema: { + type: "object" as const, + properties: {}, + }, + outputSchema: { + type: "object" as const, + properties: { + temperature: { type: "number" as const }, + }, + required: ["temperature"], + }, + }; + + beforeEach(() => { + // Cache the tool's output schema before each test + cacheToolOutputSchemas([toolWithOutputSchema]); + }); + + it("should display structured content when present", () => { + const structuredResult = { + content: [], + structuredContent: { + temperature: 25, + }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: structuredResult, + }); + + expect(screen.getByText("Structured Content:")).toBeInTheDocument(); + expect( + screen.getByText(/Valid according to output schema/), + ).toBeInTheDocument(); + }); + + it("should show validation error for invalid structured content", () => { + const invalidResult = { + content: [], + structuredContent: { + temperature: "25", // String instead of number + }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: invalidResult, + }); + + expect(screen.getByText(/Validation Error:/)).toBeInTheDocument(); + }); + + it("should show error when tool with output schema doesn't return structured content", () => { + const resultWithoutStructured = { + content: [{ type: "text", text: "some result" }], + // No structuredContent + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: resultWithoutStructured, + }); + + expect( + screen.getByText( + /Tool has an output schema but did not return structured content/, + ), + ).toBeInTheDocument(); + }); + + it("should show unstructured content title when both structured and unstructured exist", () => { + const resultWithBoth = { + content: [{ type: "text", text: '{"temperature": 25}' }], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: resultWithBoth, + }); + + expect(screen.getByText("Structured Content:")).toBeInTheDocument(); + expect(screen.getByText("Unstructured Content:")).toBeInTheDocument(); + }); + + it("should not show unstructured content title when only unstructured exists", () => { + const resultWithUnstructuredOnly = { + content: [{ type: "text", text: "some result" }], + }; + + renderToolsTab({ + selectedTool: mockTools[0], // Tool without output schema + toolResult: resultWithUnstructuredOnly, + }); + + expect( + screen.queryByText("Unstructured Content:"), + ).not.toBeInTheDocument(); + }); + + it("should show compatibility check when tool has output schema", () => { + const compatibleResult = { + content: [{ type: "text", text: '{"temperature": 25}' }], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: compatibleResult, + }); + + // Should show compatibility result + expect( + screen.getByText(/structured content matches/i), + ).toBeInTheDocument(); + }); + + it("should accept multiple content blocks with structured output", () => { + const multipleBlocksResult = { + content: [ + { type: "text", text: "Here is the weather data:" }, + { type: "text", text: '{"temperature": 25}' }, + { type: "text", text: "Have a nice day!" }, + ], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: multipleBlocksResult, + }); + + // Should show compatible result with multiple blocks + expect( + screen.getByText(/structured content matches.*multiple/i), + ).toBeInTheDocument(); + }); + + it("should accept mixed content types with structured output", () => { + const mixedContentResult = { + content: [ + { type: "text", text: "Weather report:" }, + { type: "text", text: '{"temperature": 25}' }, + { type: "image", data: "base64data", mimeType: "image/png" }, + ], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: mixedContentResult, + }); + + // Should render without crashing - the validation logic has been updated + expect(screen.getAllByText("weatherTool")).toHaveLength(2); + }); + + it("should reject when no text blocks match structured content", () => { + const noMatchResult = { + content: [ + { type: "text", text: "Some text" }, + { type: "text", text: '{"humidity": 60}' }, // Different structure + ], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: noMatchResult, + }); + + // Should render without crashing - the validation logic has been updated + expect(screen.getAllByText("weatherTool")).toHaveLength(2); + }); + + it("should reject when no text blocks are present", () => { + const noTextBlocksResult = { + content: [{ type: "image", data: "base64data", mimeType: "image/png" }], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: noTextBlocksResult, + }); + + // Should render without crashing - the validation logic has been updated + expect(screen.getAllByText("weatherTool")).toHaveLength(2); + }); + + it("should not show compatibility check when tool has no output schema", () => { + const resultWithBoth = { + content: [{ type: "text", text: '{"data": "value"}' }], + structuredContent: { different: "data" }, + }; + + renderToolsTab({ + selectedTool: mockTools[0], // Tool without output schema + toolResult: resultWithBoth, + }); + + // Should not show any compatibility messages + expect( + screen.queryByText( + /structured content matches|no text blocks|no.*matches/i, + ), + ).not.toBeInTheDocument(); + }); + }); + + describe("Resource Link Content Type", () => { + it("should render resource_link content type and handle expansion", async () => { + const mockOnReadResource = jest.fn(); + const resourceContent = { + "test://static/resource/1": JSON.stringify({ + contents: [ + { + uri: "test://static/resource/1", + name: "Resource 1", + mimeType: "text/plain", + text: "Resource 1: This is a plaintext resource", + }, + ], + }), + }; + + const result = { + content: [ + { + type: "resource_link", + uri: "test://static/resource/1", + name: "Resource 1", + description: "Resource 1: plaintext resource", + mimeType: "text/plain", + }, + { + type: "resource_link", + uri: "test://static/resource/2", + name: "Resource 2", + description: "Resource 2: binary blob resource", + mimeType: "application/octet-stream", + }, + { + type: "resource_link", + uri: "test://static/resource/3", + name: "Resource 3", + description: "Resource 3: plaintext resource", + mimeType: "text/plain", + }, + ], + }; + + renderToolsTab({ + selectedTool: mockTools[0], + toolResult: result, + resourceContent, + onReadResource: mockOnReadResource, + }); + + ["1", "2", "3"].forEach((id) => { + expect( + screen.getByText(`test://static/resource/${id}`), + ).toBeInTheDocument(); + expect(screen.getByText(`Resource ${id}`)).toBeInTheDocument(); + }); + + expect(screen.getAllByText("text/plain")).toHaveLength(2); + expect(screen.getByText("application/octet-stream")).toBeInTheDocument(); + + const expandButtons = screen.getAllByRole("button", { + name: /expand resource/i, + }); + expect(expandButtons).toHaveLength(3); + expect(screen.queryByText("Resource:")).not.toBeInTheDocument(); + + expandButtons.forEach((button) => { + expect(button).toHaveAttribute("aria-expanded", "false"); + }); + + const resource1Button = screen.getByRole("button", { + name: /expand resource test:\/\/static\/resource\/1/i, + }); + + await act(async () => { + fireEvent.click(resource1Button); + }); + + expect(mockOnReadResource).toHaveBeenCalledWith( + "test://static/resource/1", + ); + expect(screen.getByText("Resource:")).toBeInTheDocument(); + expect(document.body).toHaveTextContent("contents:"); + expect(document.body).toHaveTextContent('uri:"test://static/resource/1"'); + expect(resource1Button).toHaveAttribute("aria-expanded", "true"); + + await act(async () => { + fireEvent.click(resource1Button); + }); + + expect(screen.queryByText("Resource:")).not.toBeInTheDocument(); + expect(document.body).not.toHaveTextContent("contents:"); + expect(document.body).not.toHaveTextContent( + 'uri:"test://static/resource/1"', + ); + expect(resource1Button).toHaveAttribute("aria-expanded", "false"); + expect(mockOnReadResource).toHaveBeenCalledTimes(1); + }); + }); + + describe("Meta Display", () => { + const toolWithMeta = { + name: "metaTool", + description: "Tool with meta", + inputSchema: { + type: "object" as const, + properties: { + foo: { type: "string" as const }, + }, + }, + _meta: { + author: "tester", + version: 1, + }, + } as unknown as Tool; + + it("should display meta section when tool has _meta", () => { + renderToolsTab({ + tools: [toolWithMeta], + selectedTool: toolWithMeta, + }); + + expect(screen.getByText("Meta:")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /expand/i }), + ).toBeInTheDocument(); + }); + + it("should toggle meta expansion", () => { + renderToolsTab({ + tools: [toolWithMeta], + selectedTool: toolWithMeta, + }); + + // There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section + const metaHeading = screen.getByText("Meta:"); + const metaContainer = metaHeading.closest("div"); + expect(metaContainer).toBeTruthy(); + const toggleButton = within(metaContainer as HTMLElement).getByRole( + "button", + { name: /expand/i }, + ); + + // Expand Meta + fireEvent.click(toggleButton); + expect( + within(metaContainer as HTMLElement).getByRole("button", { + name: /collapse/i, + }), + ).toBeInTheDocument(); + + // Collapse Meta + fireEvent.click(toggleButton); + expect( + within(metaContainer as HTMLElement).getByRole("button", { + name: /expand/i, + }), + ).toBeInTheDocument(); + }); + }); + + describe("ToolResults Meta", () => { + it("should display meta information when present in toolResult", () => { + const resultWithMeta = { + content: [], + _meta: { info: "details", version: 2 }, + }; + + renderToolsTab({ + selectedTool: mockTools[0], + toolResult: resultWithMeta, + }); + + // Only ToolResults meta should be present since selectedTool has no _meta + expect(screen.getAllByText("Meta:")).toHaveLength(1); + expect(screen.getByText(/info/i)).toBeInTheDocument(); + expect(screen.getByText(/version/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 2eb682e5..2654feed 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -7,7 +7,11 @@ import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import DynamicJsonForm from "./DynamicJsonForm"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; -import { generateDefaultValue, isPropertyRequired } from "@/utils/schemaUtils"; +import { + generateDefaultValue, + isPropertyRequired, + normalizeUnionType, +} from "@/utils/schemaUtils"; import { CompatibilityCallToolResult, ListToolsResult, @@ -104,7 +108,7 @@ const ToolsTab = ({

{Object.entries(selectedTool.inputSchema.properties ?? []).map( ([key, value]) => { - const prop = value as JsonSchemaType; + const prop = normalizeUnionType(value as JsonSchemaType); const inputSchema = selectedTool.inputSchema as JsonSchemaType; const required = isPropertyRequired(key, inputSchema); @@ -148,7 +152,10 @@ const ToolsTab = ({ onChange={(e) => setParams({ ...params, - [key]: e.target.value, + [key]: + e.target.value === "" + ? undefined + : e.target.value, }) } className="mt-1" diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts index 338b642a..60f5b1d6 100644 --- a/client/src/utils/jsonUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -21,7 +21,16 @@ export type JsonSchemaType = { | "boolean" | "array" | "object" - | "null"; + | "null" + | ( + | "string" + | "number" + | "integer" + | "boolean" + | "array" + | "object" + | "null" + )[]; title?: string; description?: string; required?: string[]; diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 9fc7a724..d7210a24 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -145,6 +145,33 @@ export function isPropertyRequired( return schema.required?.includes(propertyName) ?? false; } +/** + * Normalizes union types (like string|null from FastMCP) to simple types for form rendering + * @param schema The JSON schema to normalize + * @returns A normalized schema or the original schema + */ +export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { + // Handle anyOf with string and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "string", anyOf: undefined }; + } + + // Handle array type with string and null + if ( + Array.isArray(schema.type) && + schema.type.includes("string") && + schema.type.includes("null") + ) { + return { ...schema, type: "string" }; + } + + return schema; +} + /** * Formats a field key into a human-readable label * @param key The field key to format From edd10e48a9a4478ad15d68c767f21e4267bafe6a Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:01:16 +0200 Subject: [PATCH 033/120] fix: updated import path npm test passes now --- client/src/components/ToolsTab.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/ToolsTab.test.tsx b/client/src/components/ToolsTab.test.tsx index cb662d92..42c710c9 100644 --- a/client/src/components/ToolsTab.test.tsx +++ b/client/src/components/ToolsTab.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent, act } from "@testing-library/react"; import "@testing-library/jest-dom"; import { describe, it, jest, beforeEach } from "@jest/globals"; -import ToolsTab from "../ToolsTab"; +import ToolsTab from "./ToolsTab"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Tabs } from "@/components/ui/tabs"; import { cacheToolOutputSchemas } from "@/utils/schemaUtils"; From bcba0dc00715a5495a799540588fd8b91ff47cbc Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:11:43 +0200 Subject: [PATCH 034/120] fix: removed tools tab test as its probably overkill and introduces to much new code --- client/src/components/ToolsTab.test.tsx | 720 ------------------------ 1 file changed, 720 deletions(-) delete mode 100644 client/src/components/ToolsTab.test.tsx diff --git a/client/src/components/ToolsTab.test.tsx b/client/src/components/ToolsTab.test.tsx deleted file mode 100644 index 42c710c9..00000000 --- a/client/src/components/ToolsTab.test.tsx +++ /dev/null @@ -1,720 +0,0 @@ -import { render, screen, fireEvent, act } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import { describe, it, jest, beforeEach } from "@jest/globals"; -import ToolsTab from "./ToolsTab"; -import { Tool } from "@modelcontextprotocol/sdk/types.js"; -import { Tabs } from "@/components/ui/tabs"; -import { cacheToolOutputSchemas } from "@/utils/schemaUtils"; -import { within } from "@testing-library/react"; - -describe("ToolsTab", () => { - beforeEach(() => { - // Clear the output schema cache before each test - cacheToolOutputSchemas([]); - }); - - const mockTools: Tool[] = [ - { - name: "tool1", - description: "First tool", - inputSchema: { - type: "object" as const, - properties: { - num: { type: "number" as const }, - }, - }, - }, - { - name: "tool3", - description: "Integer tool", - inputSchema: { - type: "object" as const, - properties: { - count: { type: "integer" as const }, - }, - }, - }, - { - name: "tool2", - description: "Second tool", - inputSchema: { - type: "object" as const, - properties: { - num: { type: "number" as const }, - }, - }, - }, - ]; - - const defaultProps = { - tools: mockTools, - listTools: jest.fn(), - clearTools: jest.fn(), - callTool: jest.fn(async () => {}), - selectedTool: null, - setSelectedTool: jest.fn(), - toolResult: null, - nextCursor: "", - error: null, - resourceContent: {}, - onReadResource: jest.fn(), - }; - - const renderToolsTab = (props = {}) => { - return render( - - - , - ); - }; - - it("should reset input values when switching tools", async () => { - const { rerender } = renderToolsTab({ - selectedTool: mockTools[0], - }); - - // Enter a value in the first tool's input - const input = screen.getByRole("spinbutton") as HTMLInputElement; - await act(async () => { - fireEvent.change(input, { target: { value: "42" } }); - }); - expect(input.value).toBe("42"); - - // Switch to second tool - rerender( - - - , - ); - - // Verify input is reset - const newInput = screen.getByRole("spinbutton") as HTMLInputElement; - expect(newInput.value).toBe(""); - }); - - it("should handle integer type inputs", async () => { - renderToolsTab({ - selectedTool: mockTools[1], // Use the tool with integer type - }); - - const input = screen.getByRole("spinbutton", { - name: /count/i, - }) as HTMLInputElement; - expect(input).toHaveProperty("type", "number"); - fireEvent.change(input, { target: { value: "42" } }); - expect(input.value).toBe("42"); - - const submitButton = screen.getByRole("button", { name: /run tool/i }); - await act(async () => { - fireEvent.click(submitButton); - }); - - expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, { - count: 42, - }); - }); - - it("should handle union string|null type inputs (like FastMCP optional parameters)", async () => { - const unionTool = { - name: "searchTool", - description: "Search with optional category", - inputSchema: { - type: "object" as const, - properties: { - category: { - anyOf: [{ type: "string" as const }, { type: "null" as const }], - description: "Optional category parameter", - }, - }, - required: [], - }, - }; - - renderToolsTab({ - selectedTool: unionTool, - }); - - const input = screen.getByRole("textbox", { - name: /category/i, - }) as HTMLTextAreaElement; - - // Initially should be empty string - expect(input.value).toBe(""); - - // User types "hello" - fireEvent.change(input, { target: { value: "hello" } }); - expect(input.value).toBe("hello"); - - const submitButton = screen.getByRole("button", { name: /run tool/i }); - await act(async () => { - fireEvent.click(submitButton); - }); - - // Should call with the string value, not null - expect(defaultProps.callTool).toHaveBeenCalledWith(unionTool.name, { - category: "hello", - }); - }); - - it("should omit undefined optional parameters from tool calls", async () => { - const unionTool = { - name: "searchTool", - description: "Search with optional category", - inputSchema: { - type: "object" as const, - properties: { - category: { - anyOf: [{ type: "string" as const }, { type: "null" as const }], - description: "Optional category parameter", - }, - }, - required: [], - }, - }; - - renderToolsTab({ - selectedTool: unionTool, - }); - - const input = screen.getByRole("textbox", { - name: /category/i, - }) as HTMLTextAreaElement; - - // User types something then deletes it, leaving empty - fireEvent.change(input, { target: { value: "hello" } }); - fireEvent.change(input, { target: { value: "" } }); - expect(input.value).toBe(""); - - const submitButton = screen.getByRole("button", { name: /run tool/i }); - await act(async () => { - fireEvent.click(submitButton); - }); - - // Should omit the empty parameter entirely - expect(defaultProps.callTool).toHaveBeenCalledWith(unionTool.name, {}); - }); - - it("should allow typing negative numbers", async () => { - renderToolsTab({ - selectedTool: mockTools[0], - }); - - const input = screen.getByRole("spinbutton") as HTMLInputElement; - - // Complete the negative number - fireEvent.change(input, { target: { value: "-42" } }); - expect(input.value).toBe("-42"); - - const submitButton = screen.getByRole("button", { name: /run tool/i }); - await act(async () => { - fireEvent.click(submitButton); - }); - - expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[0].name, { - num: -42, - }); - }); - - it("should disable button and change text while tool is running", async () => { - // Create a promise that we can resolve later - let resolvePromise: ((value: unknown) => void) | undefined; - const mockPromise = new Promise((resolve) => { - resolvePromise = resolve; - }); - - // Mock callTool to return our promise - const mockCallTool = jest.fn().mockReturnValue(mockPromise); - - renderToolsTab({ - selectedTool: mockTools[0], - callTool: mockCallTool, - }); - - const submitButton = screen.getByRole("button", { name: /run tool/i }); - expect(submitButton.getAttribute("disabled")).toBeNull(); - - // Click the button and verify immediate state changes - await act(async () => { - fireEvent.click(submitButton); - }); - - // Verify button is disabled and text changed - expect(submitButton.getAttribute("disabled")).not.toBeNull(); - expect(submitButton.textContent).toBe("Running..."); - - // Resolve the promise to simulate tool completion - await act(async () => { - if (resolvePromise) { - await resolvePromise({}); - } - }); - - expect(submitButton.getAttribute("disabled")).toBeNull(); - }); - - describe("Output Schema Display", () => { - const toolWithOutputSchema: Tool = { - name: "weatherTool", - description: "Get weather", - inputSchema: { - type: "object" as const, - properties: { - city: { type: "string" as const }, - }, - }, - outputSchema: { - type: "object" as const, - properties: { - temperature: { type: "number" as const }, - humidity: { type: "number" as const }, - }, - required: ["temperature", "humidity"], - }, - }; - - it("should display output schema when tool has one", () => { - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - }); - - expect(screen.getByText("Output Schema:")).toBeInTheDocument(); - // Check for expand/collapse button - expect( - screen.getByRole("button", { name: /expand/i }), - ).toBeInTheDocument(); - }); - - it("should not display output schema section when tool doesn't have one", () => { - renderToolsTab({ - selectedTool: mockTools[0], // Tool without outputSchema - }); - - expect(screen.queryByText("Output Schema:")).not.toBeInTheDocument(); - }); - - it("should toggle output schema expansion", () => { - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - }); - - const toggleButton = screen.getByRole("button", { name: /expand/i }); - - // Click to expand - fireEvent.click(toggleButton); - expect( - screen.getByRole("button", { name: /collapse/i }), - ).toBeInTheDocument(); - - // Click to collapse - fireEvent.click(toggleButton); - expect( - screen.getByRole("button", { name: /expand/i }), - ).toBeInTheDocument(); - }); - }); - - describe("Structured Output Results", () => { - const toolWithOutputSchema: Tool = { - name: "weatherTool", - description: "Get weather", - inputSchema: { - type: "object" as const, - properties: {}, - }, - outputSchema: { - type: "object" as const, - properties: { - temperature: { type: "number" as const }, - }, - required: ["temperature"], - }, - }; - - beforeEach(() => { - // Cache the tool's output schema before each test - cacheToolOutputSchemas([toolWithOutputSchema]); - }); - - it("should display structured content when present", () => { - const structuredResult = { - content: [], - structuredContent: { - temperature: 25, - }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: structuredResult, - }); - - expect(screen.getByText("Structured Content:")).toBeInTheDocument(); - expect( - screen.getByText(/Valid according to output schema/), - ).toBeInTheDocument(); - }); - - it("should show validation error for invalid structured content", () => { - const invalidResult = { - content: [], - structuredContent: { - temperature: "25", // String instead of number - }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: invalidResult, - }); - - expect(screen.getByText(/Validation Error:/)).toBeInTheDocument(); - }); - - it("should show error when tool with output schema doesn't return structured content", () => { - const resultWithoutStructured = { - content: [{ type: "text", text: "some result" }], - // No structuredContent - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: resultWithoutStructured, - }); - - expect( - screen.getByText( - /Tool has an output schema but did not return structured content/, - ), - ).toBeInTheDocument(); - }); - - it("should show unstructured content title when both structured and unstructured exist", () => { - const resultWithBoth = { - content: [{ type: "text", text: '{"temperature": 25}' }], - structuredContent: { temperature: 25 }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: resultWithBoth, - }); - - expect(screen.getByText("Structured Content:")).toBeInTheDocument(); - expect(screen.getByText("Unstructured Content:")).toBeInTheDocument(); - }); - - it("should not show unstructured content title when only unstructured exists", () => { - const resultWithUnstructuredOnly = { - content: [{ type: "text", text: "some result" }], - }; - - renderToolsTab({ - selectedTool: mockTools[0], // Tool without output schema - toolResult: resultWithUnstructuredOnly, - }); - - expect( - screen.queryByText("Unstructured Content:"), - ).not.toBeInTheDocument(); - }); - - it("should show compatibility check when tool has output schema", () => { - const compatibleResult = { - content: [{ type: "text", text: '{"temperature": 25}' }], - structuredContent: { temperature: 25 }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: compatibleResult, - }); - - // Should show compatibility result - expect( - screen.getByText(/structured content matches/i), - ).toBeInTheDocument(); - }); - - it("should accept multiple content blocks with structured output", () => { - const multipleBlocksResult = { - content: [ - { type: "text", text: "Here is the weather data:" }, - { type: "text", text: '{"temperature": 25}' }, - { type: "text", text: "Have a nice day!" }, - ], - structuredContent: { temperature: 25 }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: multipleBlocksResult, - }); - - // Should show compatible result with multiple blocks - expect( - screen.getByText(/structured content matches.*multiple/i), - ).toBeInTheDocument(); - }); - - it("should accept mixed content types with structured output", () => { - const mixedContentResult = { - content: [ - { type: "text", text: "Weather report:" }, - { type: "text", text: '{"temperature": 25}' }, - { type: "image", data: "base64data", mimeType: "image/png" }, - ], - structuredContent: { temperature: 25 }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: mixedContentResult, - }); - - // Should render without crashing - the validation logic has been updated - expect(screen.getAllByText("weatherTool")).toHaveLength(2); - }); - - it("should reject when no text blocks match structured content", () => { - const noMatchResult = { - content: [ - { type: "text", text: "Some text" }, - { type: "text", text: '{"humidity": 60}' }, // Different structure - ], - structuredContent: { temperature: 25 }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: noMatchResult, - }); - - // Should render without crashing - the validation logic has been updated - expect(screen.getAllByText("weatherTool")).toHaveLength(2); - }); - - it("should reject when no text blocks are present", () => { - const noTextBlocksResult = { - content: [{ type: "image", data: "base64data", mimeType: "image/png" }], - structuredContent: { temperature: 25 }, - }; - - renderToolsTab({ - tools: [toolWithOutputSchema], - selectedTool: toolWithOutputSchema, - toolResult: noTextBlocksResult, - }); - - // Should render without crashing - the validation logic has been updated - expect(screen.getAllByText("weatherTool")).toHaveLength(2); - }); - - it("should not show compatibility check when tool has no output schema", () => { - const resultWithBoth = { - content: [{ type: "text", text: '{"data": "value"}' }], - structuredContent: { different: "data" }, - }; - - renderToolsTab({ - selectedTool: mockTools[0], // Tool without output schema - toolResult: resultWithBoth, - }); - - // Should not show any compatibility messages - expect( - screen.queryByText( - /structured content matches|no text blocks|no.*matches/i, - ), - ).not.toBeInTheDocument(); - }); - }); - - describe("Resource Link Content Type", () => { - it("should render resource_link content type and handle expansion", async () => { - const mockOnReadResource = jest.fn(); - const resourceContent = { - "test://static/resource/1": JSON.stringify({ - contents: [ - { - uri: "test://static/resource/1", - name: "Resource 1", - mimeType: "text/plain", - text: "Resource 1: This is a plaintext resource", - }, - ], - }), - }; - - const result = { - content: [ - { - type: "resource_link", - uri: "test://static/resource/1", - name: "Resource 1", - description: "Resource 1: plaintext resource", - mimeType: "text/plain", - }, - { - type: "resource_link", - uri: "test://static/resource/2", - name: "Resource 2", - description: "Resource 2: binary blob resource", - mimeType: "application/octet-stream", - }, - { - type: "resource_link", - uri: "test://static/resource/3", - name: "Resource 3", - description: "Resource 3: plaintext resource", - mimeType: "text/plain", - }, - ], - }; - - renderToolsTab({ - selectedTool: mockTools[0], - toolResult: result, - resourceContent, - onReadResource: mockOnReadResource, - }); - - ["1", "2", "3"].forEach((id) => { - expect( - screen.getByText(`test://static/resource/${id}`), - ).toBeInTheDocument(); - expect(screen.getByText(`Resource ${id}`)).toBeInTheDocument(); - }); - - expect(screen.getAllByText("text/plain")).toHaveLength(2); - expect(screen.getByText("application/octet-stream")).toBeInTheDocument(); - - const expandButtons = screen.getAllByRole("button", { - name: /expand resource/i, - }); - expect(expandButtons).toHaveLength(3); - expect(screen.queryByText("Resource:")).not.toBeInTheDocument(); - - expandButtons.forEach((button) => { - expect(button).toHaveAttribute("aria-expanded", "false"); - }); - - const resource1Button = screen.getByRole("button", { - name: /expand resource test:\/\/static\/resource\/1/i, - }); - - await act(async () => { - fireEvent.click(resource1Button); - }); - - expect(mockOnReadResource).toHaveBeenCalledWith( - "test://static/resource/1", - ); - expect(screen.getByText("Resource:")).toBeInTheDocument(); - expect(document.body).toHaveTextContent("contents:"); - expect(document.body).toHaveTextContent('uri:"test://static/resource/1"'); - expect(resource1Button).toHaveAttribute("aria-expanded", "true"); - - await act(async () => { - fireEvent.click(resource1Button); - }); - - expect(screen.queryByText("Resource:")).not.toBeInTheDocument(); - expect(document.body).not.toHaveTextContent("contents:"); - expect(document.body).not.toHaveTextContent( - 'uri:"test://static/resource/1"', - ); - expect(resource1Button).toHaveAttribute("aria-expanded", "false"); - expect(mockOnReadResource).toHaveBeenCalledTimes(1); - }); - }); - - describe("Meta Display", () => { - const toolWithMeta = { - name: "metaTool", - description: "Tool with meta", - inputSchema: { - type: "object" as const, - properties: { - foo: { type: "string" as const }, - }, - }, - _meta: { - author: "tester", - version: 1, - }, - } as unknown as Tool; - - it("should display meta section when tool has _meta", () => { - renderToolsTab({ - tools: [toolWithMeta], - selectedTool: toolWithMeta, - }); - - expect(screen.getByText("Meta:")).toBeInTheDocument(); - expect( - screen.getByRole("button", { name: /expand/i }), - ).toBeInTheDocument(); - }); - - it("should toggle meta expansion", () => { - renderToolsTab({ - tools: [toolWithMeta], - selectedTool: toolWithMeta, - }); - - // There might be multiple Expand buttons (Output Schema, Meta). We need the one within Meta section - const metaHeading = screen.getByText("Meta:"); - const metaContainer = metaHeading.closest("div"); - expect(metaContainer).toBeTruthy(); - const toggleButton = within(metaContainer as HTMLElement).getByRole( - "button", - { name: /expand/i }, - ); - - // Expand Meta - fireEvent.click(toggleButton); - expect( - within(metaContainer as HTMLElement).getByRole("button", { - name: /collapse/i, - }), - ).toBeInTheDocument(); - - // Collapse Meta - fireEvent.click(toggleButton); - expect( - within(metaContainer as HTMLElement).getByRole("button", { - name: /expand/i, - }), - ).toBeInTheDocument(); - }); - }); - - describe("ToolResults Meta", () => { - it("should display meta information when present in toolResult", () => { - const resultWithMeta = { - content: [], - _meta: { info: "details", version: 2 }, - }; - - renderToolsTab({ - selectedTool: mockTools[0], - toolResult: resultWithMeta, - }); - - // Only ToolResults meta should be present since selectedTool has no _meta - expect(screen.getAllByText("Meta:")).toHaveLength(1); - expect(screen.getByText(/info/i)).toBeInTheDocument(); - expect(screen.getByText(/version/i)).toBeInTheDocument(); - }); - }); -}); From 5469993e2784200ad7efe57049949e164381068d Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:35:42 +0200 Subject: [PATCH 035/120] fix: add boolean union type support for FastMCP optional booleans --- client/src/utils/schemaUtils.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index d7210a24..62401bf8 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -160,6 +160,15 @@ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { return { ...schema, type: "string", anyOf: undefined }; } + // Handle anyOf with boolean and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "boolean", anyOf: undefined }; + } + // Handle array type with string and null if ( Array.isArray(schema.type) && @@ -169,6 +178,15 @@ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { return { ...schema, type: "string" }; } + // Handle array type with boolean and null + if ( + Array.isArray(schema.type) && + schema.type.includes("boolean") && + schema.type.includes("null") + ) { + return { ...schema, type: "boolean" }; + } + return schema; } From 1d960a1b9a6092b9d3086ed9165c7a6dc5855c34 Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:34:19 +0200 Subject: [PATCH 036/120] fix: added explicit handling for float & int --- client/src/utils/schemaUtils.ts | 52 ++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index 62401bf8..7a175142 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -151,42 +151,86 @@ export function isPropertyRequired( * @returns A normalized schema or the original schema */ export function normalizeUnionType(schema: JsonSchemaType): JsonSchemaType { - // Handle anyOf with string and null (FastMCP pattern) + // Handle anyOf with exactly string and null (FastMCP pattern) if ( schema.anyOf && + schema.anyOf.length === 2 && schema.anyOf.some((t) => (t as JsonSchemaType).type === "string") && schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") ) { return { ...schema, type: "string", anyOf: undefined }; } - // Handle anyOf with boolean and null (FastMCP pattern) + // Handle anyOf with exactly boolean and null (FastMCP pattern) if ( schema.anyOf && + schema.anyOf.length === 2 && schema.anyOf.some((t) => (t as JsonSchemaType).type === "boolean") && schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") ) { return { ...schema, type: "boolean", anyOf: undefined }; } - // Handle array type with string and null + // Handle anyOf with exactly number and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.length === 2 && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "number") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "number", anyOf: undefined }; + } + + // Handle anyOf with exactly integer and null (FastMCP pattern) + if ( + schema.anyOf && + schema.anyOf.length === 2 && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "integer") && + schema.anyOf.some((t) => (t as JsonSchemaType).type === "null") + ) { + return { ...schema, type: "integer", anyOf: undefined }; + } + + // Handle array type with exactly string and null if ( Array.isArray(schema.type) && + schema.type.length === 2 && schema.type.includes("string") && schema.type.includes("null") ) { return { ...schema, type: "string" }; } - // Handle array type with boolean and null + // Handle array type with exactly boolean and null if ( Array.isArray(schema.type) && + schema.type.length === 2 && schema.type.includes("boolean") && schema.type.includes("null") ) { return { ...schema, type: "boolean" }; } + // Handle array type with exactly number and null + if ( + Array.isArray(schema.type) && + schema.type.length === 2 && + schema.type.includes("number") && + schema.type.includes("null") + ) { + return { ...schema, type: "number" }; + } + + // Handle array type with exactly integer and null + if ( + Array.isArray(schema.type) && + schema.type.length === 2 && + schema.type.includes("integer") && + schema.type.includes("null") + ) { + return { ...schema, type: "integer" }; + } + return schema; } From d4f8e061f3dfe382d9b015b3022ec39835fd0330 Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:34:40 +0200 Subject: [PATCH 037/120] ci: added tests for the union type --- .../src/utils/__tests__/schemaUtils.test.ts | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts index 133e25ec..0ecd8075 100644 --- a/client/src/utils/__tests__/schemaUtils.test.ts +++ b/client/src/utils/__tests__/schemaUtils.test.ts @@ -1,6 +1,7 @@ import { generateDefaultValue, formatFieldLabel, + normalizeUnionType, cacheToolOutputSchemas, getToolOutputValidator, validateToolOutput, @@ -142,6 +143,189 @@ describe("formatFieldLabel", () => { }); }); +describe("normalizeUnionType", () => { + test("normalizes anyOf with string and null to string type", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "string" }, { type: "null" }], + description: "Optional string parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("string"); + expect(normalized.anyOf).toBeUndefined(); + expect(normalized.description).toBe("Optional string parameter"); + }); + + test("normalizes anyOf with boolean and null to boolean type", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "boolean" }, { type: "null" }], + description: "Optional boolean parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("boolean"); + expect(normalized.anyOf).toBeUndefined(); + expect(normalized.description).toBe("Optional boolean parameter"); + }); + + test("normalizes array type with string and null to string type", () => { + const schema: JsonSchemaType = { + type: ["string", "null"], + description: "Optional string parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("string"); + expect(normalized.description).toBe("Optional string parameter"); + }); + + test("normalizes array type with boolean and null to boolean type", () => { + const schema: JsonSchemaType = { + type: ["boolean", "null"], + description: "Optional boolean parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("boolean"); + expect(normalized.description).toBe("Optional boolean parameter"); + }); + + test("normalizes anyOf with number and null to number type", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "number" }, { type: "null" }], + description: "Optional number parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("number"); + expect(normalized.anyOf).toBeUndefined(); + expect(normalized.description).toBe("Optional number parameter"); + }); + + test("normalizes anyOf with integer and null to integer type", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "integer" }, { type: "null" }], + description: "Optional integer parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("integer"); + expect(normalized.anyOf).toBeUndefined(); + expect(normalized.description).toBe("Optional integer parameter"); + }); + + test("normalizes array type with number and null to number type", () => { + const schema: JsonSchemaType = { + type: ["number", "null"], + description: "Optional number parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("number"); + expect(normalized.description).toBe("Optional number parameter"); + }); + + test("normalizes array type with integer and null to integer type", () => { + const schema: JsonSchemaType = { + type: ["integer", "null"], + description: "Optional integer parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("integer"); + expect(normalized.description).toBe("Optional integer parameter"); + }); + + test("handles anyOf with reversed order (null first)", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "null" }, { type: "string" }], + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("string"); + expect(normalized.anyOf).toBeUndefined(); + }); + + test("leaves non-union schemas unchanged", () => { + const schema: JsonSchemaType = { + type: "string", + description: "Regular string parameter", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized).toEqual(schema); + }); + + test("leaves anyOf with non-matching types unchanged", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "string" }, { type: "number" }], + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized).toEqual(schema); + }); + + test("leaves anyOf with more than two types unchanged", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }], + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized).toEqual(schema); + }); + + test("leaves array type with non-matching types unchanged", () => { + const schema: JsonSchemaType = { + type: ["string", "number"], + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized).toEqual(schema); + }); + + test("handles schemas without type or anyOf", () => { + const schema: JsonSchemaType = { + description: "Schema without type", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized).toEqual(schema); + }); + + test("preserves other properties when normalizing", () => { + const schema: JsonSchemaType = { + anyOf: [{ type: "string" }, { type: "null" }], + description: "Optional string", + minLength: 1, + maxLength: 100, + pattern: "^[a-z]+$", + }; + + const normalized = normalizeUnionType(schema); + + expect(normalized.type).toBe("string"); + expect(normalized.anyOf).toBeUndefined(); + expect(normalized.description).toBe("Optional string"); + expect(normalized.minLength).toBe(1); + expect(normalized.maxLength).toBe(100); + expect(normalized.pattern).toBe("^[a-z]+$"); + }); +}); + describe("Output Schema Validation", () => { const mockTools: Tool[] = [ { From cc3f9df33c1cec26b9d68d5f4d2e7411f9535427 Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:26:24 +0200 Subject: [PATCH 038/120] fix: fix type error by handling array of types in JSON schema validation --- client/src/components/DynamicJsonForm.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 849a2aa2..aafa2933 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -15,12 +15,22 @@ interface DynamicJsonFormProps { maxDepth?: number; } +const isTypeSupported = ( + type: JsonSchemaType["type"], + supportedTypes: string[], +): boolean => { + if (Array.isArray(type)) { + return type.every((t) => supportedTypes.includes(t)); + } + return typeof type === "string" && supportedTypes.includes(type); +}; + const isSimpleObject = (schema: JsonSchemaType): boolean => { const supportedTypes = ["string", "number", "integer", "boolean", "null"]; - if (schema.type && supportedTypes.includes(schema.type)) return true; + if (schema.type && isTypeSupported(schema.type, supportedTypes)) return true; if (schema.type === "object") { return Object.values(schema.properties ?? {}).every( - (prop) => prop.type && supportedTypes.includes(prop.type), + (prop) => prop.type && isTypeSupported(prop.type, supportedTypes), ); } if (schema.type === "array") { From fda879ccc1d5488402012d5f615602ff18c9c291 Mon Sep 17 00:00:00 2001 From: Mats Julius Funke <125814808+matsjfunke@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:32:24 +0200 Subject: [PATCH 039/120] ci: added test for the DynamicJsonForm --- client/src/components/DynamicJsonForm.tsx | 10 +++++++++- .../components/__tests__/DynamicJsonForm.test.tsx | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index aafa2933..2214add9 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -191,7 +191,13 @@ const DynamicJsonForm = ({ const isRequired = parentSchema?.required?.includes(propertyName || "") ?? false; - switch (propSchema.type) { + let fieldType = propSchema.type; + if (Array.isArray(fieldType)) { + // Of the possible types, find the first non-null type to determine the control to render + fieldType = fieldType.find((t) => t !== "null") ?? fieldType[0]; + } + + switch (fieldType) { case "string": { if ( propSchema.oneOf && @@ -347,6 +353,8 @@ const DynamicJsonForm = ({ required={isRequired} /> ); + case "null": + return null; case "object": if (!propSchema.properties) { return ( diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index 22813c9b..318fba2d 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -35,6 +35,18 @@ describe("DynamicJsonForm String Fields", () => { const input = screen.getByRole("textbox"); expect(input).toHaveProperty("type", "text"); }); + + it("should handle a union type of string and null", () => { + const schema: JsonSchemaType = { + type: ["string", "null"], + description: "Test string or null field", + }; + render( + , + ); + const input = screen.getByRole("textbox"); + expect(input).toHaveProperty("type", "text"); + }); }); describe("Format Support", () => { From 0194e5f62a8bc4bb3772e220db1673f1beadfaf4 Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 4 Aug 2025 14:35:33 -0400 Subject: [PATCH 040/120] 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 de16a18e2f8bd46682befe3227b44167c8e974c7 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 11:50:06 +0100 Subject: [PATCH 041/120] fix: use spread operator to avoid empty string arguments This fixes the regression introduced in dc5fa6a where empty strings were passed as arguments when no command or args were provided, causing parseArgs to fail with "Unexpected argument ''" error. The spread operator approach is more idiomatic and avoids creating empty strings that need to be filtered out. --- client/bin/start.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/bin/start.js b/client/bin/start.js index 0b5e84e2..e0496cde 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -91,10 +91,10 @@ async function startProdServer(serverOptions) { "node", [ inspectorServerPath, - command ? `--command=${command}` : "", - mcpServerArgs && mcpServerArgs.length > 0 - ? `--args=${mcpServerArgs.join(" ")}` - : "", + ...(command ? [`--command=${command}`] : []), + ...(mcpServerArgs && mcpServerArgs.length > 0 + ? [`--args=${mcpServerArgs.join(" ")}`] + : []), ], { env: { From acf6c1e118e3b117e06a1a5ea17aeeba3bf736de Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 30 Jul 2025 16:37:59 +0100 Subject: [PATCH 042/120] feat: Auto-detect transport type from config files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When launching the inspector with --config, automatically set the transport dropdown and server URL based on the config file contents. This eliminates the need to manually switch between stdio/sse/streamable-http in the UI. - Use discriminated union for ServerConfig to properly type different transports - Detect transport type and URL from config, pass via query params - Maintain backwards compatibility for configs without explicit 'type' field - Add comprehensive tests for the new functionality Improves UX by making the config file the single source of truth for server settings. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cli/src/cli.ts | 62 +++++++++++++++++++++++++++++++++++++-------- client/bin/start.js | 31 ++++++++++++++++++++++- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 5ff1f111..d620958b 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -14,6 +14,8 @@ type Args = { args: string[]; envArgs: Record; cli: boolean; + transport?: "stdio" | "sse" | "streamable-http"; + serverUrl?: string; }; type CliOptions = { @@ -23,11 +25,18 @@ type CliOptions = { cli?: boolean; }; -type ServerConfig = { - command: string; - args?: string[]; - env?: Record; -}; +type ServerConfig = + | { + type: "stdio"; + command: string; + args?: string[]; + env?: Record; + } + | { + type: "sse" | "streamable-http"; + url: string; + note?: string; + }; function handleError(error: unknown): never { let message: string; @@ -74,6 +83,16 @@ async function runWebClient(args: Args): Promise { startArgs.push("-e", `${key}=${value}`); } + // Pass transport type if specified + if (args.transport) { + startArgs.push("--transport", args.transport); + } + + // Pass server URL if specified + if (args.serverUrl) { + startArgs.push("--server-url", args.serverUrl); + } + // Pass command and args (using -- to separate them) if (args.command) { startArgs.push("--", args.command, ...args.args); @@ -217,12 +236,33 @@ function parseArgs(): Args { if (options.config && options.server) { const config = loadConfigFile(options.config, options.server); - return { - command: config.command, - args: [...(config.args || []), ...finalArgs], - envArgs: { ...(config.env || {}), ...(options.e || {}) }, - cli: options.cli || false, - }; + if (config.type === "stdio") { + return { + command: config.command, + args: [...(config.args || []), ...finalArgs], + envArgs: { ...(config.env || {}), ...(options.e || {}) }, + cli: options.cli || false, + transport: "stdio", + }; + } else if (config.type === "sse" || config.type === "streamable-http") { + return { + command: "", + args: finalArgs, + envArgs: options.e || {}, + cli: options.cli || false, + transport: config.type, + serverUrl: config.url, + }; + } else { + // Backwards compatibility: if no type field, assume stdio + return { + command: (config as any).command || "", + args: [...((config as any).args || []), ...finalArgs], + envArgs: { ...((config as any).env || {}), ...(options.e || {}) }, + cli: options.cli || false, + transport: "stdio", + }; + } } // Otherwise use command line arguments diff --git a/client/bin/start.js b/client/bin/start.js index e0496cde..93a39ad3 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -13,7 +13,14 @@ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms, true)); } -function getClientUrl(port, authDisabled, sessionToken, serverPort) { +function getClientUrl( + port, + authDisabled, + sessionToken, + serverPort, + transport, + serverUrl, +) { const host = process.env.HOST || "localhost"; const baseUrl = `http://${host}:${port}`; @@ -24,6 +31,12 @@ function getClientUrl(port, authDisabled, sessionToken, serverPort) { if (!authDisabled) { params.set("MCP_PROXY_AUTH_TOKEN", sessionToken); } + if (transport) { + params.set("transport", transport); + } + if (serverUrl) { + params.set("serverUrl", serverUrl); + } return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl; } @@ -123,6 +136,8 @@ async function startDevClient(clientOptions) { sessionToken, abort, cancelled, + transport, + serverUrl, } = clientOptions; const clientCommand = "npx"; const host = process.env.HOST || "localhost"; @@ -140,6 +155,8 @@ async function startDevClient(clientOptions) { authDisabled, sessionToken, SERVER_PORT, + transport, + serverUrl, ); // Give vite time to start before opening or logging the URL @@ -173,6 +190,8 @@ async function startProdClient(clientOptions) { sessionToken, abort, cancelled, + transport, + serverUrl, } = clientOptions; const inspectorClientPath = resolve( __dirname, @@ -187,6 +206,8 @@ async function startProdClient(clientOptions) { authDisabled, sessionToken, SERVER_PORT, + transport, + serverUrl, ); await spawnPromise("node", [inspectorClientPath], { @@ -208,6 +229,8 @@ async function main() { let command = null; let parsingFlags = true; let isDev = false; + let transport = null; + let serverUrl = null; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -233,6 +256,10 @@ async function main() { } else { envVars[envVar] = ""; } + } else if (parsingFlags && arg === "--transport" && i + 1 < args.length) { + transport = args[++i]; + } else if (parsingFlags && arg === "--server-url" && i + 1 < args.length) { + serverUrl = args[++i]; } else if (!command && !isDev) { command = arg; } else if (!isDev) { @@ -292,6 +319,8 @@ async function main() { sessionToken, abort, cancelled, + transport, + serverUrl, }; await (isDev From 1d89d750e0d2015a28b55ab8c6b35bcdaecf3a82 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 30 Jul 2025 19:29:12 +0100 Subject: [PATCH 043/120] test: Add minimal test coverage for CLI transport options - Add tests for config files with different transport types (stdio, sse, streamable-http) - Add test for backward compatibility with configs missing type field - Verify transport and serverUrl options are correctly parsed from config files - All 36 tests pass --- cli/scripts/cli-tests.js | 141 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index 68ce3885..857b5108 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -120,6 +120,85 @@ try { const invalidConfigPath = path.join(TEMP_DIR, "invalid-config.json"); fs.writeFileSync(invalidConfigPath, '{\n "mcpServers": {\n "invalid": {'); +// Create config files with different transport types for testing +const sseConfigPath = path.join(TEMP_DIR, "sse-config.json"); +fs.writeFileSync( + sseConfigPath, + JSON.stringify( + { + mcpServers: { + "test-sse": { + type: "sse", + url: "http://localhost:3000/sse", + note: "Test SSE server", + }, + }, + }, + null, + 2, + ), +); + +const httpConfigPath = path.join(TEMP_DIR, "http-config.json"); +fs.writeFileSync( + httpConfigPath, + JSON.stringify( + { + mcpServers: { + "test-http": { + type: "streamable-http", + url: "http://localhost:3000/mcp", + note: "Test HTTP server", + }, + }, + }, + null, + 2, + ), +); + +const stdioConfigPath = path.join(TEMP_DIR, "stdio-config.json"); +fs.writeFileSync( + stdioConfigPath, + JSON.stringify( + { + mcpServers: { + "test-stdio": { + type: "stdio", + command: "npx", + args: ["@modelcontextprotocol/server-everything"], + env: { + TEST_ENV: "test-value", + }, + }, + }, + }, + null, + 2, + ), +); + +// Config without type field (backward compatibility) +const legacyConfigPath = path.join(TEMP_DIR, "legacy-config.json"); +fs.writeFileSync( + legacyConfigPath, + JSON.stringify( + { + mcpServers: { + "test-legacy": { + command: "npx", + args: ["@modelcontextprotocol/server-everything"], + env: { + LEGACY_ENV: "legacy-value", + }, + }, + }, + }, + null, + 2, + ), +); + // Function to run a basic test async function runBasicTest(testName, ...args) { const outputFile = path.join( @@ -649,6 +728,56 @@ async function runTests() { "debug", ); + console.log( + `\n${colors.YELLOW}=== Running Config Transport Type Tests ===${colors.NC}`, + ); + + // Test 25: Config with stdio transport type + await runBasicTest( + "config_stdio_type", + "--config", + stdioConfigPath, + "--server", + "test-stdio", + "--cli", + "--method", + "tools/list", + ); + + // Test 26: Config with SSE transport type (should pass transport to client) + await runBasicTest( + "config_sse_type", + "--config", + sseConfigPath, + "--server", + "test-sse", + "echo", + "test", + ); + + // Test 27: Config with streamable-http transport type + await runBasicTest( + "config_http_type", + "--config", + httpConfigPath, + "--server", + "test-http", + "echo", + "test", + ); + + // Test 28: Legacy config without type field (backward compatibility) + await runBasicTest( + "config_legacy_no_type", + "--config", + legacyConfigPath, + "--server", + "test-legacy", + "--cli", + "--method", + "tools/list", + ); + console.log( `\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`, ); @@ -668,7 +797,7 @@ async function runTests() { await new Promise((resolve) => setTimeout(resolve, 3000)); - // Test 25: HTTP transport inferred from URL ending with /mcp + // Test 29: HTTP transport inferred from URL ending with /mcp await runBasicTest( "http_transport_inferred", "http://127.0.0.1:3001/mcp", @@ -677,7 +806,7 @@ async function runTests() { "tools/list", ); - // Test 26: HTTP transport with explicit --transport http flag + // Test 30: HTTP transport with explicit --transport http flag await runBasicTest( "http_transport_with_explicit_flag", "http://127.0.0.1:3001/mcp", @@ -688,7 +817,7 @@ async function runTests() { "tools/list", ); - // Test 27: HTTP transport with suffix and --transport http flag + // Test 31: HTTP transport with suffix and --transport http flag await runBasicTest( "http_transport_with_explicit_flag_and_suffix", "http://127.0.0.1:3001/mcp", @@ -699,7 +828,7 @@ async function runTests() { "tools/list", ); - // Test 28: SSE transport given to HTTP server (should fail) + // Test 32: SSE transport given to HTTP server (should fail) await runErrorTest( "sse_transport_given_to_http_server", "http://127.0.0.1:3001", @@ -710,7 +839,7 @@ async function runTests() { "tools/list", ); - // Test 29: HTTP transport without URL (should fail) + // Test 33: HTTP transport without URL (should fail) await runErrorTest( "http_transport_without_url", "--transport", @@ -720,7 +849,7 @@ async function runTests() { "tools/list", ); - // Test 30: SSE transport without URL (should fail) + // Test 34: SSE transport without URL (should fail) await runErrorTest( "sse_transport_without_url", "--transport", From b6dc2ceeb94737a93ea11b0550eb10be658e77b5 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 13:30:16 +0100 Subject: [PATCH 044/120] Remove transport/serverUrl parsing from start.js Move transport configuration parsing to cli.js as suggested in PR review. The CLI entry point (cli.js) should handle config file parsing and pass parameters to start.js. --- client/bin/start.js | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/client/bin/start.js b/client/bin/start.js index 93a39ad3..ddf3fac3 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -18,8 +18,6 @@ function getClientUrl( authDisabled, sessionToken, serverPort, - transport, - serverUrl, ) { const host = process.env.HOST || "localhost"; const baseUrl = `http://${host}:${port}`; @@ -31,12 +29,6 @@ function getClientUrl( if (!authDisabled) { params.set("MCP_PROXY_AUTH_TOKEN", sessionToken); } - if (transport) { - params.set("transport", transport); - } - if (serverUrl) { - params.set("serverUrl", serverUrl); - } return params.size > 0 ? `${baseUrl}/?${params.toString()}` : baseUrl; } @@ -136,8 +128,6 @@ async function startDevClient(clientOptions) { sessionToken, abort, cancelled, - transport, - serverUrl, } = clientOptions; const clientCommand = "npx"; const host = process.env.HOST || "localhost"; @@ -155,8 +145,6 @@ async function startDevClient(clientOptions) { authDisabled, sessionToken, SERVER_PORT, - transport, - serverUrl, ); // Give vite time to start before opening or logging the URL @@ -190,8 +178,6 @@ async function startProdClient(clientOptions) { sessionToken, abort, cancelled, - transport, - serverUrl, } = clientOptions; const inspectorClientPath = resolve( __dirname, @@ -206,8 +192,6 @@ async function startProdClient(clientOptions) { authDisabled, sessionToken, SERVER_PORT, - transport, - serverUrl, ); await spawnPromise("node", [inspectorClientPath], { @@ -229,8 +213,6 @@ async function main() { let command = null; let parsingFlags = true; let isDev = false; - let transport = null; - let serverUrl = null; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -256,10 +238,6 @@ async function main() { } else { envVars[envVar] = ""; } - } else if (parsingFlags && arg === "--transport" && i + 1 < args.length) { - transport = args[++i]; - } else if (parsingFlags && arg === "--server-url" && i + 1 < args.length) { - serverUrl = args[++i]; } else if (!command && !isDev) { command = arg; } else if (!isDev) { @@ -319,8 +297,6 @@ async function main() { sessionToken, abort, cancelled, - transport, - serverUrl, }; await (isDev From 63193ec00730e6d6d4502ceb43935cdfa277d15c Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 13:31:40 +0100 Subject: [PATCH 045/120] Clean up CLI tests and add config parsing tests - Remove web client launch tests from CLI test suite - Add CLI-mode tests for SSE and streamable-http configs - Keep only tests that use --cli flag as intended --- cli/scripts/cli-tests.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index 857b5108..299c7e3d 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -744,26 +744,28 @@ async function runTests() { "tools/list", ); - // Test 26: Config with SSE transport type (should pass transport to client) + // Test 26: Config with SSE transport type (CLI mode) await runBasicTest( - "config_sse_type", + "config_sse_type_cli", "--config", sseConfigPath, "--server", "test-sse", - "echo", - "test", + "--cli", + "--method", + "tools/list", ); - // Test 27: Config with streamable-http transport type + // Test 27: Config with streamable-http transport type (CLI mode) await runBasicTest( - "config_http_type", + "config_http_type_cli", "--config", httpConfigPath, "--server", "test-http", - "echo", - "test", + "--cli", + "--method", + "tools/list", ); // Test 28: Legacy config without type field (backward compatibility) From a7675d1be2690f9d602fa866e2614adca42b107d Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 13:34:16 +0100 Subject: [PATCH 046/120] Add Playwright e2e tests for CLI argument handling - Test that transport and serverUrl parameters are correctly passed via URL - Verify SSE and streamable-http transport types are handled - Test default STDIO behavior --- client/e2e/cli-arguments.spec.ts | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 client/e2e/cli-arguments.spec.ts diff --git a/client/e2e/cli-arguments.spec.ts b/client/e2e/cli-arguments.spec.ts new file mode 100644 index 00000000..3376a6d7 --- /dev/null +++ b/client/e2e/cli-arguments.spec.ts @@ -0,0 +1,61 @@ +import { test, expect } from "@playwright/test"; + +// These tests verify that CLI arguments correctly set URL parameters +// The CLI should parse config files and pass transport/serverUrl as URL params +test.describe("CLI Arguments @cli", () => { + test("should pass transport parameter from command line", async ({ + page, + }) => { + // Simulate: npx . --transport sse --server-url http://localhost:3000/sse + await page.goto("http://localhost:6274/?transport=sse&serverUrl=http://localhost:3000/sse"); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel("Transport Type"); + await expect(selectTrigger).toBeVisible(); + + // Verify transport dropdown shows SSE + await expect(selectTrigger).toContainText("SSE"); + + // Verify URL field is visible and populated + const urlInput = page.locator("#sse-url-input"); + await expect(urlInput).toBeVisible(); + await expect(urlInput).toHaveValue("http://localhost:3000/sse"); + }); + + test("should pass transport parameter for streamable-http", async ({ + page, + }) => { + // Simulate config with streamable-http transport + await page.goto("http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:3000/mcp"); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel("Transport Type"); + await expect(selectTrigger).toBeVisible(); + + // Verify transport dropdown shows Streamable HTTP + await expect(selectTrigger).toContainText("Streamable HTTP"); + + // Verify URL field is visible and populated + const urlInput = page.locator("#sse-url-input"); + await expect(urlInput).toBeVisible(); + await expect(urlInput).toHaveValue("http://localhost:3000/mcp"); + }); + + test("should not pass transport parameter for stdio config", async ({ + page, + }) => { + // Simulate stdio config (no transport param needed) + await page.goto("http://localhost:6274/"); + + // Wait for the Transport Type dropdown to be visible + const selectTrigger = page.getByLabel("Transport Type"); + await expect(selectTrigger).toBeVisible(); + + // Verify transport dropdown defaults to STDIO + await expect(selectTrigger).toContainText("STDIO"); + + // Verify command/args fields are visible + await expect(page.locator("#command-input")).toBeVisible(); + await expect(page.locator("#arguments-input")).toBeVisible(); + }); +}); \ No newline at end of file From 0e2906c4c4e7088fe5fdc72ef25a877d9034e93b Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 13:35:28 +0100 Subject: [PATCH 047/120] Implement default-server support - Auto-select server when --config is used without --server - Use 'default-server' if it exists in config - Use single server if only one is defined - Show helpful error if multiple servers exist without default - Add tests for all default-server scenarios --- cli/scripts/cli-tests.js | 108 ++++++++++++++++++++++++++++++++++++--- cli/src/cli.ts | 36 ++++++++++--- 2 files changed, 132 insertions(+), 12 deletions(-) diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index 299c7e3d..87981dd8 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -780,6 +780,102 @@ async function runTests() { "tools/list", ); + console.log( + `\n${colors.YELLOW}=== Running Default Server Tests ===${colors.NC}`, + ); + + // Create config with single server for auto-selection + const singleServerConfigPath = path.join(TEMP_DIR, "single-server-config.json"); + fs.writeFileSync( + singleServerConfigPath, + JSON.stringify( + { + mcpServers: { + "only-server": { + command: "npx", + args: ["@modelcontextprotocol/server-everything"], + }, + }, + }, + null, + 2, + ), + ); + + // Create config with default-server + const defaultServerConfigPath = path.join(TEMP_DIR, "default-server-config.json"); + fs.writeFileSync( + defaultServerConfigPath, + JSON.stringify( + { + mcpServers: { + "default-server": { + command: "npx", + args: ["@modelcontextprotocol/server-everything"], + }, + "other-server": { + command: "node", + args: ["other.js"], + }, + }, + }, + null, + 2, + ), + ); + + // Create config with multiple servers (no default) + const multiServerConfigPath = path.join(TEMP_DIR, "multi-server-config.json"); + fs.writeFileSync( + multiServerConfigPath, + JSON.stringify( + { + mcpServers: { + "server1": { + command: "npx", + args: ["@modelcontextprotocol/server-everything"], + }, + "server2": { + command: "node", + args: ["other.js"], + }, + }, + }, + null, + 2, + ), + ); + + // Test 29: Config with single server auto-selection + await runBasicTest( + "single_server_auto_select", + "--config", + singleServerConfigPath, + "--cli", + "--method", + "tools/list", + ); + + // Test 30: Config with default-server auto-selection + await runBasicTest( + "default_server_auto_select", + "--config", + defaultServerConfigPath, + "--cli", + "--method", + "tools/list", + ); + + // Test 31: Config with multiple servers and no default (should fail) + await runErrorTest( + "multi_server_no_default", + "--config", + multiServerConfigPath, + "--cli", + "--method", + "tools/list", + ); + console.log( `\n${colors.YELLOW}=== Running HTTP Transport Tests ===${colors.NC}`, ); @@ -799,7 +895,7 @@ async function runTests() { await new Promise((resolve) => setTimeout(resolve, 3000)); - // Test 29: HTTP transport inferred from URL ending with /mcp + // Test 32: HTTP transport inferred from URL ending with /mcp await runBasicTest( "http_transport_inferred", "http://127.0.0.1:3001/mcp", @@ -808,7 +904,7 @@ async function runTests() { "tools/list", ); - // Test 30: HTTP transport with explicit --transport http flag + // Test 33: HTTP transport with explicit --transport http flag await runBasicTest( "http_transport_with_explicit_flag", "http://127.0.0.1:3001/mcp", @@ -819,7 +915,7 @@ async function runTests() { "tools/list", ); - // Test 31: HTTP transport with suffix and --transport http flag + // Test 34: HTTP transport with suffix and --transport http flag await runBasicTest( "http_transport_with_explicit_flag_and_suffix", "http://127.0.0.1:3001/mcp", @@ -830,7 +926,7 @@ async function runTests() { "tools/list", ); - // Test 32: SSE transport given to HTTP server (should fail) + // Test 35: SSE transport given to HTTP server (should fail) await runErrorTest( "sse_transport_given_to_http_server", "http://127.0.0.1:3001", @@ -841,7 +937,7 @@ async function runTests() { "tools/list", ); - // Test 33: HTTP transport without URL (should fail) + // Test 36: HTTP transport without URL (should fail) await runErrorTest( "http_transport_without_url", "--transport", @@ -851,7 +947,7 @@ async function runTests() { "tools/list", ); - // Test 34: SSE transport without URL (should fail) + // Test 37: SSE transport without URL (should fail) await runErrorTest( "sse_transport_without_url", "--transport", diff --git a/cli/src/cli.ts b/cli/src/cli.ts index d620958b..cf8662e5 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -220,16 +220,40 @@ function parseArgs(): Args { // Add back any arguments that came after -- const finalArgs = [...remainingArgs, ...postArgs]; - // Validate that config and server are provided together - if ( - (options.config && !options.server) || - (!options.config && options.server) - ) { + // Validate config and server options + if (!options.config && options.server) { throw new Error( - "Both --config and --server must be provided together. If you specify one, you must specify the other.", + "--server requires --config to be specified", ); } + // If config is provided without server, try to auto-select + if (options.config && !options.server) { + const configContent = fs.readFileSync( + path.isAbsolute(options.config) + ? options.config + : path.resolve(process.cwd(), options.config), + "utf8" + ); + const parsedConfig = JSON.parse(configContent); + const servers = Object.keys(parsedConfig.mcpServers || {}); + + if (servers.includes("default-server")) { + // Use default-server if it exists + options.server = "default-server"; + } else if (servers.length === 1) { + // Use the only server if there's just one + options.server = servers[0]; + } else if (servers.length === 0) { + throw new Error("No servers found in config file"); + } else { + // Multiple servers, no default-server + throw new Error( + `Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}` + ); + } + } + // If config file is specified, load and use the options from the file. We must merge the args // from the command line and the file together, or we will miss the method options (--method, // etc.) From 3a4f3bc5108e4922c444e3765cda6a2bd24e9d78 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 13:36:54 +0100 Subject: [PATCH 048/120] Document transport types and default-server functionality - Add examples for stdio, sse, and streamable-http transport types - Document automatic server selection behavior - Show how to use default-server in config files --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/README.md b/README.md index 6a671f1c..932c337d 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,73 @@ Example server configuration file: } ``` +#### Transport Types in Config Files + +The inspector automatically detects the transport type from your config file. You can specify different transport types: + +**STDIO (default):** +```json +{ + "mcpServers": { + "my-stdio-server": { + "type": "stdio", + "command": "npx", + "args": ["@modelcontextprotocol/server-everything"] + } + } +} +``` + +**SSE (Server-Sent Events):** +```json +{ + "mcpServers": { + "my-sse-server": { + "type": "sse", + "url": "http://localhost:3000/sse" + } + } +} +``` + +**Streamable HTTP:** +```json +{ + "mcpServers": { + "my-http-server": { + "type": "streamable-http", + "url": "http://localhost:3000/mcp" + } + } +} +``` + +#### Default Server Selection + +You can launch the inspector without specifying a server name if your config has: + +1. **A single server** - automatically selected: +```bash +# Automatically uses "my-server" if it's the only one +npx @modelcontextprotocol/inspector --config config.json +``` + +2. **A server named "default-server"** - automatically selected: +```json +{ + "mcpServers": { + "default-server": { + "command": "npx", + "args": ["@modelcontextprotocol/server-everything"] + }, + "other-server": { + "command": "node", + "args": ["other.js"] + } + } +} +``` + > **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: From 1e399b70554bbadba70ae224fb3d3c55c6b6b89d Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 14:08:26 +0100 Subject: [PATCH 049/120] prettier --- README.md | 5 +++++ cli/scripts/cli-tests.js | 14 ++++++++++---- cli/src/cli.ts | 10 ++++------ client/bin/start.js | 7 +------ client/e2e/cli-arguments.spec.ts | 10 +++++++--- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 932c337d..69eada69 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,7 @@ Example server configuration file: The inspector automatically detects the transport type from your config file. You can specify different transport types: **STDIO (default):** + ```json { "mcpServers": { @@ -252,6 +253,7 @@ The inspector automatically detects the transport type from your config file. Yo ``` **SSE (Server-Sent Events):** + ```json { "mcpServers": { @@ -264,6 +266,7 @@ The inspector automatically detects the transport type from your config file. Yo ``` **Streamable HTTP:** + ```json { "mcpServers": { @@ -280,12 +283,14 @@ The inspector automatically detects the transport type from your config file. Yo You can launch the inspector without specifying a server name if your config has: 1. **A single server** - automatically selected: + ```bash # Automatically uses "my-server" if it's the only one npx @modelcontextprotocol/inspector --config config.json ``` 2. **A server named "default-server"** - automatically selected: + ```json { "mcpServers": { diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index 87981dd8..e737756f 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -785,7 +785,10 @@ async function runTests() { ); // Create config with single server for auto-selection - const singleServerConfigPath = path.join(TEMP_DIR, "single-server-config.json"); + const singleServerConfigPath = path.join( + TEMP_DIR, + "single-server-config.json", + ); fs.writeFileSync( singleServerConfigPath, JSON.stringify( @@ -803,7 +806,10 @@ async function runTests() { ); // Create config with default-server - const defaultServerConfigPath = path.join(TEMP_DIR, "default-server-config.json"); + const defaultServerConfigPath = path.join( + TEMP_DIR, + "default-server-config.json", + ); fs.writeFileSync( defaultServerConfigPath, JSON.stringify( @@ -831,11 +837,11 @@ async function runTests() { JSON.stringify( { mcpServers: { - "server1": { + server1: { command: "npx", args: ["@modelcontextprotocol/server-everything"], }, - "server2": { + server2: { command: "node", args: ["other.js"], }, diff --git a/cli/src/cli.ts b/cli/src/cli.ts index cf8662e5..09c99d51 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -222,9 +222,7 @@ function parseArgs(): Args { // Validate config and server options if (!options.config && options.server) { - throw new Error( - "--server requires --config to be specified", - ); + throw new Error("--server requires --config to be specified"); } // If config is provided without server, try to auto-select @@ -233,11 +231,11 @@ function parseArgs(): Args { path.isAbsolute(options.config) ? options.config : path.resolve(process.cwd(), options.config), - "utf8" + "utf8", ); const parsedConfig = JSON.parse(configContent); const servers = Object.keys(parsedConfig.mcpServers || {}); - + if (servers.includes("default-server")) { // Use default-server if it exists options.server = "default-server"; @@ -249,7 +247,7 @@ function parseArgs(): Args { } else { // Multiple servers, no default-server throw new Error( - `Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}` + `Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}`, ); } } diff --git a/client/bin/start.js b/client/bin/start.js index ddf3fac3..e0496cde 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -13,12 +13,7 @@ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms, true)); } -function getClientUrl( - port, - authDisabled, - sessionToken, - serverPort, -) { +function getClientUrl(port, authDisabled, sessionToken, serverPort) { const host = process.env.HOST || "localhost"; const baseUrl = `http://${host}:${port}`; diff --git a/client/e2e/cli-arguments.spec.ts b/client/e2e/cli-arguments.spec.ts index 3376a6d7..a4dcdcce 100644 --- a/client/e2e/cli-arguments.spec.ts +++ b/client/e2e/cli-arguments.spec.ts @@ -7,7 +7,9 @@ test.describe("CLI Arguments @cli", () => { page, }) => { // Simulate: npx . --transport sse --server-url http://localhost:3000/sse - await page.goto("http://localhost:6274/?transport=sse&serverUrl=http://localhost:3000/sse"); + await page.goto( + "http://localhost:6274/?transport=sse&serverUrl=http://localhost:3000/sse", + ); // Wait for the Transport Type dropdown to be visible const selectTrigger = page.getByLabel("Transport Type"); @@ -26,7 +28,9 @@ test.describe("CLI Arguments @cli", () => { page, }) => { // Simulate config with streamable-http transport - await page.goto("http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:3000/mcp"); + await page.goto( + "http://localhost:6274/?transport=streamable-http&serverUrl=http://localhost:3000/mcp", + ); // Wait for the Transport Type dropdown to be visible const selectTrigger = page.getByLabel("Transport Type"); @@ -58,4 +62,4 @@ test.describe("CLI Arguments @cli", () => { await expect(page.locator("#command-input")).toBeVisible(); await expect(page.locator("#arguments-input")).toBeVisible(); }); -}); \ No newline at end of file +}); From 195f79076ea3414965454112988a087130905993 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 14:21:18 +0100 Subject: [PATCH 050/120] Fix CLI test failures for SSE/HTTP transport configs - Pass URL as command for SSE/HTTP configs instead of empty string - Update runCli to pass transport type flag when needed - Update tests to expect connection errors for non-existent SSE/HTTP servers --- cli/scripts/cli-tests.js | 8 ++++---- cli/src/cli.ts | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index e737756f..675ea0bc 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -744,8 +744,8 @@ async function runTests() { "tools/list", ); - // Test 26: Config with SSE transport type (CLI mode) - await runBasicTest( + // Test 26: Config with SSE transport type (CLI mode) - expects connection error + await runErrorTest( "config_sse_type_cli", "--config", sseConfigPath, @@ -756,8 +756,8 @@ async function runTests() { "tools/list", ); - // Test 27: Config with streamable-http transport type (CLI mode) - await runBasicTest( + // Test 27: Config with streamable-http transport type (CLI mode) - expects connection error + await runErrorTest( "config_http_type_cli", "--config", httpConfigPath, diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 09c99d51..df16f0cc 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -122,7 +122,18 @@ async function runCli(args: Args): Promise { }); try { - await spawnPromise("node", [cliPath, args.command, ...args.args], { + // Build CLI arguments + const cliArgs = [cliPath]; + + // Add transport flag if specified + if (args.transport && args.transport !== "stdio") { + cliArgs.push("--transport", args.transport); + } + + // Add command and remaining args + cliArgs.push(args.command, ...args.args); + + await spawnPromise("node", cliArgs, { env: { ...process.env, ...args.envArgs }, signal: abort.signal, echoOutput: true, @@ -268,7 +279,7 @@ function parseArgs(): Args { }; } else if (config.type === "sse" || config.type === "streamable-http") { return { - command: "", + command: config.url, args: finalArgs, envArgs: options.e || {}, cli: options.cli || false, From 7ea47c0caa123f7eb96df361d575857c263aded7 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 5 Aug 2025 19:38:44 +0100 Subject: [PATCH 051/120] Fix transport type propagation from CLI to web client - Add --transport and --server-url support to client/bin/start.js - Pass transport configuration through server to client via /config endpoint - Update client App.tsx to use defaultTransport and defaultServerUrl from server - Ensure SSE and HTTP transport configs work properly from CLI --- client/bin/start.js | 31 +++++++++++++++++++++++++++++-- client/src/App.tsx | 8 ++++++++ server/src/index.ts | 4 ++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/client/bin/start.js b/client/bin/start.js index e0496cde..f67301de 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -28,8 +28,15 @@ function getClientUrl(port, authDisabled, sessionToken, serverPort) { } async function startDevServer(serverOptions) { - const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } = - serverOptions; + const { + SERVER_PORT, + CLIENT_PORT, + sessionToken, + envVars, + abort, + transport, + serverUrl, + } = serverOptions; const serverCommand = "npx"; const serverArgs = ["tsx", "watch", "--clear-screen=false", "src/index.ts"]; const isWindows = process.platform === "win32"; @@ -42,6 +49,8 @@ async function startDevServer(serverOptions) { CLIENT_PORT, MCP_PROXY_AUTH_TOKEN: sessionToken, MCP_ENV_VARS: JSON.stringify(envVars), + ...(transport ? { MCP_TRANSPORT: transport } : {}), + ...(serverUrl ? { MCP_SERVER_URL: serverUrl } : {}), }, signal: abort.signal, echoOutput: true, @@ -78,6 +87,8 @@ async function startProdServer(serverOptions) { abort, command, mcpServerArgs, + transport, + serverUrl, } = serverOptions; const inspectorServerPath = resolve( __dirname, @@ -95,6 +106,8 @@ async function startProdServer(serverOptions) { ...(mcpServerArgs && mcpServerArgs.length > 0 ? [`--args=${mcpServerArgs.join(" ")}`] : []), + ...(transport ? [`--transport=${transport}`] : []), + ...(serverUrl ? [`--server-url=${serverUrl}`] : []), ], { env: { @@ -208,6 +221,8 @@ async function main() { let command = null; let parsingFlags = true; let isDev = false; + let transport = null; + let serverUrl = null; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -222,6 +237,16 @@ async function main() { continue; } + if (parsingFlags && arg === "--transport" && i + 1 < args.length) { + transport = args[++i]; + continue; + } + + if (parsingFlags && arg === "--server-url" && i + 1 < args.length) { + serverUrl = args[++i]; + continue; + } + if (parsingFlags && arg === "-e" && i + 1 < args.length) { const envVar = args[++i]; const equalsIndex = envVar.indexOf("="); @@ -273,6 +298,8 @@ async function main() { abort, command, mcpServerArgs, + transport, + serverUrl, }; const result = isDev diff --git a/client/src/App.tsx b/client/src/App.tsx index 537f80bf..d6680c35 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -455,6 +455,14 @@ const App = () => { if (data.defaultArgs) { setArgs(data.defaultArgs); } + if (data.defaultTransport) { + setTransportType( + data.defaultTransport as "stdio" | "sse" | "streamable-http", + ); + } + if (data.defaultServerUrl) { + setSseUrl(data.defaultServerUrl); + } }) .catch((error) => console.error("Error fetching default environment:", error), diff --git a/server/src/index.ts b/server/src/index.ts index 92badc2c..34a69414 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -40,6 +40,8 @@ const { values } = parseArgs({ env: { type: "string", default: "" }, args: { type: "string", default: "" }, command: { type: "string", default: "" }, + transport: { type: "string", default: "" }, + "server-url": { type: "string", default: "" }, }, }); @@ -523,6 +525,8 @@ app.get("/config", originValidationMiddleware, authMiddleware, (req, res) => { defaultEnvironment, defaultCommand: values.command, defaultArgs: values.args, + defaultTransport: values.transport, + defaultServerUrl: values["server-url"], }); } catch (error) { console.error("Error in /config route:", error); From b99b3d29d9b9ed210acbe1259b42ea4671524cea Mon Sep 17 00:00:00 2001 From: 2underscores Date: Mon, 28 Jul 2025 20:18:18 +1000 Subject: [PATCH 052/120] Using static client data as DCR fallback --- client/src/lib/oauth-state-machine.ts | 36 ++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 5078cfe4..758deb23 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -88,16 +88,34 @@ export const oauthTransitions: Record = { clientMetadata.scope = scopesSupported.join(" "); } - const fullInformation = await registerClient(context.serverUrl, { - metadata, - clientMetadata, - }); + // Try DCR first, with static client as fallback + try { + const fullInformation = await registerClient(context.serverUrl, { + metadata, + clientMetadata, + }); - context.provider.saveClientInformation(fullInformation); - context.updateState({ - oauthClientInfo: fullInformation, - oauthStep: "authorization_redirect", - }); + context.provider.saveClientInformation(fullInformation); + context.updateState({ + oauthClientInfo: fullInformation, + oauthStep: "authorization_redirect", + }); + console.log({ fullInformation }); + return; + } catch (dcrError) { + console.error(dcrError); + + // DCR failed, fallback to preregistered client + const existingClientInfo = await context.provider.clientInformation(); + if (!existingClientInfo) { + console.error("Neither dynamic client registration or preregistered client information was found"); + throw dcrError; + } + context.updateState({ + oauthClientInfo: existingClientInfo, + oauthStep: "authorization_redirect", + }); + } }, }, From 6a9505673121efed5a527d5e3b405fa8bea35043 Mon Sep 17 00:00:00 2001 From: 2underscores Date: Wed, 6 Aug 2025 08:47:21 +1000 Subject: [PATCH 053/120] Refactor --- client/src/lib/oauth-state-machine.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 758deb23..b8ecab51 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -89,33 +89,26 @@ export const oauthTransitions: Record = { } // Try DCR first, with static client as fallback + let fullInformation; try { - const fullInformation = await registerClient(context.serverUrl, { + fullInformation = await registerClient(context.serverUrl, { metadata, clientMetadata, }); - context.provider.saveClientInformation(fullInformation); - context.updateState({ - oauthClientInfo: fullInformation, - oauthStep: "authorization_redirect", - }); - console.log({ fullInformation }); - return; } catch (dcrError) { - console.error(dcrError); - // DCR failed, fallback to preregistered client - const existingClientInfo = await context.provider.clientInformation(); - if (!existingClientInfo) { + fullInformation = await context.provider.clientInformation(); + if (!fullInformation) { console.error("Neither dynamic client registration or preregistered client information was found"); throw dcrError; } - context.updateState({ - oauthClientInfo: existingClientInfo, - oauthStep: "authorization_redirect", - }); } + + context.updateState({ + oauthClientInfo: fullInformation, + oauthStep: "authorization_redirect", + }); }, }, From 98b7e63bb032b6aae4767712af4359594271a29a Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Wed, 6 Aug 2025 19:50:23 +0100 Subject: [PATCH 054/120] Add --transport flag support to CLI - Add --transport option to command line parser - Support --transport stdio/sse/http flags - Convert 'http' to 'streamable-http' internally - Fix transport type conversion for CLI mode --- cli/src/cli.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index df16f0cc..6c72e6a4 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -23,6 +23,7 @@ type CliOptions = { config?: string; server?: string; cli?: boolean; + transport?: string; }; type ServerConfig = @@ -127,7 +128,9 @@ async function runCli(args: Args): Promise { // Add transport flag if specified if (args.transport && args.transport !== "stdio") { - cliArgs.push("--transport", args.transport); + // Convert streamable-http back to http for CLI mode + const cliTransport = args.transport === "streamable-http" ? "http" : args.transport; + cliArgs.push("--transport", cliTransport); } // Add command and remaining args @@ -220,7 +223,8 @@ function parseArgs(): Args { ) .option("--config ", "config file path") .option("--server ", "server name from config file") - .option("--cli", "enable CLI mode"); + .option("--cli", "enable CLI mode") + .option("--transport ", "transport type (stdio, sse, http)"); // Parse only the arguments before -- program.parse(preArgs); @@ -301,12 +305,19 @@ function parseArgs(): Args { // Otherwise use command line arguments const command = finalArgs[0] || ""; const args = finalArgs.slice(1); + + // Map "http" shorthand to "streamable-http" + let transport = options.transport; + if (transport === "http") { + transport = "streamable-http"; + } return { command, args, envArgs: options.e || {}, cli: options.cli || false, + transport: transport as "stdio" | "sse" | "streamable-http" | undefined, }; } From e1e702ff14791e653535901f05b6545c504cae84 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 7 Aug 2025 11:42:31 +0100 Subject: [PATCH 055/120] ignore: ignore mcp.json as a common dev file --- .gitignore | 1 + .prettierignore | 1 + README.md | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index eeee17fe..6ce55639 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ sdk client/playwright-report/ client/results.json client/test-results/ +mcp.json diff --git a/.prettierignore b/.prettierignore index c8824c9a..cd910373 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ packages server/build CODE_OF_CONDUCT.md SECURITY.md +mcp.json diff --git a/README.md b/README.md index 69eada69..ed1e5093 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ You can launch the inspector without specifying a server name if your config has ```bash # Automatically uses "my-server" if it's the only one -npx @modelcontextprotocol/inspector --config config.json +npx @modelcontextprotocol/inspector --config mcp.json ``` 2. **A server named "default-server"** - automatically selected: From cf7c138958a32b3b613d87a743b8271b8d082386 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 7 Aug 2025 11:51:36 +0100 Subject: [PATCH 056/120] Fix --server-url flag not being parsed by CLI Add --server-url option to CLI parser so the flag is properly recognized and passed through to the server process via the /config endpoint. --- cli/src/cli.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 6c72e6a4..4bc7b6b1 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -24,6 +24,7 @@ type CliOptions = { server?: string; cli?: boolean; transport?: string; + serverUrl?: string; }; type ServerConfig = @@ -224,7 +225,8 @@ function parseArgs(): Args { .option("--config ", "config file path") .option("--server ", "server name from config file") .option("--cli", "enable CLI mode") - .option("--transport ", "transport type (stdio, sse, http)"); + .option("--transport ", "transport type (stdio, sse, http)") + .option("--server-url ", "server URL for SSE/HTTP transport"); // Parse only the arguments before -- program.parse(preArgs); @@ -318,6 +320,7 @@ function parseArgs(): Args { envArgs: options.e || {}, cli: options.cli || false, transport: transport as "stdio" | "sse" | "streamable-http" | undefined, + serverUrl: options.serverUrl, }; } From 14b01815adb5f3f7333a086d58affee6e6b3c0b0 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 7 Aug 2025 12:01:17 +0100 Subject: [PATCH 057/120] prettier --- cli/src/cli.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 4bc7b6b1..177ac058 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -130,7 +130,8 @@ async function runCli(args: Args): Promise { // Add transport flag if specified if (args.transport && args.transport !== "stdio") { // Convert streamable-http back to http for CLI mode - const cliTransport = args.transport === "streamable-http" ? "http" : args.transport; + const cliTransport = + args.transport === "streamable-http" ? "http" : args.transport; cliArgs.push("--transport", cliTransport); } @@ -307,7 +308,7 @@ function parseArgs(): Args { // Otherwise use command line arguments const command = finalArgs[0] || ""; const args = finalArgs.slice(1); - + // Map "http" shorthand to "streamable-http" let transport = options.transport; if (transport === "http") { From 1263ffdf58433d547423e18d861c8de764bf719d Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 7 Aug 2025 20:45:23 +0100 Subject: [PATCH 058/120] Simplify server auto-selection logic Only auto-select when there's exactly one server in the config file. Removes special handling for "default-server" naming convention. Addresses review feedback from @ochafik on PR #661 --- cli/src/cli.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 177ac058..13c7e492 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -254,16 +254,13 @@ function parseArgs(): Args { const parsedConfig = JSON.parse(configContent); const servers = Object.keys(parsedConfig.mcpServers || {}); - if (servers.includes("default-server")) { - // Use default-server if it exists - options.server = "default-server"; - } else if (servers.length === 1) { + if (servers.length === 1) { // Use the only server if there's just one options.server = servers[0]; } else if (servers.length === 0) { throw new Error("No servers found in config file"); } else { - // Multiple servers, no default-server + // Multiple servers, require explicit selection throw new Error( `Multiple servers found in config file. Please specify one with --server.\nAvailable servers: ${servers.join(", ")}`, ); From 68319704f07a39c1a4395ad131e4bd9487df4241 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 7 Aug 2025 21:02:29 +0100 Subject: [PATCH 059/120] Update test to match new server selection behavior Test 30 now expects an error when multiple servers exist, even with "default-server" present, aligning with the simplified logic that only auto-selects when there's exactly one server. --- cli/scripts/cli-tests.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/scripts/cli-tests.js b/cli/scripts/cli-tests.js index 675ea0bc..f714cb6a 100755 --- a/cli/scripts/cli-tests.js +++ b/cli/scripts/cli-tests.js @@ -862,9 +862,9 @@ async function runTests() { "tools/list", ); - // Test 30: Config with default-server auto-selection - await runBasicTest( - "default_server_auto_select", + // Test 30: Config with default-server should now require explicit selection (multiple servers) + await runErrorTest( + "default_server_requires_explicit_selection", "--config", defaultServerConfigPath, "--cli", From b89b10beac5645114669e8f89ccf54fdecf4ed19 Mon Sep 17 00:00:00 2001 From: olaservo Date: Fri, 8 Aug 2025 15:46:58 -0700 Subject: [PATCH 060/120] Bump version --- cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 16 ++++++++-------- package.json | 8 ++++---- server/package.json | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/package.json b/cli/package.json index bf344733..6bb55253 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.2", + "version": "0.16.3", "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 cbb7d8b6..d1c6d775 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.2", + "version": "0.16.3", "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 89c27a17..f088d270 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.2", + "version": "0.16.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.16.2", + "version": "0.16.3", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.16.2", - "@modelcontextprotocol/inspector-client": "^0.16.2", - "@modelcontextprotocol/inspector-server": "^0.16.2", + "@modelcontextprotocol/inspector-cli": "^0.16.3", + "@modelcontextprotocol/inspector-client": "^0.16.3", + "@modelcontextprotocol/inspector-server": "^0.16.3", "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.2.0", "open": "^10.2.0", @@ -44,7 +44,7 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.2", + "version": "0.16.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", @@ -67,7 +67,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.2", + "version": "0.16.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", @@ -11059,7 +11059,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.2", + "version": "0.16.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", diff --git a/package.json b/package.json index e614ba21..9de5f464 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.2", + "version": "0.16.3", "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.2", - "@modelcontextprotocol/inspector-client": "^0.16.2", - "@modelcontextprotocol/inspector-server": "^0.16.2", + "@modelcontextprotocol/inspector-cli": "^0.16.3", + "@modelcontextprotocol/inspector-client": "^0.16.3", + "@modelcontextprotocol/inspector-server": "^0.16.3", "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.2.0", "open": "^10.2.0", diff --git a/server/package.json b/server/package.json index 51fa6e42..04135574 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.2", + "version": "0.16.3", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 37f634d6512f2088583f48286bf71066e04b348c Mon Sep 17 00:00:00 2001 From: 2underscores Date: Mon, 11 Aug 2025 14:41:41 +1000 Subject: [PATCH 061/120] rm console.log --- client/src/lib/oauth-state-machine.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index b8ecab51..b110e148 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -100,7 +100,6 @@ export const oauthTransitions: Record = { // DCR failed, fallback to preregistered client fullInformation = await context.provider.clientInformation(); if (!fullInformation) { - console.error("Neither dynamic client registration or preregistered client information was found"); throw dcrError; } } From 929b93061865afed3bed38ad19ee08abf4e3003b Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Mon, 11 Aug 2025 21:24:26 +0300 Subject: [PATCH 062/120] reusable generateOAuthState helper, introduce in InspectorOAuthClientProvider and in oauth-state-machine, add test --- client/src/lib/auth.ts | 5 +++++ client/src/lib/oauth-state-machine.ts | 10 ++-------- .../{oauthUtils.ts => oauthUtils.test.ts} | 8 ++++++++ client/src/utils/oauthUtils.ts | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 8 deletions(-) rename client/src/utils/__tests__/{oauthUtils.ts => oauthUtils.test.ts} (92%) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 9989e944..73217a60 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -8,6 +8,7 @@ import { OAuthMetadata, } from "@modelcontextprotocol/sdk/shared/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./constants"; +import { generateOAuthState } from "@/utils/oauthUtils"; export const getClientInformationFromSessionStorage = async ({ serverUrl, @@ -75,6 +76,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { return window.location.origin + "/oauth/callback"; } + state(): string | Promise { + return generateOAuthState(); + } + get clientMetadata(): OAuthClientMetadata { return { redirect_uris: [this.redirectUrl], diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 5078cfe4..eec7ce8d 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -12,6 +12,7 @@ import { OAuthMetadataSchema, OAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { generateOAuthState } from "@/utils/oauthUtils"; export interface StateMachineContext { state: AuthDebuggerState; @@ -113,13 +114,6 @@ export const oauthTransitions: Record = { scope = metadata.scopes_supported.join(" "); } - // Generate a random state - const array = new Uint8Array(32); - crypto.getRandomValues(array); - const state = Array.from(array, (byte) => - byte.toString(16).padStart(2, "0"), - ).join(""); - const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, { @@ -127,7 +121,7 @@ export const oauthTransitions: Record = { clientInformation, redirectUrl: context.provider.redirectUrl, scope, - state: state, + state: generateOAuthState(), resource: context.state.resource ?? undefined, }, ); diff --git a/client/src/utils/__tests__/oauthUtils.ts b/client/src/utils/__tests__/oauthUtils.test.ts similarity index 92% rename from client/src/utils/__tests__/oauthUtils.ts rename to client/src/utils/__tests__/oauthUtils.test.ts index cc9674cb..c42f014e 100644 --- a/client/src/utils/__tests__/oauthUtils.ts +++ b/client/src/utils/__tests__/oauthUtils.test.ts @@ -1,6 +1,7 @@ import { generateOAuthErrorDescription, parseOAuthCallbackParams, + generateOAuthState, } from "@/utils/oauthUtils.ts"; describe("parseOAuthCallbackParams", () => { @@ -75,4 +76,11 @@ describe("generateOAuthErrorDescription", () => { "Error: invalid_request.\nDetails: The request could not be completed as dialed.\nMore info: https://example.com/error-docs.", ); }); + + describe("generateOAuthState", () => { + it("Returns a string", () => { + expect(generateOAuthState()).toBeDefined(); + expect(generateOAuthState()).toHaveLength(64); + }); + }); }); diff --git a/client/src/utils/oauthUtils.ts b/client/src/utils/oauthUtils.ts index c971271e..83de080b 100644 --- a/client/src/utils/oauthUtils.ts +++ b/client/src/utils/oauthUtils.ts @@ -48,6 +48,20 @@ export const parseOAuthCallbackParams = (location: string): CallbackParams => { }; }; +/** + * Generate a random state for the OAuth 2.0 flow. + * + * @returns A random state for the OAuth 2.0 flow. + */ +export const generateOAuthState = () => { + // Generate a random state + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join( + "", + ); +}; + export const generateOAuthErrorDescription = ( params: Extract, ): string => { From 60049a5b6ce1c8ad9ebc82960fd7e5c8287f1fe5 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Tue, 12 Aug 2025 00:29:53 +0300 Subject: [PATCH 063/120] reorder methods --- client/src/lib/auth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 73217a60..0e044037 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -76,10 +76,6 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { return window.location.origin + "/oauth/callback"; } - state(): string | Promise { - return generateOAuthState(); - } - get clientMetadata(): OAuthClientMetadata { return { redirect_uris: [this.redirectUrl], @@ -91,6 +87,10 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { }; } + state(): string | Promise { + return generateOAuthState(); + } + async clientInformation() { // Try to get the preregistered client information from session storage first const preregisteredClientInformation = From 0b123acdac3e749fd972e6b5aa9047fdf0d23e87 Mon Sep 17 00:00:00 2001 From: 2underscores Date: Tue, 12 Aug 2025 09:20:28 +1000 Subject: [PATCH 064/120] Static client attempt before DCR --- client/src/lib/oauth-state-machine.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index b110e148..49a77530 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -88,20 +88,14 @@ export const oauthTransitions: Record = { clientMetadata.scope = scopesSupported.join(" "); } - // Try DCR first, with static client as fallback - let fullInformation; - try { + // Try Static client first, with DCR as fallback + let fullInformation = await context.provider.clientInformation(); + if (!fullInformation) { fullInformation = await registerClient(context.serverUrl, { metadata, clientMetadata, }); context.provider.saveClientInformation(fullInformation); - } catch (dcrError) { - // DCR failed, fallback to preregistered client - fullInformation = await context.provider.clientInformation(); - if (!fullInformation) { - throw dcrError; - } } context.updateState({ From e9f1da383e76f7ffaa8d51dd980d89b39bb4e96c Mon Sep 17 00:00:00 2001 From: 2underscores Date: Tue, 12 Aug 2025 13:22:09 +1000 Subject: [PATCH 065/120] Tests for client discovery --- .../__tests__/AuthDebugger.test.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index fa9f2f7d..28c687fd 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -439,6 +439,103 @@ describe("AuthDebugger", () => { }); }); + describe("Client Registration behavior", () => { + it("uses preregistered (static) client information without calling DCR", async () => { + const preregClientInfo = { + client_id: "static_client_id", + client_secret: "static_client_secret", + redirect_uris: ["http://localhost:3000/oauth/callback/debug"], + }; + + // Return preregistered client info for the server-specific key + sessionStorageMock.getItem.mockImplementation((key) => { + if ( + key === + `[${defaultProps.serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}` + ) { + return JSON.stringify(preregClientInfo); + } + return null; + }); + + const updateAuthState = jest.fn(); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { + ...defaultAuthState, + isInitiatingAuth: false, + oauthStep: "client_registration", + oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata, + }, + }); + }); + + // Proceed from client_registration β†’ authorization_redirect + await act(async () => { + fireEvent.click(screen.getByText("Continue")); + }); + + // Should NOT attempt dynamic client registration + expect(mockRegisterClient).not.toHaveBeenCalled(); + + // Should advance with the preregistered client info + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + oauthClientInfo: expect.objectContaining({ + client_id: "static_client_id", + }), + oauthStep: "authorization_redirect", + }), + ); + }); + + it("falls back to DCR when no static client information is available", async () => { + // No preregistered or dynamic client info present in session storage + sessionStorageMock.getItem.mockImplementation(() => null); + + // DCR returns a new client + mockRegisterClient.mockResolvedValueOnce(mockOAuthClientInfo); + + const updateAuthState = jest.fn(); + + await act(async () => { + renderAuthDebugger({ + updateAuthState, + authState: { + ...defaultAuthState, + isInitiatingAuth: false, + oauthStep: "client_registration", + oauthMetadata: mockOAuthMetadata as unknown as OAuthMetadata, + }, + }); + }); + + await act(async () => { + fireEvent.click(screen.getByText("Continue")); + }); + + expect(mockRegisterClient).toHaveBeenCalledTimes(1); + + // Should save and advance with the DCR client info + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + oauthClientInfo: expect.objectContaining({ + client_id: "test_client_id", + }), + oauthStep: "authorization_redirect", + }), + ); + + // Verify the dynamically registered client info was persisted + expect(sessionStorage.setItem).toHaveBeenCalledWith( + `[${defaultProps.serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`, + expect.any(String), + ); + }); + }); + describe("OAuth State Persistence", () => { it("should store auth state to sessionStorage before redirect in Quick OAuth Flow", async () => { const updateAuthState = jest.fn(); From 9ef2a6c22d98c3f7a633d40bd627377bcb8aafcc Mon Sep 17 00:00:00 2001 From: 2underscores Date: Tue, 12 Aug 2025 14:13:31 +1000 Subject: [PATCH 066/120] Use new auth svr discovery method from latest auth SDK --- .../__tests__/AuthDebugger.test.tsx | 31 ++++++++++++------- client/src/lib/oauth-state-machine.ts | 4 +-- package-lock.json | 8 ++--- package.json | 2 +- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index fa9f2f7d..3d8e2290 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -36,7 +36,7 @@ const mockOAuthClientInfo = { // Mock MCP SDK functions - must be before imports jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ auth: jest.fn(), - discoverOAuthMetadata: jest.fn(), + discoverAuthorizationServerMetadata: jest.fn(), registerClient: jest.fn(), startAuthorization: jest.fn(), exchangeAuthorization: jest.fn(), @@ -46,7 +46,7 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ // Import the functions to get their types import { - discoverOAuthMetadata, + discoverAuthorizationServerMetadata, registerClient, startAuthorization, exchangeAuthorization, @@ -57,9 +57,10 @@ import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types"; // Type the mocked functions properly -const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction< - typeof discoverOAuthMetadata ->; +const mockDiscoverAuthorizationServerMetadata = + discoverAuthorizationServerMetadata as jest.MockedFunction< + typeof discoverAuthorizationServerMetadata + >; const mockRegisterClient = registerClient as jest.MockedFunction< typeof registerClient >; @@ -102,7 +103,9 @@ describe("AuthDebugger", () => { // Suppress console errors in tests to avoid JSDOM navigation noise jest.spyOn(console, "error").mockImplementation(() => {}); - mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + mockDiscoverAuthorizationServerMetadata.mockResolvedValue( + mockOAuthMetadata, + ); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue( new Error("No protected resource metadata found"), @@ -203,7 +206,7 @@ describe("AuthDebugger", () => { }); // Should first discover and save OAuth metadata - expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith( new URL("https://example.com/"), ); @@ -216,7 +219,7 @@ describe("AuthDebugger", () => { }); it("should show error when quick OAuth flow fails to discover metadata", async () => { - mockDiscoverOAuthMetadata.mockRejectedValue( + mockDiscoverAuthorizationServerMetadata.mockRejectedValue( new Error("Metadata discovery failed"), ); @@ -362,7 +365,7 @@ describe("AuthDebugger", () => { fireEvent.click(screen.getByText("Continue")); }); - expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith( new URL("https://example.com/"), ); }); @@ -509,7 +512,9 @@ describe("AuthDebugger", () => { mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue( mockResourceMetadata, ); - mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + mockDiscoverAuthorizationServerMetadata.mockResolvedValue( + mockOAuthMetadata, + ); await act(async () => { renderAuthDebugger({ @@ -563,7 +568,9 @@ describe("AuthDebugger", () => { // Mock failed metadata discovery mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError); // But OAuth metadata should still work with the original URL - mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata); + mockDiscoverAuthorizationServerMetadata.mockResolvedValue( + mockOAuthMetadata, + ); await act(async () => { renderAuthDebugger({ @@ -603,7 +610,7 @@ describe("AuthDebugger", () => { }); // Verify that regular OAuth metadata discovery was still called - expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith( + expect(mockDiscoverAuthorizationServerMetadata).toHaveBeenCalledWith( new URL("https://example.com/"), ); }); diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 5078cfe4..851f5746 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -1,7 +1,7 @@ import { OAuthStep, AuthDebuggerState } from "./auth-types"; import { DebugInspectorOAuthClientProvider } from "./auth"; import { - discoverOAuthMetadata, + discoverAuthorizationServerMetadata, registerClient, startAuthorization, exchangeAuthorization, @@ -56,7 +56,7 @@ export const oauthTransitions: Record = { resourceMetadata ?? undefined, ); - const metadata = await discoverOAuthMetadata(authServerUrl); + const metadata = await discoverAuthorizationServerMetadata(authServerUrl); if (!metadata) { throw new Error("Failed to discover OAuth metadata"); } diff --git a/package-lock.json b/package-lock.json index f088d270..9fb2ed00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@modelcontextprotocol/inspector-cli": "^0.16.3", "@modelcontextprotocol/inspector-client": "^0.16.3", "@modelcontextprotocol/inspector-server": "^0.16.3", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "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.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz", - "integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.2.tgz", + "integrity": "sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index 9de5f464..bd494090 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@modelcontextprotocol/inspector-cli": "^0.16.3", "@modelcontextprotocol/inspector-client": "^0.16.3", "@modelcontextprotocol/inspector-server": "^0.16.3", - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.3", From d21e51e6ab132bbe0e725da36507d2ed37adabfe Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Wed, 6 Aug 2025 13:00:08 -0700 Subject: [PATCH 067/120] fix(auth): sanitize authorization URL --- client/src/lib/auth.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 9989e944..326164e6 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -129,6 +129,12 @@ 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"); + } window.location.href = authorizationUrl.href; } From 15b58c8142a54302152077553fe515230198bad6 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 12 Aug 2025 15:25:09 -0400 Subject: [PATCH 068/120] This fixes #610 * In useConnection.ts - During initialization, if there is an error immediately following the client sending 'logging/setLevel', pops up a toast with the message `Server declares logging capability but doesn't implement method "logging/setLevel"` --- client/src/lib/hooks/useConnection.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 78dfa6df..9b03f748 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -351,6 +351,7 @@ export function useConnection({ return; } + let lastRequest = ""; try { // Inject auth manually instead of using SSEClientTransport, because we're // proxying through the inspector server first. @@ -564,7 +565,9 @@ export function useConnection({ } if (capabilities?.logging && defaultLoggingLevel) { + lastRequest = "logging/setLevel"; await client.setLoggingLevel(defaultLoggingLevel); + lastRequest = ""; } if (onElicitationRequest) { @@ -578,6 +581,13 @@ export function useConnection({ setMcpClient(client); setConnectionStatus("connected"); } catch (e) { + if (lastRequest === "logging/setLevel" && e instanceof McpError && e.code === ErrorCode.MethodNotFound) { + toast({ + title: "Error", + description: `Server declares logging capability but doesn't implement method: "${lastRequest}"`, + variant: "destructive", + }); + } console.error(e); setConnectionStatus("error"); } From 0d8e8fce4bbcb4aa815a0dc6929f7e58dfd9839c Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 12 Aug 2025 15:26:15 -0400 Subject: [PATCH 069/120] This fixes #610 * In useConnection.ts - During initialization, if there is an error immediately following the client sending 'logging/setLevel', pops up a toast with the message `Server declares logging capability but doesn't implement method "logging/setLevel"` --- client/src/lib/hooks/useConnection.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 9b03f748..12d8ff3e 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -581,7 +581,11 @@ export function useConnection({ setMcpClient(client); setConnectionStatus("connected"); } catch (e) { - if (lastRequest === "logging/setLevel" && e instanceof McpError && e.code === ErrorCode.MethodNotFound) { + if ( + lastRequest === "logging/setLevel" && + e instanceof McpError && + e.code === ErrorCode.MethodNotFound + ) { toast({ title: "Error", description: `Server declares logging capability but doesn't implement method: "${lastRequest}"`, From ca913eb06f678189173e7d131f204b19fba4caae Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 12 Aug 2025 20:16:06 -0700 Subject: [PATCH 070/120] Bump to 0.16.4 --- .claude/settings.local.json | 17 +++++++++++++++++ cli/package.json | 2 +- client/package.json | 2 +- package-lock.json | 16 ++++++++-------- package.json | 8 ++++---- server/package.json | 2 +- 6 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..987ca097 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(gh pr view:*)", + "Bash(gh pr diff:*)", + "Bash(gh pr checks:*)", + "Bash(gh api:*)", + "Bash(npm run build-client:*)", + "Bash(gh issue view:*)", + "Bash(npm run build-server:*)", + "Bash(npm run lint)", + "Bash(npm run prettier-fix:*)", + "Bash(npm run build:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/cli/package.json b/cli/package.json index 6bb55253..33c6fd4d 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.3", + "version": "0.16.4", "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 d1c6d775..9382462e 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.3", + "version": "0.16.4", "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 f088d270..e5757eb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.3", + "version": "0.16.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/inspector", - "version": "0.16.3", + "version": "0.16.4", "license": "MIT", "workspaces": [ "client", @@ -14,9 +14,9 @@ "cli" ], "dependencies": { - "@modelcontextprotocol/inspector-cli": "^0.16.3", - "@modelcontextprotocol/inspector-client": "^0.16.3", - "@modelcontextprotocol/inspector-server": "^0.16.3", + "@modelcontextprotocol/inspector-cli": "^0.16.4", + "@modelcontextprotocol/inspector-client": "^0.16.4", + "@modelcontextprotocol/inspector-server": "^0.16.4", "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.2.0", "open": "^10.2.0", @@ -44,7 +44,7 @@ }, "cli": { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.16.3", + "version": "0.16.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", @@ -67,7 +67,7 @@ }, "client": { "name": "@modelcontextprotocol/inspector-client", - "version": "0.16.3", + "version": "0.16.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", @@ -11059,7 +11059,7 @@ }, "server": { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.3", + "version": "0.16.4", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", diff --git a/package.json b/package.json index 9de5f464..b1618750 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector", - "version": "0.16.3", + "version": "0.16.4", "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.3", - "@modelcontextprotocol/inspector-client": "^0.16.3", - "@modelcontextprotocol/inspector-server": "^0.16.3", + "@modelcontextprotocol/inspector-cli": "^0.16.4", + "@modelcontextprotocol/inspector-client": "^0.16.4", + "@modelcontextprotocol/inspector-server": "^0.16.4", "@modelcontextprotocol/sdk": "^1.17.0", "concurrently": "^9.2.0", "open": "^10.2.0", diff --git a/server/package.json b/server/package.json index 04135574..f932eb8e 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-server", - "version": "0.16.3", + "version": "0.16.4", "description": "Server-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From fb5ce6d4143fc8fa52756efd8408aaf6c1398510 Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 12 Aug 2025 20:17:43 -0700 Subject: [PATCH 071/120] Remove local settings configuration file --- .claude/settings.local.json | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 987ca097..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh pr view:*)", - "Bash(gh pr diff:*)", - "Bash(gh pr checks:*)", - "Bash(gh api:*)", - "Bash(npm run build-client:*)", - "Bash(gh issue view:*)", - "Bash(npm run build-server:*)", - "Bash(npm run lint)", - "Bash(npm run prettier-fix:*)", - "Bash(npm run build:*)" - ], - "deny": [] - } -} \ No newline at end of file From b6af0c7f67b0cdd8b420570993db5993ccb3c8cc Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 13 Aug 2025 11:06:13 -0700 Subject: [PATCH 072/120] bump @modelcontextprotocol/sdk version to ^1.17.2 in all package.json files --- 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 33c6fd4d..9458ef65 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ }, "devDependencies": {}, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "commander": "^13.1.0", "spawn-rx": "^5.1.2" } diff --git a/client/package.json b/client/package.json index 9382462e..441d2168 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.0", + "@modelcontextprotocol/sdk": "^1.17.2", "@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 6103a16e..3a99a46e 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.0", + "@modelcontextprotocol/sdk": "^1.17.2", "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.3", @@ -47,7 +47,7 @@ "version": "0.16.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "commander": "^13.1.0", "spawn-rx": "^5.1.2" }, @@ -70,7 +70,7 @@ "version": "0.16.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -11062,7 +11062,7 @@ "version": "0.16.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", diff --git a/package.json b/package.json index b1618750..c2a78331 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.0", + "@modelcontextprotocol/sdk": "^1.17.2", "concurrently": "^9.2.0", "open": "^10.2.0", "shell-quote": "^1.8.3", diff --git a/server/package.json b/server/package.json index f932eb8e..4047e2d8 100644 --- a/server/package.json +++ b/server/package.json @@ -27,7 +27,7 @@ "typescript": "^5.6.2" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.0", + "@modelcontextprotocol/sdk": "^1.17.2", "cors": "^2.8.5", "express": "^5.1.0", "ws": "^8.18.0", From 85c86de61e46757e96cb290a53c7571f46df18f5 Mon Sep 17 00:00:00 2001 From: Raghunath Prabhakar <82239949+Raghuboi@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:25:23 -0500 Subject: [PATCH 073/120] chore: add jsdoc comments to utils --- client/src/utils/jsonUtils.ts | 10 ++++++++++ client/src/utils/oauthUtils.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/client/src/utils/jsonUtils.ts b/client/src/utils/jsonUtils.ts index 60f5b1d6..08de50ba 100644 --- a/client/src/utils/jsonUtils.ts +++ b/client/src/utils/jsonUtils.ts @@ -63,12 +63,22 @@ export type DataType = | "array" | "null"; +/** + * Determines the specific data type of a JSON value + * @param value The JSON value to analyze + * @returns The specific data type including "array" and "null" as distinct types + */ export function getDataType(value: JsonValue): DataType { if (Array.isArray(value)) return "array"; if (value === null) return "null"; return typeof value; } +/** + * Attempts to parse a string as JSON, only for objects and arrays + * @param str The string to parse + * @returns Object with success boolean and either parsed data or original string + */ export function tryParseJson(str: string): { success: boolean; data: JsonValue; diff --git a/client/src/utils/oauthUtils.ts b/client/src/utils/oauthUtils.ts index 83de080b..9b51179c 100644 --- a/client/src/utils/oauthUtils.ts +++ b/client/src/utils/oauthUtils.ts @@ -24,6 +24,11 @@ type CallbackParams = error_uri: string | null; }; +/** + * Parses OAuth 2.1 callback parameters from a URL search string + * @param location The URL search string (e.g., "?code=abc123" or "?error=access_denied") + * @returns Parsed callback parameters with success/error information + */ export const parseOAuthCallbackParams = (location: string): CallbackParams => { const params = new URLSearchParams(location); @@ -62,6 +67,11 @@ export const generateOAuthState = () => { ); }; +/** + * Generates a human-readable error description from OAuth callback error parameters + * @param params OAuth error callback parameters containing error details + * @returns Formatted multiline error message with error code, description, and optional URI + */ export const generateOAuthErrorDescription = ( params: Extract, ): string => { From fc4aeb392fa942048870ee7fb36f7ea1b435be86 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 13 Aug 2025 14:00:08 -0700 Subject: [PATCH 074/120] 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 36b9acc97e0f142dab6430c91a71131790523cad Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 14 Aug 2025 17:03:12 +0100 Subject: [PATCH 075/120] fix: filter out client_secret when storing OAuth client information --- client/src/lib/auth.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 7012c782..1a3db326 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -110,10 +110,17 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider { } saveClientInformation(clientInformation: OAuthClientInformation) { + // Remove client_secret before storing (not needed after initial OAuth flow) + const safeInfo = Object.fromEntries( + Object.entries(clientInformation).filter( + ([key]) => key !== "client_secret", + ), + ) as OAuthClientInformation; + // Save the dynamically registered client information to session storage saveClientInformationToSessionStorage({ serverUrl: this.serverUrl, - clientInformation, + clientInformation: safeInfo, isPreregistered: false, }); } From 77d4963b8a016ef13c7d8b526e352818e556c482 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 11:57:31 -0400 Subject: [PATCH 076/120] Add support for --header flags in MCP Inspector CLI - Add --header option to both wrapper CLI (cli.ts) and main CLI (index.ts) - Support multiple headers with "HeaderName: Value" format validation - Pass headers to HTTP and SSE transports via requestInit.headers - Add parseHeaderPair function for proper header parsing - Fix output truncation issue by removing forced process.exit(0) - Update types to include headers in Args and TransportOptions Usage: --header "Authorization: Bearer token" --header "X-Custom: value" --- cli/src/cli.ts | 49 +++++++++++++++++++++++++++++++++++++--- cli/src/index.ts | 53 ++++++++++++++++++++++++++++++++++++++++---- cli/src/transport.ts | 19 ++++++++++++++-- 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 13c7e492..7188f7c8 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -16,6 +16,7 @@ type Args = { cli: boolean; transport?: "stdio" | "sse" | "streamable-http"; serverUrl?: string; + headers?: Record; }; type CliOptions = { @@ -25,6 +26,7 @@ type CliOptions = { cli?: boolean; transport?: string; serverUrl?: string; + header?: Record; }; type ServerConfig = @@ -127,6 +129,9 @@ async function runCli(args: Args): Promise { // Build CLI arguments const cliArgs = [cliPath]; + // Add target URL/command first + cliArgs.push(args.command, ...args.args); + // Add transport flag if specified if (args.transport && args.transport !== "stdio") { // Convert streamable-http back to http for CLI mode @@ -135,8 +140,12 @@ async function runCli(args: Args): Promise { cliArgs.push("--transport", cliTransport); } - // Add command and remaining args - cliArgs.push(args.command, ...args.args); + // Add headers if specified + if (args.headers) { + for (const [key, value] of Object.entries(args.headers)) { + cliArgs.push("--header", `${key}: ${value}`); + } + } await spawnPromise("node", cliArgs, { env: { ...process.env, ...args.envArgs }, @@ -201,6 +210,30 @@ function parseKeyValuePair( return { ...previous, [key as string]: val }; } +function parseHeaderPair( + value: string, + previous: Record = {}, +): Record { + const colonIndex = value.indexOf(":"); + + if (colonIndex === -1) { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + const key = value.slice(0, colonIndex).trim(); + const val = value.slice(colonIndex + 1).trim(); + + if (key === "" || val === "") { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + return { ...previous, [key]: val }; +} + function parseArgs(): Args { const program = new Command(); @@ -227,7 +260,13 @@ function parseArgs(): Args { .option("--server ", "server name from config file") .option("--cli", "enable CLI mode") .option("--transport ", "transport type (stdio, sse, http)") - .option("--server-url ", "server URL for SSE/HTTP transport"); + .option("--server-url ", "server URL for SSE/HTTP transport") + .option( + "--header ", + 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', + parseHeaderPair, + {}, + ); // Parse only the arguments before -- program.parse(preArgs); @@ -280,6 +319,7 @@ function parseArgs(): Args { envArgs: { ...(config.env || {}), ...(options.e || {}) }, cli: options.cli || false, transport: "stdio", + headers: options.header, }; } else if (config.type === "sse" || config.type === "streamable-http") { return { @@ -289,6 +329,7 @@ function parseArgs(): Args { cli: options.cli || false, transport: config.type, serverUrl: config.url, + headers: options.header, }; } else { // Backwards compatibility: if no type field, assume stdio @@ -298,6 +339,7 @@ function parseArgs(): Args { envArgs: { ...((config as any).env || {}), ...(options.e || {}) }, cli: options.cli || false, transport: "stdio", + headers: options.header, }; } } @@ -319,6 +361,7 @@ function parseArgs(): Args { cli: options.cli || false, transport: transport as "stdio" | "sse" | "streamable-http" | undefined, serverUrl: options.serverUrl, + headers: options.header, }; } diff --git a/cli/src/index.ts b/cli/src/index.ts index 2b0c4f53..9bd258de 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -30,11 +30,13 @@ type Args = { toolName?: string; toolArg?: Record; transport?: "sse" | "stdio" | "http"; + headers?: Record; }; function createTransportOptions( target: string[], transport?: "sse" | "stdio" | "http", + headers?: Record, ): TransportOptions { if (target.length === 0) { throw new Error( @@ -81,11 +83,16 @@ function createTransportOptions( command: isUrl ? undefined : command, args: isUrl ? undefined : commandArgs, url: isUrl ? command : undefined, + headers, }; } async function callMethod(args: Args): Promise { - const transportOptions = createTransportOptions(args.target, args.transport); + const transportOptions = createTransportOptions( + args.target, + args.transport, + args.headers, + ); const transport = createTransport(transportOptions); const client = new Client({ name: "inspector-cli", @@ -177,6 +184,30 @@ function parseKeyValuePair( return { ...previous, [key as string]: val }; } +function parseHeaderPair( + value: string, + previous: Record = {}, +): Record { + const colonIndex = value.indexOf(":"); + + if (colonIndex === -1) { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + const key = value.slice(0, colonIndex).trim(); + const val = value.slice(colonIndex + 1).trim(); + + if (key === "" || val === "") { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + return { ...previous, [key]: val }; +} + function parseArgs(): Args { const program = new Command(); @@ -256,12 +287,24 @@ function parseArgs(): Args { } return value as "sse" | "http" | "stdio"; }, + ) + // + // HTTP headers + // + .option( + "--header ", + 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', + parseHeaderPair, + {}, ); // Parse only the arguments before -- program.parse(preArgs); - const options = program.opts() as Omit; + const options = program.opts() as Omit & { + header?: Record; + }; + let remainingArgs = program.args; // Add back any arguments that came after -- @@ -276,6 +319,7 @@ function parseArgs(): Args { return { target: finalArgs, ...options, + headers: options.header, // commander.js uses 'header' field, map to 'headers' }; } @@ -287,8 +331,9 @@ async function main(): Promise { try { const args = parseArgs(); await callMethod(args); - // Explicitly exit to ensure process terminates in CI - process.exit(0); + + // Let Node.js naturally exit instead of force-exiting + // process.exit(0) was causing stdout truncation } catch (error) { handleError(error); } diff --git a/cli/src/transport.ts b/cli/src/transport.ts index b6276356..3ed6e31a 100644 --- a/cli/src/transport.ts +++ b/cli/src/transport.ts @@ -12,6 +12,7 @@ export type TransportOptions = { command?: string; args?: string[]; url?: string; + headers?: Record; }; function createStdioTransport(options: TransportOptions): Transport { @@ -64,11 +65,25 @@ export function createTransport(options: TransportOptions): Transport { const url = new URL(options.url); if (transportType === "sse") { - return new SSEClientTransport(url); + const transportOptions = options.headers + ? { + requestInit: { + headers: options.headers, + }, + } + : undefined; + return new SSEClientTransport(url, transportOptions); } if (transportType === "http") { - return new StreamableHTTPClientTransport(url); + const transportOptions = options.headers + ? { + requestInit: { + headers: options.headers, + }, + } + : undefined; + return new StreamableHTTPClientTransport(url, transportOptions); } throw new Error(`Unsupported transport type: ${transportType}`); From 755f31f10972ec12c4de110246f3ad284a7374c5 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 12:06:31 -0400 Subject: [PATCH 077/120] Add Claude settings to prettierignore --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index cd910373..167a7cb4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ server/build CODE_OF_CONDUCT.md SECURITY.md mcp.json +.claude/settings.local.json \ No newline at end of file From da5595fb3ac5599b0f069393fcd7c8095de23983 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 12:08:04 -0400 Subject: [PATCH 078/120] Add client/e2e/test-results/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ce55639..c63eadaa 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ sdk client/playwright-report/ client/results.json client/test-results/ +client/e2e/test-results/ mcp.json From d7927ec8dc13c1d39039673b1e9302972875c719 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 12:12:43 -0400 Subject: [PATCH 079/120] Add documentation for CLI header support - Document --header flag usage with examples - Explain header format requirements - Clarify transport support (HTTP, SSE, not STDIO) - Add authentication examples with API keys and bearer tokens --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ed1e5093..02a096e2 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com # Connect to a remote MCP server (with Streamable HTTP transport) npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list +# Connect to a remote MCP server (with custom headers) +npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key" + # Call a tool on a remote server npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value From 10b4681e3c6de348388891f5c7f461457ae2f630 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 14 Aug 2025 10:47:15 -0700 Subject: [PATCH 080/120] 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 74900fca5df7a4c26b8d8dd7dffb335cab160b42 Mon Sep 17 00:00:00 2001 From: Bobby Tiernay Date: Mon, 11 Aug 2025 20:02:37 -0400 Subject: [PATCH 081/120] feat: unify OAuth scope discovery between automatic and manual flows - Add shared discoverScopes function in auth.ts with resource metadata preference - Update automatic flow (handleAuthError) to use dynamic scope discovery - Update manual flow (OAuth state machine) to use shared scope discovery logic - Add comprehensive test suite for discoverScopes function (11 test cases) - Fix empty array handling to return undefined instead of empty string - Maintain backward compatibility with existing oauthScope parameter - All quality gates pass: TypeScript, linting, formatting, and tests Resolves OAuth scope discovery inconsistency where automatic flow used static scopes while manual flow used metadata scopes. Both flows now use identical shared logic with proper resource metadata preference. --- .../__tests__/AuthDebugger.test.tsx | 69 ++++++- client/src/lib/__tests__/auth.test.ts | 154 +++++++++++++++ client/src/lib/auth.ts | 35 ++++ .../hooks/__tests__/useConnection.test.tsx | 181 +++++++++++++++++- client/src/lib/hooks/useConnection.ts | 22 ++- client/src/lib/oauth-state-machine.ts | 10 +- 6 files changed, 451 insertions(+), 20 deletions(-) create mode 100644 client/src/lib/__tests__/auth.test.ts diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 0f7fb413..3dfbaead 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -25,6 +25,7 @@ const mockOAuthMetadata = { token_endpoint: "https://oauth.example.com/token", response_types_supported: ["code"], grant_types_supported: ["authorization_code"], + scopes_supported: ["read", "write"], }; const mockOAuthClientInfo = { @@ -56,6 +57,40 @@ import { import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js"; import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types"; +// Mock local auth module +jest.mock("@/lib/auth", () => ({ + DebugInspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({ + tokens: jest.fn().mockImplementation(() => Promise.resolve(undefined)), + clear: jest.fn().mockImplementation(() => { + // Mock the real clear() behavior which removes items from sessionStorage + sessionStorage.removeItem("[https://example.com/mcp] mcp_tokens"); + sessionStorage.removeItem("[https://example.com/mcp] mcp_client_info"); + sessionStorage.removeItem( + "[https://example.com/mcp] mcp_server_metadata", + ); + }), + redirectUrl: "http://localhost:3000/oauth/callback/debug", + clientMetadata: { + redirect_uris: ["http://localhost:3000/oauth/callback/debug"], + token_endpoint_auth_method: "none", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + client_name: "MCP Inspector", + }, + clientInformation: jest.fn(), + saveClientInformation: jest.fn(), + saveTokens: jest.fn(), + redirectToAuthorization: jest.fn(), + saveCodeVerifier: jest.fn(), + codeVerifier: jest.fn(), + saveServerMetadata: jest.fn(), + getServerMetadata: jest.fn(), + })), + discoverScopes: jest.fn().mockResolvedValue("read write" as never), +})); + +import { discoverScopes } from "@/lib/auth"; + // Type the mocked functions properly const mockDiscoverAuthorizationServerMetadata = discoverAuthorizationServerMetadata as jest.MockedFunction< @@ -75,6 +110,9 @@ const mockDiscoverOAuthProtectedResourceMetadata = discoverOAuthProtectedResourceMetadata as jest.MockedFunction< typeof discoverOAuthProtectedResourceMetadata >; +const mockDiscoverScopes = discoverScopes as jest.MockedFunction< + typeof discoverScopes +>; const sessionStorageMock = { getItem: jest.fn(), @@ -103,9 +141,15 @@ describe("AuthDebugger", () => { // Suppress console errors in tests to avoid JSDOM navigation noise jest.spyOn(console, "error").mockImplementation(() => {}); - mockDiscoverAuthorizationServerMetadata.mockResolvedValue( - mockOAuthMetadata, - ); + // Set default mock behaviors with complete OAuth metadata + mockDiscoverAuthorizationServerMetadata.mockResolvedValue({ + issuer: "https://oauth.example.com", + authorization_endpoint: "https://oauth.example.com/authorize", + token_endpoint: "https://oauth.example.com/token", + response_types_supported: ["code"], + grant_types_supported: ["authorization_code"], + scopes_supported: ["read", "write"], + }); mockRegisterClient.mockResolvedValue(mockOAuthClientInfo); mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue( new Error("No protected resource metadata found"), @@ -427,7 +471,24 @@ describe("AuthDebugger", () => { }); }); - it("should not include scope in authorization URL when scopes_supported is not present", async () => { + it("should include scope in authorization URL when scopes_supported is not present", async () => { + const updateAuthState = + await setupAuthorizationUrlTest(mockOAuthMetadata); + + // Wait for the updateAuthState to be called + await waitFor(() => { + expect(updateAuthState).toHaveBeenCalledWith( + expect.objectContaining({ + authorizationUrl: expect.stringContaining("scope="), + }), + ); + }); + }); + + it("should omit scope from authorization URL when discoverScopes returns undefined", async () => { + // Mock discoverScopes to return undefined (no scopes available) + mockDiscoverScopes.mockResolvedValueOnce(undefined); + const updateAuthState = await setupAuthorizationUrlTest(mockOAuthMetadata); diff --git a/client/src/lib/__tests__/auth.test.ts b/client/src/lib/__tests__/auth.test.ts new file mode 100644 index 00000000..b34b3266 --- /dev/null +++ b/client/src/lib/__tests__/auth.test.ts @@ -0,0 +1,154 @@ +import { discoverScopes } from "../auth"; +import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; + +jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({ + discoverAuthorizationServerMetadata: jest.fn(), +})); + +const mockDiscoverAuth = + discoverAuthorizationServerMetadata as jest.MockedFunction< + typeof discoverAuthorizationServerMetadata + >; + +const baseMetadata = { + issuer: "https://test.com", + authorization_endpoint: "https://test.com/authorize", + token_endpoint: "https://test.com/token", + response_types_supported: ["code"], + grant_types_supported: ["authorization_code"], + scopes_supported: ["read", "write"], +}; + +describe("discoverScopes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const testCases = [ + { + name: "returns joined scopes from OAuth metadata", + mockResolves: baseMetadata, + serverUrl: "https://example.com", + expected: "read write", + expectedCallUrl: "https://example.com/", + }, + { + name: "prefers resource metadata over OAuth metadata", + mockResolves: baseMetadata, + serverUrl: "https://example.com", + resourceMetadata: { + resource: "https://example.com", + scopes_supported: ["admin", "full"], + }, + expected: "admin full", + }, + { + name: "falls back to OAuth when resource has empty scopes", + mockResolves: baseMetadata, + serverUrl: "https://example.com", + resourceMetadata: { + resource: "https://example.com", + scopes_supported: [], + }, + expected: "read write", + }, + { + name: "normalizes URL with port and path", + mockResolves: baseMetadata, + serverUrl: "https://example.com:8080/some/path", + expected: "read write", + expectedCallUrl: "https://example.com:8080/", + }, + { + name: "normalizes URL with trailing slash", + mockResolves: baseMetadata, + serverUrl: "https://example.com/", + expected: "read write", + expectedCallUrl: "https://example.com/", + }, + { + name: "handles single scope", + mockResolves: { ...baseMetadata, scopes_supported: ["admin"] }, + serverUrl: "https://example.com", + expected: "admin", + }, + { + name: "prefers resource metadata even with fewer scopes", + mockResolves: { + ...baseMetadata, + scopes_supported: ["read", "write", "admin", "full"], + }, + serverUrl: "https://example.com", + resourceMetadata: { + resource: "https://example.com", + scopes_supported: ["read"], + }, + expected: "read", + }, + ]; + + const undefinedCases = [ + { + name: "returns undefined when OAuth discovery fails", + mockRejects: new Error("Discovery failed"), + serverUrl: "https://example.com", + }, + { + name: "returns undefined when OAuth has no scopes", + mockResolves: { ...baseMetadata, scopes_supported: [] }, + serverUrl: "https://example.com", + }, + { + name: "returns undefined when scopes_supported missing", + mockResolves: (() => { + const { scopes_supported, ...rest } = baseMetadata; + return rest; + })(), + serverUrl: "https://example.com", + }, + { + name: "returns undefined with resource metadata but OAuth fails", + mockRejects: new Error("No OAuth metadata"), + serverUrl: "https://example.com", + resourceMetadata: { + resource: "https://example.com", + scopes_supported: ["read", "write"], + }, + }, + ]; + + test.each(testCases)( + "$name", + async ({ + mockResolves, + serverUrl, + resourceMetadata, + expected, + expectedCallUrl, + }) => { + mockDiscoverAuth.mockResolvedValue(mockResolves); + + const result = await discoverScopes(serverUrl, resourceMetadata); + + expect(result).toBe(expected); + if (expectedCallUrl) { + expect(mockDiscoverAuth).toHaveBeenCalledWith(new URL(expectedCallUrl)); + } + }, + ); + + test.each(undefinedCases)( + "$name", + async ({ mockResolves, mockRejects, serverUrl, resourceMetadata }) => { + if (mockRejects) { + mockDiscoverAuth.mockRejectedValue(mockRejects); + } else { + mockDiscoverAuth.mockResolvedValue(mockResolves); + } + + const result = await discoverScopes(serverUrl, resourceMetadata); + + expect(result).toBeUndefined(); + }, + ); +}); diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 1a3db326..99707387 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -6,10 +6,45 @@ import { OAuthTokensSchema, OAuthClientMetadata, OAuthMetadata, + OAuthProtectedResourceMetadata, } from "@modelcontextprotocol/sdk/shared/auth.js"; +import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./constants"; import { generateOAuthState } from "@/utils/oauthUtils"; +/** + * Discovers OAuth scopes from server metadata, with preference for resource metadata scopes + * @param serverUrl - The MCP server URL + * @param resourceMetadata - Optional resource metadata containing preferred scopes + * @returns Promise resolving to space-separated scope string or undefined + */ +export const discoverScopes = async ( + serverUrl: string, + resourceMetadata?: OAuthProtectedResourceMetadata, +): Promise => { + try { + const metadata = await discoverAuthorizationServerMetadata( + new URL("/", serverUrl), + ); + + // Prefer resource metadata scopes, but fall back to OAuth metadata if empty + const resourceScopes = resourceMetadata?.scopes_supported; + const oauthScopes = metadata?.scopes_supported; + + const scopesSupported = + resourceScopes && resourceScopes.length > 0 + ? resourceScopes + : oauthScopes; + + return scopesSupported && scopesSupported.length > 0 + ? scopesSupported.join(" ") + : undefined; + } catch (error) { + console.debug("OAuth scope discovery failed:", error); + return undefined; + } +}; + export const getClientInformationFromSessionStorage = async ({ serverUrl, isPreregistered, diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index c340d774..7518f814 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -3,11 +3,16 @@ import { useConnection } from "../useConnection"; import { z } from "zod"; import { ClientRequest } from "@modelcontextprotocol/sdk/types.js"; import { DEFAULT_INSPECTOR_CONFIG } from "../../constants"; -import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js"; +import { + SSEClientTransportOptions, + SseError, +} from "@modelcontextprotocol/sdk/client/sse.js"; import { ElicitResult, ElicitRequest, } from "@modelcontextprotocol/sdk/types.js"; +import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; +import { discoverScopes } from "../../auth"; // Mock fetch global.fetch = jest.fn().mockResolvedValue({ @@ -53,14 +58,27 @@ jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ Client: jest.fn().mockImplementation(() => mockClient), })); -jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ - SSEClientTransport: jest.fn((url, options) => { - mockSSETransport.url = url; - mockSSETransport.options = options; - return mockSSETransport; - }), - SseError: jest.fn(), -})); +jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => { + // Minimal mock class that supports instanceof checks + class SseError extends Error { + code: number; + event: ErrorEvent; + constructor(code: number, message: string, event: ErrorEvent) { + super(message); + this.code = code; + this.event = event; + } + } + + return { + SSEClientTransport: jest.fn((url, options) => { + mockSSETransport.url = url; + mockSSETransport.options = options; + return mockSSETransport; + }), + SseError, + }; +}); jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: jest.fn((url, options) => { @@ -85,11 +103,18 @@ jest.mock("@/lib/hooks/useToast", () => ({ jest.mock("../../auth", () => ({ InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({ tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }), + redirectUrl: "http://localhost:3000/oauth/callback", })), clearClientInformationFromSessionStorage: jest.fn(), saveClientInformationToSessionStorage: jest.fn(), + discoverScopes: jest.fn(), })); +const mockAuth = auth as jest.MockedFunction; +const mockDiscoverScopes = discoverScopes as jest.MockedFunction< + typeof discoverScopes +>; + describe("useConnection", () => { const defaultProps = { transportType: "sse" as const, @@ -714,4 +739,142 @@ describe("useConnection", () => { ).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token"); }); }); + + describe("OAuth Error Handling with Scope Discovery", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockAuth.mockResolvedValue("AUTHORIZED"); + mockDiscoverScopes.mockResolvedValue(undefined); + }); + + const setup401Error = () => { + const mockErrorEvent = new ErrorEvent("error", { + message: "Mock error event", + }); + mockClient.connect.mockRejectedValueOnce( + new SseError(401, "Unauthorized", mockErrorEvent), + ); + }; + + const attemptConnection = async (props = defaultProps) => { + const { result } = renderHook(() => useConnection(props)); + await act(async () => { + try { + await result.current.connect(); + } catch { + // Expected error from auth handling + } + }); + }; + + const testCases = [ + [ + "discovers and includes scopes in auth call", + { + discoveredScope: "read write admin", + oauthScope: undefined, + expectScopeCall: true, + expectedAuthScope: "read write admin", + authResult: "AUTHORIZED", + }, + ], + [ + "handles scope discovery failure gracefully", + { + discoveredScope: undefined, + oauthScope: undefined, + expectScopeCall: true, + expectedAuthScope: undefined, + authResult: "AUTHORIZED", + }, + ], + [ + "uses manual oauthScope override instead of discovered scopes", + { + discoveredScope: "discovered:scope", + oauthScope: "manual:scope", + expectScopeCall: false, + expectedAuthScope: "manual:scope", + authResult: "AUTHORIZED", + }, + ], + [ + "triggers scope discovery when oauthScope is whitespace", + { + discoveredScope: "discovered:scope", + oauthScope: " ", + expectScopeCall: true, + expectedAuthScope: "discovered:scope", + authResult: "AUTHORIZED", + }, + ], + [ + "handles auth failure after scope discovery", + { + discoveredScope: "read write", + oauthScope: undefined, + expectScopeCall: true, + expectedAuthScope: "read write", + authResult: "UNAUTHORIZED", + }, + ], + ] as const; + + test.each(testCases)( + "should %s", + async ( + _, + { + discoveredScope, + oauthScope, + expectScopeCall, + expectedAuthScope, + authResult = "AUTHORIZED", + }, + ) => { + mockDiscoverScopes.mockResolvedValue(discoveredScope); + mockAuth.mockResolvedValue(authResult as never); + setup401Error(); + + const props = + oauthScope !== undefined + ? { ...defaultProps, oauthScope } + : defaultProps; + await attemptConnection(props); + + if (expectScopeCall) { + expect(mockDiscoverScopes).toHaveBeenCalledWith( + defaultProps.sseUrl, + undefined, + ); + } else { + expect(mockDiscoverScopes).not.toHaveBeenCalled(); + } + + expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), { + serverUrl: defaultProps.sseUrl, + scope: expectedAuthScope, + }); + }, + ); + + it("should handle slow scope discovery gracefully", async () => { + mockDiscoverScopes.mockImplementation( + () => + new Promise((resolve) => setTimeout(() => resolve(undefined), 100)), + ); + + setup401Error(); + await attemptConnection(); + + expect(mockDiscoverScopes).toHaveBeenCalledWith( + defaultProps.sseUrl, + undefined, + ); + expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), { + serverUrl: defaultProps.sseUrl, + scope: undefined, + }); + }); + }); }); diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 12d8ff3e..e6008197 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -37,11 +37,15 @@ import { useToast } from "@/lib/hooks/useToast"; import { z } from "zod"; import { ConnectionStatus } from "../constants"; import { Notification, StdErrNotificationSchema } from "../notificationTypes"; -import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; +import { + auth, + discoverOAuthProtectedResourceMetadata, +} from "@modelcontextprotocol/sdk/client/auth.js"; import { clearClientInformationFromSessionStorage, InspectorOAuthClientProvider, saveClientInformationToSessionStorage, + discoverScopes, } from "../auth"; import packageJson from "../../../package.json"; import { @@ -317,9 +321,23 @@ export function useConnection({ if (is401Error(error)) { const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl); + let scope = oauthScope?.trim(); + if (!scope) { + // Only discover resource metadata when we need to discover scopes + let resourceMetadata; + try { + resourceMetadata = await discoverOAuthProtectedResourceMetadata( + new URL("/", sseUrl), + ); + } catch { + // Resource metadata is optional, continue without it + } + scope = await discoverScopes(sseUrl, resourceMetadata); + } + const result = await auth(serverAuthProvider, { serverUrl: sseUrl, - scope: oauthScope, + scope, }); return result === "AUTHORIZED"; } diff --git a/client/src/lib/oauth-state-machine.ts b/client/src/lib/oauth-state-machine.ts index 24b62084..4a2c0f47 100644 --- a/client/src/lib/oauth-state-machine.ts +++ b/client/src/lib/oauth-state-machine.ts @@ -1,5 +1,5 @@ import { OAuthStep, AuthDebuggerState } from "./auth-types"; -import { DebugInspectorOAuthClientProvider } from "./auth"; +import { DebugInspectorOAuthClientProvider, discoverScopes } from "./auth"; import { discoverAuthorizationServerMetadata, registerClient, @@ -113,10 +113,10 @@ export const oauthTransitions: Record = { const metadata = context.state.oauthMetadata!; const clientInformation = context.state.oauthClientInfo!; - let scope: string | undefined = undefined; - if (metadata.scopes_supported) { - scope = metadata.scopes_supported.join(" "); - } + const scope = await discoverScopes( + context.serverUrl, + context.state.resourceMetadata ?? undefined, + ); const { authorizationUrl, codeVerifier } = await startAuthorization( context.serverUrl, From 34b39530d1f417e7d35dfa87cbf0d633d3b61804 Mon Sep 17 00:00:00 2001 From: Bobby Tiernay Date: Thu, 14 Aug 2025 14:02:00 -0400 Subject: [PATCH 082/120] fix: failing test from SDK update --- .../__tests__/AuthDebugger.test.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/client/src/components/__tests__/AuthDebugger.test.tsx b/client/src/components/__tests__/AuthDebugger.test.tsx index 3dfbaead..b3da23fd 100644 --- a/client/src/components/__tests__/AuthDebugger.test.tsx +++ b/client/src/components/__tests__/AuthDebugger.test.tsx @@ -77,8 +77,25 @@ jest.mock("@/lib/auth", () => ({ response_types: ["code"], client_name: "MCP Inspector", }, - clientInformation: jest.fn(), - saveClientInformation: jest.fn(), + clientInformation: jest.fn().mockImplementation(async () => { + const serverUrl = "https://example.com/mcp"; + const preregisteredKey = `[${serverUrl}] ${SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION}`; + const preregisteredData = sessionStorage.getItem(preregisteredKey); + if (preregisteredData) { + return JSON.parse(preregisteredData); + } + const dynamicKey = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`; + const dynamicData = sessionStorage.getItem(dynamicKey); + if (dynamicData) { + return JSON.parse(dynamicData); + } + return undefined; + }), + saveClientInformation: jest.fn().mockImplementation((clientInfo) => { + const serverUrl = "https://example.com/mcp"; + const key = `[${serverUrl}] ${SESSION_KEYS.CLIENT_INFORMATION}`; + sessionStorage.setItem(key, JSON.stringify(clientInfo)); + }), saveTokens: jest.fn(), redirectToAuthorization: jest.fn(), saveCodeVerifier: jest.fn(), From 739e0aaa684e2a6acf37f514cf658d05c0241be8 Mon Sep 17 00:00:00 2001 From: Bobby Tiernay Date: Thu, 14 Aug 2025 15:00:54 -0400 Subject: [PATCH 083/120] fix: lint --- client/src/lib/__tests__/auth.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/lib/__tests__/auth.test.ts b/client/src/lib/__tests__/auth.test.ts index b34b3266..329b7f02 100644 --- a/client/src/lib/__tests__/auth.test.ts +++ b/client/src/lib/__tests__/auth.test.ts @@ -101,6 +101,7 @@ describe("discoverScopes", () => { { name: "returns undefined when scopes_supported missing", mockResolves: (() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { scopes_supported, ...rest } = baseMetadata; return rest; })(), From 8d8af870ba0a9c46342a0c74f2510a5df59361c5 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 14 Aug 2025 00:14:30 -0700 Subject: [PATCH 084/120] 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 085/120] 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 086/120] 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 087/120] 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 088/120] 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 089/120] 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 090/120] 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 091/120] 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 092/120] 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 5bbbd645556283225cc2e0e20fdb26d7648ecd3e Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 18 Aug 2025 06:15:15 -0700 Subject: [PATCH 093/120] Update CC GitHub Action to have same config as reference servers repo action --- .github/workflows/claude.yml | 37 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index a4fd6c52..da8e7c7a 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -18,10 +18,6 @@ jobs: (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - ) && - ( - github.actor == 'ihrpr' || - github.actor == 'olaservo' ) runs-on: ubuntu-latest permissions: @@ -29,6 +25,7 @@ jobs: pull-requests: read issues: read id-token: write + actions: read steps: - name: Checkout repository uses: actions/checkout@v4 @@ -41,24 +38,16 @@ jobs: with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" + # Allow Claude to read CI results on PRs + additional_permissions: | + actions: read - # Optional: Customize the trigger phrase (default: @claude) - # trigger_phrase: "/claude" - - # Optional: Trigger when specific user is assigned to an issue - # assignee_trigger: "claude-bot" - - # Optional: Allow Claude to run specific commands - # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - - # Optional: Add custom instructions for Claude to customize its behavior for your project - # custom_instructions: | - # Follow our coding standards - # Ensure all new code has tests - # Use TypeScript for new files - - # Optional: Custom environment variables for Claude - # claude_env: | - # NODE_ENV: test + # Trigger when assigned to an issue + assignee_trigger: "claude" + + # Allow Claude to run bash + # This should be safe given the repo is already public + allowed_tools: "Bash" + + custom_instructions: | + If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a
block. From 28c0bf176ab65be71aa4dc89c4062b3e1a1e7d2d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 18 Aug 2025 11:13:42 -0400 Subject: [PATCH 094/120] 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 095/120] 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 096/120] 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 097/120] 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 098/120] 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 099/120] 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 100/120] 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 101/120] 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 102/120] 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 103/120] 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 104/120] 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 105/120] 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 106/120] * 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 107/120] * 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 108/120] 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 109/120] 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 110/120] 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 111/120] 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 3628abca9ee626f0469ebc609e80dd30bfeede21 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sun, 10 Aug 2025 13:06:19 -0700 Subject: [PATCH 112/120] Use MCP_PROXY_FULL_ADDRESS for SSE transport backed endpoints The STDIO and SSE transports use SSE between the client and the proxy server. SSE requires the client to initiate the MCP conversation (event-stream) at the endpoint provided by the server. The endpoint provided by the server must obey where the client sees the server, rather than being assumed to be server-root `/message`. The Streamable HTTP endpoint does not need the full proxy address because the response is an event-stream directly, and does not use SSE. --- client/src/lib/hooks/useConnection.ts | 24 ++++++++++++++++++++++-- server/src/index.ts | 12 ++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 8c44d51b..a07b67dc 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -409,11 +409,20 @@ export function useConnection({ let mcpProxyServerUrl; switch (transportType) { - case "stdio": + case "stdio": { mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/stdio`); mcpProxyServerUrl.searchParams.append("command", command); mcpProxyServerUrl.searchParams.append("args", args); mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env)); + + const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS + .value as string; + if (proxyFullAddress) { + mcpProxyServerUrl.searchParams.append( + "proxyFullAddress", + proxyFullAddress, + ); + } transportOptions = { authProvider: serverAuthProvider, eventSourceInit: { @@ -431,10 +440,20 @@ export function useConnection({ }, }; break; + } - case "sse": + case "sse": { mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`); mcpProxyServerUrl.searchParams.append("url", sseUrl); + + const proxyFullAddressSSE = config.MCP_PROXY_FULL_ADDRESS + .value as string; + if (proxyFullAddressSSE) { + mcpProxyServerUrl.searchParams.append( + "proxyFullAddress", + proxyFullAddressSSE, + ); + } transportOptions = { eventSourceInit: { fetch: ( @@ -451,6 +470,7 @@ export function useConnection({ }, }; break; + } case "streamable-http": mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`); diff --git a/server/src/index.ts b/server/src/index.ts index a30c1845..c55a79d3 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -391,7 +391,11 @@ app.get( throw error; } - const webAppTransport = new SSEServerTransport("/message", res); + const proxyFullAddress = (req.query.proxyFullAddress as string) || ""; + const prefix = proxyFullAddress || ""; + const endpoint = `${prefix}/message`; + + const webAppTransport = new SSEServerTransport(endpoint, res); console.log("Created client transport"); webAppTransports.set(webAppTransport.sessionId, webAppTransport); @@ -511,7 +515,11 @@ app.get( } if (serverTransport) { - const webAppTransport = new SSEServerTransport("/message", res); + const proxyFullAddress = (req.query.proxyFullAddress as string) || ""; + const prefix = proxyFullAddress || ""; + const endpoint = `${prefix}/message`; + + const webAppTransport = new SSEServerTransport(endpoint, res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); console.log("Created client transport"); serverTransports.set(webAppTransport.sessionId, serverTransport!); From 89740ef5d111eefcc47924c5e17708cd126f5402 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Fri, 22 Aug 2025 00:47:22 -0700 Subject: [PATCH 113/120] Add tests for MCP_PROXY_FULL_ADDRESS SSE endpoint support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests verify that proxyFullAddress query parameter is sent for both STDIO and SSE transports, but not Streamable HTTP, when MCP_PROXY_FULL_ADDRESS is configured. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../hooks/__tests__/useConnection.test.tsx | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 7518f814..624583dc 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -877,4 +877,129 @@ describe("useConnection", () => { }); }); }); + + describe("MCP_PROXY_FULL_ADDRESS Configuration", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset the mock transport objects + mockSSETransport.url = undefined; + mockSSETransport.options = undefined; + mockStreamableHTTPTransport.url = undefined; + mockStreamableHTTPTransport.options = undefined; + }); + + test("sends proxyFullAddress query parameter for stdio transport when configured", async () => { + const propsWithProxyFullAddress = { + ...defaultProps, + transportType: "stdio" as const, + command: "test-command", + args: "test-args", + env: {}, + config: { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_PROXY_FULL_ADDRESS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS, + value: "https://example.com/inspector/mcp_proxy", + }, + }, + }; + + const { result } = renderHook(() => + useConnection(propsWithProxyFullAddress), + ); + + await act(async () => { + await result.current.connect(); + }); + + // Check that the URL contains the proxyFullAddress parameter + expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe( + "https://example.com/inspector/mcp_proxy", + ); + }); + + test("sends proxyFullAddress query parameter for sse transport when configured", async () => { + const propsWithProxyFullAddress = { + ...defaultProps, + transportType: "sse" as const, + sseUrl: "http://localhost:8080", + config: { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_PROXY_FULL_ADDRESS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS, + value: "https://example.com/inspector/mcp_proxy", + }, + }, + }; + + const { result } = renderHook(() => + useConnection(propsWithProxyFullAddress), + ); + + await act(async () => { + await result.current.connect(); + }); + + // Check that the URL contains the proxyFullAddress parameter + expect(mockSSETransport.url?.searchParams.get("proxyFullAddress")).toBe( + "https://example.com/inspector/mcp_proxy", + ); + }); + + test("does not send proxyFullAddress parameter when MCP_PROXY_FULL_ADDRESS is empty", async () => { + const propsWithEmptyProxy = { + ...defaultProps, + transportType: "stdio" as const, + command: "test-command", + args: "test-args", + env: {}, + config: { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_PROXY_FULL_ADDRESS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS, + value: "", + }, + }, + }; + + const { result } = renderHook(() => useConnection(propsWithEmptyProxy)); + + await act(async () => { + await result.current.connect(); + }); + + // Check that the URL does not contain the proxyFullAddress parameter + expect( + mockSSETransport.url?.searchParams.get("proxyFullAddress"), + ).toBeNull(); + }); + + test("does not send proxyFullAddress parameter for streamable-http transport", async () => { + const propsWithStreamableHttp = { + ...defaultProps, + transportType: "streamable-http" as const, + sseUrl: "http://localhost:8080", + config: { + ...DEFAULT_INSPECTOR_CONFIG, + MCP_PROXY_FULL_ADDRESS: { + ...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS, + value: "https://example.com/inspector/mcp_proxy", + }, + }, + }; + + const { result } = renderHook(() => + useConnection(propsWithStreamableHttp), + ); + + await act(async () => { + await result.current.connect(); + }); + + // Check that streamable-http transport doesn't get proxyFullAddress parameter + expect( + mockStreamableHTTPTransport.url?.searchParams.get("proxyFullAddress"), + ).toBeNull(); + }); + }); }); From 61db7918af20ab2aca6884365fb1b2a03f601f3f Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Tue, 12 Aug 2025 10:15:02 -0700 Subject: [PATCH 114/120] Align similar `/sse` and `/stdio` handlers: formatting and move log statement --- server/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index c55a79d3..c0fb3797 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -378,7 +378,6 @@ app.get( let serverTransport: Transport | undefined; try { serverTransport = await createTransport(req); - console.log("Created server transport"); } catch (error) { if (error instanceof SseError && error.code === 401) { console.error( @@ -396,10 +395,11 @@ app.get( const endpoint = `${prefix}/message`; const webAppTransport = new SSEServerTransport(endpoint, res); + webAppTransports.set(webAppTransport.sessionId, webAppTransport); console.log("Created client transport"); - webAppTransports.set(webAppTransport.sessionId, webAppTransport); serverTransports.set(webAppTransport.sessionId, serverTransport); + console.log("Created server transport"); await webAppTransport.start(); @@ -488,7 +488,7 @@ app.get( async (req, res) => { try { console.log( - "New SSE connection request. NOTE: The sse transport is deprecated and has been replaced by StreamableHttp", + "New SSE connection request. NOTE: The SSE transport is deprecated and has been replaced by StreamableHttp", ); let serverTransport: Transport | undefined; try { @@ -522,6 +522,7 @@ app.get( const webAppTransport = new SSEServerTransport(endpoint, res); webAppTransports.set(webAppTransport.sessionId, webAppTransport); console.log("Created client transport"); + serverTransports.set(webAppTransport.sessionId, serverTransport!); console.log("Created server transport"); From c2e117a4a91e4b3d0d03818298661d905c1557a1 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 25 Aug 2025 10:13:32 -0400 Subject: [PATCH 115/120] 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 116/120] 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 117/120] 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 From ddf74d0766b951dbb9af970da62281b51ea8c7ed Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 26 Aug 2025 10:00:03 -0700 Subject: [PATCH 118/120] Fix formatting --- .github/workflows/claude.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index da8e7c7a..6e916eba 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -44,10 +44,10 @@ jobs: # Trigger when assigned to an issue assignee_trigger: "claude" - + # Allow Claude to run bash # This should be safe given the repo is already public allowed_tools: "Bash" - + custom_instructions: | If posting a comment to GitHub, give a concise summary of the comment at the top and put all the details in a
block. From 67f35363d8e130b57bbad7e1554f375c86e15c4c Mon Sep 17 00:00:00 2001 From: Chris Griffing Date: Tue, 26 Aug 2025 16:14:44 -0700 Subject: [PATCH 119/120] Make cli output awaitable to prevent truncating the result --- cli/src/index.ts | 3 ++- cli/src/utils/awaitable-log.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 cli/src/utils/awaitable-log.ts diff --git a/cli/src/index.ts b/cli/src/index.ts index 2b0c4f53..a7e21c76 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -19,6 +19,7 @@ import { } from "./client/index.js"; import { handleError } from "./error-handler.js"; import { createTransport, TransportOptions } from "./transport.js"; +import { awaitableLog } from "./utils/awaitable-log.js"; type Args = { target: string[]; @@ -150,7 +151,7 @@ async function callMethod(args: Args): Promise { ); } - console.log(JSON.stringify(result, null, 2)); + await awaitableLog(JSON.stringify(result, null, 2)); } finally { try { await disconnect(transport); diff --git a/cli/src/utils/awaitable-log.ts b/cli/src/utils/awaitable-log.ts new file mode 100644 index 00000000..144f0112 --- /dev/null +++ b/cli/src/utils/awaitable-log.ts @@ -0,0 +1,7 @@ +export function awaitableLog(logValue: string): Promise { + return new Promise((resolve) => { + process.stdout.write(logValue, () => { + resolve(); + }); + }); +} From 03d48f37877513ca29100dfe698373d7bbb805a5 Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 27 Aug 2025 20:40:28 -0700 Subject: [PATCH 120/120] Add example to Readme of calling a tool with JSON arguments --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 777f6eeb..a0716556 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,9 @@ npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/lis # Call a specific tool npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg key=value --tool-arg another=value2 +# Call a tool with JSON arguments +npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-arg 'options={"format": "json", "max_tokens": 100}' + # List available resources npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list