mcp-server-diff 2.1.5 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -533,25 +533,26 @@ npx mcp-server-diff -b "..." -t "..." -o summary # One-line summary (default)
533
533
 
534
534
  ### HTTP Headers & Authentication
535
535
 
536
- For authenticated HTTP endpoints, pass headers with `-H`:
536
+ For authenticated HTTP endpoints, pass headers with `-H` (target) or `--base-header`:
537
537
 
538
538
  ```bash
539
- # Direct header value
539
+ # Direct header value for target
540
540
  npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
541
541
  -H "Authorization: Bearer your-token-here"
542
542
 
543
543
  # Read from environment variable (keeps secrets out of shell history)
544
544
  export MCP_TOKEN="your-secret-token"
545
545
  npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
546
- -H "Authorization: env:MCP_TOKEN"
546
+ -H "Authorization: Bearer env:MCP_TOKEN"
547
547
 
548
- # Prompt for secret interactively (hidden input)
548
+ # Prompt for secret interactively (hidden input, named "token")
549
549
  npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
550
- -H "Authorization: secret:"
550
+ -H "Authorization: Bearer secret:token"
551
551
 
552
- # Multiple headers
553
- npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
554
- -H "Authorization: env:TOKEN" -H "X-Custom-Header: value"
552
+ # Headers for both sides (e.g., comparing two authenticated servers)
553
+ npx mcp-server-diff \
554
+ -b "https://api.example.com/v1/mcp" --base-header "Authorization: Bearer secret:v1token" \
555
+ -t "https://api.example.com/v2/mcp" -H "Authorization: Bearer secret:v2token"
555
556
  ```
556
557
 
557
558
  ### Config File
@@ -593,7 +594,9 @@ npx mcp-server-diff -c servers.json -o diff
593
594
  |--------|-------------|
594
595
  | `-b, --base <cmd\|url>` | Base server command (stdio) or URL (http) |
595
596
  | `-t, --target <cmd\|url>` | Target server command (stdio) or URL (http) |
596
- | `-H, --header <header>` | HTTP header (repeatable). Use `env:VAR` or `secret:` for values |
597
+ | `-H, --header <header>` | HTTP header for target (repeatable) |
598
+ | `-B, --base-header <header>` | HTTP header for base server (repeatable) |
599
+ | `-T, --target-header <header>` | HTTP header for target (same as `-H`) |
597
600
  | `-c, --config <file>` | Config file with base and targets |
598
601
  | `-o, --output <format>` | Output: `diff`, `json`, `markdown`, `summary` (default) |
599
602
  | `-v, --verbose` | Verbose output |
@@ -601,6 +604,11 @@ npx mcp-server-diff -c servers.json -o diff
601
604
  | `-h, --help` | Show help |
602
605
  | `--version` | Show version |
603
606
 
607
+ **Header value patterns:**
608
+ - `Bearer your-token` — literal value
609
+ - `Bearer env:VAR_NAME` — read from environment variable
610
+ - `Bearer secret:name` — prompt once for "name", reuse if used multiple times
611
+
604
612
  ---
605
613
 
606
614
  ## License
