mcptrustchecker 1.0.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +379 -0
  3. package/dist/acquire/clientConfig.d.ts +47 -0
  4. package/dist/acquire/clientConfig.js +124 -0
  5. package/dist/acquire/discover.d.ts +12 -0
  6. package/dist/acquire/discover.js +53 -0
  7. package/dist/acquire/index.d.ts +24 -0
  8. package/dist/acquire/index.js +175 -0
  9. package/dist/acquire/live.d.ts +47 -0
  10. package/dist/acquire/live.js +357 -0
  11. package/dist/acquire/manifest.d.ts +10 -0
  12. package/dist/acquire/manifest.js +94 -0
  13. package/dist/acquire/npm.d.ts +10 -0
  14. package/dist/acquire/npm.js +55 -0
  15. package/dist/acquire/source.d.ts +16 -0
  16. package/dist/acquire/source.js +109 -0
  17. package/dist/cli/index.d.ts +15 -0
  18. package/dist/cli/index.js +396 -0
  19. package/dist/config.d.ts +15 -0
  20. package/dist/config.js +81 -0
  21. package/dist/data/capabilityLexicon.d.ts +51 -0
  22. package/dist/data/capabilityLexicon.js +204 -0
  23. package/dist/data/confusables.d.ts +17 -0
  24. package/dist/data/confusables.js +65 -0
  25. package/dist/data/injectionPatterns.d.ts +55 -0
  26. package/dist/data/injectionPatterns.js +216 -0
  27. package/dist/data/knownCves.d.ts +24 -0
  28. package/dist/data/knownCves.js +49 -0
  29. package/dist/data/protectedPackages.d.ts +30 -0
  30. package/dist/data/protectedPackages.js +89 -0
  31. package/dist/data/ruleCatalog.d.ts +16 -0
  32. package/dist/data/ruleCatalog.js +94 -0
  33. package/dist/data/sourcePatterns.d.ts +25 -0
  34. package/dist/data/sourcePatterns.js +92 -0
  35. package/dist/data/unicode.d.ts +34 -0
  36. package/dist/data/unicode.js +99 -0
  37. package/dist/detectors/capability.d.ts +14 -0
  38. package/dist/detectors/capability.js +281 -0
  39. package/dist/detectors/collision.d.ts +11 -0
  40. package/dist/detectors/collision.js +63 -0
  41. package/dist/detectors/flowGraph.d.ts +43 -0
  42. package/dist/detectors/flowGraph.js +100 -0
  43. package/dist/detectors/index.d.ts +18 -0
  44. package/dist/detectors/index.js +26 -0
  45. package/dist/detectors/injection.d.ts +9 -0
  46. package/dist/detectors/injection.js +178 -0
  47. package/dist/detectors/meta.d.ts +8 -0
  48. package/dist/detectors/meta.js +33 -0
  49. package/dist/detectors/posture.d.ts +9 -0
  50. package/dist/detectors/posture.js +128 -0
  51. package/dist/detectors/source.d.ts +12 -0
  52. package/dist/detectors/source.js +87 -0
  53. package/dist/detectors/supplyChain.d.ts +17 -0
  54. package/dist/detectors/supplyChain.js +243 -0
  55. package/dist/detectors/toxicFlow.d.ts +22 -0
  56. package/dist/detectors/toxicFlow.js +201 -0
  57. package/dist/detectors/unicode.d.ts +21 -0
  58. package/dist/detectors/unicode.js +244 -0
  59. package/dist/engine.d.ts +27 -0
  60. package/dist/engine.js +135 -0
  61. package/dist/index.d.ts +38 -0
  62. package/dist/index.js +46 -0
  63. package/dist/lockfile.d.ts +30 -0
  64. package/dist/lockfile.js +87 -0
  65. package/dist/policy.d.ts +13 -0
  66. package/dist/policy.js +48 -0
  67. package/dist/report/badge.d.ts +14 -0
  68. package/dist/report/badge.js +18 -0
  69. package/dist/report/json.d.ts +4 -0
  70. package/dist/report/json.js +4 -0
  71. package/dist/report/markdown.d.ts +6 -0
  72. package/dist/report/markdown.js +110 -0
  73. package/dist/report/sarif.d.ts +7 -0
  74. package/dist/report/sarif.js +105 -0
  75. package/dist/report/terminal.d.ts +15 -0
  76. package/dist/report/terminal.js +252 -0
  77. package/dist/scoring/capability.d.ts +10 -0
  78. package/dist/scoring/capability.js +48 -0
  79. package/dist/scoring/index.d.ts +10 -0
  80. package/dist/scoring/index.js +96 -0
  81. package/dist/scoring/model.d.ts +50 -0
  82. package/dist/scoring/model.js +101 -0
  83. package/dist/types.d.ts +361 -0
  84. package/dist/types.js +12 -0
  85. package/dist/util/ansi.d.ts +26 -0
  86. package/dist/util/ansi.js +42 -0
  87. package/dist/util/capabilities.d.ts +17 -0
  88. package/dist/util/capabilities.js +68 -0
  89. package/dist/util/distance.d.ts +23 -0
  90. package/dist/util/distance.js +119 -0
  91. package/dist/util/hash.d.ts +20 -0
  92. package/dist/util/hash.js +80 -0
  93. package/dist/util/text.d.ts +45 -0
  94. package/dist/util/text.js +170 -0
  95. package/dist/version.d.ts +13 -0
  96. package/dist/version.js +13 -0
  97. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Illia Haidar · https://mcptrustchecker.com · support@mcptrustchecker.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,379 @@
