From 99b962d4452ca5fd49e519e528c0d4a2f3406daa Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 26 Jul 2025 09:15:27 -0700 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 dbacec64bd962f78d576b9c17e147ff913bd08ba Mon Sep 17 00:00:00 2001 From: olaservo Date: Fri, 1 Aug 2025 20:40:28 -0700 Subject: [PATCH 05/18] 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 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 06/18] 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 77d4963b8a016ef13c7d8b526e352818e556c482 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 11:57:31 -0400 Subject: [PATCH 07/18] 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 08/18] 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 09/18] 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 10/18] 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 5bbbd645556283225cc2e0e20fdb26d7648ecd3e Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 18 Aug 2025 06:15:15 -0700 Subject: [PATCH 11/18] 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 3628abca9ee626f0469ebc609e80dd30bfeede21 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sun, 10 Aug 2025 13:06:19 -0700 Subject: [PATCH 12/18] 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 13/18] 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 14/18] 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 8f2e53c273d66ec7db095f0c231f79523849db9d Mon Sep 17 00:00:00 2001 From: pavan-mellamputi-socure <104933528+pavan-mellamputi-socure@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:48:26 -0700 Subject: [PATCH 15/18] Fix a typo in progress notification method name It shoud be `notifications/progress` not `notification/progress` (not singular). This matches the latest spec. https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress --- client/src/lib/hooks/useConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 8c44d51b..f3f30e53 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -167,7 +167,7 @@ export function useConnection({ // Add progress notification to `Server Notification` window in the UI if (onNotification) { onNotification({ - method: "notification/progress", + method: "notifications/progress", params, }); } From ddf74d0766b951dbb9af970da62281b51ea8c7ed Mon Sep 17 00:00:00 2001 From: olaservo Date: Tue, 26 Aug 2025 10:00:03 -0700 Subject: [PATCH 16/18] 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 17/18] 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 18/18] 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