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:
cliffhall
2025-04-15 17:59:26 -04:00
parent f7272d8d8c
commit 6a99feaf33
15 changed files with 162 additions and 2922 deletions

View File

@@ -30,6 +30,10 @@ jobs:
working-directory: ./client working-directory: ./client
run: npm test run: npm test
- name: Run cli tests
working-directory: ./cli
run: npm test
- run: npm run build - run: npm run build
publish: publish:

4
.gitignore vendored
View File

@@ -1,11 +1,11 @@
.DS_Store .DS_Store
.vscode
.idea
node_modules/ node_modules/
*-workspace/ *-workspace/
server/build server/build
client/dist client/dist
client/tsconfig.app.tsbuildinfo client/tsconfig.app.tsbuildinfo
client/tsconfig.node.tsbuildinfo client/tsconfig.node.tsbuildinfo
.vscode
bin/build
cli/build cli/build
test-output test-output

View File

@@ -1,217 +0,0 @@
#!/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));
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 runWebClient(args) {
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 ?? "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;
let serverOk;
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) {
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();

View File

@@ -1,26 +0,0 @@
{
"name": "@modelcontextprotocol/inspector-bin",
"version": "0.9.0",
"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": "node scripts/make-executable.js && node scripts/copy-cli.js",
"test": "node scripts/cli-tests.js"
},
"dependencies": {
"commander": "^13.1.0",
"spawn-rx": "^5.1.2"
},
"devDependencies": {}
}

View File

@@ -1,20 +0,0 @@
/**
* Cross-platform script to copy the built file to cli.js
*/
import { promises as fs } from "fs";
import path from "path";
const SOURCE_FILE = path.resolve("build/index.js");
const TARGET_FILE = path.resolve("cli.js");
async function copyFile() {
try {
await fs.copyFile(SOURCE_FILE, TARGET_FILE);
console.log(`Successfully copied ${SOURCE_FILE} to ${TARGET_FILE}`);
} catch (error) {
console.error("Error copying file:", error);
process.exit(1);
}
}
copyFile();

View File

@@ -1,16 +0,0 @@
{
"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"]
}

View File

