Skip to content

Add CLI and config file support #177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4c4c8a0
Add CLI and config file support
nbarraud Mar 11, 2025
9f42629
Forgot package files
nbarraud Mar 11, 2025
f4e6f4d
Removed package-info.ts
nbarraud Mar 11, 2025
dae3890
Change --tool-args to --tool-arg for consistency
nbarraud Mar 11, 2025
a63de62
Update URL to use 127.0.0.1 instead of localhost
nbarraud Mar 30, 2025
5b22143
CLI and config file support
nbarraud Mar 30, 2025
65c589c
Fixes to the formatting
nbarraud Mar 30, 2025
1dfe10b
Merge branch 'main' into cli-and-config-file-support
nbarraud Mar 30, 2025
1832be2
Update package.json
nbarraud Mar 31, 2025
86a1ade
Update package.json
nbarraud Mar 31, 2025
cf86040
Update package-lock.json
nbarraud Mar 31, 2025
3fc6301
Moved some dependencies in repos where they are being used.
nbarraud Mar 31, 2025
6eb6b3f
Update package-lock.json
nbarraud Mar 31, 2025
952e13e
Update cli-tests.js
nbarraud Mar 31, 2025
73d4cec
prettier-fix
nbarraud Mar 31, 2025
8423776
Fixed conflicts
nbarraud Apr 12, 2025
bbff5c5
Merge branch 'main' into cli-and-config-file-support
nbarraud Apr 12, 2025
204a90b
Resolved new conflicts
nbarraud Apr 14, 2025
dcef9dd
Merge branch 'cli-and-config-file-support' of https://github.com/nbar…
nbarraud Apr 14, 2025
de233c9
Conflict resolution
nbarraud Apr 14, 2025
3784816
Merge branch 'main' into cli-and-config-file-support
nbarraud Apr 14, 2025
485e703
Merge branch 'main' into cli-and-config-file-support
nbarraud Apr 14, 2025
fe02efd
Merge branch 'main' into cli-and-config-file-support
nbarraud Apr 15, 2025
fd7dbba
Merge branch 'main' into cli-and-config-file-support
nbarraud Apr 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
.DS_Store
node_modules
node_modules/
*-workspace/
server/build
client/dist
client/tsconfig.app.tsbuildinfo
client/tsconfig.node.tsbuildinfo
.vscode
bin/build
cli/build
test-output
87 changes: 84 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MCPP) server (default port 6277). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MCPP respectively, as a mnemonic). You can customize the ports if needed:
Expand Down Expand Up @@ -59,6 +59,36 @@ The MCP Inspector supports the following configuration settings. To change them,

These settings can be adjusted in real-time through the UI and will persist across sessions.

The inspector also 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 server 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"
}
}
}
}
```

### From this repository

If you're working on the inspector itself:
Expand All @@ -83,6 +113,57 @@ npm run build
npm start
```

### 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-arg key=value --tool-arg 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-arg param=value

# List resources from a remote server
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method resources/list
```

### UI Mode vs CLI Mode: When to Use Each

| Use Case | UI Mode | CLI Mode |
| ------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Server Development** | Visual interface for interactive testing and debugging during development | Scriptable commands for quick testing and continuous integration; creates feedback loops with AI coding assistants like Cursor for rapid development |
| **Resource Exploration** | Interactive browser with hierarchical navigation and JSON visualization | Programmatic listing and reading for automation and scripting |
| **Tool Testing** | Form-based parameter input with real-time response visualization | Command-line tool execution with JSON output for scripting |
| **Prompt Engineering** | Interactive sampling with streaming responses and visual comparison | Batch processing of prompts with machine-readable output |
| **Debugging** | Request history, visualized errors, and real-time notifications | Direct JSON output for log analysis and integration with other tools |
| **Automation** | N/A | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants |
| **Learning MCP** | Rich visual interface helps new users understand server capabilities | Simplified commands for focused learning of specific endpoints |

## License

This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details.
213 changes: 155 additions & 58 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,34 @@
#!/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, true));
}

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 envVar = args[++i];
const equalsIndex = envVar.indexOf("=");

if (equalsIndex !== -1) {
const key = envVar.substring(0, equalsIndex);
const value = envVar.substring(equalsIndex + 1);
envVars[key] = value;
} else {
envVars[envVar] = "";
}
} else if (!command) {
command = arg;
} else {
mcpServerArgs.push(arg);
}
}

async function runWebClient(args) {
const inspectorServerPath = resolve(
__dirname,
"..",
"server",
"build",
"index.js",
);

// Path to the client entry point
const inspectorClientPath = resolve(
__dirname,
Expand All @@ -60,43 +37,38 @@ async function main() {
"bin",
"cli.js",
);

const CLIENT_PORT = process.env.CLIENT_PORT ?? "6274";
const SERVER_PORT = process.env.SERVER_PORT ?? "6277";

console.log("Starting MCP inspector...");

const abort = new AbortController();

let cancelled = false;
process.on("SIGINT", () => {
cancelled = true;
abort.abort();
});
let server, serverOk;
let server;
let serverOk;
try {
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,
},
);

// Make sure server started before starting client
serverOk = await Promise.race([server, delay(2 * 1000)]);
} catch (error) {}

if (serverOk) {
try {
await spawnPromise("node", [inspectorClientPath], {
Expand All @@ -108,13 +80,138 @@ async function main() {
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 <env>",
"environment variables in KEY=VALUE format",
parseKeyValuePair,
{},
)
.option("--config <path>", "config file path")
.option("--server <n>", "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();
if (args.cli) {
runCli(args);
} else {
await runWebClient(args);
}
} catch (error) {
handleError(error);
}
}
main();
Loading