package/dist/cli/index.js CHANGED
@@ -56870,6 +56870,8 @@ function parseCliArgs() {
56870
56870
  base: { type: "string", short: "b" },
56871
56871
  target: { type: "string", short: "t" },
56872
56872
  header: { type: "string", short: "H", multiple: true },
56873
+ "base-header": { type: "string", short: "B", multiple: true },
56874
+ "target-header": { type: "string", short: "T", multiple: true },
56873
56875
  config: { type: "string", short: "c" },
56874
56876
  output: { type: "string", short: "o", default: "summary" },
56875
56877
  verbose: { type: "boolean", short: "v", default: false },
@@ -56895,17 +56897,18 @@ USAGE:
56895
56897
  mcp-server-diff --config servers.json
56896
56898
 
56897
56899
  OPTIONS:
56898
- -b, --base <command> Base server command (stdio) or URL (http)
56899
- -t, --target <command> Target server command (stdio) or URL (http)
56900
- -H, --header <header> HTTP header (can be repeated, e.g. -H "Authorization: Bearer ...")
56901
- Use env:VAR_NAME to read value from environment variable
56902
- Use secret: to prompt for value securely (hidden input)
56903
- -c, --config <file> Config file with base and targets
56904
- -o, --output <format> Output format: diff, json, markdown, summary (default: summary)
56905
- -v, --verbose Verbose output
56906
- -q, --quiet Quiet mode (only output diffs)
56907
- -h, --help Show this help
56908
- --version Show version
56900
+ -b, --base <command> Base server command (stdio) or URL (http)
56901
+ -t, --target <command> Target server command (stdio) or URL (http)
56902
+ -H, --header <header> HTTP header for target (repeatable)
56903
+ -B, --base-header <header> HTTP header for base server (repeatable)
56904
+ -T, --target-header <hdr> HTTP header for target server (repeatable, same as -H)
56905
+ Values support: env:VAR_NAME, secret:name, "Bearer secret:token"
56906
+ -c, --config <file> Config file with base and targets
56907
+ -o, --output <format> Output format: diff, json, markdown, summary (default: summary)
56908
+ -v, --verbose Verbose output
56909
+ -q, --quiet Quiet mode (only output diffs)
56910
+ -h, --help Show this help
56911
+ --version Show version
56909
56912
 
56910
56913
  CONFIG FILE FORMAT:
56911
56914
  {
@@ -56990,8 +56993,10 @@ function commandToConfig(command, name, headers) {
56990
56993
  /**
56991
56994
  * Parse header strings into a record
56992
56995
  * Accepts formats: "Header: value" or "Header=value"
56993
- * Values starting with "env:" read from environment variables
56994
- * Values starting with "secret:" will be prompted (collected separately)
56996
+ * Special value patterns:
56997
+ * env:VAR_NAME - reads from environment variable
56998
+ * secret:name - prompts for secret (name is the prompt label)
56999
+ * "Bearer secret:token" - prefix + secret (prompts for "token", prepends "Bearer ")
56995
57000
  */
56996
57001
  function parseHeaders(headerStrings, secretValues) {
56997
57002
  const headers = {};
@@ -57013,14 +57018,17 @@ function parseHeaders(headerStrings, secretValues) {
57013
57018
  }
57014
57019
  value = envValue;
57015
57020
  }
57016
- else if (value.startsWith("secret:")) {
57017
- // Use prompted secret value
57018
- const secretKey = `${key}`;
57019
- if (secretValues?.has(secretKey)) {
57020
- value = secretValues.get(secretKey);
57021
- }
57022
- else {
57023
- throw new Error(`Secret value for header ${key} not collected`);
57021
+ else if (value.includes("secret:")) {
57022
+ // Replace secret:name with the prompted value
57023
+ const secretMatch = value.match(/secret:(\w+)/);
57024
+ if (secretMatch) {
57025
+ const secretName = secretMatch[1];
57026
+ if (secretValues?.has(secretName)) {
57027
+ value = value.replace(`secret:${secretName}`, secretValues.get(secretName));
57028
+ }
57029
+ else {
57030
+ throw new Error(`Secret value for "${secretName}" not collected`);
57031
+ }
57024
57032
  }
57025
57033
  }
57026
57034
  headers[key] = value;
@@ -57029,21 +57037,19 @@ function parseHeaders(headerStrings, secretValues) {
57029
57037
  return headers;
57030
57038
  }
57031
57039
  /**
57032
- * Find headers that need secret prompts
57040
+ * Find secrets that need prompts, returns array of {name, label} objects
57033
57041
  */
57034
- function findSecretHeaders(headerStrings) {
57042
+ function findSecrets(headerStrings) {
57035
57043
  const secrets = [];
57036
57044
  if (!headerStrings)
57037
57045
  return secrets;
57038
57046
  for (const h of headerStrings) {
57039
- const colonIdx = h.indexOf(":");
57040
- const eqIdx = h.indexOf("=");
57041
- const sepIdx = colonIdx > 0 ? colonIdx : eqIdx;
57042
- if (sepIdx > 0) {
57043
- const key = h.substring(0, sepIdx).trim();
57044
- const value = h.substring(sepIdx + 1).trim();
57045
- if (value.startsWith("secret:")) {
57046
- secrets.push(key);
57047
+ const secretMatch = h.match(/secret:(\w+)/);
57048
+ if (secretMatch) {
57049
+ const name = secretMatch[1];
57050
+ // Don't add duplicates
57051
+ if (!secrets.find((s) => s.name === name)) {
57052
+ secrets.push({ name, label: name });
57047
57053
  }
57048
57054
  }
57049
57055
  }
@@ -57054,44 +57060,46 @@ function findSecretHeaders(headerStrings) {
57054
57060
  */
57055
57061
  async function promptSecret(prompt) {
57056
57062
  return new Promise((resolve) => {
57057
- const rl = external_readline_namespaceObject.createInterface({
57058
- input: process.stdin,
57059
- output: process.stdout,
57060
- });
57063
+ process.stdout.write(`${prompt}: `);
57061
57064
  // Hide input by using raw mode if available
57062
- if (process.stdin.isTTY) {
57063
- process.stdout.write(`${prompt}: `);
57065
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
57064
57066
  process.stdin.setRawMode(true);
57065
57067
  process.stdin.resume();
57068
+ process.stdin.setEncoding("utf8");
57066
57069
  let value = "";
57067
57070
  const onData = (char) => {
57068
- const c = char.toString();
57069
- if (c === "\n" || c === "\r") {
57071
+ if (char === "\n" || char === "\r" || char === "\u0004") {
57070
57072
  process.stdin.setRawMode(false);
57073
+ process.stdin.pause();
57071
57074
  process.stdin.removeListener("data", onData);
57072
- rl.close();
57073
57075
  process.stdout.write("\n");
57074
57076
  resolve(value);
57075
57077
  }
57076
- else if (c === "\u0003") {
57078
+ else if (char === "\u0003") {
57077
57079
  // Ctrl+C
57080
+ process.stdin.setRawMode(false);
57081
+ process.stdout.write("\n");
57078
57082
  process.exit(1);
57079
57083
  }
57080
- else if (c === "\u007F" || c === "\b") {
57084
+ else if (char === "\u007F" || char === "\b") {
57081
57085
  // Backspace
57082
57086
  if (value.length > 0) {
57083
57087
  value = value.slice(0, -1);
57084
57088
  }
57085
57089
  }
57086
57090
  else {
57087
- value += c;
57091
+ value += char;
57088
57092
  }
57089
57093
  };
57090
57094
  process.stdin.on("data", onData);
57091
57095
  }
57092
57096
  else {
57093
- // Non-TTY: just read the line (won't be hidden)
57094
- rl.question(`${prompt}: `, (answer) => {
57097
+ // Non-TTY: use readline (won't be hidden)
57098
+ const rl = external_readline_namespaceObject.createInterface({
57099
+ input: process.stdin,
57100
+ output: process.stdout,
57101
+ });
57102
+ rl.question("", (answer) => {
57095
57103
  rl.close();
57096
57104
  resolve(answer);
57097
57105
  });
@@ -57101,14 +57109,14 @@ async function promptSecret(prompt) {
57101
57109
  /**
57102
57110
  * Prompt for secret values with hidden input
57103
57111
  */
57104
- async function promptSecrets(headerNames) {
57105
- const secrets = new Map();
57106
- if (headerNames.length === 0)
57107
- return secrets;
57108
- for (const name of headerNames) {
57109
- secrets.set(name, await promptSecret(`Enter value for header "${name}"`));
57112
+ async function promptSecrets(secrets) {
57113
+ const values = new Map();
57114
+ if (secrets.length === 0)
57115
+ return values;
57116
+ for (const { name, label } of secrets) {
57117
+ values.set(name, await promptSecret(`Enter ${label}`));
57110
57118
  }
57111
- return secrets;
57119
+ return values;
57112
57120
  }
57113
57121
  /**
57114
57122
  * Probe a server and return results
@@ -57336,14 +57344,21 @@ async function main() {
57336
57344
  config = loadConfig(values.config);
57337
57345
  }
57338
57346
  else if (values.base && values.target) {
57339
- const headerStrings = values.header;
57340
- // Prompt for any secret: values before parsing
57341
- const secretHeaderNames = findSecretHeaders(headerStrings);
57342
- const secretValues = await promptSecrets(secretHeaderNames);
57343
- const headers = parseHeaders(headerStrings, secretValues);
57347
+ // Combine -H and --target-header for target, use --base-header for base
57348
+ const baseHeaderStrings = values["base-header"];
57349
+ const targetHeaderStrings = [
57350
+ ...(values.header || []),
57351
+ ...(values["target-header"] || []),
57352
+ ];
57353
+ // Find all secrets needed from both header sets
57354
+ const allHeaderStrings = [...(baseHeaderStrings || []), ...targetHeaderStrings];
57355
+ const secrets = findSecrets(allHeaderStrings);
57356
+ const secretValues = await promptSecrets(secrets);
57357
+ const baseHeaders = parseHeaders(baseHeaderStrings, secretValues);
57358
+ const targetHeaders = parseHeaders(targetHeaderStrings.length > 0 ? targetHeaderStrings : undefined, secretValues);
57344
57359
  config = {
57345
- base: commandToConfig(values.base, "base"),
57346
- targets: [commandToConfig(values.target, "target", headers)],
57360
+ base: commandToConfig(values.base, "base", baseHeaders),
57361
+ targets: [commandToConfig(values.target, "target", targetHeaders)],
57347
57362
  };
57348
57363
  }
57349
57364
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-diff",
3
- "version": "2.1.5",
3
+ "version": "2.1.8",
4
4
  "description": "Diff MCP server public interfaces - CLI tool and GitHub Action",
5
5
  "main": "dist/index.js",
6
6
  "bin": {