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
@@ -0,0 +1,96 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Deterministic scorer. Turns a set of findings into an auditable Trust Score:
4
+ * additive penalties from 100, diminishing returns per rule, per-category caps,
5
+ * then weakest-link hard gates. Same methodology version + same findings ⇒
6
+ * identical score, and every point is reconstructable from `vector`.
7
+ */
8
+ import { METHODOLOGY_VERSION } from '../version.js';
9
+ import { ALL_CATEGORIES, bandForScore, CATEGORY_CAP, CONFIDENCE_MULT, DIMINISHING, isCapabilityRule, SEVERITY_WEIGHT, stricterGrade, } from './model.js';
10
+ const CONFIDENCE_ORDER = { confirmed: 0, strong: 1, heuristic: 2, speculative: 3 };
11
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
12
+ function round2(n) {
13
+ return Math.round(n * 100) / 100;
14
+ }
15
+ /** Compute the Trust Score for a set of findings (capability rules excluded). */
16
+ export function computeScore(findings) {
17
+ // The grade reflects TRUST only: capability/blast-radius findings raise the
18
+ // capability level (computed elsewhere) but never lower the grade.
19
+ const threat = findings.filter((f) => !isCapabilityRule(f.ruleId));
20
+ // Deterministic order: strongest confidence, then highest severity first, so
21
+ // the full-weight slot in each rule goes to the most-certain finding.
22
+ const scored = threat
23
+ .filter((f) => f.severity !== 'info')
24
+ .slice()
25
+ .sort((a, b) => {
26
+ if (a.ruleId !== b.ruleId)
27
+ return a.ruleId < b.ruleId ? -1 : 1;
28
+ // The full-weight (rank-0) slot must go to the HIGHEST-penalty finding, so
29
+ // that adding a lower-penalty finding can never raise the score (monotonicity).
30
+ const pa = SEVERITY_WEIGHT[a.severity] * CONFIDENCE_MULT[a.confidence];
31
+ const pb = SEVERITY_WEIGHT[b.severity] * CONFIDENCE_MULT[b.confidence];
32
+ if (pa !== pb)
33
+ return pb - pa;
34
+ if (CONFIDENCE_ORDER[a.confidence] !== CONFIDENCE_ORDER[b.confidence])
35
+ return CONFIDENCE_ORDER[a.confidence] - CONFIDENCE_ORDER[b.confidence];
36
+ return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
37
+ });
38
+ const rankByRule = new Map();
39
+ const vector = [];
40
+ const categoryRaw = Object.fromEntries(ALL_CATEGORIES.map((c) => [c, 0]));
41
+ for (const f of scored) {
42
+ const rank = rankByRule.get(f.ruleId) ?? 0;
43
+ rankByRule.set(f.ruleId, rank + 1);
44
+ const rawWeight = SEVERITY_WEIGHT[f.severity];
45
+ const confidenceMult = CONFIDENCE_MULT[f.confidence];
46
+ const diminishingFactor = DIMINISHING[Math.min(rank, DIMINISHING.length - 1)];
47
+ const appliedPenalty = round2(rawWeight * confidenceMult * diminishingFactor);
48
+ categoryRaw[f.category] += appliedPenalty;
49
+ vector.push({
50
+ ruleId: f.ruleId,
51
+ category: f.category,
52
+ severity: f.severity,
53
+ confidence: f.confidence,
54
+ rawWeight,
55
+ confidenceMult,
56
+ diminishingFactor,
57
+ appliedPenalty,
58
+ });
59
+ }
60
+ const categorySubtotals = Object.fromEntries(ALL_CATEGORIES.map((c) => [c, round2(Math.min(CATEGORY_CAP[c], categoryRaw[c]))]));
61
+ const totalPenalty = ALL_CATEGORIES.reduce((sum, c) => sum + categorySubtotals[c], 0);
62
+ const score = Math.max(0, Math.min(100, Math.round(100 - totalPenalty)));
63
+ const band = bandForScore(score);
64
+ // Hard gates. Most gates fire only on `confirmed` findings so a guess can't
65
+ // force a cap — but ANY critical (regardless of confidence) floors the grade
66
+ // at D, so a critical-severity issue can never be graded A/B.
67
+ const confirmed = threat.filter((f) => f.confidence === 'confirmed');
68
+ const confirmedCritical = confirmed.filter((f) => f.severity === 'critical').length;
69
+ const confirmedHigh = confirmed.filter((f) => f.severity === 'high').length;
70
+ const anyCritical = threat.some((f) => f.severity === 'critical');
71
+ const gatesFired = [];
72
+ let gateCap;
73
+ const applyCap = (cap, reason) => {
74
+ gateCap = gateCap ? stricterGrade(gateCap, cap) : cap;
75
+ gatesFired.push(reason);
76
+ };
77
+ if (confirmedCritical > 0)
78
+ applyCap('F', `${confirmedCritical} confirmed critical finding(s) → grade capped at F`);
79
+ else if (anyCritical)
80
+ applyCap('D', `a critical finding is present → grade capped at D`);
81
+ if (confirmedHigh >= 2)
82
+ applyCap('D', `${confirmedHigh} confirmed high findings → grade capped at D`);
83
+ else if (confirmedHigh === 1)
84
+ applyCap('C', `1 confirmed high finding → grade capped at C`);
85
+ const grade = gateCap ? stricterGrade(band, gateCap) : band;
86
+ return {
87
+ score,
88
+ grade,
89
+ band,
90
+ gateCap,
91
+ categorySubtotals,
92
+ vector,
93
+ gatesFired,
94
+ methodologyVersion: METHODOLOGY_VERSION,
95
+ };
96
+ }
@@ -0,0 +1,50 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * The scoring model — every constant that can move a Trust Score lives here so
4
+ * the methodology is auditable in one place. A plain weighted average is
5
+ * gameable, so we use additive penalties from 100 with diminishing returns,
6
+ * per-category caps, and weakest-link gates — severity is kept distinct from
7
+ * risk, and the result is fully deterministic.
8
+ */
9
+ import type { Category, Confidence, Grade, Severity } from '../types.js';
10
+ /** Points for the FIRST finding of a severity, before modifiers. */
11
+ export declare const SEVERITY_WEIGHT: Record<Severity, number>;
12
+ /** Confidence multiplier — a guess must not carry a confirmed finding's weight. */
13
+ export declare const CONFIDENCE_MULT: Record<Confidence, number>;
14
+ /** Diminishing returns for the Nth finding of the same rule (index clamped to last). */
15
+ export declare const DIMINISHING: number[];
16
+ /** Maximum points any single category may subtract. */
17
+ export declare const CATEGORY_CAP: Record<Category, number>;
18
+ export declare const ALL_CATEGORIES: Category[];
19
+ /** Higher score = safer. Bands are fixed and published. */
20
+ export declare const GRADE_BANDS: {
21
+ grade: Grade;
22
+ min: number;
23
+ }[];
24
+ /** Strictness ordering: F is worst (0), A is best (4). */
25
+ export declare const GRADE_RANK: Record<Grade, number>;
26
+ export declare function bandForScore(score: number): Grade;
27
+ /** Return the stricter (worse) of two grades. */
28
+ export declare function stricterGrade(a: Grade, b: Grade): Grade;
29
+ /**
30
+ * MCP Trust Checker scores two independent axes:
31
+ *
32
+ * - TRUST (the A–F grade): does anything suggest the server is malicious or
33
+ * negligent? Driven by *threat* findings (poisoning, secrets, unicode
34
+ * smuggling, typosquat, CVEs, rug-pull, annotation lies, a single tool built
35
+ * as an exfiltration primitive).
36
+ * - CAPABILITY (a level): how much could this server do if the model driving
37
+ * it were manipulated? Driven by *capability* findings (code execution,
38
+ * filesystem writes, network egress, the cross-tool toxic-flow surface).
39
+ *
40
+ * A legitimate but powerful server (a scraper, a browser, a filesystem tool) is
41
+ * high-CAPABILITY but should still be high-TRUST — it isn't a bad actor, it just
42
+ * has a large blast radius. Keeping the axes separate is what stops the grade
43
+ * from collapsing every capable server into "F".
44
+ *
45
+ * These rules describe *capability* and therefore do NOT lower the trust grade;
46
+ * they raise the capability level and are shown as "capability observations".
47
+ */
48
+ export declare const CAPABILITY_RULES: Set<string>;
49
+ /** True if a rule describes capability/blast-radius rather than a trust threat. */
50
+ export declare function isCapabilityRule(ruleId: string): boolean;
@@ -0,0 +1,101 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * The scoring model — every constant that can move a Trust Score lives here so
4
+ * the methodology is auditable in one place. A plain weighted average is
5
+ * gameable, so we use additive penalties from 100 with diminishing returns,
6
+ * per-category caps, and weakest-link gates — severity is kept distinct from
7
+ * risk, and the result is fully deterministic.
8
+ */
9
+ /** Points for the FIRST finding of a severity, before modifiers. */
10
+ export const SEVERITY_WEIGHT = {
11
+ critical: 45,
12
+ high: 22,
13
+ medium: 9,
14
+ low: 3,
15
+ info: 0,
16
+ };
17
+ /** Confidence multiplier — a guess must not carry a confirmed finding's weight. */
18
+ export const CONFIDENCE_MULT = {
19
+ confirmed: 1.0,
20
+ strong: 0.7,
21
+ heuristic: 0.4,
22
+ speculative: 0.2,
23
+ };
24
+ /** Diminishing returns for the Nth finding of the same rule (index clamped to last). */
25
+ export const DIMINISHING = [1.0, 0.5, 0.25, 0.1];
26
+ /** Maximum points any single category may subtract. */
27
+ export const CATEGORY_CAP = {
28
+ injection: 50,
29
+ exfiltration: 50,
30
+ permissions: 35,
31
+ 'supply-chain': 30,
32
+ network: 25,
33
+ hygiene: 10,
34
+ };
35
+ export const ALL_CATEGORIES = [
36
+ 'injection',
37
+ 'exfiltration',
38
+ 'permissions',
39
+ 'supply-chain',
40
+ 'network',
41
+ 'hygiene',
42
+ ];
43
+ /** Higher score = safer. Bands are fixed and published. */
44
+ export const GRADE_BANDS = [
45
+ { grade: 'A', min: 90 },
46
+ { grade: 'B', min: 80 },
47
+ { grade: 'C', min: 70 },
48
+ { grade: 'D', min: 60 },
49
+ { grade: 'F', min: 0 },
50
+ ];
51
+ /** Strictness ordering: F is worst (0), A is best (4). */
52
+ export const GRADE_RANK = { F: 0, D: 1, C: 2, B: 3, A: 4 };
53
+ export function bandForScore(score) {
54
+ for (const b of GRADE_BANDS)
55
+ if (score >= b.min)
56
+ return b.grade;
57
+ return 'F';
58
+ }
59
+ /** Return the stricter (worse) of two grades. */
60
+ export function stricterGrade(a, b) {
61
+ return GRADE_RANK[a] <= GRADE_RANK[b] ? a : b;
62
+ }
63
+ /**
64
+ * MCP Trust Checker scores two independent axes:
65
+ *
66
+ * - TRUST (the A–F grade): does anything suggest the server is malicious or
67
+ * negligent? Driven by *threat* findings (poisoning, secrets, unicode
68
+ * smuggling, typosquat, CVEs, rug-pull, annotation lies, a single tool built
69
+ * as an exfiltration primitive).
70
+ * - CAPABILITY (a level): how much could this server do if the model driving
71
+ * it were manipulated? Driven by *capability* findings (code execution,
72
+ * filesystem writes, network egress, the cross-tool toxic-flow surface).
73
+ *
74
+ * A legitimate but powerful server (a scraper, a browser, a filesystem tool) is
75
+ * high-CAPABILITY but should still be high-TRUST — it isn't a bad actor, it just
76
+ * has a large blast radius. Keeping the axes separate is what stops the grade
77
+ * from collapsing every capable server into "F".
78
+ *
79
+ * These rules describe *capability* and therefore do NOT lower the trust grade;
80
+ * they raise the capability level and are shown as "capability observations".
81
+ */
82
+ export const CAPABILITY_RULES = new Set([
83
+ 'MTC-CAP-001', // command/code execution
84
+ 'MTC-CAP-002', // filesystem mutation
85
+ 'MTC-CAP-004', // open-world + sensitive read
86
+ 'MTC-CAP-005', // missing destructiveHint
87
+ 'MTC-CAP-006', // unconstrained command param
88
+ 'MTC-CAP-007', // unconstrained URL/host param
89
+ 'MTC-CAP-008', // unconstrained path param
90
+ 'MTC-CAP-009', // declared sampling capability
91
+ 'MTC-CAP-010', // declared elicitation capability (the secret-seeking variant is MTC-CAP-011, a threat)
92
+ 'MTC-FLOW-002', // cross-tool trifecta (capability co-presence, not malice)
93
+ 'MTC-FLOW-003', // read + egress in one tool
94
+ 'MTC-FLOW-004', // source + sink co-exist
95
+ 'MTC-FLOW-005', // untrusted input reaches an action
96
+ 'MTC-NET-005', // remote endpoint (informational)
97
+ ]);
98
+ /** True if a rule describes capability/blast-radius rather than a trust threat. */
99
+ export function isCapabilityRule(ruleId) {
100
+ return CAPABILITY_RULES.has(ruleId);
101
+ }
@@ -0,0 +1,361 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * MCP Trust Checker core type model.
4
+ *
5
+ * Everything in the engine is expressed against a single normalized, transport-
6
+ * agnostic view of a server — the {@link ServerSurface}. Acquisition adapters
7
+ * (stdio / HTTP / static JSON / client config) all produce a `ServerSurface`;
8
+ * every detector consumes one and emits {@link Finding}s; the scorer turns
9
+ * findings into an auditable {@link Score}. Keeping this model small and pure is
10
+ * what makes the whole tool deterministic and offline-capable.
11
+ */
12
+ /** Where a surface was acquired from (drives which detectors have signal). */
13
+ export type SurfaceSourceKind = 'stdio' | 'http' | 'manifest' | 'client-config' | 'package';
14
+ export interface SurfaceSource {
15
+ kind: SurfaceSourceKind;
16
+ /** Human-readable origin: a path, URL, or package spec. */
17
+ origin: string;
18
+ }
19
+ /** A JSON-Schema-ish object as advertised by a tool's `inputSchema`. */
20
+ export interface JsonSchema {
21
+ type?: string | string[];
22
+ properties?: Record<string, JsonSchema>;
23
+ items?: JsonSchema | JsonSchema[];
24
+ required?: string[];
25
+ enum?: unknown[];
26
+ description?: string;
27
+ title?: string;
28
+ format?: string;
29
+ [k: string]: unknown;
30
+ }
31
+ /**
32
+ * Tool behavior hints as advertised by the server.
33
+ *
34
+ * SECURITY NOTE: these are attacker-controllable and MUST NOT be trusted for
35
+ * security decisions. MCP Trust Checker reads them only to flag when they *contradict*
36
+ * a tool's derived capabilities (annotation-vs-behavior mismatch).
37
+ */
38
+ export interface ToolAnnotations {
39
+ title?: string;
40
+ readOnlyHint?: boolean;
41
+ destructiveHint?: boolean;
42
+ idempotentHint?: boolean;
43
+ openWorldHint?: boolean;
44
+ [k: string]: unknown;
45
+ }
46
+ export interface ToolDef {
47
+ name: string;
48
+ title?: string;
49
+ description?: string;
50
+ inputSchema?: JsonSchema;
51
+ outputSchema?: JsonSchema;
52
+ annotations?: ToolAnnotations;
53
+ }
54
+ export interface PromptArgument {
55
+ name: string;
56
+ description?: string;
57
+ required?: boolean;
58
+ }
59
+ export interface PromptDef {
60
+ name: string;
61
+ title?: string;
62
+ description?: string;
63
+ arguments?: PromptArgument[];
64
+ }
65
+ export interface ResourceDef {
66
+ uri?: string;
67
+ uriTemplate?: string;
68
+ name?: string;
69
+ title?: string;
70
+ description?: string;
71
+ mimeType?: string;
72
+ }
73
+ export interface TransportInfo {
74
+ kind: 'stdio' | 'http' | 'sse' | 'unknown';
75
+ /** For HTTP/SSE transports. */
76
+ url?: string;
77
+ /** For stdio transports. */
78
+ command?: string;
79
+ args?: string[];
80
+ /**
81
+ * True when the spawned `command`/`args` originate from untrusted metadata
82
+ * without an executable allowlist — the systemic stdio-RCE class.
83
+ */
84
+ userControlledCommand?: boolean;
85
+ /** Execution-hijacking env vars that were stripped before spawning, if any. */
86
+ droppedEnv?: string[];
87
+ }
88
+ /** Package/provenance metadata, if the target maps to a known package. */
89
+ export interface PackageMeta {
90
+ registry?: 'npm' | 'pypi' | 'unknown';
91
+ name?: string;
92
+ version?: string;
93
+ /** Raw install/lifecycle scripts, if known. */
94
+ scripts?: Record<string, string>;
95
+ dependencies?: string[];
96
+ repositoryUrl?: string | null;
97
+ license?: string | null;
98
+ weeklyDownloads?: number | null;
99
+ /** ISO 8601 publish timestamp of the resolved version, if known. */
100
+ publishedAt?: string | null;
101
+ /** Whether the install spec pins an exact version (false = @latest/floating). */
102
+ pinned?: boolean;
103
+ /** The raw version token from the install spec, if any (e.g. "latest", "^1.2.0"). */
104
+ requestedSpec?: string;
105
+ }
106
+ /** The single normalized object every detector operates on. */
107
+ export interface ServerSurface {
108
+ /** Stable identity used for lockfile pinning (package spec, url, or path). */
109
+ id: string;
110
+ source: SurfaceSource;
111
+ server: {
112
+ name?: string;
113
+ version?: string;
114
+ title?: string;
115
+ /** Free-text server instructions — a first-class line-jumping surface. */
116
+ instructions?: string;
117
+ protocolVersion?: string;
118
+ capabilities?: Record<string, unknown>;
119
+ };
120
+ tools: ToolDef[];
121
+ prompts: PromptDef[];
122
+ resources: ResourceDef[];
123
+ transport?: TransportInfo;
124
+ packageMeta?: PackageMeta;
125
+ /**
126
+ * Server implementation source, when available (a local package directory or
127
+ * an extracted tarball). Enables implementation-level analysis — reading what
128
+ * the code *does*, not only what the tool metadata *claims*.
129
+ */
130
+ sourceFiles?: SourceFile[];
131
+ }
132
+ export interface SourceFile {
133
+ /** Path relative to the package root. */
134
+ path: string;
135
+ content: string;
136
+ }
137
+ export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
138
+ /**
139
+ * How sure we are — the "severity ≠ risk" split: a heuristic
140
+ * keyword hit and a decoded hidden-instruction payload can share a severity
141
+ * but must not carry the same weight — and only `confirmed` findings can fire
142
+ * a hard grade gate.
143
+ */
144
+ export type Confidence = 'confirmed' | 'strong' | 'heuristic' | 'speculative';
145
+ export type Category = 'injection' | 'exfiltration' | 'permissions' | 'supply-chain' | 'network' | 'hygiene';
146
+ export interface FindingLocation {
147
+ kind: 'tool' | 'prompt' | 'resource' | 'server' | 'package' | 'transport' | 'flow';
148
+ /** Name of the tool/prompt/resource, when applicable. */
149
+ name?: string;
150
+ /** Field within the object, e.g. `description` or `inputSchema.properties.path.description`. */
151
+ field?: string;
152
+ }
153
+ export interface Finding {
154
+ /** Stable rule id, e.g. `MTC-UNI-001`. Used for docs, baselines and SARIF. */
155
+ ruleId: string;
156
+ title: string;
157
+ category: Category;
158
+ severity: Severity;
159
+ confidence: Confidence;
160
+ /** What was found and why it matters. */
161
+ description: string;
162
+ remediation?: string;
163
+ location?: FindingLocation;
164
+ /** Concrete evidence: a decoded payload, matched snippet, offending name, etc. */
165
+ evidence?: string;
166
+ /** External references (OWASP, CVE, blog posts, spec sections). */
167
+ references?: string[];
168
+ /** OWASP MCP Top 10 / LLM Top 10 mapping id, when available. */
169
+ owasp?: string;
170
+ /** Structured extras for machine consumers. */
171
+ data?: Record<string, unknown>;
172
+ }
173
+ export type CapabilityTag = 'untrusted-input' | 'sensitive-source' | 'external-sink' | 'code-exec' | 'file-write';
174
+ export interface ToolCapability {
175
+ tool: string;
176
+ tags: CapabilityTag[];
177
+ /** Why each tag was assigned (keyword/schema evidence), for explainability. */
178
+ reasons: Partial<Record<CapabilityTag, string[]>>;
179
+ }
180
+ /** How much a server could do if the model driving it were manipulated. */
181
+ export type CapabilityLevel = 'minimal' | 'moderate' | 'high' | 'critical';
182
+ export interface CapabilityProfile {
183
+ level: CapabilityLevel;
184
+ /** Human-readable reasons the level was assigned. */
185
+ reasons: string[];
186
+ /** The union of capability tags observed across the server's tools. */
187
+ tags: CapabilityTag[];
188
+ }
189
+ export interface ToxicFlow {
190
+ id: string;
191
+ severity: Severity;
192
+ confidence: Confidence;
193
+ /** Tool(s) that supply the untrusted input, if any. */
194
+ untrustedInput: string[];
195
+ /** Tool(s) that read sensitive data, if any. */
196
+ sensitiveSource: string[];
197
+ /** Tool(s) that can exfiltrate / act externally. */
198
+ externalSink: string[];
199
+ /** True when a single tool holds more than one role (self-contained primitive). */
200
+ selfContained: boolean;
201
+ /** The concrete attack chain of tools (e.g. ["fetch_url","read_file","http_request"]). */
202
+ path?: string[];
203
+ /** True when at least one leg of the path is a direct schema wire (higher plausibility). */
204
+ pathWired?: boolean;
205
+ description: string;
206
+ }
207
+ export type Grade = 'A' | 'B' | 'C' | 'D' | 'F';
208
+ export interface ScoreVectorItem {
209
+ ruleId: string;
210
+ category: Category;
211
+ severity: Severity;
212
+ confidence: Confidence;
213
+ /** Base weight for the severity. */
214
+ rawWeight: number;
215
+ confidenceMult: number;
216
+ /** Diminishing-returns multiplier for the Nth finding of this rule. */
217
+ diminishingFactor: number;
218
+ /** rawWeight × confidenceMult × diminishingFactor, rounded to 2dp. */
219
+ appliedPenalty: number;
220
+ }
221
+ export interface Score {
222
+ /** 0–100, higher is safer. */
223
+ score: number;
224
+ /** Final grade after gates. */
225
+ grade: Grade;
226
+ /** Grade implied by the raw number, before hard gates. */
227
+ band: Grade;
228
+ /** Strictest grade cap forced by a hard gate, if any. */
229
+ gateCap?: Grade;
230
+ /** Points subtracted per category (after per-category caps). */
231
+ categorySubtotals: Record<Category, number>;
232
+ /** Fully itemized, reconstructable penalty vector. */
233
+ vector: ScoreVectorItem[];
234
+ /** Human-readable descriptions of every gate that fired. */
235
+ gatesFired: string[];
236
+ methodologyVersion: string;
237
+ }
238
+ export type IntegrityStatus = 'first-seen' | 'unchanged' | 'drift';
239
+ export interface SurfaceChange {
240
+ kind: 'tool-added' | 'tool-removed' | 'tool-changed' | 'instructions-changed';
241
+ name?: string;
242
+ detail: string;
243
+ }
244
+ export interface IntegrityResult {
245
+ status: IntegrityStatus;
246
+ currentDigest: string;
247
+ previousDigest?: string;
248
+ /** Populated when status is `drift`. */
249
+ changes?: SurfaceChange[];
250
+ }
251
+ export interface ScanReport {
252
+ tool: {
253
+ name: string;
254
+ version: string;
255
+ methodologyVersion: string;
256
+ };
257
+ target: {
258
+ id: string;
259
+ source: SurfaceSource;
260
+ server?: {
261
+ name?: string;
262
+ version?: string;
263
+ };
264
+ };
265
+ /** ISO timestamp. Omitted for reproducible (deterministic) reports. */
266
+ scannedAt?: string;
267
+ findings: Finding[];
268
+ score: Score;
269
+ capabilities: ToolCapability[];
270
+ /** The server's blast-radius rating (independent of the trust grade). */
271
+ capabilityProfile: CapabilityProfile;
272
+ toxicFlows: ToxicFlow[];
273
+ integrity?: IntegrityResult;
274
+ /** SHA-256 of the canonicalized surface — the rug-pull fingerprint. */
275
+ surfaceDigest: string;
276
+ stats: {
277
+ tools: number;
278
+ prompts: number;
279
+ resources: number;
280
+ findingsBySeverity: Record<Severity, number>;
281
+ };
282
+ }
283
+ export interface DetectorContext {
284
+ surface: ServerSurface;
285
+ config: ResolvedConfig;
286
+ /** Per-tool capability tags, computed once by the engine and shared. */
287
+ capabilities: ToolCapability[];
288
+ /**
289
+ * Tool names exposed by OTHER servers in the same scan, for cross-server
290
+ * name-collision / shadowing detection. Empty when scanning a single target.
291
+ */
292
+ siblingTools?: {
293
+ server: string;
294
+ name: string;
295
+ }[];
296
+ }
297
+ export interface Detector {
298
+ id: string;
299
+ /** Pipeline stage number (see docs/architecture.md). */
300
+ stage: number;
301
+ title: string;
302
+ run(ctx: DetectorContext): Finding[] | Promise<Finding[]>;
303
+ }
304
+ export interface McpTrustCheckerConfig {
305
+ /** Rule ids to disable entirely. */
306
+ disabledRules?: string[];
307
+ /** Rule ids whose findings are allowed (suppressed) — an explicit waiver list. */
308
+ allowlist?: string[];
309
+ /** Fail CI when the score is below this threshold (0–100). */
310
+ failUnder?: number;
311
+ /** Fail CI when the grade is worse than this (e.g. "B"). */
312
+ minGrade?: Grade;
313
+ /** Path to the integrity lockfile. */
314
+ lockfile?: string;
315
+ /**
316
+ * When true, the toxic-flow graph assumes the client also exposes generic
317
+ * built-in tools (web-fetch as untrusted input, file/network as sinks), which
318
+ * can complete a trifecta on their own.
319
+ */
320
+ includeBuiltins?: boolean;
321
+ /** Additional package names to hard-protect against typosquatting. */
322
+ protectedPackages?: string[];
323
+ /** Zero-width character count above which a string is treated as an encoded payload. */
324
+ invisibleCharThreshold?: number;
325
+ /**
326
+ * Location-scoped waivers (a baseline). Unlike `allowlist` (which silences a
327
+ * whole rule everywhere), each entry silences a specific finding on a specific
328
+ * tool/field — with a justification that stays in the config for audit.
329
+ * Also loadable from a standalone `.mtcignore` JSON file next to the config.
330
+ */
331
+ suppress?: Suppression[];
332
+ /** Organisational policy the scan is gated against (see {@link Policy}). */
333
+ policy?: Policy;
334
+ }
335
+ /** A single baseline waiver. `rule` is required; the rest narrow the match. */
336
+ export interface Suppression {
337
+ rule: string;
338
+ /** Only suppress on this tool/prompt/resource name (location.name). */
339
+ tool?: string;
340
+ /** Only suppress on this field (location.field). */
341
+ field?: string;
342
+ /** Why the waiver is safe — kept for audit, never affects matching. */
343
+ reason?: string;
344
+ }
345
+ /** Policy-as-code: declarative rules a server must satisfy, gated in CI. */
346
+ export interface Policy {
347
+ /** Fail if the Trust grade is worse than this. */
348
+ minGrade?: Grade;
349
+ /** Fail if the Capability blast-radius exceeds this level. */
350
+ maxCapability?: CapabilityLevel;
351
+ /** Fail if any of these rule ids fired. */
352
+ denyRules?: string[];
353
+ /** Fail if the server exposes any of these capability tags. */
354
+ denyCapabilities?: CapabilityTag[];
355
+ }
356
+ /** Config with all defaults resolved — what detectors actually see. */
357
+ export interface ResolvedConfig extends Required<Omit<McpTrustCheckerConfig, 'minGrade' | 'lockfile' | 'policy'>> {
358
+ minGrade?: Grade;
359
+ lockfile?: string;
360
+ policy?: Policy;
361
+ }
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * MCP Trust Checker core type model.
4
+ *
5
+ * Everything in the engine is expressed against a single normalized, transport-
6
+ * agnostic view of a server — the {@link ServerSurface}. Acquisition adapters
7
+ * (stdio / HTTP / static JSON / client config) all produce a `ServerSurface`;
8
+ * every detector consumes one and emits {@link Finding}s; the scorer turns
9
+ * findings into an auditable {@link Score}. Keeping this model small and pure is
10
+ * what makes the whole tool deterministic and offline-capable.
11
+ */
12
+ export {};
@@ -0,0 +1,26 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Tiny zero-dependency ANSI helper. Respects NO_COLOR and non-TTY output.
4
+ * A security tool should not pull a coloring library into its dependency tree.
5
+ */
6
+ export declare const c: {
7
+ enabled: boolean;
8
+ bold: (s: string) => string;
9
+ dim: (s: string) => string;
10
+ underline: (s: string) => string;
11
+ red: (s: string) => string;
12
+ green: (s: string) => string;
13
+ yellow: (s: string) => string;
14
+ blue: (s: string) => string;
15
+ magenta: (s: string) => string;
16
+ cyan: (s: string) => string;
17
+ gray: (s: string) => string;
18
+ white: (s: string) => string;
19
+ bgRed: (s: string) => string;
20
+ bgGreen: (s: string) => string;
21
+ bgYellow: (s: string) => string;
22
+ };
23
+ /** Visible length of a string, ignoring ANSI escapes. */
24
+ export declare function visibleLength(s: string): number;
25
+ /** Pad a (possibly colored) string to a visible width. */
26
+ export declare function padVisible(s: string, width: number): string;