1
+ <div align="center">
2
+
3
+ # 🛡️ MCP Trust Checker
4
+
5
+ ### The local-first, deterministic security scanner for MCP servers
6
+
7
+ **Know whether a Model Context Protocol server is safe *before* you connect it to your data.**
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+ [![Node](https://img.shields.io/badge/node-%E2%89%A520-3c873a.svg)](package.json)
11
+ [![Methodology](https://img.shields.io/badge/methodology-mcptrustchecker--1.0-6f42c1.svg)](docs/methodology.md)
12
+ [![Tests](https://img.shields.io/badge/tests-211%20passing-brightgreen.svg)](test)
13
+ [![Rules](https://img.shields.io/badge/rules-74-orange.svg)](docs/rules.md)
14
+ [![No account](https://img.shields.io/badge/account-not%20required-brightgreen.svg)](#why-this-is-different)
15
+ [![Offline](https://img.shields.io/badge/runs-100%25%20offline-brightgreen.svg)](#why-this-is-different)
16
+
17
+ <br/>
18
+
19
+ <img src="docs/hero.svg" alt="MCP Trust Checker terminal output: an MCP server graded D with a detected toxic-flow trifecta" width="720"/>
20
+
21
+ <br/>
22
+
23
+ ```bash
24
+ npx mcptrustchecker # 🔍 scan every MCP server you already have installed — zero config
25
+ ```
26
+
27
+ <sub>· offline · deterministic · no account · no API key · <a href="#the-algorithm-the-capability-flow-trust-model">one novel core</a> ·</sub>
28
+
29
+ </div>
30
+
31
+ ---
32
+
33
+ ## What makes the algorithm unique
34
+
35
+ The **Capability-Flow Trust Model** (methodology `mcptrustchecker-1.0`) is an **original algorithm designed from scratch for this project** by [Illia Haidar](https://github.com/illiahaidar) — it is not a wrapper around, or derivative of, any existing scanner or methodology. It is named, versioned, fully specified in [docs/methodology.md](docs/methodology.md), and citable via [CITATION.cff](CITATION.cff).
36
+
37
+ MCP Trust Checker scores an MCP server the way an **attacker** reasons about it — not as a bag of regex hits, but as a **Capability-Flow Trust Model**. Every tool is reduced to the roles it can actually play — *untrusted-input ingress*, *sensitive-data source*, *external / exec sink* — derived from behavior, **never** from the server's own (attacker-controllable) annotations. Those roles are wired into a **cross-tool toxic-flow graph** that hunts the *lethal trifecta*: the moment untrusted content, private data, and an exfiltration path co-exist in one agent session — whether inside a single tool or composed across several tools plus the client's built-ins. That is the exact shape behind real-world MCP data-exfiltration exploits, and MCP Trust Checker proves the primitive exists **statically**, with an honest confidence split so a single-tool completion reads `confirmed` and a cross-tool composition reads `strong` — never overclaiming.
38
+
39
+ Three more things sit under that graph:
40
+
41
+ - **It decodes, not strips.** Unicode Tags-block and variation-selector payloads are *recovered and printed back to you as evidence*, so a hidden "read `~/.ssh/id_rsa` and BCC the attacker" becomes visible text instead of a silent flag.
42
+ - **It has rug-pull integrity built in.** The canonical surface is hashed and pinned; any post-approval mutation of a tool definition trips a `confirmed` finding with a per-tool diff.
43
+ - **Every point of the 0–100 Trust Score is an auditable, deterministic penalty vector** — fixed severity weights, a confidence multiplier, diminishing returns, per-category caps, and weakest-link gates. Fully reconstructable, identical on every run, gameable by no one. **No LLM in the loop, no telemetry, no account.**
44
+
45
+ > And it is **comprehensive by design**: the full catalog of known MCP attack techniques is covered in one offline pass — no API key, no LLM. [See the full coverage map ↓](#coverage-the-full-catalog-of-mcp-attack-techniques)
46
+
47
+ **Jump to:** [Why it's different](#why-this-is-different) · [Quick start](#quick-start) · [The algorithm](#the-algorithm-the-capability-flow-trust-model) · [Coverage](#coverage-the-full-catalog-of-mcp-attack-techniques) · [Scoring](#the-trust-score-is-auditable-by-construction) · [Embed as a library](#embed-the-exact-same-engine-marketplaces--platforms) · [CI / GitHub](#ci--github-integration) · [Rules](docs/rules.md)
48
+
49
+ ---
50
+
51
+ ## The one question millions of MCP users can't answer
52
+
53
+ > **"Is this server safe to give access to my files, my tokens, and my conversations?"**
54
+
55
+ An MCP server hands an AI assistant a set of *tools*. Those tool descriptions are read by the model, not by you — a perfect place to hide instructions. A single server that can *read a file* and *make an HTTP request* is already a data-exfiltration weapon. And a server can look harmless on day one, then silently redefine its tools after you approve it. MCP Trust Checker turns all of that into a transparent letter grade you can act on.
56
+
57
+ ```
58
+ ╭────────────╮
59
+ │ GRADE D │ Trust Score 69/100
60
+ ╰────────────╯ methodology mcptrustchecker-1.0
61
+
62
+ Toxic flows (untrusted-input → sensitive-source → external-sink)
63
+ [critical] The three trifecta roles are co-present across tools;
64
+ a prompt-injected agent can chain them.
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Why this is different
70
+
71
+ MCP Trust Checker's wedge is **accuracy + explainability + privacy**, with one genuinely novel core — the cross-tool toxic-flow graph. Every property below holds together in a single offline binary — no account, no LLM in the loop, no telemetry:
72
+
73
+ - 🔒 **Runs 100% offline** — no account, token, API key, or hosted service; your data never leaves the machine.
74
+ - 🎯 **Deterministic** — same input ⇒ byte-identical score, on every run and every machine.
75
+ - 🕸️ **Cross-tool toxic-flow graph** — proves the lethal trifecta statically, composed across tools, not just within one.
76
+ - 🔬 **Reads the code, not just the claim** — when the server's source is available (`scan ./path`), it grades what the implementation *does* (eval / shell-spawn / hardcoded egress / credential reads / obfuscated payloads), so a poisoned server can't hide behind honest-looking tool descriptions. Metadata **and** implementation, in one deterministic pass.
77
+ - 🔎 **Decodes, not strips** — hidden Unicode payloads (Tags block / variation-selector) are recovered and shown as evidence.
78
+ - 📌 **Rug-pull integrity** — the full tool surface is hashed and pinned; any post-approval drift trips a `confirmed` finding with a per-tool diff.
79
+ - 🧾 **Auditable Trust Score** — every point is a published, reproducible penalty vector.
80
+ - ⚙️ **SARIF + GitHub Action + CI gates** — machine-readable output and pass/fail thresholds out of the box.
81
+ - 📦 **Embeddable library** — the identical, versioned engine a marketplace can reuse on-site.
82
+ - 🪶 **MIT, plain-data rules** — every rule is transparent and contribution-friendly.
83
+
84
+ ---
85
+
86
+ ## Measured accuracy
87
+
88
+ Most scanners *assert* they have a low false-positive rate. This one **measures** it. A labeled corpus of malicious and benign MCP servers lives in [`benchmark/`](benchmark/); `npm run benchmark` scores it and reports the numbers (and fails CI on a regression):
89
+
90
+ | Metric | Score |
91
+ | --- | :---: |
92
+ | Precision | **100%** |
93
+ | Recall | **100%** |
94
+ | F1 | **100%** |
95
+ | False-positive rate | **0%** |
96
+
97
+ *(64 labeled servers, held-out cases flagged; "concerning" := Trust grade C or worse. Reproduce with `npm run benchmark`.)* The corpus is honest and versioned — it grows with every calibration case, and the CI gate holds precision/recall ≥ 90%.
98
+
99
+ ---
100
+
101
+ ## Install
102
+
103
+ ```bash
104
+ npx mcptrustchecker scan ./tools.json # zero-install
105
+ npm i -g mcptrustchecker # CLI everywhere
106
+ npm i mcptrustchecker # embed the engine in your app
107
+ ```
108
+
109
+ Requires Node ≥ 20. Live scanning uses the official [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk).
110
+
111
+ ---
112
+
113
+ ## Quick start
114
+
115
+ **Zero-config — one command, scans everything you have installed:**
116
+
117
+ ```bash
118
+ npx mcptrustchecker # auto-discovers Claude Desktop/Code, Cursor, Windsurf, Continue, VS Code configs
119
+ ```
120
+
121
+ **Or point it at anything:**
122
+
123
+ ```bash
124
+ mcptrustchecker scan ./tools.json # an offline manifest (deterministic)
125
+ mcptrustchecker scan ./path/to/mcp-server # a local package dir — analyzes the CODE too
126
+ mcptrustchecker scan --command "npx -y @some/mcp-server" # a local stdio server (sandboxed)
127
+ mcptrustchecker scan https://mcp.example.com/mcp # a live HTTP/SSE endpoint
128
+ mcptrustchecker scan @modelcontextprotocol/server-filesystem --online # a package name (typosquat/CVE)
129
+ ```
130
+
131
+ **Outputs & CI gates:**
132
+
133
+ ```bash
134
+ mcptrustchecker scan ./tools.json --sarif > mcptrustchecker.sarif # GitHub code scanning
135
+ mcptrustchecker scan ./tools.json --md > report.md # PR comment
136
+ mcptrustchecker scan ./tools.json --json > report.json # machine-readable
137
+ mcptrustchecker scan ./tools.json --badge > badge.json # shields.io endpoint
138
+ mcptrustchecker scan ./tools.json --fail-under 80 # exit 1 below a threshold
139
+ mcptrustchecker scan ./tools.json --min-grade B # exit 1 below a grade
140
+ ```
141
+
142
+ The terminal report is **detailed by default** — every finding prints its full description (**what** the problem is and **why** it matters), the exact location, the offending **evidence**, a **fix**, and its OWASP mapping, grouped most-severe-first. Add `--details` for external references, or `--quiet` for just the grade line.
143
+
144
+ ---
145
+
146
+ ## The algorithm: the Capability-Flow Trust Model
147
+
148
+ A 9-stage pipeline over a **normalized, transport-agnostic surface** (tools, prompts, resources, server instructions, transport, package metadata). Each stage emits findings; the scorer turns them into an auditable grade.
149
+
150
+ ```
151
+ INPUT ─ manifest.json │ live stdio/http │ client config │ package name
152
+
153
+
154
+ [0] SAFE ACQUISITION allow-listed command (bare-name only) · scrubbed env · timeouts · SSRF guard
155
+
156
+ [1] UNICODE INTEGRITY decode Tags/variation-selector payloads; BiDi; zero-width; homoglyph; ANSI
157
+
158
+ [2] INJECTION HEURISTICS tool-poisoning · line-jumping · shadowing · secrecy · exfil · embedded secrets
159
+
160
+ [3] CAPABILITY EXTRACTION tag tools (untrusted-input / sensitive-source / sink / exec / write);
161
+ │ annotation mismatch; sampling/elicitation; schema injection preconditions
162
+
163
+ [4] TOXIC-FLOW GRAPH ★ the lethal trifecta across tools AND client built-ins; + name-collision
164
+
165
+ [5] SUPPLY-CHAIN typosquat/combosquat/homoglyph · install-scripts · provenance · unpinned · deps
166
+
167
+ [6] TRANSPORT POSTURE stdio-RCE · plaintext HTTP · 0.0.0.0 · DNS-rebinding · known-CVE version matcher
168
+
169
+ [7] RUG-PULL INTEGRITY SHA-256 pin of the full schema → diff on every rescan
170
+
171
+ [8] SCORING deterministic penalties · diminishing returns · category caps · hard gates
172
+
173
+ OUTPUT ─ terminal │ JSON │ SARIF 2.1.0 │ Markdown │ badge
174
+ ```
175
+
176
+ **★ The flagship — cross-tool toxic-flow analysis.** The most dangerous MCP failures aren't one bad tool; they're an innocent *combination*. Give an agent (1) exposure to **untrusted content**, (2) access to **sensitive data**, and (3) a way to **communicate externally**, and you have an exfiltration primitive. MCP Trust Checker derives each tool's roles from behavior (not from its self-declared, attacker-controllable `annotations`) and checks whether the three are co-reachable across every tool, every server, and optionally the client's own built-ins (`--include-builtins`). One tool holding all three → **critical, confirmed**; the roles spread across tools → **critical, strong**.
177
+
178
+ Full depth: **[docs/methodology.md](docs/methodology.md)**.
179
+
180
+ ---
181
+
182
+ ## Coverage: the full catalog of MCP attack techniques
183
+
184
+ MCP Trust Checker covers the full catalog of known MCP attack techniques in one offline pass — from tool-poisoning and Unicode smuggling to supply-chain risk and cross-tool toxic flows — plus the flow graph, the decoder, the integrity pin, and the auditable score on top. ★ marks a check that goes beyond what static scanners typically catch.
185
+
186
+ <details>
187
+ <summary><b>📋 Full technique → rule coverage map</b> (37 techniques — click to expand)</summary>
188
+
189
+ <br/>
190
+
191
+ | Attack / technique | MCP Trust Checker rule(s) |
192
+ | --- | --- |
193
+ | Tool poisoning (hidden instructions in descriptions) | `MTC-INJ-AUTH-*`, `MTC-INJ-SECRECY-*`, `MTC-INJ-TARGET-*`, `MTC-INJ-POISON` |
194
+ | Prompt injection / instruction override | `MTC-INJ-AUTH-2`, `MTC-INJ-SECRECY-1` |
195
+ | Line jumping (pre-invocation seeding) | `MTC-INJ-SEQ-1` |
196
+ | Tool shadowing via description redirect | `MTC-INJ-SHADOW-1` |
197
+ | Cross-**server** tool-name collision / homoglyph name | `MTC-INJ-SHADOW-2` ★ |
198
+ | Tool-selection ranking manipulation | `MTC-INJ-SHADOW-3` ★ |
199
+ | Invisible-Unicode channels (zero-width/BiDi/Tags/VS) | `MTC-UNI-001..008` (decoded) ★ |
200
+ | Homoglyph / mixed-script | `MTC-UNI-009` |
201
+ | ANSI terminal-escape deception | `MTC-UNI-010` ★ |
202
+ | Encoded-payload smuggling (base64 + decode) | `MTC-INJ-ENC-1/2` |
203
+ | Shell/command-injection strings in prose | `MTC-INJ-CMD-1`, `MTC-CAP-001` |
204
+ | Command/code-execution capability | `MTC-CAP-001` |
205
+ | Filesystem-mutation capability | `MTC-CAP-002` |
206
+ | Annotation spoofing (readOnly/destructive lie) | `MTC-CAP-003` |
207
+ | openWorldHint + sensitive read (trifecta signal) | `MTC-CAP-004` ★ |
208
+ | **Toxic-flow analysis / lethal trifecta** | `MTC-FLOW-001..005` ★ |
209
+ | Command-injection sink precondition (schema) | `MTC-CAP-006` ★ |
210
+ | SSRF / cloud-metadata sink precondition (schema) | `MTC-CAP-007` ★ |
211
+ | Path-traversal precondition (schema) | `MTC-CAP-008` ★ |
212
+ | Sampling-capability abuse | `MTC-CAP-009` ★ (static proxy) |
213
+ | Elicitation abuse / consent phishing | `MTC-CAP-010` ★ (static proxy) |
214
+ | Rug pull / silent tool-definition mutation | `MTC-TOFU-001` + lockfile |
215
+ | Unpinned / @latest auto-update (rug-pull enabler) | `MTC-SUP-013` ★ |
216
+ | Typosquat / combosquat / homoglyph squat | `MTC-SUP-001..006` |
217
+ | Install-script / provenance risk | `MTC-SUP-010/011/012` |
218
+ | Dependency squat / advisory match | `MTC-SUP-014` ★ |
219
+ | Known-CVE version matching | `MTC-NET-001` |
220
+ | stdio-RCE (unallowlisted command) | `MTC-NET-002` + sandboxed acquisition |
221
+ | Plaintext HTTP / 0.0.0.0 bind | `MTC-NET-003/004` |
222
+ | DNS rebinding on localhost transport | `MTC-NET-006` ★ |
223
+ | Embedded credential value in metadata | `MTC-INJ-SECRET-1` ★ |
224
+ | Empty/malformed surface ≠ clean | `MTC-META-001` ★ |
225
+ | Config discovery across MCP clients | client-config parser + zero-config auto-discovery |
226
+ | Malicious URL / exfil endpoint in tool metadata | `MTC-INJ-URL-1` ★ |
227
+ | **Implementation-level sinks** — eval / shell-spawn / hardcoded egress / deserialization | `MTC-SRC-001…007` ★ |
228
+ | Credential-path read / environment dump in server code | `MTC-SRC-006` ★ |
229
+ | Hardcoded secret in the server's source (not just metadata) | `MTC-SRC-008` ★ |
230
+ </details>
231
+
232
+ **Deliberately out of scope** (so the deterministic, offline, no-account promise holds): LLM-as-judge semantic classification, hosted threat-intel, a runtime guardrail *proxy*, live authN/replay/signing probing, and OAuth-endpoint source analysis. Where a runtime-only class has a static proxy — a *declared* sampling/elicitation capability, an *unbounded* URL/command/path parameter — MCP Trust Checker flags the precondition offline instead.
233
+
234
+ Full list: **[docs/rules.md](docs/rules.md)** · run `mcptrustchecker rules`.
235
+
236
+ ---
237
+
238
+ ## Two axes: Trust (grade) and Capability (blast radius)
239
+
240
+ A single number can't answer "should I use this server?" — because **"powerful" and "malicious" are different questions.** A web scraper legitimately needs to fetch untrusted pages and act on them; that's a large *blast radius*, not evidence it's a bad actor. So MCP Trust Checker reports two independent things:
241
+
242
+ - **Trust — the A–F grade.** Driven by *threat* signals: prompt-injection with concealment, embedded secrets, Unicode smuggling, typosquatting, known CVEs, rug-pull drift, annotation lies, a single tool built as an exfiltration primitive. Answers **"any sign this server is malicious or negligent?"**
243
+ - **Capability — a level (Minimal → Critical).** Driven by what the server *can do*: code execution, filesystem writes, network egress, the cross-tool toxic-flow surface. Answers **"how much damage if the model driving it is manipulated?"** — a fact to size access against, **not** a mark against the server.
244
+
245
+ ```
246
+ firecrawl Trust B (81/100) Capability CRITICAL → trustworthy, but huge blast radius — grant carefully
247
+ poisoned Trust F Capability HIGH → actual malice signals — avoid
248
+ memory Trust A (100) Capability MINIMAL → safe and low-power
249
+ ```
250
+
251
+ This is why MCP Trust Checker doesn't collapse every capable server into "F" (which would make the grade useless). Popularity is never an input — popular packages get compromised — but a legitimate powerful server keeps a high Trust grade while its Capability is surfaced honestly.
252
+
253
+ ## The Trust grade is auditable by construction
254
+
255
+ ```
256
+ TrustScore = clamp( 100 − Σ_categories min(CategoryCap, Σ penalty), 0, 100 )
257
+ penalty = severity_weight × confidence_multiplier × diminishing_factor (threat findings only)
258
+ ```
259
+
260
+ | Severity | Weight | | Confidence | × |
261
+ | --- | ---: | --- | --- | ---: |
262
+ | Critical | 45 | | Confirmed | 1.0 |
263
+ | High | 22 | | Strong | 0.7 |
264
+ | Medium | 9 | | Heuristic | 0.4 |
265
+ | Low | 3 | | Speculative | 0.2 |
266
+
267
+ - **Diminishing returns** (`1 · ½ · ¼ · …`) so 40 copies of one nit can't tank a server and benign passes can't dilute one critical.
268
+ - **Per-category caps** (injection 50, exfiltration 50, permissions 35, supply-chain 30, network 25, hygiene 10).
269
+ - **Hard gates** (weakest-link): a **confirmed critical → F**; **any** critical → at most **D**; a confirmed high → at most **C**; two → **D**. Most gates fire only on `confirmed` findings so a guess never forces a cap — but no critical of any confidence can score above D.
270
+ - **Bands:** A 90–100 · B 80–89 · C 70–79 · D 60–69 · F 0–59.
271
+
272
+ Every report ships the full itemized `vector` and `methodologyVersion`. **Same methodology version + same target ⇒ byte-identical score.** Details: **[docs/scoring.md](docs/scoring.md)**.
273
+
274
+ ---
275
+
276
+ ## Embed the exact same engine (marketplaces & platforms)
277
+
278
+ MCP Trust Checker is a library first. A marketplace can vet every listed server with **the identical, versioned open-source engine** users audit on GitHub — so "we run unique security checks on every MCP server" becomes a *verifiable* claim.
279
+
280
+ ```ts
281
+ import { surfaceFromManifest, scanSurface, renderBadge } from 'mcptrustchecker';
282
+
283
+ const surface = surfaceFromManifest(toolsJson, 'acme/weather-mcp');
284
+ const report = await scanSurface(surface);
285
+
286
+ report.score.grade; // 'A' … 'F'
287
+ report.score.score; // 0 … 100
288
+ report.score.methodologyVersion; // 'mcptrustchecker-1.0' ← pin & display this
289
+ report.toxicFlows; // enumerated exfiltration primitives
290
+ renderBadge(report); // shields.io endpoint JSON for a live trust badge
291
+ ```
292
+
293
+ `scanSurface` is pure, deterministic, and offline. See **[examples/programmatic.ts](examples/programmatic.ts)**.
294
+
295
+ ---
296
+
297
+ ## Rug-pull protection (Trust On First Use)
298
+
299
+ ```bash
300
+ mcptrustchecker pin ./tools.json # writes mcptrustchecker.lock — commit it to git
301
+ mcptrustchecker diff ./tools.json # exits non-zero if the surface changed
302
+ ```
303
+
304
+ A tool that quietly rewrites its description after you approve it (the MCPoison / rug-pull class) shows up as **drift** with a human-readable diff, and re-approval is required.
305
+
306
+ ---
307
+
308
+ ## CI / GitHub integration
309
+
310
+ ```yaml
311
+ # .github/workflows/mcptrustchecker.yml
312
+ name: MCP Trust Checker
313
+ on: [push, pull_request]
314
+ jobs:
315
+ scan:
316
+ runs-on: ubuntu-latest
317
+ permissions: { contents: read, security-events: write }
318
+ steps:
319
+ - uses: actions/checkout@v4
320
+ - uses: illiahaidar/mcptrustchecker@v0
321
+ with:
322
+ target: ./tools.json
323
+ min-grade: B
324
+ sarif: true # uploads to the Security tab
325
+ ```
326
+
327
+ See **[docs/ci-integration.md](docs/ci-integration.md)** and **[action.yml](action.yml)**.
328
+
329
+ ---
330
+
331
+ ## Safety: scanning is itself an attack surface
332
+
333
+ Connecting to an MCP config can run arbitrary commands. MCP Trust Checker's acquisition is sandboxed by default:
334
+
335
+ - stdio commands are **allow-listed by bare name** (`npx, uvx, python, python3, node, docker, deno`) — a **path-qualified** command (e.g. `/tmp/evil/node`) is refused, closing the basename-spoof bypass.
336
+ - child env is **scrubbed** of execution-hijacking variables (`NODE_OPTIONS`, `LD_PRELOAD`, `DYLD_*`, `PYTHON*`, `()`-functions) — an allow-listed runtime can't be redirected.
337
+ - servers in a client config are **not spawned** unless you pass `--run`; config-derived HTTP targets are **SSRF-guarded** (private/loopback/link-local blocked).
338
+ - HTTP targets are scheme/host-validated; responses are size-capped; all connects are timeout-bounded.
339
+
340
+ ---
341
+
342
+ ## Configuration
343
+
344
+ `mcptrustchecker.config.json` (auto-discovered) overrides any default — see [examples/mcptrustchecker.config.json](examples/mcptrustchecker.config.json). Two adoption features worth calling out:
345
+
346
+ - **Baseline / suppressions** — waive a specific finding on a specific tool with a justification (`suppress: [{ rule, tool?, field?, reason }]`), or drop a standalone `.mtcignore` JSON array in your repo. Location-scoped waivers keep CI green without silencing a rule everywhere.
347
+ - **Policy-as-code** — declare what "acceptable" means once and gate every scan on it: `policy: { minGrade, maxCapability, denyRules, denyCapabilities }`. Violations print and fail the run.
348
+
349
+ ---
350
+
351
+ ## What MCP Trust Checker is *not*
352
+
353
+ - Not a runtime proxy/firewall — it analyzes the declared surface, it doesn't sit in the request path.
354
+ - Static analysis proves a toxic-flow **primitive exists**, not that a chain **will execute**.
355
+ - It does not test server-side auth/authorization (noted, not claimed).
356
+ - Heuristics have false positives; every finding carries a `confidence`, and grade gates fire only on `confirmed` findings — so a guess never forces a grade. [Report false positives](.github/ISSUE_TEMPLATE/false-positive.md); the rules are open.
357
+
358
+ ---
359
+
360
+ ## Documentation
361
+
362
+ - **[docs/methodology.md](docs/methodology.md)** — every pipeline stage in depth
363
+ - **[docs/scoring.md](docs/scoring.md)** — the scoring model & reproducibility contract
364
+ - **[docs/rules.md](docs/rules.md)** — the complete 74-rule catalogue
365
+ - **[docs/architecture.md](docs/architecture.md)** — code layout & how to extend
366
+ - **[docs/ci-integration.md](docs/ci-integration.md)** — Action, SARIF, baselines
367
+ - **[SECURITY.md](SECURITY.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)**
368
+
369
+ ---
370
+
371
+ ## Contributing
372
+
373
+ Issues, rules, and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). New detectors implement one small interface; new threat data is a plain, reviewable data file with a test.
374
+
375
+ ## License
376
+
377
+ [MIT](LICENSE) © Illia Haidar · [mcptrustchecker.com](https://mcptrustchecker.com) · [support@mcptrustchecker.com](mailto:support@mcptrustchecker.com).
378
+
379
+ Created and maintained by **[Illia Haidar](https://github.com/illiahaidar)** — author of the Capability-Flow Trust Model.
@@ -0,0 +1,47 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Parse an MCP client config (`claude_desktop_config.json`, Cursor, Windsurf,
4
+ * VS Code, Continue, …) into a list of server specs. All these clients share
5
+ * the `mcpServers` map shape.
6
+ */
7
+ export interface ConfigServerSpec {
8
+ id: string;
9
+ transport: 'stdio' | 'http';
10
+ command?: string;
11
+ args?: string[];
12
+ env?: Record<string, string>;
13
+ url?: string;
14
+ }
15
+ /** Detect whether a parsed JSON object is a client config (vs a manifest). */
16
+ export declare function isClientConfig(json: unknown): boolean;
17
+ /** Extract server specs from a client config object. */
18
+ export declare function parseClientConfig(json: unknown): ConfigServerSpec[];
19
+ /**
20
+ * Expand `${VAR}` / `$VAR` references using the given env (typically the
21
+ * server entry's own `env` block merged over process.env) — exactly what MCP
22
+ * clients do before spawning, so an auth header like `Authorization:${AUTH}`
23
+ * resolves. Unknown variables are left untouched rather than silently blanked.
24
+ */
25
+ export declare function expandVars(input: string, env: Record<string, string | undefined>): string;
26
+ /** Strip a trailing `@version` from a package spec, preserving the scope. */
27
+ export declare function stripVersion(spec: string): string;
28
+ /** Split a package spec into its name and version token (if any). */
29
+ export declare function versionOf(spec: string): string | undefined;
30
+ /**
31
+ * Best-effort: pull the npm/PyPI package name (and version token) out of an
32
+ * `npx`/`uvx`-style stdio spec so the supply-chain stage has something to check
33
+ * without running the server.
34
+ */
35
+ export declare function packageSpecFromStdio(command: string, args?: string[]): {
36
+ name: string;
37
+ version?: string;
38
+ } | undefined;
39
+ /** Package name only (back-compat convenience). */
40
+ export declare function packageFromStdio(command: string, args?: string[]): string | undefined;
41
+ /**
42
+ * Redact secrets from a spawn arg vector before storing/printing them — a
43
+ * security scanner must not leak the user's own tokens in its report. Handles
44
+ * inline `Authorization:Bearer …` values, header flags (secret value only), and
45
+ * bare values following a dedicated secret flag (redacted regardless of length).
46
+ */
47
+ export declare function redactSensitiveArgs(args: string[]): string[];
@@ -0,0 +1,124 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Parse an MCP client config (`claude_desktop_config.json`, Cursor, Windsurf,
4
+ * VS Code, Continue, …) into a list of server specs. All these clients share
5
+ * the `mcpServers` map shape.
6
+ */
7
+ /** Detect whether a parsed JSON object is a client config (vs a manifest). */
8
+ export function isClientConfig(json) {
9
+ return Boolean(json && typeof json === 'object' && ('mcpServers' in json || 'servers' in json));
10
+ }
11
+ /** Extract server specs from a client config object. */
12
+ export function parseClientConfig(json) {
13
+ const root = (json ?? {});
14
+ const servers = (root.mcpServers ?? root.servers ?? {});
15
+ const specs = [];
16
+ for (const [id, raw] of Object.entries(servers)) {
17
+ if (!raw || typeof raw !== 'object')
18
+ continue;
19
+ const s = raw;
20
+ const url = typeof s.url === 'string' ? s.url : undefined;
21
+ if (url) {
22
+ specs.push({ id, transport: 'http', url });
23
+ }
24
+ else if (typeof s.command === 'string') {
25
+ specs.push({
26
+ id,
27
+ transport: 'stdio',
28
+ command: s.command,
29
+ args: Array.isArray(s.args) ? s.args : [],
30
+ env: s.env ?? undefined,
31
+ });
32
+ }
33
+ }
34
+ return specs;
35
+ }
36
+ /**
37
+ * Expand `${VAR}` / `$VAR` references using the given env (typically the
38
+ * server entry's own `env` block merged over process.env) — exactly what MCP
39
+ * clients do before spawning, so an auth header like `Authorization:${AUTH}`
40
+ * resolves. Unknown variables are left untouched rather than silently blanked.
41
+ */
42
+ export function expandVars(input, env) {
43
+ return input.replace(/\$\{(\w+)\}|\$(\w+)/g, (match, braced, bare) => {
44
+ const name = braced ?? bare;
45
+ const val = env[name];
46
+ return val === undefined ? match : val;
47
+ });
48
+ }
49
+ /** Strip a trailing `@version` from a package spec, preserving the scope. */
50
+ export function stripVersion(spec) {
51
+ if (spec.startsWith('@')) {
52
+ const slash = spec.indexOf('/');
53
+ if (slash < 0)
54
+ return spec;
55
+ const rest = spec.slice(slash + 1);
56
+ const at = rest.indexOf('@');
57
+ return at >= 0 ? spec.slice(0, slash + 1 + at) : spec;
58
+ }
59
+ const at = spec.indexOf('@');
60
+ return at >= 0 ? spec.slice(0, at) : spec;
61
+ }
62
+ /** Split a package spec into its name and version token (if any). */
63
+ export function versionOf(spec) {
64
+ if (spec.startsWith('@')) {
65
+ const slash = spec.indexOf('/');
66
+ if (slash < 0)
67
+ return undefined;
68
+ const at = spec.slice(slash + 1).indexOf('@');
69
+ return at >= 0 ? spec.slice(slash + 1 + at + 1) : undefined;
70
+ }
71
+ const at = spec.indexOf('@');
72
+ return at >= 0 ? spec.slice(at + 1) : undefined;
73
+ }
74
+ /**
75
+ * Best-effort: pull the npm/PyPI package name (and version token) out of an
76
+ * `npx`/`uvx`-style stdio spec so the supply-chain stage has something to check
77
+ * without running the server.
78
+ */
79
+ export function packageSpecFromStdio(command, args = []) {
80
+ const base = command.replace(/\\/g, '/');
81
+ const runner = base.slice(base.lastIndexOf('/') + 1).replace(/\.(cmd|exe)$/, '');
82
+ const skip = new Set(['-y', '--yes', 'dlx', 'exec', '-q', '--quiet', 'run']);
83
+ if (['npx', 'bunx', 'pnpm', 'yarn', 'uvx', 'pipx'].includes(runner)) {
84
+ for (const a of args) {
85
+ if (a.startsWith('-') || skip.has(a))
86
+ continue;
87
+ return { name: stripVersion(a), version: versionOf(a) };
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ /** Package name only (back-compat convenience). */
93
+ export function packageFromStdio(command, args = []) {
94
+ return packageSpecFromStdio(command, args)?.name;
95
+ }
96
+ function redactValue(a) {
97
+ return a
98
+ .replace(/(bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>')
99
+ // Redact the WHOLE Authorization value (Basic <base64>, Digest …), not just
100
+ // the first token — the credential lives after the scheme word.
101
+ .replace(/(authorization\s*[:=]\s*)(?!bearer\b).+$/gi, '$1<redacted>')
102
+ .replace(/(cookie\s*[:=]\s*).+$/gi, '$1<redacted>')
103
+ .replace(/((?:api[_-]?key|token|secret|password|access[_-]?token)\s*[:=]\s*)\S+/gi, '$1<redacted>');
104
+ }
105
+ // Flags whose FOLLOWING arg is the secret itself → always redact (any length).
106
+ const VALUE_IS_SECRET = /^(--token|--api-?key|--auth|--authorization|--secret|--password|--pass|--bearer|--key|--pat)$/i;
107
+ // Header flags whose following arg is "Name: value" → redact only a secret value.
108
+ const HEADER_FLAG = /^(--header|-h|--headers)$/i;
109
+ /**
110
+ * Redact secrets from a spawn arg vector before storing/printing them — a
111
+ * security scanner must not leak the user's own tokens in its report. Handles
112
+ * inline `Authorization:Bearer …` values, header flags (secret value only), and
113
+ * bare values following a dedicated secret flag (redacted regardless of length).
114
+ */
115
+ export function redactSensitiveArgs(args) {
116
+ return args.map((a, i) => {
117
+ const prev = args[i - 1] ?? '';
118
+ if (VALUE_IS_SECRET.test(prev))
119
+ return '<redacted>';
120
+ if (HEADER_FLAG.test(prev))
121
+ return redactValue(a); // header value only if it looks secret
122
+ return redactValue(a); // inline secret patterns anywhere
123
+ });
124
+ }
@@ -0,0 +1,12 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Zero-config discovery of installed MCP client configs, so `mcptrustchecker` with no
4
+ * arguments "just works" — the same one-command onboarding as the popular
5
+ * scanners. All the supported clients share the `mcpServers`/`servers` map.
6
+ */
7
+ export interface DiscoveredConfig {
8
+ client: string;
9
+ path: string;
10
+ }
11
+ /** Return config files that exist AND parse as an MCP client config. */
12
+ export declare function discoverClientConfigs(): DiscoveredConfig[];
@@ -0,0 +1,53 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Zero-config discovery of installed MCP client configs, so `mcptrustchecker` with no
4
+ * arguments "just works" — the same one-command onboarding as the popular
5
+ * scanners. All the supported clients share the `mcpServers`/`servers` map.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { isClientConfig } from './clientConfig.js';
11
+ /** Candidate config locations across the common MCP clients and OSes. */
12
+ function candidates() {
13
+ const home = homedir();
14
+ const cwd = process.cwd();
15
+ const appData = process.env.APPDATA;
16
+ const list = [
17
+ // Claude Desktop
18
+ { client: 'Claude Desktop', path: join(home, 'Library/Application Support/Claude/claude_desktop_config.json') },
19
+ { client: 'Claude Desktop', path: join(home, '.config/Claude/claude_desktop_config.json') },
20
+ appData ? { client: 'Claude Desktop', path: join(appData, 'Claude/claude_desktop_config.json') } : null,
21
+ // Claude Code
22
+ { client: 'Claude Code', path: join(home, '.claude.json') },
23
+ // Cursor
24
+ { client: 'Cursor', path: join(home, '.cursor/mcp.json') },
25
+ { client: 'Cursor (project)', path: join(cwd, '.cursor/mcp.json') },
26
+ // Windsurf
27
+ { client: 'Windsurf', path: join(home, '.codeium/windsurf/mcp_config.json') },
28
+ // Continue
29
+ { client: 'Continue', path: join(home, '.continue/config.json') },
30
+ // VS Code (project) & generic project configs
31
+ { client: 'VS Code (project)', path: join(cwd, '.vscode/mcp.json') },
32
+ { client: 'project', path: join(cwd, '.mcp.json') },
33
+ ];
34
+ return list.filter((c) => c !== null);
35
+ }
36
+ /** Return config files that exist AND parse as an MCP client config. */
37
+ export function discoverClientConfigs() {
38
+ const found = [];
39
+ const seen = new Set();
40
+ for (const c of candidates()) {
41
+ if (seen.has(c.path) || !existsSync(c.path))
42
+ continue;
43
+ seen.add(c.path);
44
+ try {
45
+ if (isClientConfig(JSON.parse(readFileSync(c.path, 'utf8'))))
46
+ found.push(c);
47
+ }
48
+ catch {
49
+ /* unreadable / not JSON — skip */
50
+ }
51
+ }
52
+ return found;
53
+ }