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 CHANGED
@@ -9,10 +9,11 @@
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)
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
- ## Two axes: Trust (grade) and Capability (blast radius)
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.** 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:
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 is surfaced honestly.
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),
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcptrustchecker",
3
- "version": "1.2.0",
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",