Refactor project components
* Remove bin folder, leaving cli, server, and client * This fixes #315 * In .gitignore, - add .idea - remove bin/build * Remove bin and bin/cli.js * Remove bin/scripts/copy-cli.js * Refactor/move bin/scripts to cli/scripts * Refactor/move bin/src/index.ts to cli/src/cli.ts * Refactor/renamed client/bin/cli.js to client/bin/client.js * In .github/workflows/main.yml, - add run of cli tests * In cli/pacakge.json - change main and bin/mcp-inspector-cli properties to build/cli.js * In client/package.json, - change bin/mcp-inspector-client properties to build/start.js * In pacakge.json - change bin/mcp-inspector property to ./cli/build/cli.js - removed bin and cli/bin from files list - removed @modelcontextprotocol/inspector-bin dependency - rearranged and corrected scripts
This commit is contained in:
287
cli/src/cli.ts
Normal file
287
cli/src/cli.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/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<string, string>;
|
||||
cli: boolean;
|
||||
};
|
||||
|
||||
type CliOptions = {
|
||||
e?: Record<string, string>;
|
||||
config?: string;
|
||||
server?: string;
|
||||
cli?: boolean;
|
||||
};
|
||||
|
||||
type ServerConfig = {
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
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<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms, true));
|
||||
}
|
||||
|
||||
async function runWebClient(args: Args): Promise<void> {
|
||||
const inspectorServerPath = resolve(
|
||||
__dirname,
|
||||
"../../",
|
||||
"server",
|
||||
"build",
|
||||
"index.js",
|
||||
);
|
||||
|
||||
// Path to the client entry point
|
||||
const inspectorClientPath = resolve(
|
||||
__dirname,
|
||||
"../../",
|
||||
"client",
|
||||
"bin",
|
||||
"client.js",
|
||||
);
|
||||
|
||||
const CLIENT_PORT: string = process.env.CLIENT_PORT ?? "6274";
|
||||
const SERVER_PORT: string = process.env.SERVER_PORT ?? "6277";
|
||||
|
||||
console.log("Starting MCP inspector...");
|
||||
|
||||
const abort = new AbortController();
|
||||
let cancelled: boolean = false;
|
||||
process.on("SIGINT", () => {
|
||||
cancelled = true;
|
||||
abort.abort();
|
||||
});
|
||||
|
||||
let server: ReturnType<typeof spawnPromise>;
|
||||
let serverOk: unknown;
|
||||
|
||||
try {
|
||||
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,
|
||||
},
|
||||
);
|
||||
|
||||
// Make sure server started before starting client
|
||||
serverOk = await Promise.race([server, delay(2 * 1000)]);
|
||||
} catch (error) {}
|
||||
|
||||
if (serverOk) {
|
||||
try {
|
||||
await spawnPromise("node", [inspectorClientPath], {
|
||||
env: { ...process.env, PORT: CLIENT_PORT },
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!cancelled || process.env.DEBUG) throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runCli(args: Args): Promise<void> {
|
||||
const projectRoot = resolve(__dirname, "..");
|
||||
const cliPath = resolve(projectRoot, "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<string, string> = {},
|
||||
): Record<string, string> {
|
||||
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 <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() 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<void> {
|
||||
process.on("uncaughtException", (error) => {
|
||||
handleError(error);
|
||||
});
|
||||
|
||||
try {
|
||||
const args = parseArgs();
|
||||
|
||||
if (args.cli) {
|
||||
runCli(args);
|
||||
} else {
|
||||
await runWebClient(args);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user