mcptrustchecker 1.2.0 โ 1.3.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 +14 -7
- package/dist/engine.js +2 -0
- package/dist/report/markdown.js +12 -0
- package/dist/report/sarif.js +2 -0
- package/dist/report/terminal.js +23 -0
- package/dist/scoring/coverage.d.ts +14 -0
- package/dist/scoring/coverage.js +62 -0
- package/dist/types.d.ts +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](package.json)
|
|
11
11
|
[](docs/methodology.md)
|
|
12
|
-
[](test)
|
|
13
13
|
[](docs/rules.md)
|
|
14
14
|
[](#why-this-is-different)
|
|
15
15
|
[](#why-this-is-different)
|
|
16
|
+
[](https://mcptrustchecker.com/api)
|
|
16
17
|
|
|
17
18
|
<br/>
|
|
18
19
|
|
|
@@ -24,6 +25,8 @@
|
|
|
24
25
|
npx mcptrustchecker # ๐ scan every MCP server you already have installed โ zero config
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
**Zero install?** Run the same deterministic engine as a **free public web API โ [mcptrustchecker.com/api](https://mcptrustchecker.com/api)**
|
|
29
|
+
|
|
27
30
|
<sub>ยท offline ยท deterministic ยท no account ยท OAuth browser login for protected servers ยท reads the real published npm/PyPI source ยท <a href="#the-algorithm-the-capability-flow-trust-model">one novel core</a> ยท</sub>
|
|
28
31
|
|
|
29
32
|
</div>
|
|
@@ -148,6 +151,8 @@ mcptrustchecker scan ./tools.json --min-grade B # exit 1 below a gra
|
|
|
148
151
|
|
|
149
152
|
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.
|
|
150
153
|
|
|
154
|
+
**No install? Use the free hosted API.** A hosted version of the exact same deterministic engine runs at **[mcptrustchecker.com/api](https://mcptrustchecker.com/api)** โ POST a `tools.json` manifest or a published npm/PyPI package name over a single HTTPS request and get the same auditable AโF Trust Score back as JSON. It is **completely free** (grab a key in seconds, no card, no account), and it's the quickest way to try the scanner without installing anything. The CLI and library stay 100% offline โ the hosted API is an optional convenience.
|
|
155
|
+
|
|
151
156
|
---
|
|
152
157
|
|
|
153
158
|
## The algorithm: the Capability-Flow Trust Model
|
|
@@ -242,20 +247,22 @@ Full list: **[docs/rules.md](docs/rules.md)** ยท run `mcptrustchecker rules`.
|
|
|
242
247
|
|
|
243
248
|
---
|
|
244
249
|
|
|
245
|
-
##
|
|
250
|
+
## Three axes: Trust (grade), Capability (blast radius), Coverage (depth)
|
|
246
251
|
|
|
247
|
-
A single number can't answer "should I use this server?" โ because **"powerful" and "malicious" are different questions
|
|
252
|
+
A single number can't answer "should I use this server?" โ because **"powerful" and "malicious" are different questions**, and a clean grade means nothing if the scan barely saw the server. So MCP Trust Checker reports three independent things:
|
|
248
253
|
|
|
249
254
|
- **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?"**
|
|
250
255
|
- **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.
|
|
256
|
+
- **Coverage โ a level (Live โ Source โ Manifest โ Metadata โ Empty).** How much the scan could actually inspect. A Grade A from a `metadata`-only scan (no tools enumerated, no code read) is **not** the assurance of a Grade A from a `live` scan that saw the real runtime tools. Coverage never changes the score โ it states the score's reach, so a shallow scan is never mistaken for a clean bill of health. When coverage is partial, the report says exactly what it could not see.
|
|
251
257
|
|
|
252
258
|
```
|
|
253
|
-
firecrawl Trust B (81/100) Capability CRITICAL โ trustworthy, but huge blast radius โ grant carefully
|
|
254
|
-
poisoned Trust F Capability HIGH โ actual malice signals โ avoid
|
|
255
|
-
memory Trust A (100) Capability MINIMAL โ safe and low-power
|
|
259
|
+
firecrawl Trust B (81/100) Capability CRITICAL Coverage LIVE โ trustworthy, but huge blast radius โ grant carefully
|
|
260
|
+
poisoned Trust F Capability HIGH Coverage LIVE โ actual malice signals โ avoid
|
|
261
|
+
memory Trust A (100) Capability MINIMAL Coverage LIVE โ safe and low-power
|
|
262
|
+
some-pkg Trust A (98) Capability MINIMAL Coverage METADATA โ clean *so far* โ 0 tools seen; scan the live server
|
|
256
263
|
```
|
|
257
264
|
|
|
258
|
-
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
|
|
265
|
+
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 and the scan's Coverage are surfaced honestly.
|
|
259
266
|
|
|
260
267
|
## The Trust grade is auditable by construction
|
|
261
268
|
|
package/dist/engine.js
CHANGED
|
@@ -14,6 +14,7 @@ import { checkIntegrity } from './lockfile.js';
|
|
|
14
14
|
import { surfaceDigest } from './util/hash.js';
|
|
15
15
|
import { computeScore } from './scoring/index.js';
|
|
16
16
|
import { computeCapabilityProfile } from './scoring/capability.js';
|
|
17
|
+
import { computeCoverage } from './scoring/coverage.js';
|
|
17
18
|
import { METHODOLOGY_VERSION, TOOL_NAME, TOOL_VERSION } from './version.js';
|
|
18
19
|
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
19
20
|
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
@@ -146,6 +147,7 @@ export async function scanSurface(rawSurface, options = {}) {
|
|
|
146
147
|
score,
|
|
147
148
|
capabilities,
|
|
148
149
|
capabilityProfile: computeCapabilityProfile(capabilities, flows),
|
|
150
|
+
coverage: computeCoverage(surface),
|
|
149
151
|
toxicFlows: flows,
|
|
150
152
|
integrity,
|
|
151
153
|
surfaceDigest: surfaceDigest(surface),
|
package/dist/report/markdown.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Markdown report โ for PR comments, job summaries, and README embeds.
|
|
4
4
|
*/
|
|
5
5
|
import { ALL_CATEGORIES } from '../scoring/model.js';
|
|
6
|
+
import { coverageLabel } from '../scoring/coverage.js';
|
|
7
|
+
const COVERAGE_EMOJI = { live: '๐ข', source: '๐ข', manifest: '๐ก', metadata: '๐ก', empty: '๐ด' };
|
|
6
8
|
const SEV_EMOJI = {
|
|
7
9
|
critical: '๐ฅ',
|
|
8
10
|
high: '๐ง',
|
|
@@ -29,6 +31,16 @@ export function renderMarkdown(report) {
|
|
|
29
31
|
`**Surface:** ${report.stats.tools} tools, ${report.stats.prompts} prompts, ${report.stats.resources} resources ยท ` +
|
|
30
32
|
`**Methodology:** \`${s.methodologyVersion}\``);
|
|
31
33
|
md.push('');
|
|
34
|
+
const cov = report.coverage;
|
|
35
|
+
md.push(`**Coverage:** ${COVERAGE_EMOJI[cov.level] ?? 'โช'} ${cov.level.toUpperCase()} โ ${coverageLabel(cov.level)}`);
|
|
36
|
+
if (cov.caveats.length) {
|
|
37
|
+
md.push('');
|
|
38
|
+
md.push('> [!NOTE]');
|
|
39
|
+
md.push('> This grade reflects only what was inspected:');
|
|
40
|
+
for (const note of cov.caveats)
|
|
41
|
+
md.push(`> - ${mdInline(note)}`);
|
|
42
|
+
}
|
|
43
|
+
md.push('');
|
|
32
44
|
const bs = report.stats.findingsBySeverity;
|
|
33
45
|
md.push(`**Findings:** ${bs.critical} critical ยท ${bs.high} high ยท ${bs.medium} medium ยท ${bs.low} low ยท ${bs.info} info`);
|
|
34
46
|
md.push('');
|
package/dist/report/sarif.js
CHANGED
|
@@ -95,6 +95,8 @@ export function renderSarif(report) {
|
|
|
95
95
|
trustScore: report.score.score,
|
|
96
96
|
grade: report.score.grade,
|
|
97
97
|
capabilityLevel: report.capabilityProfile.level,
|
|
98
|
+
coverageLevel: report.coverage.level,
|
|
99
|
+
coverageCaveats: report.coverage.caveats,
|
|
98
100
|
methodologyVersion: report.score.methodologyVersion,
|
|
99
101
|
surfaceDigest: report.surfaceDigest,
|
|
100
102
|
},
|
package/dist/report/terminal.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Human-facing terminal report. Zero-dependency ANSI, degrades to plain text
|
|
4
4
|
* when piped or under NO_COLOR.
|
|
5
5
|
*/
|
|
6
|
+
import { coverageLabel } from '../scoring/coverage.js';
|
|
6
7
|
import { c, padVisible } from '../util/ansi.js';
|
|
7
8
|
import { ALL_CATEGORIES, isCapabilityRule } from '../scoring/model.js';
|
|
8
9
|
const SEV_LABEL = {
|
|
@@ -45,6 +46,14 @@ function capColor(level, s) {
|
|
|
45
46
|
return c.bold(c.red(s));
|
|
46
47
|
}
|
|
47
48
|
}
|
|
49
|
+
/** A deep scan (live/source) reads normal; a shallow one (manifest/metadata/empty) is a caution. */
|
|
50
|
+
function coverageColor(level, s) {
|
|
51
|
+
if (level === 'live' || level === 'source')
|
|
52
|
+
return c.green(s);
|
|
53
|
+
if (level === 'empty')
|
|
54
|
+
return c.red(s);
|
|
55
|
+
return c.yellow(s);
|
|
56
|
+
}
|
|
48
57
|
/** Usable terminal width (clamped), for wrapping detailed descriptions. */
|
|
49
58
|
const TERM = Math.max(64, Math.min(process.stdout.columns || 100, 118));
|
|
50
59
|
function line(char = 'โ', width = Math.min(TERM, 72)) {
|
|
@@ -97,6 +106,10 @@ export function renderTerminal(report, opts = {}) {
|
|
|
97
106
|
out.push(` ${gradeColor(score.grade, `โญ${'โ'.repeat(badge.length)}โฎ`)}`);
|
|
98
107
|
out.push(` ${gradeColor(score.grade, `โ${badge}โ`)} ${c.bold(`Trust Score ${score.score}/100`)} ${c.gray('(malice/negligence signals)')}`);
|
|
99
108
|
out.push(` ${gradeColor(score.grade, `โฐ${'โ'.repeat(badge.length)}โฏ`)} ${capColor(cap.level, `Capability ${cap.level.toUpperCase()}`)} ${c.gray('(blast radius)')}`);
|
|
109
|
+
const cov = report.coverage;
|
|
110
|
+
// Align under "Capability": 3 leading spaces + the 2 box borders + badge width + 3-space gap.
|
|
111
|
+
const pad = ' '.repeat(badge.length + 8);
|
|
112
|
+
out.push(`${pad}${coverageColor(cov.level, `Coverage ${cov.level.toUpperCase()}`)} ${c.gray('(' + coverageLabel(cov.level) + ')')}`);
|
|
100
113
|
if (cap.reasons.length) {
|
|
101
114
|
out.push(` ${c.gray('methodology ' + score.methodologyVersion)} ยท ${c.gray(cap.reasons.slice(0, 2).join('; '))}`);
|
|
102
115
|
}
|
|
@@ -114,6 +127,16 @@ export function renderTerminal(report, opts = {}) {
|
|
|
114
127
|
.join(c.gray(' ยท '));
|
|
115
128
|
out.push(`${c.gray('Threats ')} ${counts || c.green('none')}`);
|
|
116
129
|
out.push(`${c.gray('Capability')} ${capColor(cap.level, cap.level)} ${c.gray(`(${capabilityFindings.length} observation${capabilityFindings.length === 1 ? '' : 's'})`)}`);
|
|
130
|
+
// Coverage caveats โ what the scan could NOT see, so a clean grade on a shallow
|
|
131
|
+
// scan is never read as a thorough all-clear.
|
|
132
|
+
if (cov.caveats.length) {
|
|
133
|
+
out.push('');
|
|
134
|
+
out.push(`${coverageColor(cov.level, 'โ')} ${c.gray('Coverage โ this grade reflects only what was inspected:')}`);
|
|
135
|
+
for (const note of cov.caveats) {
|
|
136
|
+
for (const wl of wrap(note, TERM - 4))
|
|
137
|
+
out.push(` ${c.gray(wl)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
117
140
|
// Category penalties.
|
|
118
141
|
const cats = ALL_CATEGORIES.filter((cat) => score.categorySubtotals[cat] > 0);
|
|
119
142
|
if (cats.length) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*! MCP Trust Checker ยท https://mcptrustchecker.com ยท support@mcptrustchecker.com ยท ยฉ 2026 Illia Haidar ยท MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Coverage โ a third, honest axis alongside Trust (grade) and Capability (blast
|
|
4
|
+
* radius). It states how much of the target the scan could actually inspect, so
|
|
5
|
+
* a clean grade from a shallow scan is never mistaken for a thorough one.
|
|
6
|
+
*
|
|
7
|
+
* Purely descriptive and deterministic: derived only from what the acquired
|
|
8
|
+
* surface contains. It never changes the score โ it explains the score's reach.
|
|
9
|
+
*/
|
|
10
|
+
import type { Coverage, CoverageLevel, ServerSurface } from '../types.js';
|
|
11
|
+
/** Derive the coverage descriptor from an acquired surface. */
|
|
12
|
+
export declare function computeCoverage(surface: ServerSurface): Coverage;
|
|
13
|
+
/** A short human label for a coverage level (terminal/markdown headline). */
|
|
14
|
+
export declare function coverageLabel(level: CoverageLevel): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*! MCP Trust Checker ยท https://mcptrustchecker.com ยท support@mcptrustchecker.com ยท ยฉ 2026 Illia Haidar ยท MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Coverage โ a third, honest axis alongside Trust (grade) and Capability (blast
|
|
4
|
+
* radius). It states how much of the target the scan could actually inspect, so
|
|
5
|
+
* a clean grade from a shallow scan is never mistaken for a thorough one.
|
|
6
|
+
*
|
|
7
|
+
* Purely descriptive and deterministic: derived only from what the acquired
|
|
8
|
+
* surface contains. It never changes the score โ it explains the score's reach.
|
|
9
|
+
*/
|
|
10
|
+
/** Derive the coverage descriptor from an acquired surface. */
|
|
11
|
+
export function computeCoverage(surface) {
|
|
12
|
+
const toolSurface = (Array.isArray(surface.tools) && surface.tools.length > 0) ||
|
|
13
|
+
(Array.isArray(surface.prompts) && surface.prompts.length > 0) ||
|
|
14
|
+
(Array.isArray(surface.resources) && surface.resources.length > 0);
|
|
15
|
+
const implementationSource = Array.isArray(surface.sourceFiles) && surface.sourceFiles.length > 0;
|
|
16
|
+
const m = surface.packageMeta;
|
|
17
|
+
const packageMetadata = Boolean(m && (m.name || m.version || (Array.isArray(m.dependencies) && m.dependencies.length > 0) || m.tarballSha256));
|
|
18
|
+
const liveTransport = surface.source?.kind === 'stdio' || surface.source?.kind === 'http';
|
|
19
|
+
// Richest signal wins for the headline label; `inputs` keeps the full detail.
|
|
20
|
+
let level;
|
|
21
|
+
if (liveTransport)
|
|
22
|
+
level = 'live';
|
|
23
|
+
else if (implementationSource)
|
|
24
|
+
level = 'source';
|
|
25
|
+
else if (toolSurface)
|
|
26
|
+
level = 'manifest';
|
|
27
|
+
else if (packageMetadata)
|
|
28
|
+
level = 'metadata';
|
|
29
|
+
else
|
|
30
|
+
level = 'empty';
|
|
31
|
+
const caveats = [];
|
|
32
|
+
if (level === 'empty') {
|
|
33
|
+
caveats.push('Nothing scannable was found on this target โ an empty surface is not a clean bill of health.');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (!toolSurface) {
|
|
37
|
+
caveats.push('No tools were enumerated, so prompt-injection, capability and toxic-flow analysis had no tool surface to inspect. ' +
|
|
38
|
+
'To grade a packageโs real runtime tools, scan the running server: --command "npx -y <package>".');
|
|
39
|
+
}
|
|
40
|
+
if (!implementationSource && !liveTransport) {
|
|
41
|
+
caveats.push(packageMetadata
|
|
42
|
+
? 'The implementation source was not read โ this reflects registry metadata only. Add --online to fetch and analyze the published package source.'
|
|
43
|
+
: 'The implementation source was not read โ this reflects the declared tool metadata only.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { level, inputs: { toolSurface, implementationSource, packageMetadata, liveTransport }, caveats };
|
|
47
|
+
}
|
|
48
|
+
/** A short human label for a coverage level (terminal/markdown headline). */
|
|
49
|
+
export function coverageLabel(level) {
|
|
50
|
+
switch (level) {
|
|
51
|
+
case 'live':
|
|
52
|
+
return 'running server โ real runtime tools';
|
|
53
|
+
case 'source':
|
|
54
|
+
return 'published/local source read';
|
|
55
|
+
case 'manifest':
|
|
56
|
+
return 'static tool list only โ no source';
|
|
57
|
+
case 'metadata':
|
|
58
|
+
return 'registry metadata only โ no tools, no source';
|
|
59
|
+
case 'empty':
|
|
60
|
+
return 'nothing inspected';
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -208,6 +208,29 @@ export interface CapabilityProfile {
|
|
|
208
208
|
/** The union of capability tags observed across the server's tools. */
|
|
209
209
|
tags: CapabilityTag[];
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* The depth of what the scan actually observed. A Trust grade is only ever as
|
|
213
|
+
* meaningful as its coverage: a clean grade from a metadata-only scan (no tools
|
|
214
|
+
* enumerated, no source read) is NOT the same assurance as a clean grade from a
|
|
215
|
+
* live scan that saw the real runtime tools and read the code.
|
|
216
|
+
*/
|
|
217
|
+
export type CoverageLevel = 'live' | 'source' | 'manifest' | 'metadata' | 'empty';
|
|
218
|
+
export interface Coverage {
|
|
219
|
+
level: CoverageLevel;
|
|
220
|
+
/** Which analysis inputs had signal โ drives which detectors could contribute. */
|
|
221
|
+
inputs: {
|
|
222
|
+
/** Tool/prompt/resource surface enumerated (injection, capability, toxic-flow). */
|
|
223
|
+
toolSurface: boolean;
|
|
224
|
+
/** Implementation source analyzed (MTC-SRC sinks). */
|
|
225
|
+
implementationSource: boolean;
|
|
226
|
+
/** Package/provenance metadata present (supply-chain). */
|
|
227
|
+
packageMetadata: boolean;
|
|
228
|
+
/** Spoke to a running server (live stdio/HTTP), so the tools are runtime-real. */
|
|
229
|
+
liveTransport: boolean;
|
|
230
|
+
};
|
|
231
|
+
/** Honest notes on what the scan could NOT see; empty when coverage is complete. */
|
|
232
|
+
caveats: string[];
|
|
233
|
+
}
|
|
211
234
|
export interface ToxicFlow {
|
|
212
235
|
id: string;
|
|
213
236
|
severity: Severity;
|
|
@@ -291,6 +314,8 @@ export interface ScanReport {
|
|
|
291
314
|
capabilities: ToolCapability[];
|
|
292
315
|
/** The server's blast-radius rating (independent of the trust grade). */
|
|
293
316
|
capabilityProfile: CapabilityProfile;
|
|
317
|
+
/** How much of the target the scan actually inspected (a grade's depth). */
|
|
318
|
+
coverage: Coverage;
|
|
294
319
|
toxicFlows: ToxicFlow[];
|
|
295
320
|
integrity?: IntegrityResult;
|
|
296
321
|
/** SHA-256 of the canonicalized surface โ the rug-pull fingerprint. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcptrustchecker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Local-first, deterministic security scanner for Model Context Protocol (MCP) servers. Cross-tool toxic-flow analysis, Unicode-smuggling decode, prompt-injection & supply-chain detection, with an auditable 0โ100 Trust Score.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|