@@ -3,18 +3,21 @@
"version": "0.9.0", "version": "0.9.0",
"description": "CLI for the Model Context Protocol inspector", "description": "CLI for the Model Context Protocol inspector",
"license": "MIT", "license": "MIT",
"author": "Nicolas Barraud", "author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://github.com/nbarraud", "homepage": "https://modelcontextprotocol.io",
"main": "build/index.js", "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"main": "build/cli.js",
"type": "module", "type": "module",
"bin": { "bin": {
"mcp-inspector-cli": "build/index.js" "mcp-inspector-cli": "build/cli.js"
}, },
"files": [ "files": [
"build" "build"
], ],
"scripts": { "scripts": {
"build": "tsc" "build": "tsc",
"postbuild": "node scripts/make-executable.js",
"test": "node scripts/cli-tests.js"
}, },
"devDependencies": {}, "devDependencies": {},
"dependencies": { "dependencies": {

View File

@@ -44,30 +44,12 @@ console.log(`${colors.BLUE}- Resource-related options (--uri)${colors.NC}`);
console.log( console.log(
`${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`, `${colors.BLUE}- Prompt-related options (--prompt-name, --prompt-args)${colors.NC}`,
); );
console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}`); console.log(`${colors.BLUE}- Logging options (--log-level)${colors.NC}\n`);
console.log("");
// Get directory paths // Get directory paths
const SCRIPTS_DIR = __dirname; const SCRIPTS_DIR = __dirname;
const BIN_DIR = path.resolve(SCRIPTS_DIR, ".."); const PROJECT_ROOT = path.join(SCRIPTS_DIR, "../../");
const PROJECT_ROOT = path.resolve(BIN_DIR, ".."); const BUILD_DIR = path.resolve(SCRIPTS_DIR, "../build");
// Compile bin and cli projects
console.log(
`${colors.YELLOW}Compiling MCP Inspector bin and cli...${colors.NC}`,
);
try {
process.chdir(BIN_DIR);
execSync("npm run build", { stdio: "inherit" });
process.chdir(path.join(PROJECT_ROOT, "cli"));
execSync("npm run build", { stdio: "inherit" });
process.chdir(BIN_DIR);
} catch (error) {
console.error(
`${colors.RED}Error during compilation: ${error.message}${colors.NC}`,
);
process.exit(1);
}
// Define the test server command using npx // Define the test server command using npx
const TEST_CMD = "npx"; const TEST_CMD = "npx";
@@ -80,9 +62,10 @@ if (!fs.existsSync(OUTPUT_DIR)) {
} }
// Create a temporary directory for test files // Create a temporary directory for test files
const TEMP_DIR = fs.mkdirSync(path.join(os.tmpdir(), "mcp-inspector-tests-"), { const TEMP_DIR = fs.mkdirSync(path.join(os.tmpdir(), "mcp-inspector-tests"), {
recursive: true, recursive: true,
}); });
process.on("exit", () => { process.on("exit", () => {
try { try {
fs.rmSync(TEMP_DIR, { recursive: true, force: true }); fs.rmSync(TEMP_DIR, { recursive: true, force: true });
@@ -125,7 +108,7 @@ async function runBasicTest(testName, ...args) {
// Run the command and capture output // Run the command and capture output
console.log( console.log(
`${colors.BLUE}Command: node ${BIN_DIR}/cli.js ${args.join(" ")}${colors.NC}`, `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
); );
try { try {
@@ -134,7 +117,7 @@ async function runBasicTest(testName, ...args) {
// Spawn the process // Spawn the process
return new Promise((resolve) => { return new Promise((resolve) => {
const child = spawn("node", [path.join(BIN_DIR, "cli.js"), ...args], { const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}); });
@@ -205,7 +188,7 @@ async function runErrorTest(testName, ...args) {
// Run the command and capture output // Run the command and capture output
console.log( console.log(
`${colors.BLUE}Command: node ${BIN_DIR}/cli.js ${args.join(" ")}${colors.NC}`, `${colors.BLUE}Command: node ${BUILD_DIR}/cli.js ${args.join(" ")}${colors.NC}`,
); );
try { try {
@@ -214,7 +197,7 @@ async function runErrorTest(testName, ...args) {
// Spawn the process // Spawn the process
return new Promise((resolve) => { return new Promise((resolve) => {
const child = spawn("node", [path.join(BIN_DIR, "cli.js"), ...args], { const child = spawn("node", [path.join(BUILD_DIR, "cli.js"), ...args], {
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
}); });

View File

@@ -6,7 +6,7 @@ import { platform } from "os";
import { execSync } from "child_process"; import { execSync } from "child_process";
import path from "path"; import path from "path";
const TARGET_FILE = path.resolve("build/index.js"); const TARGET_FILE = path.resolve("build/cli.js");
async function makeExecutable() { async function makeExecutable() {
try { try {

View File

@@ -46,13 +46,13 @@ function handleError(error: unknown): never {
} }
function delay(ms: number): Promise<void> { function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms, true));
} }
async function runWebClient(args: Args): Promise<void> { async function runWebClient(args: Args): Promise<void> {
const inspectorServerPath = resolve( const inspectorServerPath = resolve(
__dirname, __dirname,
"..", "../../",
"server", "server",
"build", "build",
"index.js", "index.js",
@@ -61,10 +61,10 @@ async function runWebClient(args: Args): Promise<void> {
// Path to the client entry point // Path to the client entry point
const inspectorClientPath = resolve( const inspectorClientPath = resolve(
__dirname, __dirname,
"..", "../../",
"client", "client",
"bin", "bin",
"cli.js", "client.js",
); );
const CLIENT_PORT: string = process.env.CLIENT_PORT ?? "6274"; const CLIENT_PORT: string = process.env.CLIENT_PORT ?? "6274";
@@ -120,7 +120,7 @@ async function runWebClient(args: Args): Promise<void> {
async function runCli(args: Args): Promise<void> { async function runCli(args: Args): Promise<void> {
const projectRoot = resolve(__dirname, ".."); const projectRoot = resolve(__dirname, "..");
const cliPath = resolve(projectRoot, "cli", "build", "index.js"); const cliPath = resolve(projectRoot, "build", "index.js");
const abort = new AbortController(); const abort = new AbortController();

120
client/bin/start.js Executable file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env node
import { resolve, dirname } from "path";
import { spawnPromise } from "spawn-rx";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
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);
}
}
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 = 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;
try {
server = spawnPromise(
"node",
[
inspectorServerPath,
...(command ? [`--env`, command] : []),
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
],
{
env: {
...process.env,
PORT: SERVER_PORT,
MCP_ENV_VARS: JSON.stringify(envVars),
},
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;
}
}
return 0;
}
main()
.then((_) => process.exit(0))
.catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -8,7 +8,7 @@
"bugs": "https://github.com/modelcontextprotocol/inspector/issues", "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module", "type": "module",
"bin": { "bin": {
"mcp-inspector-client": "./bin/cli.js" "mcp-inspector-client": "./bin/start.js"
}, },
"files": [ "files": [
"bin", "bin",

2593
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,42 +8,36 @@
"bugs": "https://github.com/modelcontextprotocol/inspector/issues", "bugs": "https://github.com/modelcontextprotocol/inspector/issues",
"type": "module", "type": "module",
"bin": { "bin": {
"mcp-inspector": "./bin/cli.js" "mcp-inspector": "./cli/build/cli.js"
}, },
"files": [ "files": [
"bin",
"client/bin", "client/bin",
"client/dist", "client/dist",
"server/build", "server/build",
"cli/bin",
"cli/build" "cli/build"
], ],
"workspaces": [ "workspaces": [
"client", "client",
"server", "server",
"cli", "cli"
"bin"
], ],
"scripts": { "scripts": {
"dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"", "build": "npm run build-server && npm run build-client && npm run build-cli",
"dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows\"",
"test": "npm run prettier-check && cd client && npm test",
"test-cli": "cd bin && npm run test",
"build-bin": "cd bin && npm run build",
"build-server": "cd server && npm run build", "build-server": "cd server && npm run build",
"build-client": "cd client && npm run build", "build-client": "cd client && npm run build",
"build-cli": "cd cli && npm run build", "build-cli": "cd cli && npm run build",
"build": "npm run build-bin && npm run build-server && npm run build-client && npm run build-cli", "dev": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev\"",
"dev:windows": "concurrently \"cd client && npm run dev\" \"cd server && npm run dev:windows\"",
"start": "node client/bin/start.js",
"start-server": "cd server && npm run start", "start-server": "cd server && npm run start",
"start-client": "cd client && npm run preview", "start-client": "cd client && npm run preview",
"start": "node ./bin/cli.js", "test": "npm run prettier-check && cd client && npm test",
"prepare": "npm run build", "test-cli": "cd cli && npm run test",
"prettier-fix": "prettier --write .", "prettier-fix": "prettier --write .",
"prettier-check": "prettier --check .", "prettier-check": "prettier --check .",
"publish-all": "npm publish --workspaces --access public && npm publish --access public" "publish-all": "npm publish --workspaces --access public && npm publish --access public"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/inspector-bin": "^0.9.0",
"@modelcontextprotocol/inspector-cli": "^0.9.0", "@modelcontextprotocol/inspector-cli": "^0.9.0",
"@modelcontextprotocol/inspector-client": "^0.9.0", "@modelcontextprotocol/inspector-client": "^0.9.0",
"@modelcontextprotocol/inspector-server": "^0.9.0", "@modelcontextprotocol/inspector-server": "^0.9.0",