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,42 @@
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
+ const enabled = (() => {
7
+ if (process.env.NO_COLOR !== undefined)
8
+ return false;
9
+ if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== '0')
10
+ return true;
11
+ return Boolean(process.stdout.isTTY);
12
+ })();
13
+ function wrap(open, close) {
14
+ return (s) => (enabled ? `[${open}m${s}[${close}m` : s);
15
+ }
16
+ export const c = {
17
+ enabled,
18
+ bold: wrap(1, 22),
19
+ dim: wrap(2, 22),
20
+ underline: wrap(4, 24),
21
+ red: wrap(31, 39),
22
+ green: wrap(32, 39),
23
+ yellow: wrap(33, 39),
24
+ blue: wrap(34, 39),
25
+ magenta: wrap(35, 39),
26
+ cyan: wrap(36, 39),
27
+ gray: wrap(90, 39),
28
+ white: wrap(37, 39),
29
+ bgRed: wrap(41, 49),
30
+ bgGreen: wrap(42, 49),
31
+ bgYellow: wrap(43, 49),
32
+ };
33
+ /** Visible length of a string, ignoring ANSI escapes. */
34
+ export function visibleLength(s) {
35
+ // eslint-disable-next-line no-control-regex
36
+ return s.replace(/\[[0-9;]*m/g, '').length;
37
+ }
38
+ /** Pad a (possibly colored) string to a visible width. */
39
+ export function padVisible(s, width) {
40
+ const pad = Math.max(0, width - visibleLength(s));
41
+ return s + ' '.repeat(pad);
42
+ }
@@ -0,0 +1,17 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Capability extraction: derive each tool's roles (untrusted-input,
4
+ * sensitive-source, external-sink, code-exec, file-write) from its name,
5
+ * description verbs, and parameter shape. This is the substrate the toxic-flow
6
+ * graph runs over. It is deliberately derived from behavior, never from the
7
+ * server's self-declared `annotations` (which are untrusted).
8
+ */
9
+ import type { CapabilityTag, ResolvedConfig, ServerSurface, ToolCapability, ToolDef } from '../types.js';
10
+ /** Extract capability tags + evidence for a single tool. */
11
+ export declare function extractToolCapability(tool: ToolDef): ToolCapability;
12
+ /** Extract capabilities for every tool in a surface (non-object tools skipped). */
13
+ export declare function extractCapabilities(surface: ServerSurface, _config: ResolvedConfig): ToolCapability[];
14
+ /** Tags that can act as an exfiltration/impact sink. */
15
+ export declare const SINK_TAGS: CapabilityTag[];
16
+ /** True if a capability set can serve as an external sink. */
17
+ export declare function isSink(tags: CapabilityTag[]): boolean;
@@ -0,0 +1,68 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Capability extraction: derive each tool's roles (untrusted-input,
4
+ * sensitive-source, external-sink, code-exec, file-write) from its name,
5
+ * description verbs, and parameter shape. This is the substrate the toxic-flow
6
+ * graph runs over. It is deliberately derived from behavior, never from the
7
+ * server's self-declared `annotations` (which are untrusted).
8
+ */
9
+ import { CAPABILITY_SIGNALS, PARAM_NAME_SIGNALS } from '../data/capabilityLexicon.js';
10
+ import { normalizeForMatch } from './text.js';
11
+ function tokenize(text) {
12
+ return new Set(normalizeForMatch(text).split(' ').filter(Boolean));
13
+ }
14
+ function keywordMatches(haystack, tokens, keyword) {
15
+ const k = normalizeForMatch(keyword);
16
+ if (k.includes(' '))
17
+ return haystack.includes(k);
18
+ return tokens.has(k);
19
+ }
20
+ function paramNames(tool) {
21
+ const props = tool.inputSchema?.properties;
22
+ if (!props)
23
+ return [];
24
+ return Object.keys(props);
25
+ }
26
+ /** Extract capability tags + evidence for a single tool. */
27
+ export function extractToolCapability(tool) {
28
+ const name = typeof tool?.name === 'string' ? tool.name : '';
29
+ const desc = typeof tool?.description === 'string' ? tool.description : '';
30
+ const haystack = normalizeForMatch(`${name} ${desc}`);
31
+ const tokens = tokenize(`${name} ${desc}`);
32
+ const reasons = {};
33
+ const tags = new Set();
34
+ const addReason = (tag, why) => {
35
+ tags.add(tag);
36
+ (reasons[tag] ??= []).push(why);
37
+ };
38
+ for (const sig of CAPABILITY_SIGNALS) {
39
+ for (const kw of sig.keywords) {
40
+ if (keywordMatches(haystack, tokens, kw)) {
41
+ addReason(sig.tag, `keyword "${kw}"`);
42
+ break; // one reason per signal is enough
43
+ }
44
+ }
45
+ }
46
+ const pnames = paramNames(tool).map((n) => n.toLowerCase());
47
+ const pTokens = new Set(pnames.flatMap((n) => n.split(/[^a-z0-9]+/i).filter(Boolean)));
48
+ for (const sig of PARAM_NAME_SIGNALS) {
49
+ for (const pn of sig.names) {
50
+ if (pTokens.has(pn)) {
51
+ addReason(sig.tag, `parameter "${pn}"`);
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ return { tool: name, tags: [...tags], reasons };
57
+ }
58
+ /** Extract capabilities for every tool in a surface (non-object tools skipped). */
59
+ export function extractCapabilities(surface, _config) {
60
+ const tools = Array.isArray(surface.tools) ? surface.tools : [];
61
+ return tools.filter((t) => Boolean(t) && typeof t === 'object').map(extractToolCapability);
62
+ }
63
+ /** Tags that can act as an exfiltration/impact sink. */
64
+ export const SINK_TAGS = ['external-sink', 'code-exec'];
65
+ /** True if a capability set can serve as an external sink. */
66
+ export function isSink(tags) {
67
+ return tags.some((t) => SINK_TAGS.includes(t));
68
+ }
@@ -0,0 +1,23 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * String-distance & version helpers for the supply-chain and posture stages.
4
+ * All pure and dependency-free.
5
+ */
6
+ /**
7
+ * Damerau-Levenshtein (optimal string alignment) edit distance. Counts
8
+ * insertions, deletions, substitutions and adjacent transpositions each as 1.
9
+ * `swap` counting is what makes "sdk" vs "sdck"-style transposes cost 1.
10
+ */
11
+ export declare function damerauLevenshtein(a: string, b: string): number;
12
+ /** True if two single chars are physically adjacent on a QWERTY keyboard. */
13
+ export declare function isKeyboardAdjacent(a: string, b: string): boolean;
14
+ /**
15
+ * True when `candidate` differs from `target` by exactly one substitution and
16
+ * that substitution is a keyboard-adjacent slip (a classic typo, distinct from
17
+ * a deliberate homoglyph swap).
18
+ */
19
+ export declare function isKeyboardTypo(candidate: string, target: string): boolean;
20
+ /** Compare two versions. Returns -1 if a<b, 0 if equal, 1 if a>b. */
21
+ export declare function compareVersions(a: string, b: string): number;
22
+ /** Strip an npm scope, returning the bare package name. */
23
+ export declare function bareName(pkg: string): string;
@@ -0,0 +1,119 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * String-distance & version helpers for the supply-chain and posture stages.
4
+ * All pure and dependency-free.
5
+ */
6
+ /**
7
+ * Damerau-Levenshtein (optimal string alignment) edit distance. Counts
8
+ * insertions, deletions, substitutions and adjacent transpositions each as 1.
9
+ * `swap` counting is what makes "sdk" vs "sdck"-style transposes cost 1.
10
+ */
11
+ export function damerauLevenshtein(a, b) {
12
+ const al = a.length;
13
+ const bl = b.length;
14
+ if (al === 0)
15
+ return bl;
16
+ if (bl === 0)
17
+ return al;
18
+ const d = Array.from({ length: al + 1 }, () => new Array(bl + 1).fill(0));
19
+ for (let i = 0; i <= al; i++)
20
+ d[i][0] = i;
21
+ for (let j = 0; j <= bl; j++)
22
+ d[0][j] = j;
23
+ for (let i = 1; i <= al; i++) {
24
+ for (let j = 1; j <= bl; j++) {
25
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
26
+ let val = Math.min(d[i - 1][j] + 1, // deletion
27
+ d[i][j - 1] + 1, // insertion
28
+ d[i - 1][j - 1] + cost);
29
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
30
+ val = Math.min(val, d[i - 2][j - 2] + 1); // transposition
31
+ }
32
+ d[i][j] = val;
33
+ }
34
+ }
35
+ return d[al][bl];
36
+ }
37
+ /** QWERTY physical-adjacency map (lowercase). */
38
+ const QWERTY_ADJ = {
39
+ q: 'wa',
40
+ w: 'qeas',
41
+ e: 'wrsd',
42
+ r: 'etdf',
43
+ t: 'ryfg',
44
+ y: 'tugh',
45
+ u: 'yihj',
46
+ i: 'uojk',
47
+ o: 'ipkl',
48
+ p: 'ol',
49
+ a: 'qwsz',
50
+ s: 'awedxz',
51
+ d: 'serfcx',
52
+ f: 'drtgvc',
53
+ g: 'ftyhbv',
54
+ h: 'gyujnb',
55
+ j: 'huikmn',
56
+ k: 'jiolm',
57
+ l: 'kop',
58
+ z: 'asx',
59
+ x: 'zsdc',
60
+ c: 'xdfv',
61
+ v: 'cfgb',
62
+ b: 'vghn',
63
+ n: 'bhjm',
64
+ m: 'njk',
65
+ };
66
+ /** True if two single chars are physically adjacent on a QWERTY keyboard. */
67
+ export function isKeyboardAdjacent(a, b) {
68
+ return (QWERTY_ADJ[a.toLowerCase()] ?? '').includes(b.toLowerCase());
69
+ }
70
+ /**
71
+ * True when `candidate` differs from `target` by exactly one substitution and
72
+ * that substitution is a keyboard-adjacent slip (a classic typo, distinct from
73
+ * a deliberate homoglyph swap).
74
+ */
75
+ export function isKeyboardTypo(candidate, target) {
76
+ if (candidate.length !== target.length)
77
+ return false;
78
+ let diffs = 0;
79
+ let adjacent = false;
80
+ for (let i = 0; i < candidate.length; i++) {
81
+ if (candidate[i] !== target[i]) {
82
+ diffs += 1;
83
+ adjacent = isKeyboardAdjacent(candidate[i], target[i]);
84
+ if (diffs > 1)
85
+ return false;
86
+ }
87
+ }
88
+ return diffs === 1 && adjacent;
89
+ }
90
+ /** Parse a dotted version into numeric segments (non-numeric parts → 0). */
91
+ function parseVersion(v) {
92
+ return v
93
+ .replace(/^[v=]/, '')
94
+ .split(/[.+-]/)
95
+ .map((s) => {
96
+ const n = Number.parseInt(s, 10);
97
+ return Number.isNaN(n) ? 0 : n;
98
+ });
99
+ }
100
+ /** Compare two versions. Returns -1 if a<b, 0 if equal, 1 if a>b. */
101
+ export function compareVersions(a, b) {
102
+ const pa = parseVersion(a);
103
+ const pb = parseVersion(b);
104
+ const len = Math.max(pa.length, pb.length);
105
+ for (let i = 0; i < len; i++) {
106
+ const x = pa[i] ?? 0;
107
+ const y = pb[i] ?? 0;
108
+ if (x < y)
109
+ return -1;
110
+ if (x > y)
111
+ return 1;
112
+ }
113
+ return 0;
114
+ }
115
+ /** Strip an npm scope, returning the bare package name. */
116
+ export function bareName(pkg) {
117
+ const slash = pkg.indexOf('/');
118
+ return pkg.startsWith('@') && slash >= 0 ? pkg.slice(slash + 1) : pkg;
119
+ }
@@ -0,0 +1,20 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Canonicalization & hashing for the rug-pull (TOFU) integrity fingerprint.
4
+ *
5
+ * Trust binds to *content*, not to a name (the MCPoison lesson). We therefore
6
+ * hash the full canonicalized tool surface — name + description + full schema +
7
+ * annotations + the spawn command — so any silent redefinition changes the
8
+ * digest and is caught on rescan.
9
+ */
10
+ import type { ServerSurface, ToolDef } from '../types.js';
11
+ /** Deterministic JSON: object keys sorted recursively. */
12
+ export declare function stableStringify(value: unknown): string;
13
+ /** Canonical per-tool projection used for both the digest and drift diffs. */
14
+ export declare function canonicalTool(tool: ToolDef): Record<string, unknown>;
15
+ /** The canonical, security-relevant projection of a whole surface. */
16
+ export declare function canonicalSurface(surface: ServerSurface): Record<string, unknown>;
17
+ /** SHA-256 over the canonical surface — the rug-pull fingerprint. */
18
+ export declare function surfaceDigest(surface: ServerSurface): string;
19
+ /** SHA-256 over a single canonical tool (used to diff which tools changed). */
20
+ export declare function toolDigest(tool: ToolDef): string;
@@ -0,0 +1,80 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Canonicalization & hashing for the rug-pull (TOFU) integrity fingerprint.
4
+ *
5
+ * Trust binds to *content*, not to a name (the MCPoison lesson). We therefore
6
+ * hash the full canonicalized tool surface — name + description + full schema +
7
+ * annotations + the spawn command — so any silent redefinition changes the
8
+ * digest and is caught on rescan.
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ /** Deterministic JSON: object keys sorted recursively. */
12
+ export function stableStringify(value) {
13
+ return JSON.stringify(sortDeep(value, 0, new WeakSet()));
14
+ }
15
+ // Bound the recursion so a hostile, deeply-nested schema can't overflow the
16
+ // stack during hashing. 200 is far beyond any legitimate JSON-Schema depth.
17
+ const MAX_HASH_DEPTH = 200;
18
+ function sortDeep(value, depth, seen) {
19
+ if (depth > MAX_HASH_DEPTH)
20
+ return '__mcptrustchecker_truncated__';
21
+ if (value && typeof value === 'object') {
22
+ // A shared subtree (DAG) would otherwise expand exponentially even under the
23
+ // depth cap — visit each node once.
24
+ if (seen.has(value))
25
+ return '__mcptrustchecker_cycle__';
26
+ seen.add(value);
27
+ }
28
+ if (Array.isArray(value))
29
+ return value.map((v) => sortDeep(v, depth + 1, seen));
30
+ if (value && typeof value === 'object') {
31
+ const out = {};
32
+ for (const key of Object.keys(value).sort()) {
33
+ out[key] = sortDeep(value[key], depth + 1, seen);
34
+ }
35
+ return out;
36
+ }
37
+ return value;
38
+ }
39
+ /** Canonical per-tool projection used for both the digest and drift diffs. */
40
+ export function canonicalTool(tool) {
41
+ return {
42
+ name: typeof tool.name === 'string' ? tool.name : String(tool.name ?? ''),
43
+ description: typeof tool.description === 'string' ? tool.description : '',
44
+ inputSchema: tool.inputSchema ?? null,
45
+ outputSchema: tool.outputSchema ?? null,
46
+ annotations: tool.annotations ?? null,
47
+ };
48
+ }
49
+ /** The canonical, security-relevant projection of a whole surface. */
50
+ export function canonicalSurface(surface) {
51
+ const tools = (Array.isArray(surface.tools) ? [...surface.tools] : [])
52
+ .filter((t) => Boolean(t) && typeof t === 'object')
53
+ // Code-unit sort, NOT localeCompare — ICU collation is locale-dependent, so
54
+ // the same tool set could canonicalize differently on two machines and raise
55
+ // a false rug-pull DRIFT. This must be byte-reproducible everywhere.
56
+ .sort((a, b) => {
57
+ const x = String(a.name ?? '');
58
+ const y = String(b.name ?? '');
59
+ return x < y ? -1 : x > y ? 1 : 0;
60
+ })
61
+ .map(canonicalTool);
62
+ return {
63
+ server: {
64
+ name: surface.server.name ?? '',
65
+ instructions: surface.server.instructions ?? '',
66
+ },
67
+ tools,
68
+ transport: surface.transport
69
+ ? { kind: surface.transport.kind, command: surface.transport.command ?? '', args: surface.transport.args ?? [] }
70
+ : null,
71
+ };
72
+ }
73
+ /** SHA-256 over the canonical surface — the rug-pull fingerprint. */
74
+ export function surfaceDigest(surface) {
75
+ return createHash('sha256').update(stableStringify(canonicalSurface(surface))).digest('hex');
76
+ }
77
+ /** SHA-256 over a single canonical tool (used to diff which tools changed). */
78
+ export function toolDigest(tool) {
79
+ return createHash('sha256').update(stableStringify(canonicalTool(tool))).digest('hex');
80
+ }
@@ -0,0 +1,45 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Text extraction & normalization helpers shared by the lexical detectors.
4
+ * The point of `collectTextFields` is that every human/LLM-facing string in the
5
+ * surface is scanned uniformly, each carrying a precise location so findings can
6
+ * point at exactly which field is poisoned.
7
+ */
8
+ import type { FindingLocation, JsonSchema, ServerSurface } from '../types.js';
9
+ import { INJECTION_PATTERNS } from '../data/injectionPatterns.js';
10
+ export interface TextField {
11
+ text: string;
12
+ location: FindingLocation;
13
+ /** Which injection channel this field belongs to. */
14
+ channel: 'tool-description' | 'param-description' | 'server-instructions' | 'other';
15
+ }
16
+ /** Walk an inputSchema collecting every `description` string with a JSON path. */
17
+ export declare function collectSchemaStrings(schema: JsonSchema | undefined, basePath: string): {
18
+ path: string;
19
+ text: string;
20
+ }[];
21
+ /**
22
+ * Collect every scannable text field across the whole surface. Defensive: only
23
+ * string values are emitted and non-object tools/prompts/resources are skipped,
24
+ * so a caller that hands us a hand-built (unnormalized) surface — or a live
25
+ * server returning junk — can never crash a text detector.
26
+ */
27
+ export declare function collectTextFields(surface: ServerSurface): TextField[];
28
+ /** Lowercased text with punctuation collapsed to spaces, for keyword matching. */
29
+ export declare function normalizeForMatch(text: string): string;
30
+ /** Count the longest run of consecutive shouted words (excluding tech acronyms). */
31
+ export declare function longestAllCapsRun(text: string): number;
32
+ /**
33
+ * Detect a long base64-looking blob (potential encoded payload). Deliberately
34
+ * rejects pure-hex runs (git SHAs, dash-stripped UUIDs, account/hex IDs) and
35
+ * requires genuine base64 shape (a padding/base64 symbol, or real mixed case
36
+ * with digits), so ordinary identifiers in descriptions don't misfire.
37
+ */
38
+ export declare function hasBase64Blob(text: string): boolean;
39
+ /** Re-export the compiled injection patterns as case-insensitive/global regexes. */
40
+ export declare function compiledInjectionPatterns(): {
41
+ id: string;
42
+ kind: string;
43
+ regex: RegExp;
44
+ meta: (typeof INJECTION_PATTERNS)[number];
45
+ }[];
@@ -0,0 +1,170 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Text extraction & normalization helpers shared by the lexical detectors.
4
+ * The point of `collectTextFields` is that every human/LLM-facing string in the
5
+ * surface is scanned uniformly, each carrying a precise location so findings can
6
+ * point at exactly which field is poisoned.
7
+ */
8
+ import { INJECTION_PATTERNS } from '../data/injectionPatterns.js';
9
+ /** Walk an inputSchema collecting every `description` string with a JSON path. */
10
+ export function collectSchemaStrings(schema, basePath) {
11
+ const out = [];
12
+ if (!schema || typeof schema !== 'object')
13
+ return out;
14
+ // Depth cap: an untrusted manifest can nest `properties`/`items` arbitrarily
15
+ // deep; bound the recursion so a hostile schema can't overflow the stack.
16
+ const MAX_DEPTH = 64;
17
+ const visit = (node, path, depth) => {
18
+ if (!node || typeof node !== 'object' || depth > MAX_DEPTH)
19
+ return;
20
+ if (typeof node.description === 'string' && node.description.length > 0) {
21
+ out.push({ path: `${path}.description`, text: node.description });
22
+ }
23
+ if (typeof node.title === 'string' && node.title.length > 0) {
24
+ out.push({ path: `${path}.title`, text: node.title });
25
+ }
26
+ if (Array.isArray(node.enum)) {
27
+ for (const [i, v] of node.enum.entries()) {
28
+ if (typeof v === 'string')
29
+ out.push({ path: `${path}.enum[${i}]`, text: v });
30
+ }
31
+ }
32
+ if (node.properties) {
33
+ for (const [key, child] of Object.entries(node.properties)) {
34
+ visit(child, `${path}.properties.${key}`, depth + 1);
35
+ }
36
+ }
37
+ if (node.items && !Array.isArray(node.items))
38
+ visit(node.items, `${path}.items`, depth + 1);
39
+ if (Array.isArray(node.items))
40
+ node.items.forEach((it, i) => visit(it, `${path}.items[${i}]`, depth + 1));
41
+ };
42
+ visit(schema, basePath, 0);
43
+ return out;
44
+ }
45
+ function isObj(v) {
46
+ return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
47
+ }
48
+ /**
49
+ * Collect every scannable text field across the whole surface. Defensive: only
50
+ * string values are emitted and non-object tools/prompts/resources are skipped,
51
+ * so a caller that hands us a hand-built (unnormalized) surface — or a live
52
+ * server returning junk — can never crash a text detector.
53
+ */
54
+ export function collectTextFields(surface) {
55
+ const fields = [];
56
+ // Cap each field's length: a legit MCP description is never this long, and an
57
+ // uncapped multi-MB field is a DoS vector for the text detectors' regexes.
58
+ const MAX_FIELD = 100_000;
59
+ const add = (text, location, channel) => {
60
+ if (typeof text === 'string' && text.length > 0) {
61
+ fields.push({ text: text.length > MAX_FIELD ? text.slice(0, MAX_FIELD) : text, location, channel });
62
+ }
63
+ };
64
+ add(surface.server?.name, { kind: 'server', field: 'name' }, 'other');
65
+ add(surface.server?.instructions, { kind: 'server', field: 'instructions' }, 'server-instructions');
66
+ for (const tool of Array.isArray(surface.tools) ? surface.tools : []) {
67
+ if (!isObj(tool))
68
+ continue;
69
+ const name = typeof tool.name === 'string' ? tool.name : undefined;
70
+ add(tool.name, { kind: 'tool', name, field: 'name' }, 'other');
71
+ add(tool.title, { kind: 'tool', name, field: 'title' }, 'tool-description');
72
+ add(tool.description, { kind: 'tool', name, field: 'description' }, 'tool-description');
73
+ for (const s of collectSchemaStrings(tool.inputSchema, 'inputSchema')) {
74
+ add(s.text, { kind: 'tool', name, field: s.path }, 'param-description');
75
+ }
76
+ }
77
+ for (const prompt of Array.isArray(surface.prompts) ? surface.prompts : []) {
78
+ if (!isObj(prompt))
79
+ continue;
80
+ const name = typeof prompt.name === 'string' ? prompt.name : undefined;
81
+ add(prompt.description, { kind: 'prompt', name, field: 'description' }, 'tool-description');
82
+ for (const arg of Array.isArray(prompt.arguments) ? prompt.arguments : []) {
83
+ if (isObj(arg))
84
+ add(arg.description, { kind: 'prompt', name, field: `arguments.${String(arg.name)}.description` }, 'param-description');
85
+ }
86
+ }
87
+ for (const res of Array.isArray(surface.resources) ? surface.resources : []) {
88
+ if (!isObj(res))
89
+ continue;
90
+ const name = typeof res.name === 'string' ? res.name : typeof res.uri === 'string' ? res.uri : undefined;
91
+ add(res.description, { kind: 'resource', name, field: 'description' }, 'tool-description');
92
+ }
93
+ return fields;
94
+ }
95
+ /** Lowercased text with punctuation collapsed to spaces, for keyword matching. */
96
+ export function normalizeForMatch(text) {
97
+ return text
98
+ .toLowerCase()
99
+ .replace(/[_\-./]+/g, ' ')
100
+ .replace(/\s+/g, ' ')
101
+ .trim();
102
+ }
103
+ // Common technical acronyms / HTTP verbs that are legitimately upper-case and
104
+ // must not count toward a "shouting" run (e.g. "GET, POST, PUT, DELETE").
105
+ const TECH_ACRONYMS = new Set([
106
+ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT',
107
+ 'API', 'URL', 'URI', 'JSON', 'HTML', 'XML', 'CSV', 'YAML', 'SQL', 'HTTP', 'HTTPS',
108
+ 'REST', 'RPC', 'GRPC', 'ID', 'UUID', 'UI', 'UX', 'OK', 'CRUD', 'JWT', 'CORS',
109
+ 'CPU', 'GPU', 'RAM', 'PDF', 'PNG', 'JPG', 'GIF', 'SVG', 'TCP', 'UDP', 'DNS',
110
+ 'SSH', 'TLS', 'SSL', 'AWS', 'GCP', 'IAM', 'S3', 'EC2', 'K8S', 'CI', 'CD', 'MCP',
111
+ 'AND', 'OR', 'NOT', 'IN', 'IS', 'TODO', 'FIXME', 'NOTE', 'WARN', 'ISO', 'UTC',
112
+ ]);
113
+ /** Count the longest run of consecutive shouted words (excluding tech acronyms). */
114
+ export function longestAllCapsRun(text) {
115
+ if (typeof text !== 'string')
116
+ return 0;
117
+ const words = text.split(/\s+/);
118
+ let best = 0;
119
+ let run = 0;
120
+ for (const w of words) {
121
+ const letters = w.replace(/[^A-Za-z]/g, '');
122
+ if (letters.length >= 2 && letters === letters.toUpperCase() && !TECH_ACRONYMS.has(letters)) {
123
+ run += 1;
124
+ best = Math.max(best, run);
125
+ }
126
+ else {
127
+ run = 0;
128
+ }
129
+ }
130
+ return best;
131
+ }
132
+ /**
133
+ * Detect a long base64-looking blob (potential encoded payload). Deliberately
134
+ * rejects pure-hex runs (git SHAs, dash-stripped UUIDs, account/hex IDs) and
135
+ * requires genuine base64 shape (a padding/base64 symbol, or real mixed case
136
+ * with digits), so ordinary identifiers in descriptions don't misfire.
137
+ */
138
+ export function hasBase64Blob(text) {
139
+ if (typeof text !== 'string')
140
+ return false;
141
+ // Bounded quantifier + input cap: an unbounded {24,} over a multi-MB run can
142
+ // overflow the regex engine's stack.
143
+ const t = text.length > 100_000 ? text.slice(0, 100_000) : text;
144
+ // Scan every candidate run (not just the first) so a genuine blob isn't masked
145
+ // by an earlier slash-separated word list appearing before it.
146
+ for (const m of t.matchAll(/(?:^|[^A-Za-z0-9+/])([A-Za-z0-9+/]{24,4096}={0,2})(?:[^A-Za-z0-9+/=]|$)/g)) {
147
+ const blob = m[1];
148
+ if (/^[0-9a-f]+$/i.test(blob))
149
+ continue; // pure hex → an ID, not base64
150
+ // `+` and `=` are prose-rare and a strong base64 signal. `/` alone is NOT —
151
+ // it is common in prose ("and/or", paths, slash-separated word lists like
152
+ // "methodology/results/structure"), so it must not qualify on its own.
153
+ const hasStrongSymbol = /[+=]/.test(blob);
154
+ // Without a strong symbol, require the high-entropy mix real encoded data has
155
+ // (lower + upper + digit). A slash-delimited word list is letters-only → excluded.
156
+ const mixedCaseDigits = /[a-z]/.test(blob) && /[A-Z]/.test(blob) && /[0-9]/.test(blob);
157
+ if (hasStrongSymbol || mixedCaseDigits)
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+ /** Re-export the compiled injection patterns as case-insensitive/global regexes. */
163
+ export function compiledInjectionPatterns() {
164
+ return INJECTION_PATTERNS.map((p) => ({
165
+ id: p.id,
166
+ kind: p.kind,
167
+ regex: new RegExp(p.pattern.source, 'gi'),
168
+ meta: p,
169
+ }));
170
+ }
@@ -0,0 +1,13 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /** Package version. Keep in sync with package.json. */
3
+ export declare const TOOL_VERSION = "1.0.0";
4
+ /**
5
+ * Methodology version. Bump this whenever scoring weights, gates, rule
6
+ * severities, or bundled threat data change in a way that could move a score.
7
+ * The reproducibility contract is: same methodology version + same target ⇒
8
+ * identical Trust Score.
9
+ */
10
+ export declare const METHODOLOGY_VERSION = "mcptrustchecker-1.0";
11
+ /** Version of the bundled Unicode/confusables tables. */
12
+ export declare const UNICODE_DATA_VERSION = "17.0.0";
13
+ export declare const TOOL_NAME = "mcptrustchecker";
@@ -0,0 +1,13 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /** Package version. Keep in sync with package.json. */
3
+ export const TOOL_VERSION = '1.0.0';
4
+ /**
5
+ * Methodology version. Bump this whenever scoring weights, gates, rule
6
+ * severities, or bundled threat data change in a way that could move a score.
7
+ * The reproducibility contract is: same methodology version + same target ⇒
8
+ * identical Trust Score.
9
+ */
10
+ export const METHODOLOGY_VERSION = 'mcptrustchecker-1.0';
11
+ /** Version of the bundled Unicode/confusables tables. */
12
+ export const UNICODE_DATA_VERSION = '17.0.0';
13
+ export const TOOL_NAME = 'mcptrustchecker';