Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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