From 4c4c8a08844e562dfa4f99e9fe3cac7a66fc4cc6 Mon Sep 17 00:00:00 2001 From: Nicolas Barraud Date: Mon, 10 Mar 2025 20:19:23 -0400 Subject: [PATCH] Add CLI and config file support --- .gitignore | 2 + README.md | 77 +++++++++- bin/cli.js | 211 ++++++++++++++++++------- bin/package.json | 23 +++ bin/src/index.ts | 289 +++++++++++++++++++++++++++++++++++ bin/tests/cli-tests.sh | 231 ++++++++++++++++++++++++++++ bin/tsconfig.json | 16 ++ cli/package.json | 24 +++ cli/src/client/connection.ts | 51 +++++++ cli/src/client/index.ts | 6 + cli/src/client/prompts.ts | 34 +++++ cli/src/client/resources.ts | 43 ++++++ cli/src/client/tools.ts | 95 ++++++++++++ cli/src/client/types.ts | 1 + cli/src/error-handler.ts | 20 +++ cli/src/index.ts | 254 ++++++++++++++++++++++++++++++ cli/src/package-info.ts | 24 +++ cli/src/transport.ts | 76 +++++++++ cli/tsconfig.json | 17 +++ sample-config.json | 19 +++ 20 files changed, 1456 insertions(+), 57 deletions(-) create mode 100644 bin/package.json create mode 100644 bin/src/index.ts create mode 100755 bin/tests/cli-tests.sh create mode 100644 bin/tsconfig.json create mode 100644 cli/package.json create mode 100644 cli/src/client/connection.ts create mode 100644 cli/src/client/index.ts create mode 100644 cli/src/client/prompts.ts create mode 100644 cli/src/client/resources.ts create mode 100644 cli/src/client/tools.ts create mode 100644 cli/src/client/types.ts create mode 100644 cli/src/error-handler.ts create mode 100644 cli/src/index.ts create mode 100644 cli/src/package-info.ts create mode 100644 cli/src/transport.ts create mode 100644 cli/tsconfig.json create mode 100644 sample-config.json diff --git a/.gitignore b/.gitignore index 0f4928e..e526774 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ server/build client/dist client/tsconfig.app.tsbuildinfo client/tsconfig.node.tsbuildinfo +bin/build +cli/build \ No newline at end of file diff --git a/README.md b/README.md index a6da9f4..01b8bde 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ You can pass both arguments and environment variables to your MCP server. Argume npx @modelcontextprotocol/inspector build/index.js arg1 arg2 # Pass environment variables only -npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js +npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js # Pass both environment variables and arguments -npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js arg1 arg2 +npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/index.js arg1 arg2 # Use -- to separate inspector flags from server arguments -npx @modelcontextprotocol/inspector -e KEY=$VALUE -- node build/index.js -e server-flag +npx @modelcontextprotocol/inspector -e key=$VALUE -- node build/index.js -e server-flag ``` The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed: @@ -36,6 +36,77 @@ The inspector runs both a client UI (default port 5173) and an MCP proxy server CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js ``` +### Using a Configuration File + +The inspector supports configuration files to store settings for different MCP servers. This is useful when working with multiple servers or complex configurations: + +```bash +npx @modelcontextprotocol/inspector --config path/to/config.json --server everything +``` + +Example configuration file: + +```json +{ + "mcpServers": { + "everything": { + "command": "npx", + "args": ["@modelcontextprotocol/server-everything"], + "env": { + "hello": "Hello MCP!" + } + }, + "my-server": { + "command": "node", + "args": ["build/index.js", "arg1", "arg2"], + "env": { + "key": "value", + "key2": "value2" + } + } + } +} +``` + +### CLI Mode + +CLI mode enables programmatic interaction with MCP servers from the command line, ideal for scripting, automation, and integration with coding assistants. This creates an efficient feedback loop for MCP server development. + +```bash +npx @modelcontextprotocol/inspector --cli node build/index.js +``` + +The CLI mode supports most operations across tools, resources, and prompts. A few examples: + +```bash +# Basic usage +npx @modelcontextprotocol/inspector --cli node build/index.js + +# With config file +npx @modelcontextprotocol/inspector --cli --config path/to/config.json --server myserver + +# List available tools +npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list + +# Call a specific tool +npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name mytool --tool-args key=value --tool-args another=value2 + +# List available resources +npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list + +# List available prompts +npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list + +# Connect to a remote MCP server +npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com + +# Call a tool on a remote server +npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-args param=value + +# List resources from a remote server +npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method resources/list +``` + For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging). ### From this repository diff --git a/bin/cli.js b/bin/cli.js index 94348fb..4efce01 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,43 +1,27 @@ #!/usr/bin/env node - -import { resolve, dirname } from "path"; +import { Command } from "commander"; +import fs from "node:fs"; +import path from "node:path"; +import { dirname, resolve } from "path"; import { spawnPromise } from "spawn-rx"; import { fileURLToPath } from "url"; - const __dirname = dirname(fileURLToPath(import.meta.url)); - +function handleError(error) { + let message; + if (error instanceof Error) { + message = error.message; + } else if (typeof error === "string") { + message = error; + } else { + message = "Unknown error"; + } + console.error(message); + process.exit(1); +} function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } - -async function main() { - // Parse command line arguments - const args = process.argv.slice(2); - const envVars = {}; - const mcpServerArgs = []; - let command = null; - let parsingFlags = true; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (parsingFlags && arg === "--") { - parsingFlags = false; - continue; - } - - if (parsingFlags && arg === "-e" && i + 1 < args.length) { - const [key, value] = args[++i].split("="); - if (key && value) { - envVars[key] = value; - } - } else if (!command) { - command = arg; - } else { - mcpServerArgs.push(arg); - } - } - +async function runWebClient(args) { const inspectorServerPath = resolve( __dirname, "..", @@ -45,7 +29,6 @@ async function main() { "build", "index.js", ); - // Path to the client entry point const inspectorClientPath = resolve( __dirname, @@ -54,63 +37,183 @@ async function main() { "bin", "cli.js", ); - const CLIENT_PORT = process.env.CLIENT_PORT ?? "5173"; const SERVER_PORT = process.env.SERVER_PORT ?? "3000"; - console.log("Starting MCP inspector..."); - const abort = new AbortController(); - let cancelled = false; process.on("SIGINT", () => { cancelled = true; abort.abort(); }); - const server = spawnPromise( "node", [ inspectorServerPath, - ...(command ? [`--env`, command] : []), - ...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []), + ...(args.command ? [`--env`, args.command] : []), + ...(args.args ? [`--args=${args.args.join(" ")}`] : []), ], { env: { ...process.env, PORT: SERVER_PORT, - MCP_ENV_VARS: JSON.stringify(envVars), + MCP_ENV_VARS: JSON.stringify(args.envArgs), }, signal: abort.signal, echoOutput: true, }, ); - const client = spawnPromise("node", [inspectorClientPath], { env: { ...process.env, PORT: CLIENT_PORT }, signal: abort.signal, echoOutput: true, }); - // Make sure our server/client didn't immediately fail await Promise.any([server, client, delay(2 * 1000)]); const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`; console.log( `\nšŸ” MCP Inspector is up and running at http://localhost:${CLIENT_PORT}${portParam} šŸš€`, ); - try { await Promise.any([server, client]); } catch (e) { - if (!cancelled || process.env.DEBUG) throw e; + if (!cancelled || process.env.DEBUG) { + throw e; + } } - - return 0; } - -main() - .then((_) => process.exit(0)) - .catch((e) => { - console.error(e); - process.exit(1); +async function runCli(args) { + const projectRoot = resolve(__dirname, ".."); + const cliPath = resolve(projectRoot, "cli", "build", "index.js"); + const abort = new AbortController(); + let cancelled = false; + process.on("SIGINT", () => { + cancelled = true; + abort.abort(); }); + try { + await spawnPromise("node", [cliPath, args.command, ...args.args], { + env: { ...process.env, ...args.envArgs }, + signal: abort.signal, + echoOutput: true, + }); + } catch (e) { + if (!cancelled || process.env.DEBUG) { + throw e; + } + } +} +function loadConfigFile(configPath, serverName) { + try { + const resolvedConfigPath = path.isAbsolute(configPath) + ? configPath + : path.resolve(process.cwd(), configPath); + if (!fs.existsSync(resolvedConfigPath)) { + throw new Error(`Config file not found: ${resolvedConfigPath}`); + } + const configContent = fs.readFileSync(resolvedConfigPath, "utf8"); + const parsedConfig = JSON.parse(configContent); + if (!parsedConfig.mcpServers || !parsedConfig.mcpServers[serverName]) { + const availableServers = Object.keys(parsedConfig.mcpServers || {}).join( + ", ", + ); + throw new Error( + `Server '${serverName}' not found in config file. Available servers: ${availableServers}`, + ); + } + const serverConfig = parsedConfig.mcpServers[serverName]; + return serverConfig; + } catch (err) { + if (err instanceof SyntaxError) { + throw new Error(`Invalid JSON in config file: ${err.message}`); + } + throw err; + } +} +function parseKeyValuePair(value, previous = {}) { + const parts = value.split("="); + const key = parts[0]; + const val = parts.slice(1).join("="); + if (val === undefined || val === "") { + throw new Error( + `Invalid parameter format: ${value}. Use key=value format.`, + ); + } + return { ...previous, [key]: val }; +} +function parseArgs() { + const program = new Command(); + const argSeparatorIndex = process.argv.indexOf("--"); + let preArgs = process.argv; + let postArgs = []; + if (argSeparatorIndex !== -1) { + preArgs = process.argv.slice(0, argSeparatorIndex); + postArgs = process.argv.slice(argSeparatorIndex + 1); + } + program + .name("inspector-bin") + .allowExcessArguments() + .allowUnknownOption() + .option( + "-e ", + "environment variables in KEY=VALUE format", + parseKeyValuePair, + {}, + ) + .option("--config ", "config file path") + .option("--server ", "server name from config file") + .option("--cli", "enable CLI mode"); + // Parse only the arguments before -- + program.parse(preArgs); + const options = program.opts(); + const remainingArgs = program.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) + ) { + throw new Error( + "Both --config and --server must be provided together. If you specify one, you must specify the other.", + ); + } + // 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.) + 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, + }; + } + // Otherwise use command line arguments + const command = finalArgs[0] || ""; + const args = finalArgs.slice(1); + return { + command, + args, + envArgs: options.e || {}, + cli: options.cli || false, + }; +} +async function main() { + process.on("uncaughtException", (error) => { + handleError(error); + }); + try { + const args = parseArgs(); + console.log(args); + if (args.cli) { + runCli(args); + } else { + await runWebClient(args); + } + } catch (error) { + handleError(error); + } +} +main(); diff --git a/bin/package.json b/bin/package.json new file mode 100644 index 0000000..3e8f029 --- /dev/null +++ b/bin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@modelcontextprotocol/inspector-bin", + "version": "0.5.1", + "description": "Model Context Protocol inspector", + "license": "MIT", + "author": "Anthropic, PBC (https://anthropic.com)", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/inspector/issues", + "type": "module", + "bin": { + "mcp-inspector": "./cli.js" + }, + "files": [ + "cli.js" + ], + "scripts": { + "build": "tsc", + "postbuild": "chmod +x build/index.js && cp build/index.js cli.js", + "test": "./tests/cli-tests.sh" + }, + "dependencies": {}, + "devDependencies": {} +} diff --git a/bin/src/index.ts b/bin/src/index.ts new file mode 100644 index 0000000..466ca40 --- /dev/null +++ b/bin/src/index.ts @@ -0,0 +1,289 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import fs from "node:fs"; +import path from "node:path"; +import { dirname, resolve } from "path"; +import { spawnPromise } from "spawn-rx"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +type Args = { + command: string; + args: string[]; + envArgs: Record; + cli: boolean; +}; + +type CliOptions = { + e?: Record; + config?: string; + server?: string; + cli?: boolean; +}; + +type ServerConfig = { + command: string; + args?: string[]; + env?: Record; +}; + +function handleError(error: unknown): never { + let message: string; + + if (error instanceof Error) { + message = error.message; + } else if (typeof error === "string") { + message = error; + } else { + message = "Unknown error"; + } + + console.error(message); + + process.exit(1); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function runWebClient(args: Args): Promise { + const inspectorServerPath = resolve( + __dirname, + "..", + "server", + "build", + "index.js", + ); + + // Path to the client entry point + const inspectorClientPath = resolve( + __dirname, + "..", + "client", + "bin", + "cli.js", + ); + + const CLIENT_PORT = process.env.CLIENT_PORT ?? "5173"; + const SERVER_PORT = process.env.SERVER_PORT ?? "3000"; + + console.log("Starting MCP inspector..."); + + const abort = new AbortController(); + + let cancelled = false; + process.on("SIGINT", () => { + cancelled = true; + abort.abort(); + }); + + const server = spawnPromise( + "node", + [ + inspectorServerPath, + ...(args.command ? [`--env`, args.command] : []), + ...(args.args ? [`--args=${args.args.join(" ")}`] : []), + ], + { + env: { + ...process.env, + PORT: SERVER_PORT, + MCP_ENV_VARS: JSON.stringify(args.envArgs), + }, + signal: abort.signal, + echoOutput: true, + }, + ); + + const client = spawnPromise("node", [inspectorClientPath], { + env: { ...process.env, PORT: CLIENT_PORT }, + signal: abort.signal, + echoOutput: true, + }); + + // Make sure our server/client didn't immediately fail + await Promise.any([server, client, delay(2 * 1000)]); + const portParam = SERVER_PORT === "3000" ? "" : `?proxyPort=${SERVER_PORT}`; + console.log( + `\nšŸ” MCP Inspector is up and running at http://localhost:${CLIENT_PORT}${portParam} šŸš€`, + ); + + try { + await Promise.any([server, client]); + } catch (e) { + if (!cancelled || process.env.DEBUG) { + throw e; + } + } +} + +async function runCli(args: Args): Promise { + const projectRoot = resolve(__dirname, ".."); + const cliPath = resolve(projectRoot, "cli", "build", "index.js"); + + const abort = new AbortController(); + + let cancelled = false; + + process.on("SIGINT", () => { + cancelled = true; + abort.abort(); + }); + + try { + await spawnPromise("node", [cliPath, args.command, ...args.args], { + env: { ...process.env, ...args.envArgs }, + signal: abort.signal, + echoOutput: true, + }); + } catch (e) { + if (!cancelled || process.env.DEBUG) { + throw e; + } + } +} + +function loadConfigFile(configPath: string, serverName: string): ServerConfig { + try { + const resolvedConfigPath = path.isAbsolute(configPath) + ? configPath + : path.resolve(process.cwd(), configPath); + + if (!fs.existsSync(resolvedConfigPath)) { + throw new Error(`Config file not found: ${resolvedConfigPath}`); + } + + const configContent = fs.readFileSync(resolvedConfigPath, "utf8"); + const parsedConfig = JSON.parse(configContent); + + if (!parsedConfig.mcpServers || !parsedConfig.mcpServers[serverName]) { + const availableServers = Object.keys(parsedConfig.mcpServers || {}).join( + ", ", + ); + throw new Error( + `Server '${serverName}' not found in config file. Available servers: ${availableServers}`, + ); + } + + const serverConfig = parsedConfig.mcpServers[serverName]; + + return serverConfig; + } catch (err: unknown) { + if (err instanceof SyntaxError) { + throw new Error(`Invalid JSON in config file: ${err.message}`); + } + + throw err; + } +} + +function parseKeyValuePair( + value: string, + previous: Record = {}, +): Record { + const parts = value.split("="); + const key = parts[0]; + const val = parts.slice(1).join("="); + + if (val === undefined || val === "") { + throw new Error( + `Invalid parameter format: ${value}. Use key=value format.`, + ); + } + + return { ...previous, [key as string]: val }; +} + +function parseArgs(): Args { + const program = new Command(); + + const argSeparatorIndex = process.argv.indexOf("--"); + let preArgs = process.argv; + let postArgs: string[] = []; + + if (argSeparatorIndex !== -1) { + preArgs = process.argv.slice(0, argSeparatorIndex); + postArgs = process.argv.slice(argSeparatorIndex + 1); + } + + program + .name("inspector-bin") + .allowExcessArguments() + .allowUnknownOption() + .option( + "-e ", + "environment variables in KEY=VALUE format", + parseKeyValuePair, + {}, + ) + .option("--config ", "config file path") + .option("--server ", "server name from config file") + .option("--cli", "enable CLI mode"); + + // Parse only the arguments before -- + program.parse(preArgs); + + const options = program.opts() as CliOptions; + const remainingArgs = program.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) + ) { + throw new Error( + "Both --config and --server must be provided together. If you specify one, you must specify the other.", + ); + } + + // 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.) + 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, + }; + } + + // Otherwise use command line arguments + const command = finalArgs[0] || ""; + const args = finalArgs.slice(1); + + return { + command, + args, + envArgs: options.e || {}, + cli: options.cli || false, + }; +} + +async function main(): Promise { + process.on("uncaughtException", (error) => { + handleError(error); + }); + + try { + const args = parseArgs(); + + if (args.cli) { + runCli(args); + } else { + await runWebClient(args); + } + } catch (error) { + handleError(error); + } +} + +main(); diff --git a/bin/tests/cli-tests.sh b/bin/tests/cli-tests.sh new file mode 100755 index 0000000..7faa522 --- /dev/null +++ b/bin/tests/cli-tests.sh @@ -0,0 +1,231 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +ORANGE='\033[0;33m' +NC='\033[0m' # No Color + +# Track test results +PASSED_TESTS=0 +FAILED_TESTS=0 +SKIPPED_TESTS=0 +TOTAL_TESTS=0 + +echo -e "${YELLOW}=== MCP Inspector CLI Test Script ===${NC}" +echo -e "${BLUE}This script tests the MCP Inspector CLI's ability to handle various command line options:${NC}" +echo -e "${BLUE}- Basic CLI mode${NC}" +echo -e "${BLUE}- Environment variables (-e)${NC}" +echo -e "${BLUE}- Config file (--config)${NC}" +echo -e "${BLUE}- Server selection (--server)${NC}" +echo -e "${BLUE}- Method selection (--method)${NC}" +echo -e "${BLUE}- Tool-related options (--tool-name, --tool-args)${NC}" +echo -e "${BLUE}- Resource-related options (--uri)${NC}" +echo -e "${BLUE}- Prompt-related options (--prompt-name, --prompt-args)${NC}" +echo -e "${BLUE}- Logging options (--log-level)${NC}" +echo "" + +# Change to the bin directory +cd "$(dirname "$0")/.." +BIN_DIR="$(pwd)" +PROJECT_ROOT="$(dirname "$BIN_DIR")" + +# Compile bin and cli projects +echo -e "${YELLOW}Compiling MCP Inspector bin and cli...${NC}" +cd "$BIN_DIR" +npm run build +cd "$PROJECT_ROOT/cli" +npm run build +cd "$BIN_DIR" + +# Create a symbolic link to handle path resolution +echo -e "${YELLOW}Setting up environment for tests...${NC}" +PARENT_DIR="$(dirname "$PROJECT_ROOT")" + +# Define the test server command using npx +TEST_CMD="npx" +TEST_ARGS=("@modelcontextprotocol/server-everything") + +# Create output directory for test results +OUTPUT_DIR="$BIN_DIR/tests/output" +mkdir -p "$OUTPUT_DIR" + +# Create a temporary directory for test files +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT INT TERM + +# Use the existing sample config file +echo -e "${BLUE}Using existing sample config file: $PROJECT_ROOT/sample-config.json${NC}" +cat "$PROJECT_ROOT/sample-config.json" + +# Create an invalid config file for testing +echo '{ + "mcpServers": { + "invalid": {' > "$TEMP_DIR/invalid-config.json" + +# Function to run a basic test +run_basic_test() { + local test_name=$1 + local output_file="$OUTPUT_DIR/${test_name//\//_}.log" + shift + + echo -e "\n${YELLOW}Testing: ${test_name}${NC}" + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Run the command and capture output + echo -e "${BLUE}Command: node ${BIN_DIR}/cli.js $*${NC}" + node "$BIN_DIR/cli.js" "$@" > "$output_file" 2>&1 + local exit_code=$? + + # Check if the test passed or failed + if [ $exit_code -eq 0 ]; then + echo -e "${GREEN}āœ“ Test passed: ${test_name}${NC}" + echo -e "${BLUE}First few lines of output:${NC}" + head -n 5 "$output_file" | sed 's/^/ /' + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + echo -e "${RED}āœ— Test failed: ${test_name}${NC}" + echo -e "${RED}Error output:${NC}" + cat "$output_file" | sed 's/^/ /' + FAILED_TESTS=$((FAILED_TESTS + 1)) + + # Stop after any error is encountered + echo -e "${YELLOW}Stopping tests due to error. Please validate and fix before continuing.${NC}" + exit 1 + fi +} + +# Function to run an error test (expected to fail) +run_error_test() { + local test_name=$1 + local output_file="$OUTPUT_DIR/${test_name//\//_}.log" + shift + + echo -e "\n${YELLOW}Testing error case: ${test_name}${NC}" + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Run the command and capture output + echo -e "${BLUE}Command: node ${BIN_DIR}/cli.js $*${NC}" + node "$BIN_DIR/cli.js" "$@" > "$output_file" 2>&1 + local exit_code=$? + + # For error tests, we expect a non-zero exit code + if [ $exit_code -ne 0 ]; then + echo -e "${GREEN}āœ“ Error test passed: ${test_name}${NC}" + echo -e "${BLUE}Error output (expected):${NC}" + head -n 5 "$output_file" | sed 's/^/ /' + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + echo -e "${RED}āœ— Error test failed: ${test_name} (expected error but got success)${NC}" + echo -e "${RED}Output:${NC}" + cat "$output_file" | sed 's/^/ /' + FAILED_TESTS=$((FAILED_TESTS + 1)) + + # Stop after any error is encountered + echo -e "${YELLOW}Stopping tests due to error. Please validate and fix before continuing.${NC}" + exit 1 + fi +} + +echo -e "\n${YELLOW}=== Running Basic CLI Mode Tests ===${NC}" + +# Test 1: Basic CLI mode with method +run_basic_test "basic_cli_mode" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "tools/list" + +# Test 2: CLI mode with non-existent method (should fail) +run_error_test "nonexistent_method" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "nonexistent/method" + +# Test 3: CLI mode without method (should fail) +run_error_test "missing_method" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" + +echo -e "\n${YELLOW}=== Running Environment Variable Tests ===${NC}" + +# Test 4: CLI mode with environment variables +run_basic_test "env_variables" "${TEST_CMD}" "${TEST_ARGS[@]}" "-e" "KEY1=value1" "-e" "KEY2=value2" "--cli" "--method" "tools/list" + +# Test 5: CLI mode with invalid environment variable format (should fail) +run_error_test "invalid_env_format" "${TEST_CMD}" "${TEST_ARGS[@]}" "-e" "INVALID_FORMAT" "--cli" "--method" "tools/list" + +echo -e "\n${YELLOW}=== Running Config File Tests ===${NC}" + +# Test 6: Using config file with CLI mode +run_basic_test "config_file" "--config" "$PROJECT_ROOT/sample-config.json" "--server" "everything" "--cli" "--method" "tools/list" + +# Test 7: Using config file without server name (should fail) +run_error_test "config_without_server" "--config" "$PROJECT_ROOT/sample-config.json" "--cli" "--method" "tools/list" + +# Test 8: Using server name without config file (should fail) +run_error_test "server_without_config" "--server" "everything" "--cli" "--method" "tools/list" + +# Test 9: Using non-existent config file (should fail) +run_error_test "nonexistent_config" "--config" "./nonexistent-config.json" "--server" "everything" "--cli" "--method" "tools/list" + +# Test 10: Using invalid config file format (should fail) +run_error_test "invalid_config" "--config" "$TEMP_DIR/invalid-config.json" "--server" "everything" "--cli" "--method" "tools/list" + +# Test 11: Using config file with non-existent server (should fail) +run_error_test "nonexistent_server" "--config" "$PROJECT_ROOT/sample-config.json" "--server" "nonexistent" "--cli" "--method" "tools/list" + +echo -e "\n${YELLOW}=== Running Tool-Related Tests ===${NC}" + +# Test 12: CLI mode with tool call +run_basic_test "tool_call" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "tools/call" "--tool-name" "echo" "--tool-args" "message=Hello" + +# Test 13: CLI mode with tool call but missing tool name (should fail) +run_error_test "missing_tool_name" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "tools/call" "--tool-args" "message=Hello" + +# Test 14: CLI mode with tool call but invalid tool args format (should fail) +run_error_test "invalid_tool_args" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "tools/call" "--tool-name" "echo" "--tool-args" "invalid_format" + +# Test 15: CLI mode with multiple tool args +run_basic_test "multiple_tool_args" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "tools/call" "--tool-name" "add" "--tool-args" "a=1" "b=2" + +echo -e "\n${YELLOW}=== Running Resource-Related Tests ===${NC}" + +# Test 16: CLI mode with resource read +run_basic_test "resource_read" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "resources/read" "--uri" "test://static/resource/1" + +# Test 17: CLI mode with resource read but missing URI (should fail) +run_error_test "missing_uri" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "resources/read" + +echo -e "\n${YELLOW}=== Running Prompt-Related Tests ===${NC}" + +# Test 18: CLI mode with prompt get +run_basic_test "prompt_get" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "prompts/get" "--prompt-name" "simple_prompt" + +# Test 19: CLI mode with prompt get and args +run_basic_test "prompt_get_with_args" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "prompts/get" "--prompt-name" "complex_prompt" "--prompt-args" "temperature=0.7" "style=concise" + +# Test 20: CLI mode with prompt get but missing prompt name (should fail) +run_error_test "missing_prompt_name" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "prompts/get" + +echo -e "\n${YELLOW}=== Running Logging Tests ===${NC}" + +# Test 21: CLI mode with log level +run_basic_test "log_level" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "logging/setLevel" "--log-level" "debug" + +# Test 22: CLI mode with invalid log level (should fail) +run_error_test "invalid_log_level" "${TEST_CMD}" "${TEST_ARGS[@]}" "--cli" "--method" "logging/setLevel" "--log-level" "invalid" + +echo -e "\n${YELLOW}=== Running Combined Option Tests ===${NC}" + +# Note about the combined options issue +echo -e "${BLUE}Testing combined options with environment variables and config file.${NC}" + +# Test 23: CLI mode with config file, environment variables, and tool call +run_basic_test "combined_options" "--config" "$PROJECT_ROOT/sample-config.json" "--server" "everything" "-e" "CLI_ENV_VAR=cli_value" "--cli" "--method" "tools/list" + +# Test 24: CLI mode with all possible options (that make sense together) +run_basic_test "all_options" "--config" "$PROJECT_ROOT/sample-config.json" "--server" "everything" "-e" "CLI_ENV_VAR=cli_value" "--cli" "--method" "tools/call" "--tool-name" "echo" "--tool-args" "message=Hello" "--log-level" "debug" + +# Print test summary +echo -e "\n${YELLOW}=== Test Summary ===${NC}" +echo -e "${GREEN}Passed: $PASSED_TESTS${NC}" +echo -e "${RED}Failed: $FAILED_TESTS${NC}" +echo -e "${ORANGE}Skipped: $SKIPPED_TESTS${NC}" +echo -e "Total: $TOTAL_TESTS" +echo -e "${BLUE}Detailed logs saved to: $OUTPUT_DIR${NC}" + +echo -e "\n${GREEN}All tests completed!${NC}" \ No newline at end of file diff --git a/bin/tsconfig.json b/bin/tsconfig.json new file mode 100644 index 0000000..b5a9261 --- /dev/null +++ b/bin/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "packages", "**/*.spec.ts"] +} diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..62a3380 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,24 @@ +{ + "name": "@modelcontextprotocol/inspector-cli", + "version": "0.5.1", + "description": "CLI for the Model Context Protocol inspector", + "license": "MIT", + "author": "Nicolas Barraud", + "homepage": "https://github.com/nbarraud", + "main": "build/index.js", + "type": "module", + "bin": { + "mcp-inspector-cli": "build/index.js" + }, + "files": [ + "build" + ], + "scripts": { + "build": "tsc" + }, + "devDependencies": {}, + "dependencies": { + "commander": "^13.1.0", + "spawn-rx": "^5.1.2" + } +} diff --git a/cli/src/client/connection.ts b/cli/src/client/connection.ts new file mode 100644 index 0000000..931f803 --- /dev/null +++ b/cli/src/client/connection.ts @@ -0,0 +1,51 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { McpResponse } from "./types.js"; + +export const validLogLevels = [ + "trace", + "debug", + "info", + "warn", + "error", +] as const; + +export type LogLevel = (typeof validLogLevels)[number]; + +export async function connect( + client: Client, + transport: Transport, +): Promise { + try { + await client.connect(transport); + } catch (error) { + throw new Error( + `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +export async function disconnect(transport: Transport): Promise { + try { + await transport.close(); + } catch (error) { + throw new Error( + `Failed to disconnect from MCP server: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +// Set logging level +export async function setLoggingLevel( + client: Client, + level: LogLevel, +): Promise { + try { + const response = await client.setLoggingLevel(level as any); + return response; + } catch (error) { + throw new Error( + `Failed to set logging level: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} diff --git a/cli/src/client/index.ts b/cli/src/client/index.ts new file mode 100644 index 0000000..095d716 --- /dev/null +++ b/cli/src/client/index.ts @@ -0,0 +1,6 @@ +// Re-export everything from the client modules +export * from "./connection.js"; +export * from "./prompts.js"; +export * from "./resources.js"; +export * from "./tools.js"; +export * from "./types.js"; diff --git a/cli/src/client/prompts.ts b/cli/src/client/prompts.ts new file mode 100644 index 0000000..0b23749 --- /dev/null +++ b/cli/src/client/prompts.ts @@ -0,0 +1,34 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { McpResponse } from "./types.js"; + +// List available prompts +export async function listPrompts(client: Client): Promise { + try { + const response = await client.listPrompts(); + return response; + } catch (error) { + throw new Error( + `Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +// Get a prompt +export async function getPrompt( + client: Client, + name: string, + args?: Record, +): Promise { + try { + const response = await client.getPrompt({ + name, + arguments: args || {}, + }); + + return response; + } catch (error) { + throw new Error( + `Failed to get prompt: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} diff --git a/cli/src/client/resources.ts b/cli/src/client/resources.ts new file mode 100644 index 0000000..bf33d64 --- /dev/null +++ b/cli/src/client/resources.ts @@ -0,0 +1,43 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { McpResponse } from "./types.js"; + +// List available resources +export async function listResources(client: Client): Promise { + try { + const response = await client.listResources(); + return response; + } catch (error) { + throw new Error( + `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +// Read a resource +export async function readResource( + client: Client, + uri: string, +): Promise { + try { + const response = await client.readResource({ uri }); + return response; + } catch (error) { + throw new Error( + `Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +// List resource templates +export async function listResourceTemplates( + client: Client, +): Promise { + try { + const response = await client.listResourceTemplates(); + return response; + } catch (error) { + throw new Error( + `Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} diff --git a/cli/src/client/tools.ts b/cli/src/client/tools.ts new file mode 100644 index 0000000..acdb487 --- /dev/null +++ b/cli/src/client/tools.ts @@ -0,0 +1,95 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Tool } from "@modelcontextprotocol/sdk/types.js"; +import { McpResponse } from "./types.js"; + +type JsonSchemaType = { + type: "string" | "number" | "integer" | "boolean" | "array" | "object"; + description?: string; + properties?: Record; + items?: JsonSchemaType; +}; + +export async function listTools(client: Client): Promise { + try { + const response = await client.listTools(); + return response; + } catch (error) { + throw new Error( + `Failed to list tools: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +function convertParameterValue(value: string, schema: JsonSchemaType): unknown { + if (!value) { + return value; + } + + if (schema.type === "number" || schema.type === "integer") { + return Number(value); + } + + if (schema.type === "boolean") { + return value.toLowerCase() === "true"; + } + + if (schema.type === "object" || schema.type === "array") { + try { + return JSON.parse(value); + } catch (error) { + return value; + } + } + + return value; +} + +function convertParameters( + tool: Tool, + params: Record, +): Record { + const result: Record = {}; + const properties = tool.inputSchema.properties || {}; + + for (const [key, value] of Object.entries(params)) { + const paramSchema = properties[key] as JsonSchemaType | undefined; + + if (paramSchema) { + result[key] = convertParameterValue(value, paramSchema); + } else { + // If no schema is found for this parameter, keep it as string + result[key] = value; + } + } + + return result; +} + +export async function callTool( + client: Client, + name: string, + 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; + + if (tool) { + // Convert parameters based on the tool's schema + convertedArgs = convertParameters(tool, args); + } + + const response = await client.callTool({ + name: name, + arguments: convertedArgs, + }); + return response; + } catch (error) { + throw new Error( + `Failed to call tool ${name}: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} diff --git a/cli/src/client/types.ts b/cli/src/client/types.ts new file mode 100644 index 0000000..bbbe1bf --- /dev/null +++ b/cli/src/client/types.ts @@ -0,0 +1 @@ +export type McpResponse = Record; diff --git a/cli/src/error-handler.ts b/cli/src/error-handler.ts new file mode 100644 index 0000000..9725774 --- /dev/null +++ b/cli/src/error-handler.ts @@ -0,0 +1,20 @@ +function formatError(error: unknown): string { + let message: string; + + if (error instanceof Error) { + message = error.message; + } else if (typeof error === "string") { + message = error; + } else { + message = "Unknown error"; + } + + return message; +} + +export function handleError(error: unknown): never { + const errorMessage = formatError(error); + console.error(errorMessage); + + process.exit(1); +} diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 0000000..0a7b557 --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1,254 @@ +#!/usr/bin/env node + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Command } from "commander"; +import { + callTool, + connect, + disconnect, + getPrompt, + listPrompts, + listResources, + listResourceTemplates, + listTools, + LogLevel, + McpResponse, + readResource, + setLoggingLevel, + validLogLevels, +} from "./client/index.js"; +import { handleError } from "./error-handler.js"; +import { packageInfo } from "./package-info.js"; +import { createTransport, TransportOptions } from "./transport.js"; + +type Args = { + target: string[]; + method?: string; + promptName?: string; + promptArgs?: Record; + uri?: string; + logLevel?: LogLevel; + toolName?: string; + toolArgs?: Record; +}; + +function createTransportOptions(target: string[]): TransportOptions { + if (target.length === 0) { + throw new Error( + "Target is required. Specify a URL or a command to execute.", + ); + } + + const [command, ...commandArgs] = target; + + if (!command) { + throw new Error("Command is required."); + } + + const isUrl = command.startsWith("http://") || command.startsWith("https://"); + + if (isUrl && commandArgs.length > 0) { + throw new Error("Arguments cannot be passed to a URL-based MCP server."); + } + + return { + transportType: isUrl ? "sse" : "stdio", + command: isUrl ? undefined : command, + args: isUrl ? undefined : commandArgs, + url: isUrl ? command : undefined, + }; +} + +async function callMethod(args: Args): Promise { + const transportOptions = createTransportOptions(args.target); + const transport = createTransport(transportOptions); + const client = new Client({ + name: packageInfo.name, + version: packageInfo.version, + }); + + try { + await connect(client, transport); + + let result: McpResponse; + + // Tools methods + if (args.method === "tools/list") { + result = await listTools(client); + } else if (args.method === "tools/call") { + if (!args.toolName) { + throw new Error( + "Tool name is required for tools/call method. Use --tool-name to specify the tool name.", + ); + } + + result = await callTool(client, args.toolName, args.toolArgs || {}); + } + // Resources methods + else if (args.method === "resources/list") { + result = await listResources(client); + } else if (args.method === "resources/read") { + if (!args.uri) { + throw new Error( + "URI is required for resources/read method. Use --uri to specify the resource URI.", + ); + } + + result = await readResource(client, args.uri); + } else if (args.method === "resources/templates/list") { + result = await listResourceTemplates(client); + } + // Prompts methods + else if (args.method === "prompts/list") { + result = await listPrompts(client); + } else if (args.method === "prompts/get") { + if (!args.promptName) { + throw new Error( + "Prompt name is required for prompts/get method. Use --prompt-name to specify the prompt name.", + ); + } + + result = await getPrompt(client, args.promptName, args.promptArgs || {}); + } + // Logging methods + else if (args.method === "logging/setLevel") { + if (!args.logLevel) { + throw new Error( + "Log level is required for logging/setLevel method. Use --log-level to specify the log level.", + ); + } + + result = await setLoggingLevel(client, args.logLevel); + } else { + throw new Error( + `Unsupported method: ${args.method}. Supported methods include: tools/list, tools/call, resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, logging/setLevel`, + ); + } + + console.log(JSON.stringify(result, null, 2)); + } finally { + try { + await disconnect(transport); + } catch (disconnectError) { + throw disconnectError; + } + } +} + +function parseKeyValuePair( + value: string, + previous: Record = {}, +): Record { + const parts = value.split("="); + const key = parts[0]; + const val = parts.slice(1).join("="); + + if (val === undefined || val === "") { + throw new Error( + `Invalid parameter format: ${value}. Use key=value format.`, + ); + } + + return { ...previous, [key as string]: val }; +} + +function parseArgs(): Args { + const program = new Command(); + + // Find if there's a -- in the arguments and split them + const argSeparatorIndex = process.argv.indexOf("--"); + let preArgs = process.argv; + let postArgs: string[] = []; + + if (argSeparatorIndex !== -1) { + preArgs = process.argv.slice(0, argSeparatorIndex); + postArgs = process.argv.slice(argSeparatorIndex + 1); + } + + program + .name("inspector-cli") + .allowUnknownOption() + .argument("", "Command and arguments or URL of the MCP server") + // + // Method selection + // + .option("--method ", "Method to invoke") + // + // Tool-related options + // + .option("--tool-name ", "Tool name (for tools/call method)") + .option( + "--tool-args ", + "Tool arguments as key=value pairs", + parseKeyValuePair, + {}, + ) + // + // Resource-related options + // + .option("--uri ", "URI of the resource (for resources/read method)") + // + // Prompt-related options + // + .option( + "--prompt-name ", + "Name of the prompt (for prompts/get method)", + ) + .option( + "--prompt-args ", + "Prompt arguments as key=value pairs", + parseKeyValuePair, + {}, + ) + // + // Logging options + // + .option( + "--log-level ", + "Logging level (for logging/setLevel method)", + (value: string) => { + if (!validLogLevels.includes(value as any)) { + throw new Error( + `Invalid log level: ${value}. Valid levels are: ${validLogLevels.join(", ")}`, + ); + } + + return value as LogLevel; + }, + ); + + // Parse only the arguments before -- + program.parse(preArgs); + + const options = program.opts() as Omit; + let remainingArgs = program.args; + + // Add back any arguments that came after -- + const finalArgs = [...remainingArgs, ...postArgs]; + + if (!options.method) { + throw new Error( + "Method is required. Use --method to specify the method to invoke.", + ); + } + + return { + target: finalArgs, + ...options, + }; +} + +async function main(): Promise { + process.on("uncaughtException", (error) => { + handleError(error); + }); + + try { + const args = parseArgs(); + await callMethod(args); + } catch (error) { + handleError(error); + } +} + +main(); diff --git a/cli/src/package-info.ts b/cli/src/package-info.ts new file mode 100644 index 0000000..844ba2a --- /dev/null +++ b/cli/src/package-info.ts @@ -0,0 +1,24 @@ +import { readFileSync } from "fs"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; + +type PackageInfo = { + name: string; + version: string; + description: string; +}; + +function getPackageInfo(): PackageInfo { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const packageJsonPath = resolve(__dirname, "..", "..", "package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + + return { + name: packageJson.name, + version: packageJson.version, + description: packageJson.description, + }; +} + +export const packageInfo: PackageInfo = getPackageInfo(); diff --git a/cli/src/transport.ts b/cli/src/transport.ts new file mode 100644 index 0000000..e693f24 --- /dev/null +++ b/cli/src/transport.ts @@ -0,0 +1,76 @@ +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { + getDefaultEnvironment, + StdioClientTransport, +} from "@modelcontextprotocol/sdk/client/stdio.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { findActualExecutable } from "spawn-rx"; + +export type TransportOptions = { + transportType: "sse" | "stdio"; + command?: string; + args?: string[]; + url?: string; +}; + +function createSSETransport(options: TransportOptions): Transport { + const baseUrl = new URL(options.url ?? ""); + const sseUrl = new URL("/sse", baseUrl); + + return new SSEClientTransport(sseUrl); +} + +function createStdioTransport(options: TransportOptions): Transport { + let args: string[] = []; + + if (options.args !== undefined) { + args = options.args; + } + + const processEnv: Record = {}; + + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + processEnv[key] = value; + } + } + + const defaultEnv = getDefaultEnvironment(); + + const env: Record = { + ...processEnv, + ...defaultEnv, + }; + + const { cmd: actualCommand, args: actualArgs } = findActualExecutable( + options.command ?? "", + args, + ); + + return new StdioClientTransport({ + command: actualCommand, + args: actualArgs, + env, + stderr: "pipe", + }); +} + +export function createTransport(options: TransportOptions): Transport { + const { transportType } = options; + + try { + if (transportType === "stdio") { + return createStdioTransport(options); + } + + if (transportType === "sse") { + return createSSETransport(options); + } + + throw new Error(`Unsupported transport type: ${transportType}`); + } catch (error) { + throw new Error( + `Failed to create transport: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..effa34f --- /dev/null +++ b/cli/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "packages", "**/*.spec.ts", "build"] +} diff --git a/sample-config.json b/sample-config.json new file mode 100644 index 0000000..d7062d5 --- /dev/null +++ b/sample-config.json @@ -0,0 +1,19 @@ +{ + "mcpServers": { + "everything": { + "command": "npx", + "args": ["@modelcontextprotocol/server-everything"], + "env": { + "HELLO": "Hello MCP!" + } + }, + "myserver": { + "command": "node", + "args": ["build/index.js", "arg1", "arg2"], + "env": { + "KEY": "value", + "KEY2": "value2" + } + } + } +}