mcptrustchecker 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,10 +9,12 @@
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
10
  [![Node](https://img.shields.io/badge/node-%E2%89%A520-3c873a.svg)](package.json)
11
11
  [![Methodology](https://img.shields.io/badge/methodology-mcptrustchecker--1.0-6f42c1.svg)](docs/methodology.md)
12
- [![Tests](https://img.shields.io/badge/tests-294%20passing-brightgreen.svg)](test)
12
+ [![Tests](https://img.shields.io/badge/tests-304%20passing-brightgreen.svg)](test)
13
13
  [![Rules](https://img.shields.io/badge/rules-78-orange.svg)](docs/rules.md)
14
14
  [![No account](https://img.shields.io/badge/account-not%20required-brightgreen.svg)](#why-this-is-different)
15
15
  [![Offline](https://img.shields.io/badge/runs-100%25%20offline-brightgreen.svg)](#why-this-is-different)
16
+ [![Free hosted API](https://img.shields.io/badge/hosted%20API-free-c4f542.svg)](https://mcptrustchecker.com/api)
17
+ [![Trust Registry](https://img.shields.io/badge/trust%20registry-browse-8b87f7.svg)](https://mcptrustchecker.com/registry)
16
18
 
17
19
  <br/>
18
20
 
@@ -24,6 +26,8 @@
24
26
  npx mcptrustchecker # 🔍 scan every MCP server you already have installed — zero config
25
27
  ```
26
28
 
29
+ **Zero install?** Run the same deterministic engine as a **free public web API → [mcptrustchecker.com/api](https://mcptrustchecker.com/api)**
30
+
27
31
  <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
32
 
29
33
  </div>
@@ -44,7 +48,7 @@ Three more things sit under that graph:
44
48
 
45
49
  > 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
50
 
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)
51
+ **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-grade-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
52
 
49
53
  ---
50
54
 
@@ -148,6 +152,10 @@ mcptrustchecker scan ./tools.json --min-grade B # exit 1 below a gra
148
152
 
149
153
  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
154
 
155
+ **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.
156
+
157
+ **Browse the MCP Trust Registry** — a growing, security-scanned catalog of MCP servers with an A–F Trust Score for each, all computed by this exact engine: **[mcptrustchecker.com/registry](https://mcptrustchecker.com/registry)**.
158
+
151
159
  ---
152
160
 
153
161
  ## The algorithm: the Capability-Flow Trust Model
@@ -242,20 +250,22 @@ Full list: **[docs/rules.md](docs/rules.md)** · run `mcptrustchecker rules`.
242
250
 
243
251
  ---
244
252
 
245
- ## Two axes: Trust (grade) and Capability (blast radius)
253
+ ## Three axes: Trust (grade), Capability (blast radius), Coverage (depth)
246
254
 
247
- 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:
255
+ 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
256
 
249
257
  - **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
258
  - **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.
259
+ - **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
260
 
252
261
  ```
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
262
+ firecrawl Trust B (81/100) Capability CRITICAL Coverage LIVE → trustworthy, but huge blast radius — grant carefully
263
+ poisoned Trust F Capability HIGH Coverage LIVE → actual malice signals — avoid
264
+ memory Trust A (100) Capability MINIMAL Coverage LIVE → safe and low-power
265
+ some-pkg Trust A (98) Capability MINIMAL Coverage METADATA → clean *so far* — 0 tools seen; scan the live server
256
266
  ```
257
267
 
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 is surfaced honestly.
268
+ 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
269
 
260
270
  ## The Trust grade is auditable by construction
261
271
 
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),
@@ -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('');
@@ -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
  },
@@ -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/dist/version.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
2
  /** Package version. Keep in sync with package.json. */
3
- export declare const TOOL_VERSION = "1.2.0";
3
+ export declare const TOOL_VERSION = "1.3.1";
4
4
  /**
5
5
  * Methodology version. Bump this whenever scoring weights, gates, rule
6
6
  * severities, or bundled threat data change in a way that could move a score.
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
2
  /** Package version. Keep in sync with package.json. */
3
- export const TOOL_VERSION = '1.2.0';
3
+ export const TOOL_VERSION = '1.3.1';
4
4
  /**
5
5
  * Methodology version. Bump this whenever scoring weights, gates, rule
6
6
  * severities, or bundled threat data change in a way that could move a score.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcptrustchecker",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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",