Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ client/results.json
client/test-results/
client/e2e/test-results/
mcp.json
.claude/settings.local.json
5 changes: 3 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
"scripts": {
"build": "tsc",
"postbuild": "node scripts/make-executable.js",
"test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js",
"test": "node scripts/cli-tests.js && node scripts/cli-tool-tests.js && node scripts/cli-header-tests.js",
"test:cli": "node scripts/cli-tests.js",
"test:cli-tools": "node scripts/cli-tool-tests.js"
"test:cli-tools": "node scripts/cli-tool-tests.js",
"test:cli-headers": "node scripts/cli-header-tests.js"
},
"devDependencies": {},
"dependencies": {
Expand Down
252 changes: 252 additions & 0 deletions cli/scripts/cli-header-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#!/usr/bin/env node

/**
* Integration tests for header functionality
* Tests the CLI header parsing end-to-end
*/

import { spawn } from "node:child_process";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const CLI_PATH = resolve(__dirname, "..", "build", "index.js");

// ANSI colors for output
const colors = {
GREEN: "\x1b[32m",
RED: "\x1b[31m",
YELLOW: "\x1b[33m",
BLUE: "\x1b[34m",
NC: "\x1b[0m", // No Color
};

let testsPassed = 0;
let testsFailed = 0;

/**
* Run a CLI test with given arguments and check for expected behavior
*/
function runHeaderTest(
testName,
args,
expectSuccess = false,
expectedInOutput = null,
) {
return new Promise((resolve) => {
console.log(`\n${colors.BLUE}Testing: ${testName}${colors.NC}`);
console.log(
`${colors.BLUE}Command: node ${CLI_PATH} ${args.join(" ")}${colors.NC}`,
);

const child = spawn("node", [CLI_PATH, ...args], {
stdio: ["pipe", "pipe", "pipe"],
timeout: 10000,
});

let stdout = "";
let stderr = "";

child.stdout.on("data", (data) => {
stdout += data.toString();
});

child.stderr.on("data", (data) => {
stderr += data.toString();
});

child.on("close", (code) => {
const output = stdout + stderr;
let passed = true;
let reason = "";

// Check exit code expectation
if (expectSuccess && code !== 0) {
passed = false;
reason = `Expected success (exit code 0) but got ${code}`;
} else if (!expectSuccess && code === 0) {
passed = false;
reason = `Expected failure (non-zero exit code) but got success`;
}

// Check expected output
if (passed && expectedInOutput && !output.includes(expectedInOutput)) {
passed = false;
reason = `Expected output to contain "${expectedInOutput}"`;
}

if (passed) {
console.log(`${colors.GREEN}PASS: ${testName}${colors.NC}`);
testsPassed++;
} else {
console.log(`${colors.RED}FAIL: ${testName}${colors.NC}`);
console.log(`${colors.RED}Reason: ${reason}${colors.NC}`);
console.log(`${colors.RED}Exit code: ${code}${colors.NC}`);
console.log(`${colors.RED}Output: ${output}${colors.NC}`);
testsFailed++;
}

resolve();
});

child.on("error", (error) => {
console.log(
`${colors.RED}ERROR: ${testName} - ${error.message}${colors.NC}`,
);
testsFailed++;
resolve();
});
});
}

async function runHeaderIntegrationTests() {
console.log(
`${colors.YELLOW}=== MCP Inspector CLI Header Integration Tests ===${colors.NC}`,
);
console.log(
`${colors.BLUE}Testing header parsing and validation${colors.NC}`,
);

// Test 1: Valid header format should parse successfully (connection will fail)
await runHeaderTest(
"Valid single header",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
"Authorization: Bearer token123",
],
false,
);

// Test 2: Multiple headers should parse successfully
await runHeaderTest(
"Multiple headers",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
"Authorization: Bearer token123",
"--header",
"X-API-Key: secret123",
],
false,
);

// Test 3: Invalid header format - no colon
await runHeaderTest(
"Invalid header format - no colon",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
"InvalidHeader",
],
false,
"Invalid header format",
);

// Test 4: Invalid header format - empty name
await runHeaderTest(
"Invalid header format - empty name",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
": value",
],
false,
"Invalid header format",
);

// Test 5: Invalid header format - empty value
await runHeaderTest(
"Invalid header format - empty value",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
"Header:",
],
false,
"Invalid header format",
);

// Test 6: Header with colons in value
await runHeaderTest(
"Header with colons in value",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
"X-Time: 2023:12:25:10:30:45",
],
false,
);

// Test 7: Whitespace handling
await runHeaderTest(
"Whitespace handling in headers",
[
"https://example.com",
"--method",
"tools/list",
"--transport",
"http",
"--header",
" X-Header : value with spaces ",
],
false,
);

console.log(`\n${colors.YELLOW}=== Test Results ===${colors.NC}`);
console.log(`${colors.GREEN}Tests passed: ${testsPassed}${colors.NC}`);
console.log(`${colors.RED}Tests failed: ${testsFailed}${colors.NC}`);

if (testsFailed === 0) {
console.log(
`${colors.GREEN}All header integration tests passed!${colors.NC}`,
);
process.exit(0);
} else {
console.log(
`${colors.RED}Some header integration tests failed.${colors.NC}`,
);
process.exit(1);
}
}

// Handle graceful shutdown
process.on("SIGINT", () => {
console.log(`\n${colors.YELLOW}Test interrupted by user${colors.NC}`);
process.exit(1);
});

process.on("SIGTERM", () => {
console.log(`\n${colors.YELLOW}Test terminated${colors.NC}`);
process.exit(1);
});

// Run the tests
runHeaderIntegrationTests().catch((error) => {
console.error(`${colors.RED}Test runner error: ${error.message}${colors.NC}`);
process.exit(1);
});
4 changes: 2 additions & 2 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,8 @@ async function main(): Promise<void> {
const args = parseArgs();
await callMethod(args);

// Let Node.js naturally exit instead of force-exiting
// process.exit(0) was causing stdout truncation
// Explicitly exit to ensure process terminates in CI
process.exit(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless it was fixed in some other way, this will re-introduce truncation of tools output on the CLI for MCP servers with a lot of tools. Being able to get tools listed out from this package on CLI (and e.g. pump them to into jq) was my entire motivation for #716.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @philfreo , this PR was also merged which wraps process.stdout.write() in a Promise: https://github.com/modelcontextprotocol/inspector/pull/754/files

} catch (error) {
handleError(error);
}
Expand Down