mcp-server-diff 2.1.0 → 2.1.6
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 +116 -1
- package/dist/cli/index.js +226 -17
- package/dist/index.js +32 -5
- package/package.json +6 -1
- package/.github/dependabot.yml +0 -21
- package/.github/workflows/ci.yml +0 -51
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/release.yml +0 -51
- package/.prettierignore +0 -3
- package/.prettierrc +0 -8
- package/CONTRIBUTING.md +0 -81
- package/action.yml +0 -250
- package/eslint.config.mjs +0 -47
- package/jest.config.mjs +0 -26
- package/src/__tests__/fixtures/http-server.ts +0 -103
- package/src/__tests__/fixtures/stdio-server.ts +0 -158
- package/src/__tests__/integration.test.ts +0 -306
- package/src/__tests__/runner.test.ts +0 -430
- package/src/cli.ts +0 -421
- package/src/diff.ts +0 -252
- package/src/git.ts +0 -262
- package/src/index.ts +0 -284
- package/src/logger.ts +0 -93
- package/src/probe.ts +0 -327
- package/src/reporter.ts +0 -214
- package/src/runner.ts +0 -902
- package/src/types.ts +0 -155
- package/tsconfig.json +0 -30
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# MCP Server Diff
|
|
2
2
|
|
|
3
3
|
[](https://github.com/marketplace/actions/mcp-server-diff)
|
|
4
|
+
[](https://www.npmjs.com/package/mcp-server-diff)
|
|
4
5
|
[](https://github.com/SamMorrowDrums/mcp-server-diff/releases)
|
|
5
6
|
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
|
-
A GitHub Action for diffing [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server **public interfaces** between versions.
|
|
8
|
+
A GitHub Action for diffing [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server **public interfaces** between versions. Compares the current branch against a baseline to surface any changes to your server's exposed tools, resources, prompts, and capabilities.
|
|
9
|
+
|
|
10
|
+
> **Also available as a standalone CLI** — see [CLI Documentation](#cli-tool) or install with `npx mcp-server-diff`
|
|
8
11
|
|
|
9
12
|
## Overview
|
|
10
13
|
|
|
@@ -496,6 +499,118 @@ jobs:
|
|
|
496
499
|
- Ensure the server binds to `0.0.0.0` or `127.0.0.1`, not just `localhost` on some systems
|
|
497
500
|
- Check firewall or container networking if running in Docker
|
|
498
501
|
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## CLI Tool
|
|
505
|
+
|
|
506
|
+
The CLI lets you diff any two MCP servers directly from your terminal—useful for local development, CI pipelines, or comparing servers across different implementations.
|
|
507
|
+
|
|
508
|
+
### Installation
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
# Run directly with npx (no install required)
|
|
512
|
+
npx mcp-server-diff --help
|
|
513
|
+
|
|
514
|
+
# Or install globally
|
|
515
|
+
npm install -g mcp-server-diff
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Basic Usage
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
# Compare two local stdio servers
|
|
522
|
+
npx mcp-server-diff -b "python -m mcp_server" -t "node dist/stdio.js"
|
|
523
|
+
|
|
524
|
+
# Compare local server vs remote HTTP endpoint
|
|
525
|
+
npx mcp-server-diff -b "go run ./cmd/server stdio" -t "https://mcp.example.com/api"
|
|
526
|
+
|
|
527
|
+
# Output formats
|
|
528
|
+
npx mcp-server-diff -b "..." -t "..." -o diff # Raw diff hunks only
|
|
529
|
+
npx mcp-server-diff -b "..." -t "..." -o json # Full JSON with details
|
|
530
|
+
npx mcp-server-diff -b "..." -t "..." -o markdown # Formatted report
|
|
531
|
+
npx mcp-server-diff -b "..." -t "..." -o summary # One-line summary (default)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### HTTP Headers & Authentication
|
|
535
|
+
|
|
536
|
+
For authenticated HTTP endpoints, pass headers with `-H` (target) or `--base-header`:
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
# Direct header value for target
|
|
540
|
+
npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
|
|
541
|
+
-H "Authorization: Bearer your-token-here"
|
|
542
|
+
|
|
543
|
+
# Read from environment variable (keeps secrets out of shell history)
|
|
544
|
+
export MCP_TOKEN="your-secret-token"
|
|
545
|
+
npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
|
|
546
|
+
-H "Authorization: Bearer env:MCP_TOKEN"
|
|
547
|
+
|
|
548
|
+
# Prompt for secret interactively (hidden input, named "token")
|
|
549
|
+
npx mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \
|
|
550
|
+
-H "Authorization: Bearer secret:token"
|
|
551
|
+
|
|
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"
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Config File
|
|
559
|
+
|
|
560
|
+
For complex comparisons or multiple targets, use a config file:
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
npx mcp-server-diff -c servers.json -o diff
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
```json
|
|
567
|
+
{
|
|
568
|
+
"base": {
|
|
569
|
+
"name": "python-server",
|
|
570
|
+
"transport": "stdio",
|
|
571
|
+
"start_command": "python -m mcp_server"
|
|
572
|
+
},
|
|
573
|
+
"targets": [
|
|
574
|
+
{
|
|
575
|
+
"name": "typescript-server",
|
|
576
|
+
"transport": "stdio",
|
|
577
|
+
"start_command": "node dist/stdio.js"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"name": "remote-server",
|
|
581
|
+
"transport": "streamable-http",
|
|
582
|
+
"server_url": "https://mcp.example.com/api",
|
|
583
|
+
"headers": {
|
|
584
|
+
"Authorization": "Bearer token"
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### CLI Options Reference
|
|
592
|
+
|
|
593
|
+
| Option | Description |
|
|
594
|
+
|--------|-------------|
|
|
595
|
+
| `-b, --base <cmd\|url>` | Base server command (stdio) or URL (http) |
|
|
596
|
+
| `-t, --target <cmd\|url>` | Target server command (stdio) or URL (http) |
|
|
597
|
+
| `-H, --header <header>` | HTTP header for target (repeatable) |
|
|
598
|
+
| `--base-header <header>` | HTTP header for base server (repeatable) |
|
|
599
|
+
| `--target-header <header>` | HTTP header for target (same as `-H`) |
|
|
600
|
+
| `-c, --config <file>` | Config file with base and targets |
|
|
601
|
+
| `-o, --output <format>` | Output: `diff`, `json`, `markdown`, `summary` (default) |
|
|
602
|
+
| `-v, --verbose` | Verbose output |
|
|
603
|
+
| `-q, --quiet` | Quiet mode (only output result) |
|
|
604
|
+
| `-h, --help` | Show help |
|
|
605
|
+
| `--version` | Show version |
|
|
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
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
499
614
|
## License
|
|
500
615
|
|
|
501
616
|
MIT License. See [LICENSE](LICENSE) for details.
|
package/dist/cli/index.js
CHANGED
|
@@ -38918,6 +38918,8 @@ var __webpack_exports__ = {};
|
|
|
38918
38918
|
var external_node_util_ = __nccwpck_require__(7975);
|
|
38919
38919
|
// EXTERNAL MODULE: external "fs"
|
|
38920
38920
|
var external_fs_ = __nccwpck_require__(9896);
|
|
38921
|
+
;// CONCATENATED MODULE: external "readline"
|
|
38922
|
+
const external_readline_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("readline");
|
|
38921
38923
|
;// CONCATENATED MODULE: ./node_modules/zod/v4/core/core.js
|
|
38922
38924
|
/** A special constant with type `never` */
|
|
38923
38925
|
const NEVER = Object.freeze({
|
|
@@ -56353,6 +56355,13 @@ const log = {
|
|
|
56353
56355
|
|
|
56354
56356
|
|
|
56355
56357
|
|
|
56358
|
+
/**
|
|
56359
|
+
* Check if an error is "Method not found" (-32601)
|
|
56360
|
+
*/
|
|
56361
|
+
function isMethodNotFound(error) {
|
|
56362
|
+
const errorStr = String(error);
|
|
56363
|
+
return errorStr.includes("-32601") || errorStr.includes("Method not found");
|
|
56364
|
+
}
|
|
56356
56365
|
/**
|
|
56357
56366
|
* Probes an MCP server and returns capability snapshots
|
|
56358
56367
|
*/
|
|
@@ -56425,7 +56434,12 @@ async function probeServer(options) {
|
|
|
56425
56434
|
log.info(` Listed ${result.tools.tools.length} tools`);
|
|
56426
56435
|
}
|
|
56427
56436
|
catch (error) {
|
|
56428
|
-
|
|
56437
|
+
if (isMethodNotFound(error)) {
|
|
56438
|
+
log.info(" Server does not implement tools/list");
|
|
56439
|
+
}
|
|
56440
|
+
else {
|
|
56441
|
+
log.warning(` Failed to list tools: ${error}`);
|
|
56442
|
+
}
|
|
56429
56443
|
}
|
|
56430
56444
|
}
|
|
56431
56445
|
else {
|
|
@@ -56439,7 +56453,12 @@ async function probeServer(options) {
|
|
|
56439
56453
|
log.info(` Listed ${result.prompts.prompts.length} prompts`);
|
|
56440
56454
|
}
|
|
56441
56455
|
catch (error) {
|
|
56442
|
-
|
|
56456
|
+
if (isMethodNotFound(error)) {
|
|
56457
|
+
log.info(" Server does not implement prompts/list");
|
|
56458
|
+
}
|
|
56459
|
+
else {
|
|
56460
|
+
log.warning(` Failed to list prompts: ${error}`);
|
|
56461
|
+
}
|
|
56443
56462
|
}
|
|
56444
56463
|
}
|
|
56445
56464
|
else {
|
|
@@ -56453,16 +56472,26 @@ async function probeServer(options) {
|
|
|
56453
56472
|
log.info(` Listed ${result.resources.resources.length} resources`);
|
|
56454
56473
|
}
|
|
56455
56474
|
catch (error) {
|
|
56456
|
-
|
|
56475
|
+
if (isMethodNotFound(error)) {
|
|
56476
|
+
log.info(" Server does not implement resources/list");
|
|
56477
|
+
}
|
|
56478
|
+
else {
|
|
56479
|
+
log.warning(` Failed to list resources: ${error}`);
|
|
56480
|
+
}
|
|
56457
56481
|
}
|
|
56458
|
-
// Also
|
|
56482
|
+
// Also try resource templates - some servers support resources but not templates
|
|
56459
56483
|
try {
|
|
56460
56484
|
const templatesResult = await client.listResourceTemplates();
|
|
56461
56485
|
result.resourceTemplates = templatesResult;
|
|
56462
56486
|
log.info(` Listed ${result.resourceTemplates.resourceTemplates.length} resource templates`);
|
|
56463
56487
|
}
|
|
56464
56488
|
catch (error) {
|
|
56465
|
-
|
|
56489
|
+
if (isMethodNotFound(error)) {
|
|
56490
|
+
log.info(" Server does not implement resources/templates/list");
|
|
56491
|
+
}
|
|
56492
|
+
else {
|
|
56493
|
+
log.warning(` Failed to list resource templates: ${error}`);
|
|
56494
|
+
}
|
|
56466
56495
|
}
|
|
56467
56496
|
}
|
|
56468
56497
|
else {
|
|
@@ -56831,6 +56860,7 @@ function diffsToMap(diffs) {
|
|
|
56831
56860
|
|
|
56832
56861
|
|
|
56833
56862
|
|
|
56863
|
+
|
|
56834
56864
|
/**
|
|
56835
56865
|
* Parse command line arguments
|
|
56836
56866
|
*/
|
|
@@ -56839,6 +56869,9 @@ function parseCliArgs() {
|
|
|
56839
56869
|
options: {
|
|
56840
56870
|
base: { type: "string", short: "b" },
|
|
56841
56871
|
target: { type: "string", short: "t" },
|
|
56872
|
+
header: { type: "string", short: "H", multiple: true },
|
|
56873
|
+
"base-header": { type: "string", multiple: true },
|
|
56874
|
+
"target-header": { type: "string", multiple: true },
|
|
56842
56875
|
config: { type: "string", short: "c" },
|
|
56843
56876
|
output: { type: "string", short: "o", default: "summary" },
|
|
56844
56877
|
verbose: { type: "boolean", short: "v", default: false },
|
|
@@ -56864,14 +56897,18 @@ USAGE:
|
|
|
56864
56897
|
mcp-server-diff --config servers.json
|
|
56865
56898
|
|
|
56866
56899
|
OPTIONS:
|
|
56867
|
-
-b, --base <command>
|
|
56868
|
-
-t, --target <command>
|
|
56869
|
-
-
|
|
56870
|
-
|
|
56871
|
-
|
|
56872
|
-
|
|
56873
|
-
-
|
|
56874
|
-
|
|
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
|
+
--base-header <header> HTTP header for base server (repeatable)
|
|
56904
|
+
--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
|
|
56875
56912
|
|
|
56876
56913
|
CONFIG FILE FORMAT:
|
|
56877
56914
|
{
|
|
@@ -56890,6 +56927,7 @@ CONFIG FILE FORMAT:
|
|
|
56890
56927
|
}
|
|
56891
56928
|
|
|
56892
56929
|
OUTPUT FORMATS:
|
|
56930
|
+
diff - Raw diff output only
|
|
56893
56931
|
summary - One line per comparison (default)
|
|
56894
56932
|
json - Raw JSON with full diff details
|
|
56895
56933
|
markdown - Formatted markdown report
|
|
@@ -56906,6 +56944,18 @@ EXAMPLES:
|
|
|
56906
56944
|
|
|
56907
56945
|
# Output raw JSON for CI
|
|
56908
56946
|
mcp-server-diff -c servers.json -o json -q
|
|
56947
|
+
|
|
56948
|
+
# Compare with HTTP headers (for authenticated endpoints)
|
|
56949
|
+
mcp-server-diff -b "go run ./cmd/server stdio" -t "https://api.example.com/mcp" \\
|
|
56950
|
+
-H "Authorization: Bearer token" -o diff
|
|
56951
|
+
|
|
56952
|
+
# Use environment variable for secret (keeps token out of shell history)
|
|
56953
|
+
mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \\
|
|
56954
|
+
-H "Authorization: env:MY_API_TOKEN"
|
|
56955
|
+
|
|
56956
|
+
# Prompt for secret interactively (hidden input)
|
|
56957
|
+
mcp-server-diff -b "./server" -t "https://api.example.com/mcp" \\
|
|
56958
|
+
-H "Authorization: secret:"
|
|
56909
56959
|
`);
|
|
56910
56960
|
}
|
|
56911
56961
|
/**
|
|
@@ -56925,12 +56975,13 @@ function loadConfig(configPath) {
|
|
|
56925
56975
|
/**
|
|
56926
56976
|
* Create a server config from a command string
|
|
56927
56977
|
*/
|
|
56928
|
-
function commandToConfig(command, name) {
|
|
56978
|
+
function commandToConfig(command, name, headers) {
|
|
56929
56979
|
if (command.startsWith("http://") || command.startsWith("https://")) {
|
|
56930
56980
|
return {
|
|
56931
56981
|
name,
|
|
56932
56982
|
transport: "streamable-http",
|
|
56933
56983
|
server_url: command,
|
|
56984
|
+
headers,
|
|
56934
56985
|
};
|
|
56935
56986
|
}
|
|
56936
56987
|
return {
|
|
@@ -56939,6 +56990,132 @@ function commandToConfig(command, name) {
|
|
|
56939
56990
|
start_command: command,
|
|
56940
56991
|
};
|
|
56941
56992
|
}
|
|
56993
|
+
/**
|
|
56994
|
+
* Parse header strings into a record
|
|
56995
|
+
* Accepts formats: "Header: value" or "Header=value"
|
|
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 ")
|
|
57000
|
+
*/
|
|
57001
|
+
function parseHeaders(headerStrings, secretValues) {
|
|
57002
|
+
const headers = {};
|
|
57003
|
+
if (!headerStrings)
|
|
57004
|
+
return headers;
|
|
57005
|
+
for (const h of headerStrings) {
|
|
57006
|
+
const colonIdx = h.indexOf(":");
|
|
57007
|
+
const eqIdx = h.indexOf("=");
|
|
57008
|
+
const sepIdx = colonIdx > 0 ? colonIdx : eqIdx;
|
|
57009
|
+
if (sepIdx > 0) {
|
|
57010
|
+
const key = h.substring(0, sepIdx).trim();
|
|
57011
|
+
let value = h.substring(sepIdx + 1).trim();
|
|
57012
|
+
// Check for env: prefix to read from environment variable
|
|
57013
|
+
if (value.startsWith("env:")) {
|
|
57014
|
+
const envVar = value.substring(4);
|
|
57015
|
+
const envValue = process.env[envVar];
|
|
57016
|
+
if (!envValue) {
|
|
57017
|
+
throw new Error(`Environment variable ${envVar} not set (referenced in header ${key})`);
|
|
57018
|
+
}
|
|
57019
|
+
value = envValue;
|
|
57020
|
+
}
|
|
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
|
+
}
|
|
57032
|
+
}
|
|
57033
|
+
}
|
|
57034
|
+
headers[key] = value;
|
|
57035
|
+
}
|
|
57036
|
+
}
|
|
57037
|
+
return headers;
|
|
57038
|
+
}
|
|
57039
|
+
/**
|
|
57040
|
+
* Find secrets that need prompts, returns array of {name, label} objects
|
|
57041
|
+
*/
|
|
57042
|
+
function findSecrets(headerStrings) {
|
|
57043
|
+
const secrets = [];
|
|
57044
|
+
if (!headerStrings)
|
|
57045
|
+
return secrets;
|
|
57046
|
+
for (const h of headerStrings) {
|
|
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 });
|
|
57053
|
+
}
|
|
57054
|
+
}
|
|
57055
|
+
}
|
|
57056
|
+
return secrets;
|
|
57057
|
+
}
|
|
57058
|
+
/**
|
|
57059
|
+
* Prompt for a secret value with hidden input
|
|
57060
|
+
*/
|
|
57061
|
+
async function promptSecret(prompt) {
|
|
57062
|
+
return new Promise((resolve) => {
|
|
57063
|
+
const rl = external_readline_namespaceObject.createInterface({
|
|
57064
|
+
input: process.stdin,
|
|
57065
|
+
output: process.stdout,
|
|
57066
|
+
});
|
|
57067
|
+
// Hide input by using raw mode if available
|
|
57068
|
+
if (process.stdin.isTTY) {
|
|
57069
|
+
process.stdout.write(`${prompt}: `);
|
|
57070
|
+
process.stdin.setRawMode(true);
|
|
57071
|
+
process.stdin.resume();
|
|
57072
|
+
let value = "";
|
|
57073
|
+
const onData = (char) => {
|
|
57074
|
+
const c = char.toString();
|
|
57075
|
+
if (c === "\n" || c === "\r") {
|
|
57076
|
+
process.stdin.setRawMode(false);
|
|
57077
|
+
process.stdin.removeListener("data", onData);
|
|
57078
|
+
rl.close();
|
|
57079
|
+
process.stdout.write("\n");
|
|
57080
|
+
resolve(value);
|
|
57081
|
+
}
|
|
57082
|
+
else if (c === "\u0003") {
|
|
57083
|
+
// Ctrl+C
|
|
57084
|
+
process.exit(1);
|
|
57085
|
+
}
|
|
57086
|
+
else if (c === "\u007F" || c === "\b") {
|
|
57087
|
+
// Backspace
|
|
57088
|
+
if (value.length > 0) {
|
|
57089
|
+
value = value.slice(0, -1);
|
|
57090
|
+
}
|
|
57091
|
+
}
|
|
57092
|
+
else {
|
|
57093
|
+
value += c;
|
|
57094
|
+
}
|
|
57095
|
+
};
|
|
57096
|
+
process.stdin.on("data", onData);
|
|
57097
|
+
}
|
|
57098
|
+
else {
|
|
57099
|
+
// Non-TTY: just read the line (won't be hidden)
|
|
57100
|
+
rl.question(`${prompt}: `, (answer) => {
|
|
57101
|
+
rl.close();
|
|
57102
|
+
resolve(answer);
|
|
57103
|
+
});
|
|
57104
|
+
}
|
|
57105
|
+
});
|
|
57106
|
+
}
|
|
57107
|
+
/**
|
|
57108
|
+
* Prompt for secret values with hidden input
|
|
57109
|
+
*/
|
|
57110
|
+
async function promptSecrets(secrets) {
|
|
57111
|
+
const values = new Map();
|
|
57112
|
+
if (secrets.length === 0)
|
|
57113
|
+
return values;
|
|
57114
|
+
for (const { name, label } of secrets) {
|
|
57115
|
+
values.set(name, await promptSecret(`Enter ${label}`));
|
|
57116
|
+
}
|
|
57117
|
+
return values;
|
|
57118
|
+
}
|
|
56942
57119
|
/**
|
|
56943
57120
|
* Probe a server and return results
|
|
56944
57121
|
*/
|
|
@@ -57027,6 +57204,23 @@ async function runComparisons(config) {
|
|
|
57027
57204
|
}
|
|
57028
57205
|
return results;
|
|
57029
57206
|
}
|
|
57207
|
+
/**
|
|
57208
|
+
* Output raw diff only
|
|
57209
|
+
*/
|
|
57210
|
+
function outputDiff(results) {
|
|
57211
|
+
for (const result of results) {
|
|
57212
|
+
if (result.diffs.length > 0) {
|
|
57213
|
+
if (results.length > 1) {
|
|
57214
|
+
console.log(`# ${result.target}`);
|
|
57215
|
+
}
|
|
57216
|
+
for (const { endpoint, diff } of result.diffs) {
|
|
57217
|
+
console.log(`## ${endpoint}`);
|
|
57218
|
+
console.log(diff);
|
|
57219
|
+
console.log("");
|
|
57220
|
+
}
|
|
57221
|
+
}
|
|
57222
|
+
}
|
|
57223
|
+
}
|
|
57030
57224
|
/**
|
|
57031
57225
|
* Output results in summary format
|
|
57032
57226
|
*/
|
|
@@ -57133,7 +57327,7 @@ async function main() {
|
|
|
57133
57327
|
process.exit(0);
|
|
57134
57328
|
}
|
|
57135
57329
|
if (values.version) {
|
|
57136
|
-
console.log("mcp-server-diff v2.1.
|
|
57330
|
+
console.log("mcp-server-diff v2.1.1");
|
|
57137
57331
|
process.exit(0);
|
|
57138
57332
|
}
|
|
57139
57333
|
// Set up logger - CLI uses console logger by default
|
|
@@ -57148,9 +57342,21 @@ async function main() {
|
|
|
57148
57342
|
config = loadConfig(values.config);
|
|
57149
57343
|
}
|
|
57150
57344
|
else if (values.base && values.target) {
|
|
57345
|
+
// Combine -H and --target-header for target, use --base-header for base
|
|
57346
|
+
const baseHeaderStrings = values["base-header"];
|
|
57347
|
+
const targetHeaderStrings = [
|
|
57348
|
+
...(values.header || []),
|
|
57349
|
+
...(values["target-header"] || []),
|
|
57350
|
+
];
|
|
57351
|
+
// Find all secrets needed from both header sets
|
|
57352
|
+
const allHeaderStrings = [...(baseHeaderStrings || []), ...targetHeaderStrings];
|
|
57353
|
+
const secrets = findSecrets(allHeaderStrings);
|
|
57354
|
+
const secretValues = await promptSecrets(secrets);
|
|
57355
|
+
const baseHeaders = parseHeaders(baseHeaderStrings, secretValues);
|
|
57356
|
+
const targetHeaders = parseHeaders(targetHeaderStrings.length > 0 ? targetHeaderStrings : undefined, secretValues);
|
|
57151
57357
|
config = {
|
|
57152
|
-
base: commandToConfig(values.base, "base"),
|
|
57153
|
-
targets: [commandToConfig(values.target, "target")],
|
|
57358
|
+
base: commandToConfig(values.base, "base", baseHeaders),
|
|
57359
|
+
targets: [commandToConfig(values.target, "target", targetHeaders)],
|
|
57154
57360
|
};
|
|
57155
57361
|
}
|
|
57156
57362
|
else {
|
|
@@ -57161,6 +57367,9 @@ async function main() {
|
|
|
57161
57367
|
const results = await runComparisons(config);
|
|
57162
57368
|
const outputFormat = values.output || "summary";
|
|
57163
57369
|
switch (outputFormat) {
|
|
57370
|
+
case "diff":
|
|
57371
|
+
outputDiff(results);
|
|
57372
|
+
break;
|
|
57164
57373
|
case "json":
|
|
57165
57374
|
outputJson(results);
|
|
57166
57375
|
break;
|
package/dist/index.js
CHANGED
|
@@ -56605,6 +56605,13 @@ const log = {
|
|
|
56605
56605
|
|
|
56606
56606
|
|
|
56607
56607
|
|
|
56608
|
+
/**
|
|
56609
|
+
* Check if an error is "Method not found" (-32601)
|
|
56610
|
+
*/
|
|
56611
|
+
function isMethodNotFound(error) {
|
|
56612
|
+
const errorStr = String(error);
|
|
56613
|
+
return errorStr.includes("-32601") || errorStr.includes("Method not found");
|
|
56614
|
+
}
|
|
56608
56615
|
/**
|
|
56609
56616
|
* Probes an MCP server and returns capability snapshots
|
|
56610
56617
|
*/
|
|
@@ -56677,7 +56684,12 @@ async function probeServer(options) {
|
|
|
56677
56684
|
log.info(` Listed ${result.tools.tools.length} tools`);
|
|
56678
56685
|
}
|
|
56679
56686
|
catch (error) {
|
|
56680
|
-
|
|
56687
|
+
if (isMethodNotFound(error)) {
|
|
56688
|
+
log.info(" Server does not implement tools/list");
|
|
56689
|
+
}
|
|
56690
|
+
else {
|
|
56691
|
+
log.warning(` Failed to list tools: ${error}`);
|
|
56692
|
+
}
|
|
56681
56693
|
}
|
|
56682
56694
|
}
|
|
56683
56695
|
else {
|
|
@@ -56691,7 +56703,12 @@ async function probeServer(options) {
|
|
|
56691
56703
|
log.info(` Listed ${result.prompts.prompts.length} prompts`);
|
|
56692
56704
|
}
|
|
56693
56705
|
catch (error) {
|
|
56694
|
-
|
|
56706
|
+
if (isMethodNotFound(error)) {
|
|
56707
|
+
log.info(" Server does not implement prompts/list");
|
|
56708
|
+
}
|
|
56709
|
+
else {
|
|
56710
|
+
log.warning(` Failed to list prompts: ${error}`);
|
|
56711
|
+
}
|
|
56695
56712
|
}
|
|
56696
56713
|
}
|
|
56697
56714
|
else {
|
|
@@ -56705,16 +56722,26 @@ async function probeServer(options) {
|
|
|
56705
56722
|
log.info(` Listed ${result.resources.resources.length} resources`);
|
|
56706
56723
|
}
|
|
56707
56724
|
catch (error) {
|
|
56708
|
-
|
|
56725
|
+
if (isMethodNotFound(error)) {
|
|
56726
|
+
log.info(" Server does not implement resources/list");
|
|
56727
|
+
}
|
|
56728
|
+
else {
|
|
56729
|
+
log.warning(` Failed to list resources: ${error}`);
|
|
56730
|
+
}
|
|
56709
56731
|
}
|
|
56710
|
-
// Also
|
|
56732
|
+
// Also try resource templates - some servers support resources but not templates
|
|
56711
56733
|
try {
|
|
56712
56734
|
const templatesResult = await client.listResourceTemplates();
|
|
56713
56735
|
result.resourceTemplates = templatesResult;
|
|
56714
56736
|
log.info(` Listed ${result.resourceTemplates.resourceTemplates.length} resource templates`);
|
|
56715
56737
|
}
|
|
56716
56738
|
catch (error) {
|
|
56717
|
-
|
|
56739
|
+
if (isMethodNotFound(error)) {
|
|
56740
|
+
log.info(" Server does not implement resources/templates/list");
|
|
56741
|
+
}
|
|
56742
|
+
else {
|
|
56743
|
+
log.warning(` Failed to list resource templates: ${error}`);
|
|
56744
|
+
}
|
|
56718
56745
|
}
|
|
56719
56746
|
}
|
|
56720
56747
|
else {
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-diff",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"description": "Diff MCP server public interfaces - CLI tool and GitHub Action",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcp-server-diff": "dist/cli/index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
9
14
|
"type": "module",
|
|
10
15
|
"scripts": {
|
|
11
16
|
"build": "npm run build:action && npm run build:cli",
|
package/.github/dependabot.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
updates:
|
|
3
|
-
- package-ecosystem: "github-actions"
|
|
4
|
-
directory: "/"
|
|
5
|
-
schedule:
|
|
6
|
-
interval: "weekly"
|
|
7
|
-
commit-message:
|
|
8
|
-
prefix: "ci"
|
|
9
|
-
labels:
|
|
10
|
-
- "dependencies"
|
|
11
|
-
- "github-actions"
|
|
12
|
-
|
|
13
|
-
- package-ecosystem: "npm"
|
|
14
|
-
directory: "/probe"
|
|
15
|
-
schedule:
|
|
16
|
-
interval: "weekly"
|
|
17
|
-
commit-message:
|
|
18
|
-
prefix: "deps"
|
|
19
|
-
labels:
|
|
20
|
-
- "dependencies"
|
|
21
|
-
- "javascript"
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
test:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
strategy:
|
|
16
|
-
matrix:
|
|
17
|
-
node-version: [20, 22]
|
|
18
|
-
|
|
19
|
-
steps:
|
|
20
|
-
- name: Checkout
|
|
21
|
-
uses: actions/checkout@v4
|
|
22
|
-
|
|
23
|
-
- name: Setup Node.js ${{ matrix.node-version }}
|
|
24
|
-
uses: actions/setup-node@v6
|
|
25
|
-
with:
|
|
26
|
-
node-version: ${{ matrix.node-version }}
|
|
27
|
-
cache: 'npm'
|
|
28
|
-
|
|
29
|
-
- name: Install dependencies
|
|
30
|
-
run: npm ci
|
|
31
|
-
|
|
32
|
-
- name: Type Check
|
|
33
|
-
run: npm run typecheck
|
|
34
|
-
|
|
35
|
-
- name: Lint
|
|
36
|
-
run: npm run lint
|
|
37
|
-
|
|
38
|
-
- name: Check Formatting
|
|
39
|
-
run: npm run format:check
|
|
40
|
-
|
|
41
|
-
- name: Run Tests
|
|
42
|
-
run: npm test
|
|
43
|
-
|
|
44
|
-
- name: Build
|
|
45
|
-
run: npm run build
|
|
46
|
-
|
|
47
|
-
- name: Verify dist is up to date
|
|
48
|
-
if: matrix.node-version == 20
|
|
49
|
-
run: |
|
|
50
|
-
# Check if dist files are up to date
|
|
51
|
-
git diff --exit-code dist/ || (echo "::error::dist/ is out of date. Run 'npm run build' and commit the changes." && exit 1)
|