@weiseer/mcp-doctor 0.1.0
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 +80 -0
- package/bin/mcp-doctor.js +130 -0
- package/lib/scan.js +47 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @weiseer/mcp-doctor
|
|
2
|
+
|
|
3
|
+
> Install-time trust gate for MCP servers. PASS / WARN / BLOCK + cited evidence.
|
|
4
|
+
|
|
5
|
+
Part of [weiseer](https://github.com/weiseer). Probe **P-010**.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
Scans MCP server packages (or your entire `claude_desktop_config.json` / `cline_config.json`) and tells you which ones to trust before you install or connect to them.
|
|
10
|
+
|
|
11
|
+
Verdict is one of:
|
|
12
|
+
- **PASS** — no significant supply-chain or vulnerability signals
|
|
13
|
+
- **WARN** — material concerns; review before installing
|
|
14
|
+
- **BLOCK** — critical issue (known CVE, typosquat, hardcoded credentials, etc.)
|
|
15
|
+
|
|
16
|
+
All scoring is **open-source and rule-based** — see [rubric.yaml](./rubric.yaml). You can argue with our methodology; we'd rather you do that than trust a black-box ML model.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @weiseer/mcp-doctor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or run on-demand:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @weiseer/mcp-doctor @modelcontextprotocol/server-github
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Common uses
|
|
31
|
+
|
|
32
|
+
**1. Check a single package before installing:**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @weiseer/mcp-doctor @some/mcp-server
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**2. Audit your existing MCP config:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @weiseer/mcp-doctor --config ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**3. CI integration (block bad MCPs in PR):**
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
- uses: weiseer/mcp-doctor-action@v1
|
|
48
|
+
with:
|
|
49
|
+
packages: '@x/server-foo @y/server-bar'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**4. README trust badge:**
|
|
53
|
+
|
|
54
|
+
```md
|
|
55
|
+

|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## What gets scored
|
|
59
|
+
|
|
60
|
+
- **Supply chain hygiene** — postinstall scripts, unpinned deps, missing provenance, repo URL integrity
|
|
61
|
+
- **Maintainer health** — release cadence, archive status, bus factor, GitHub last-push age
|
|
62
|
+
- **Known vulnerabilities** — direct + transitive CVE via OSV.dev
|
|
63
|
+
- **MCP-specific risk** — typosquat against official servers, hardcoded credentials, capability misdeclaration
|
|
64
|
+
|
|
65
|
+
Full rubric: [rubric.yaml](./rubric.yaml). Open-source by design.
|
|
66
|
+
|
|
67
|
+
## Why this exists
|
|
68
|
+
|
|
69
|
+
The MCP ecosystem has a security crisis. MCPwn (CVE-2026-33032, CVSS 9.8) exposed 2,600+ instances. The Shai-Hulud npm worm stole MCP auth tokens from 172 packages. MCPSafe found high-severity bugs in *official* MCP servers from Atlassian, GitHub, Cloudflare, Microsoft. Bumblebee shipped from Perplexity in May 2026 specifically because supply-chain scanning was missing for MCP.
|
|
70
|
+
|
|
71
|
+
We agree the problem is real and decided to ship a developer-friendly install gate that fits the existing MCP workflow rather than reinventing it.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Apache-2.0.
|
|
76
|
+
|
|
77
|
+
## Related
|
|
78
|
+
|
|
79
|
+
- Open-source rubric: [rubric.yaml](./rubric.yaml)
|
|
80
|
+
- 9 other weiseer MCP services: [github.com/weiseer](https://github.com/weiseer)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @weiseer/mcp-doctor — install-time trust gate CLI for MCP servers.
|
|
4
|
+
* v0.1.0 — Day 3 ship.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx @weiseer/mcp-doctor <package1> [<package2> ...]
|
|
8
|
+
* npx @weiseer/mcp-doctor --config /path/to/claude_desktop_config.json
|
|
9
|
+
* npx @weiseer/mcp-doctor --json
|
|
10
|
+
*
|
|
11
|
+
* Exits non-zero on any BLOCK — useful for CI integration.
|
|
12
|
+
* License: Apache-2.0. P-010.
|
|
13
|
+
*/
|
|
14
|
+
import { scanPackages } from "../lib/scan.js";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { argv, exit, stderr } from "node:process";
|
|
17
|
+
|
|
18
|
+
function parseArgs(args) {
|
|
19
|
+
const out = { packages: [], json: false, configPath: null, registry: null };
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const a = args[i];
|
|
22
|
+
if (a === "--json") out.json = true;
|
|
23
|
+
else if (a === "--config") { out.configPath = args[++i]; }
|
|
24
|
+
else if (a === "--registry") { out.registry = args[++i]; }
|
|
25
|
+
else if (a === "--help" || a === "-h") {
|
|
26
|
+
printHelp();
|
|
27
|
+
exit(0);
|
|
28
|
+
} else if (a.startsWith("--")) {
|
|
29
|
+
stderr.write(`unknown flag: ${a}\n`);
|
|
30
|
+
exit(2);
|
|
31
|
+
} else {
|
|
32
|
+
out.packages.push(a);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printHelp() {
|
|
39
|
+
const txt = `mcp-doctor — install-time trust gate for MCP servers.
|
|
40
|
+
|
|
41
|
+
USAGE
|
|
42
|
+
mcp-doctor <package1> [<package2> ...]
|
|
43
|
+
mcp-doctor --config <path-to-claude_desktop_config.json>
|
|
44
|
+
mcp-doctor --json
|
|
45
|
+
|
|
46
|
+
Open-source rubric: https://github.com/weiseer/mcp-doctor/blob/main/rubric.yaml
|
|
47
|
+
|
|
48
|
+
EXIT CODES
|
|
49
|
+
0 = all PASS or WARN
|
|
50
|
+
1 = at least one BLOCK
|
|
51
|
+
2 = invalid usage
|
|
52
|
+
`;
|
|
53
|
+
process.stdout.write(txt);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function packagesFromConfig(configPath) {
|
|
57
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
58
|
+
const mcpServers = raw.mcpServers || {};
|
|
59
|
+
const pkgs = new Set();
|
|
60
|
+
for (const [, srv] of Object.entries(mcpServers)) {
|
|
61
|
+
// Detect npx -y <pkg> args
|
|
62
|
+
if (srv.command === "npx") {
|
|
63
|
+
const args = srv.args || [];
|
|
64
|
+
// skip -y flags, pick first non-flag arg
|
|
65
|
+
for (const a of args) {
|
|
66
|
+
if (a.startsWith("-")) continue;
|
|
67
|
+
pkgs.add(a);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...pkgs];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function emoji(v) {
|
|
76
|
+
return ({ PASS: "✓", WARN: "⚠", BLOCK: "✗", ERROR: "?" })[v] || "?";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderHuman(r) {
|
|
80
|
+
const head = `${emoji(r.verdict)} ${r.verdict}: ${r.package}@${r.version || "?"} (score ${r.score}/100)${r.self_disclosure ? " [self-disclosed]" : ""}`;
|
|
81
|
+
if (r.error) return head + `\n ERROR: ${r.error}`;
|
|
82
|
+
const sigs = (r.triggered_signals || []).map(s => ` -${s.deduct}${s.hard_block ? " HARD" : ""} ${s.signal_id}: ${s.evidence}`);
|
|
83
|
+
const m = r.metadata || {};
|
|
84
|
+
const tail = ` m=${m.maintainer_count} dsr=${m.days_since_release} gh_dsp=${m.github_days_since_push} stars=${m.github_stars} deps=${m.dep_count} osv=${m.osv_vuln_count} lic=${m.license}`;
|
|
85
|
+
return [head, ...sigs, tail].join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
const args = parseArgs(argv.slice(2));
|
|
90
|
+
let packages = args.packages.slice();
|
|
91
|
+
|
|
92
|
+
if (args.configPath) {
|
|
93
|
+
try {
|
|
94
|
+
packages = packages.concat(packagesFromConfig(args.configPath));
|
|
95
|
+
} catch (e) {
|
|
96
|
+
stderr.write(`config parse failed: ${e.message}\n`);
|
|
97
|
+
exit(2);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (packages.length === 0) {
|
|
102
|
+
printHelp();
|
|
103
|
+
exit(2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Day 3 strategy: scanner is the Python engine. For v0.1 ship, the Node CLI
|
|
107
|
+
// either (a) executes a hosted endpoint or (b) wraps a bundled scanning core.
|
|
108
|
+
// Day 3 takes the simplest correct path: call the public scoring endpoint.
|
|
109
|
+
//
|
|
110
|
+
// For users who want fully-offline: --registry file:///path/to/bundled-db.json
|
|
111
|
+
// will land in Day 5.
|
|
112
|
+
const results = await scanPackages(packages, { registry: args.registry });
|
|
113
|
+
|
|
114
|
+
if (args.json) {
|
|
115
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
116
|
+
} else {
|
|
117
|
+
for (const r of results) {
|
|
118
|
+
process.stdout.write(renderHuman(r) + "\n\n");
|
|
119
|
+
}
|
|
120
|
+
const counts = results.reduce((acc, r) => { acc[r.verdict] = (acc[r.verdict] || 0) + 1; return acc; }, {});
|
|
121
|
+
process.stdout.write(`summary: ${JSON.stringify(counts)}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (results.some(r => r.verdict === "BLOCK")) exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main().catch(err => {
|
|
128
|
+
stderr.write(`mcp-doctor: fatal: ${err.message}\n`);
|
|
129
|
+
exit(2);
|
|
130
|
+
});
|
package/lib/scan.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan dispatcher for mcp-doctor CLI.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 (Day 3): calls the public scoring endpoint at weiseer-mcp-doctor backend.
|
|
5
|
+
* v0.2+ (Day 5): bundles trust DB + rubric for fully-offline scan.
|
|
6
|
+
*
|
|
7
|
+
* The hosted endpoint runs the same open-source rubric (rubric.yaml in this repo).
|
|
8
|
+
* The endpoint is rate-limited free / no auth required, capped at 1000 calls/day/IP.
|
|
9
|
+
*/
|
|
10
|
+
const ENDPOINT = process.env.MCP_DOCTOR_ENDPOINT || "https://oracle.weiseer.com/mcp-doctor/scan";
|
|
11
|
+
const LOCAL_FALLBACK = process.env.MCP_DOCTOR_LOCAL_FALLBACK === "1";
|
|
12
|
+
|
|
13
|
+
async function scanOnePackage(pkg) {
|
|
14
|
+
const url = new URL(ENDPOINT);
|
|
15
|
+
url.searchParams.set("pkg", pkg);
|
|
16
|
+
try {
|
|
17
|
+
const ctrl = new AbortController();
|
|
18
|
+
const t = setTimeout(() => ctrl.abort(), 15000);
|
|
19
|
+
const res = await fetch(url, { signal: ctrl.signal, headers: { "User-Agent": "weiseer-mcp-doctor-cli/0.1.0" } });
|
|
20
|
+
clearTimeout(t);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
return {
|
|
23
|
+
package: pkg, version: "", verdict: "ERROR", score: 0,
|
|
24
|
+
error: `endpoint http ${res.status}`,
|
|
25
|
+
triggered_signals: [], metadata: {},
|
|
26
|
+
scanned_at: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return await res.json();
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return {
|
|
32
|
+
package: pkg, version: "", verdict: "ERROR", score: 0,
|
|
33
|
+
error: `endpoint unreachable: ${e.message}`,
|
|
34
|
+
triggered_signals: [], metadata: {},
|
|
35
|
+
scanned_at: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function scanPackages(packages, opts = {}) {
|
|
41
|
+
const results = [];
|
|
42
|
+
// Sequential for now; the rate-limit per IP is generous but we are polite.
|
|
43
|
+
for (const pkg of packages) {
|
|
44
|
+
results.push(await scanOnePackage(pkg));
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weiseer/mcp-doctor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.weiseer/mcp-doctor",
|
|
5
|
+
"description": "Install-time trust gate for MCP servers — PASS/WARN/BLOCK + cited evidence + GitHub Action support.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-doctor": "./bin/mcp-doctor.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./lib/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"rubric.yaml",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/mcp-doctor.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"security",
|
|
24
|
+
"trust",
|
|
25
|
+
"supply-chain",
|
|
26
|
+
"vulnerability",
|
|
27
|
+
"audit",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"weiseer"
|
|
30
|
+
],
|
|
31
|
+
"author": "weiseer <wei@weiseer.com>",
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/weiseer/mcp-doctor.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/weiseer/mcp-doctor#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/weiseer/mcp-doctor/issues"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|