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,396 @@
1
+ #!/usr/bin/env node
2
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
3
+ /**
4
+ * MCP Trust Checker command-line interface.
5
+ *
6
+ * mcptrustchecker scan <target> scan a manifest / URL / package / client config
7
+ * mcptrustchecker pin <target> scan and (re)pin the integrity lockfile
8
+ * mcptrustchecker diff <target> fail if the surface drifted since the pin
9
+ * mcptrustchecker rules list every rule
10
+ * mcptrustchecker explain <ruleId> describe a rule
11
+ * mcptrustchecker version | help
12
+ *
13
+ * Uses only Node built-ins (node:util parseArgs) — no arg-parsing dependency.
14
+ */
15
+ import { parseArgs } from 'node:util';
16
+ import { writeFileSync } from 'node:fs';
17
+ import { spawnSync } from 'node:child_process';
18
+ import { resolveTargets } from '../acquire/index.js';
19
+ import { discoverClientConfigs } from '../acquire/discover.js';
20
+ import { scanSurface } from '../engine.js';
21
+ import { loadConfigFile, loadConfigFromPath } from '../config.js';
22
+ import { evaluatePolicy } from '../policy.js';
23
+ import { readLockfile, writeLockfile, pinSurface, emptyLockfile } from '../lockfile.js';
24
+ import { renderTerminal } from '../report/terminal.js';
25
+ import { renderJson } from '../report/json.js';
26
+ import { renderSarif } from '../report/sarif.js';
27
+ import { renderMarkdown } from '../report/markdown.js';
28
+ import { renderBadge } from '../report/badge.js';
29
+ import { GRADE_RANK } from '../scoring/model.js';
30
+ import { RULE_CATALOG, findRule } from '../data/ruleCatalog.js';
31
+ import { METHODOLOGY_VERSION, TOOL_VERSION } from '../version.js';
32
+ import { c } from '../util/ansi.js';
33
+ const KNOWN_COMMANDS = new Set(['scan', 'pin', 'diff', 'rules', 'explain', 'version', 'help']);
34
+ function fail(msg, code = 2) {
35
+ process.stderr.write(`${c.red('mcptrustchecker: error')} ${msg}\n`);
36
+ process.exit(code);
37
+ }
38
+ function printHelp() {
39
+ process.stdout.write(`${c.bold('MCP Trust Checker')} ${TOOL_VERSION} — local-first MCP security scanner (methodology ${METHODOLOGY_VERSION})
40
+
41
+ ${c.bold('Usage')}
42
+ mcptrustchecker scan every installed MCP client config (zero-config)
43
+ mcptrustchecker [scan] <target> scan a specific target (see below)
44
+ mcptrustchecker pin <target> scan and (re)pin the integrity lockfile
45
+ mcptrustchecker diff <target> exit non-zero if the surface drifted since pin
46
+ mcptrustchecker rules list every rule
47
+ mcptrustchecker explain <ruleId> describe a rule
48
+
49
+ ${c.bold('Targets')}
50
+ path/to/tools.json a pre-generated manifest (offline)
51
+ https://host/mcp a live Streamable-HTTP / SSE endpoint
52
+ claude_desktop_config.json a client config (scans each server)
53
+ @scope/package supply-chain / typosquat check (add --online for metadata)
54
+ --command "npx -y my-server" spawn a local stdio server (sandboxed)
55
+ --env KEY=VALUE env var for --command (repeatable)
56
+
57
+ ${c.bold('Output')}
58
+ --json machine-readable JSON report
59
+ --sarif SARIF 2.1.0 (GitHub code scanning)
60
+ --md, --markdown Markdown (PR comments / summaries)
61
+ --badge shields.io endpoint JSON
62
+ -o, --output <file> write output to a file
63
+
64
+ ${c.bold('CI gates')}
65
+ --fail-under <0-100> exit 1 if the Trust Score is below N
66
+ --min-grade <A-F> exit 1 if the grade is worse than this
67
+
68
+ ${c.bold('Options')}
69
+ --config <path> config file (default: auto-discovered)
70
+ --lockfile <path> integrity lockfile (default: mcptrustchecker.lock)
71
+ --no-lock skip the integrity (rug-pull) check
72
+ --pin re-pin the lockfile after scanning
73
+ --include-builtins assume client built-in tools in toxic-flow analysis
74
+ --online allow network lookups for package metadata
75
+ --run allow spawning servers found in a client config
76
+ --allow-any-command permit stdio commands outside the allowlist (dangerous)
77
+ --allowed-hosts <a,b> restrict live HTTP acquisition to these hosts
78
+ --registry <npm|pypi> registry for bare package targets (default: npm)
79
+ --details include references and full evidence per finding
80
+ --no-pager don't page long output (also NO_PAGER / $PAGER)
81
+ --quiet only print the grade line
82
+ -h, --help show this help
83
+ -v, --version show version
84
+ `);
85
+ }
86
+ async function main() {
87
+ let parsed;
88
+ try {
89
+ parsed = parseArgs({
90
+ allowPositionals: true,
91
+ options: {
92
+ json: { type: 'boolean' },
93
+ sarif: { type: 'boolean' },
94
+ md: { type: 'boolean' },
95
+ markdown: { type: 'boolean' },
96
+ badge: { type: 'boolean' },
97
+ output: { type: 'string', short: 'o' },
98
+ 'fail-under': { type: 'string' },
99
+ 'min-grade': { type: 'string' },
100
+ config: { type: 'string' },
101
+ lockfile: { type: 'string' },
102
+ 'no-lock': { type: 'boolean' },
103
+ pin: { type: 'boolean' },
104
+ command: { type: 'string' },
105
+ args: { type: 'string' },
106
+ env: { type: 'string', multiple: true },
107
+ url: { type: 'string' },
108
+ online: { type: 'boolean' },
109
+ run: { type: 'boolean' },
110
+ 'allow-any-command': { type: 'boolean' },
111
+ 'allowed-hosts': { type: 'string' },
112
+ 'include-builtins': { type: 'boolean' },
113
+ registry: { type: 'string' },
114
+ quiet: { type: 'boolean' },
115
+ details: { type: 'boolean' },
116
+ 'no-pager': { type: 'boolean' },
117
+ help: { type: 'boolean', short: 'h' },
118
+ version: { type: 'boolean', short: 'v' },
119
+ },
120
+ });
121
+ }
122
+ catch (err) {
123
+ fail(`${err.message}. Run "mcptrustchecker --help" for usage.`);
124
+ }
125
+ const { values, positionals } = parsed;
126
+ if (values.version) {
127
+ process.stdout.write(`mcptrustchecker ${TOOL_VERSION} (methodology ${METHODOLOGY_VERSION})\n`);
128
+ return;
129
+ }
130
+ if (values.help) {
131
+ printHelp();
132
+ return;
133
+ }
134
+ // If the first positional is an explicit command, the target is the next one;
135
+ // otherwise the command defaults to `scan` and the first positional is the target.
136
+ const explicit = Boolean(positionals[0] && KNOWN_COMMANDS.has(positionals[0]));
137
+ const command = explicit ? positionals[0] : 'scan';
138
+ const target = explicit ? positionals[1] : positionals[0];
139
+ if (command === 'help')
140
+ return printHelp();
141
+ if (command === 'version') {
142
+ process.stdout.write(`mcptrustchecker ${TOOL_VERSION} (methodology ${METHODOLOGY_VERSION})\n`);
143
+ return;
144
+ }
145
+ if (command === 'rules')
146
+ return printRules();
147
+ if (command === 'explain')
148
+ return printExplain(target);
149
+ // scan / pin / diff
150
+ const scanTarget = target;
151
+ const autoDiscover = !scanTarget && !values.command && !values.url;
152
+ // Reject silently-dropped extra positional targets.
153
+ const extraFrom = explicit ? 2 : 1;
154
+ if (positionals.length > extraFrom) {
155
+ fail(`unexpected extra argument(s): ${positionals.slice(extraFrom).join(', ')}. ` +
156
+ `Scan one target at a time, or point at a client config to scan several servers.`);
157
+ }
158
+ // Validate CI-gate flags up front so a typo fails loudly, not silently.
159
+ if (values['fail-under'] !== undefined) {
160
+ const n = Number(values['fail-under']);
161
+ if (!Number.isFinite(n) || n < 0 || n > 100)
162
+ fail(`--fail-under must be a number 0–100 (got "${values['fail-under']}")`);
163
+ }
164
+ if (values['min-grade'] !== undefined && !(values['min-grade'].toUpperCase() in GRADE_RANK)) {
165
+ fail(`--min-grade must be one of A, B, C, D, F (got "${values['min-grade']}")`);
166
+ }
167
+ const fileConfig = values.config ? loadConfigFromPath(values.config) : loadConfigFile(process.cwd());
168
+ const config = {
169
+ ...fileConfig,
170
+ includeBuiltins: values['include-builtins'] ?? fileConfig.includeBuiltins,
171
+ };
172
+ const lockfilePath = values.lockfile ?? fileConfig.lockfile ?? 'mcptrustchecker.lock';
173
+ const lock = values['no-lock'] ? null : (readLockfile(lockfilePath) ?? null);
174
+ const resolveOpts = {
175
+ command: values.command ? values.command.trim().split(/\s+/)[0] : undefined,
176
+ args: buildArgs(values.command, values.args),
177
+ url: values.url,
178
+ online: values.online,
179
+ run: values.run,
180
+ envVars: parseEnvFlags(values.env),
181
+ allowAnyCommand: values['allow-any-command'],
182
+ allowedHosts: values['allowed-hosts'] ? values['allowed-hosts'].split(',').map((h) => h.trim()) : undefined,
183
+ registry: values.registry === 'pypi' ? 'pypi' : 'npm',
184
+ };
185
+ let resolved = [];
186
+ if (autoDiscover) {
187
+ // Zero-config: find and scan every installed MCP client config.
188
+ const configs = discoverClientConfigs();
189
+ if (configs.length === 0) {
190
+ process.stdout.write(`${c.bold('MCP Trust Checker')} — no MCP client configs found to scan.\n\n` +
191
+ `Point at something explicitly:\n` +
192
+ ` mcptrustchecker scan ./tools.json ${c.gray('# a manifest')}\n` +
193
+ ` mcptrustchecker scan https://host/mcp ${c.gray('# a live endpoint')}\n` +
194
+ ` mcptrustchecker scan --command "npx -y <server>" ${c.gray('# a local stdio server')}\n` +
195
+ ` mcptrustchecker scan <@scope/package> --online ${c.gray('# a package name')}\n\n` +
196
+ `Run ${c.bold('mcptrustchecker --help')} for all options.\n`);
197
+ return;
198
+ }
199
+ process.stderr.write(`${c.gray('Discovered')} ${configs.length} MCP config(s): ${configs.map((x) => x.client).join(', ')}\n` +
200
+ (values.run ? '' : `${c.gray('(static scan; add')} ${c.bold('--run')}${c.gray(' to spawn servers and analyze tool descriptions)')}\n`));
201
+ for (const cfg of configs) {
202
+ try {
203
+ resolved.push(...(await resolveTargets(cfg.path, resolveOpts)));
204
+ }
205
+ catch (err) {
206
+ process.stderr.write(`${c.yellow('!')} skipped ${cfg.path}: ${err.message}\n`);
207
+ }
208
+ }
209
+ if (resolved.length === 0)
210
+ fail('no MCP servers found in the discovered configs.');
211
+ }
212
+ else {
213
+ try {
214
+ resolved = await resolveTargets(scanTarget, resolveOpts);
215
+ }
216
+ catch (err) {
217
+ fail(err.message);
218
+ }
219
+ }
220
+ // Flat tool inventory across all resolved servers, for cross-server collision.
221
+ const allTools = resolved.flatMap((r) => r.surface.tools.map((t) => ({ server: r.surface.id, name: t.name })));
222
+ const reports = [];
223
+ for (const { surface } of resolved) {
224
+ const siblingTools = allTools.filter((t) => t.server !== surface.id);
225
+ const report = await scanSurface(surface, {
226
+ config,
227
+ lockfile: values['no-lock'] ? undefined : lock,
228
+ scannedAt: new Date().toISOString(),
229
+ siblingTools,
230
+ });
231
+ reports.push(report);
232
+ }
233
+ // pin — always read the existing lockfile first (even under --no-lock) so we
234
+ // merge into it rather than clobbering previously-pinned servers.
235
+ if (command === 'pin' || values.pin) {
236
+ let updated = readLockfile(lockfilePath) ?? emptyLockfile();
237
+ for (const { surface } of resolved)
238
+ updated = pinSurface(updated, surface, new Date().toISOString());
239
+ writeLockfile(lockfilePath, updated);
240
+ process.stderr.write(`${c.green('✓')} pinned ${resolved.length} server(s) → ${lockfilePath}\n`);
241
+ }
242
+ // output
243
+ const rendered = render(reports, values);
244
+ if (values.output) {
245
+ try {
246
+ writeFileSync(values.output, rendered.endsWith('\n') ? rendered : rendered + '\n');
247
+ }
248
+ catch (err) {
249
+ fail(`could not write ${values.output}: ${err.message}`);
250
+ }
251
+ process.stderr.write(`${c.green('✓')} wrote ${values.output}\n`);
252
+ if (!values.json && !values.sarif && !values.badge)
253
+ process.stdout.write(summaryLine(reports) + '\n');
254
+ }
255
+ else {
256
+ // The human report can be long; page it (starting at the top, on the grade)
257
+ // rather than letting the terminal scroll to the end of the findings.
258
+ const isHumanReport = !values.json && !values.sarif && !values.badge && !values.md && !values.markdown && !values.quiet;
259
+ paginate(rendered, isHumanReport && !values['no-pager']);
260
+ }
261
+ // Return the exit code instead of process.exit() — exit() can truncate a large
262
+ // stdout write to a pipe before it flushes (invalid JSON/SARIF). Letting main
263
+ // resolve lets Node flush stdout, then the entrypoint sets process.exitCode.
264
+ return computeExitCode(command, reports, values, fileConfig);
265
+ }
266
+ /**
267
+ * Write to stdout, paging through $PAGER (default `less`) when the output is an
268
+ * interactive terminal and taller than the screen — so the view opens on the
269
+ * grade at the top instead of auto-scrolling to the last finding.
270
+ *
271
+ * We use plain `less -R` (raw colors) on the standard alternate screen. We
272
+ * deliberately avoid `-X`/`-F`: `-X` (no alternate screen) makes some terminals
273
+ * dump every repaint into scrollback while scrolling (duplicated output), and
274
+ * `-F` misbehaves on piped input. For default-less we also force `LESS=R` so a
275
+ * user's global `$LESS` (which may contain -X/-F) can't reintroduce the bug.
276
+ * Honors NO_PAGER / MCPTRUSTCHECKER_NO_PAGER and any custom $PAGER verbatim.
277
+ */
278
+ function paginate(text, allow) {
279
+ const body = text.endsWith('\n') ? text : `${text}\n`;
280
+ const rows = process.stdout.rows || 24;
281
+ const tallerThanScreen = body.split('\n').length > rows - 1;
282
+ const wantPager = allow &&
283
+ Boolean(process.stdout.isTTY) &&
284
+ !process.env.NO_PAGER &&
285
+ !process.env.MCPTRUSTCHECKER_NO_PAGER &&
286
+ tallerThanScreen;
287
+ if (!wantPager) {
288
+ process.stdout.write(body);
289
+ return;
290
+ }
291
+ const pagerEnv = (process.env.PAGER ?? '').trim();
292
+ const [cmd, ...preArgs] = (pagerEnv || 'less').split(/\s+/);
293
+ const useDefaultLess = /(^|\/)less$/.test(cmd ?? '') && preArgs.length === 0;
294
+ const args = useDefaultLess ? ['-R'] : preArgs;
295
+ const env = useDefaultLess ? { ...process.env, LESS: 'R' } : process.env;
296
+ try {
297
+ const res = spawnSync(cmd ?? 'less', args, { input: body, stdio: ['pipe', 'inherit', 'inherit'], env });
298
+ if (res.error)
299
+ process.stdout.write(body); // pager missing → fall back
300
+ }
301
+ catch {
302
+ process.stdout.write(body);
303
+ }
304
+ }
305
+ function parseEnvFlags(list) {
306
+ if (!list || list.length === 0)
307
+ return undefined;
308
+ const out = {};
309
+ for (const kv of list) {
310
+ const i = kv.indexOf('=');
311
+ if (i > 0)
312
+ out[kv.slice(0, i)] = kv.slice(i + 1);
313
+ }
314
+ return Object.keys(out).length ? out : undefined;
315
+ }
316
+ function buildArgs(command, args) {
317
+ const fromCommand = command ? command.trim().split(/\s+/).slice(1) : [];
318
+ const fromArgs = args ? args.trim().split(/\s+/) : [];
319
+ const all = [...fromCommand, ...fromArgs];
320
+ return all.length ? all : undefined;
321
+ }
322
+ function render(reports, values) {
323
+ const single = reports.length === 1;
324
+ if (values.json)
325
+ return single ? renderJson(reports[0]) : JSON.stringify(reports, null, 2);
326
+ if (values.sarif)
327
+ return renderSarifMerged(reports);
328
+ if (values.badge)
329
+ return single ? renderBadge(reports[0]) : JSON.stringify(reports.map((r) => JSON.parse(renderBadge(r))));
330
+ if (values.md || values.markdown)
331
+ return reports.map(renderMarkdown).join('\n\n---\n\n');
332
+ if (values.quiet)
333
+ return summaryLine(reports);
334
+ return reports.map((r) => renderTerminal(r, { details: Boolean(values.details) })).join('\n');
335
+ }
336
+ function renderSarifMerged(reports) {
337
+ if (reports.length === 1)
338
+ return renderSarif(reports[0]);
339
+ const runs = reports.flatMap((r) => JSON.parse(renderSarif(r)).runs);
340
+ return JSON.stringify({ $schema: 'https://json.schemastore.org/sarif-2.1.0.json', version: '2.1.0', runs }, null, 2);
341
+ }
342
+ function summaryLine(reports) {
343
+ return reports
344
+ .map((r) => `${r.target.id}: Trust ${r.score.grade} (${r.score.score}/100) · Capability ${r.capabilityProfile.level.toUpperCase()}`)
345
+ .join('\n');
346
+ }
347
+ function computeExitCode(command, reports, values, fileConfig) {
348
+ if (command === 'diff') {
349
+ if (reports.some((r) => r.integrity?.status === 'drift'))
350
+ return 1;
351
+ }
352
+ const failUnder = values['fail-under'] !== undefined ? Number(values['fail-under']) : fileConfig.failUnder;
353
+ const rawMinGrade = values['min-grade'] ?? fileConfig.minGrade;
354
+ const minGrade = rawMinGrade ? String(rawMinGrade).toUpperCase() : undefined;
355
+ let gated = 0;
356
+ for (const r of reports) {
357
+ if (failUnder !== undefined && r.score.score < failUnder)
358
+ gated = 1;
359
+ if (minGrade && minGrade in GRADE_RANK && GRADE_RANK[r.score.grade] < GRADE_RANK[minGrade])
360
+ gated = 1;
361
+ // Policy-as-code: report every violation, then fail.
362
+ const violations = evaluatePolicy(r, fileConfig.policy);
363
+ for (const v of violations) {
364
+ process.stderr.write(`${c.red('policy')} ${r.target.id}: ${v.message}\n`);
365
+ gated = 1;
366
+ }
367
+ }
368
+ return gated;
369
+ }
370
+ function printRules() {
371
+ const lines = [`${c.bold('MCP Trust Checker rules')} (${RULE_CATALOG.length})`, ''];
372
+ let lastCat = '';
373
+ for (const r of RULE_CATALOG) {
374
+ if (r.category !== lastCat) {
375
+ lines.push(c.gray(`— ${r.category} —`));
376
+ lastCat = r.category;
377
+ }
378
+ lines.push(` ${c.bold(r.id.padEnd(16))} ${c.gray(`[${r.severity}]`)} ${r.title}`);
379
+ }
380
+ process.stdout.write(lines.join('\n') + '\n');
381
+ }
382
+ function printExplain(id) {
383
+ if (!id)
384
+ fail('usage: mcptrustchecker explain <ruleId>');
385
+ const rule = findRule(id);
386
+ if (!rule)
387
+ fail(`unknown rule "${id}". Run "mcptrustchecker rules" to list them.`);
388
+ process.stdout.write(`${c.bold(rule.id)} — ${rule.title}\n` +
389
+ ` category: ${rule.category}\n severity: ${rule.severity}\n\n ${rule.summary}\n`);
390
+ }
391
+ main()
392
+ .then((code) => {
393
+ if (typeof code === 'number' && code !== 0)
394
+ process.exitCode = code;
395
+ })
396
+ .catch((err) => fail(err.message ?? String(err), 2));
@@ -0,0 +1,15 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Config resolution. MCP Trust Checker runs with sensible zero-config defaults; a
4
+ * `mcptrustchecker.config.json` (or `.mcptrustcheckerrc.json`) can override any of them.
5
+ */
6
+ import type { ResolvedConfig, McpTrustCheckerConfig } from './types.js';
7
+ export declare const DEFAULT_CONFIG: ResolvedConfig;
8
+ /** Load a config object from the first config file found in `cwd`, if any. */
9
+ export declare function loadConfigFile(cwd?: string): McpTrustCheckerConfig;
10
+ /** Load a config from an explicit file path (errors clearly if missing/malformed). */
11
+ export declare function loadConfigFromPath(path: string): McpTrustCheckerConfig;
12
+ /** Merge a partial config over the defaults into a fully-resolved config. */
13
+ export declare function resolveConfig(partial?: McpTrustCheckerConfig): ResolvedConfig;
14
+ /** Load a standalone `.mtcignore` baseline file (JSON array of suppressions), if present. */
15
+ export declare function loadIgnoreFile(cwd?: string): McpTrustCheckerConfig['suppress'];
package/dist/config.js ADDED
@@ -0,0 +1,81 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Config resolution. MCP Trust Checker runs with sensible zero-config defaults; a
4
+ * `mcptrustchecker.config.json` (or `.mcptrustcheckerrc.json`) can override any of them.
5
+ */
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ export const DEFAULT_CONFIG = {
9
+ disabledRules: [],
10
+ allowlist: [],
11
+ failUnder: 0,
12
+ includeBuiltins: false,
13
+ protectedPackages: [],
14
+ invisibleCharThreshold: 5,
15
+ suppress: [],
16
+ minGrade: undefined,
17
+ lockfile: undefined,
18
+ policy: undefined,
19
+ };
20
+ const CONFIG_FILENAMES = ['mcptrustchecker.config.json', '.mcptrustcheckerrc.json', '.mcptrustcheckerrc'];
21
+ const IGNORE_FILE = '.mtcignore';
22
+ /** Load a config object from the first config file found in `cwd`, if any. */
23
+ export function loadConfigFile(cwd = process.cwd()) {
24
+ let cfg = {};
25
+ for (const name of CONFIG_FILENAMES) {
26
+ const path = join(cwd, name);
27
+ if (existsSync(path)) {
28
+ try {
29
+ cfg = JSON.parse(readFileSync(path, 'utf8'));
30
+ }
31
+ catch (err) {
32
+ throw new Error(`Failed to parse config file ${path}: ${err.message}`);
33
+ }
34
+ break;
35
+ }
36
+ }
37
+ // A standalone .mtcignore baseline merges into (and extends) config suppressions.
38
+ const ignore = loadIgnoreFile(cwd);
39
+ if (ignore && ignore.length)
40
+ cfg = { ...cfg, suppress: [...(cfg.suppress ?? []), ...ignore] };
41
+ return cfg;
42
+ }
43
+ /** Load a config from an explicit file path (errors clearly if missing/malformed). */
44
+ export function loadConfigFromPath(path) {
45
+ if (!existsSync(path))
46
+ throw new Error(`Config file not found: ${path}`);
47
+ try {
48
+ return JSON.parse(readFileSync(path, 'utf8'));
49
+ }
50
+ catch (err) {
51
+ throw new Error(`Failed to parse config file ${path}: ${err.message}`);
52
+ }
53
+ }
54
+ /** Merge a partial config over the defaults into a fully-resolved config. */
55
+ export function resolveConfig(partial = {}) {
56
+ return {
57
+ disabledRules: partial.disabledRules ?? DEFAULT_CONFIG.disabledRules,
58
+ allowlist: partial.allowlist ?? DEFAULT_CONFIG.allowlist,
59
+ failUnder: partial.failUnder ?? DEFAULT_CONFIG.failUnder,
60
+ includeBuiltins: partial.includeBuiltins ?? DEFAULT_CONFIG.includeBuiltins,
61
+ protectedPackages: partial.protectedPackages ?? DEFAULT_CONFIG.protectedPackages,
62
+ invisibleCharThreshold: partial.invisibleCharThreshold ?? DEFAULT_CONFIG.invisibleCharThreshold,
63
+ suppress: partial.suppress ?? DEFAULT_CONFIG.suppress,
64
+ minGrade: partial.minGrade,
65
+ lockfile: partial.lockfile,
66
+ policy: partial.policy,
67
+ };
68
+ }
69
+ /** Load a standalone `.mtcignore` baseline file (JSON array of suppressions), if present. */
70
+ export function loadIgnoreFile(cwd = process.cwd()) {
71
+ const path = join(cwd, IGNORE_FILE);
72
+ if (!existsSync(path))
73
+ return [];
74
+ try {
75
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
76
+ return Array.isArray(parsed) ? parsed : [];
77
+ }
78
+ catch (err) {
79
+ throw new Error(`Failed to parse ${IGNORE_FILE}: ${err.message}`);
80
+ }
81
+ }
@@ -0,0 +1,51 @@
1
+ /*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
2
+ /**
3
+ * Capability lexicon — the vocabulary that maps a tool's name, description
4
+ * verbs, and parameter shape onto the roles used by the toxic-flow graph.
5
+ *
6
+ * The trifecta (the lethal-trifecta toxic flow) requires three roles
7
+ * to be simultaneously reachable in one agent session:
8
+ * UNTRUSTED_INPUT → SENSITIVE_SOURCE → EXTERNAL_SINK
9
+ * We deliberately keep UNTRUSTED_INPUT and SENSITIVE_SOURCE separate — merging
10
+ * them (as a naive "sensitive source") both over- and under-flags.
11
+ */
12
+ import type { CapabilityTag } from '../types.js';
13
+ /** Keyword → tag rules matched against a tool's name and description. */
14
+ export interface CapabilitySignal {
15
+ tag: CapabilityTag;
16
+ /** Lowercased keywords/phrases; matched as word-ish substrings. */
17
+ keywords: string[];
18
+ }
19
+ export declare const CAPABILITY_SIGNALS: CapabilitySignal[];
20
+ /**
21
+ * Parameter-name → tag hints (schema-shape signals). Deliberately conservative:
22
+ * generic names like `body`/`data`/`content` are NOT mapped (an HTTP body is not
23
+ * a file write) — file mutation is detected from tool-name verbs instead, which
24
+ * avoids the most common capability false positive.
25
+ */
26
+ export declare const PARAM_NAME_SIGNALS: {
27
+ tag: CapabilityTag;
28
+ names: string[];
29
+ }[];
30
+ /** URL/host-shaped parameter names — an unconstrained one is an SSRF precondition. */
31
+ export declare const SSRF_PARAM_NAMES: string[];
32
+ /** Path-shaped parameter names — an unconstrained one is a path-traversal precondition. */
33
+ export declare const PATH_PARAM_NAMES: string[];
34
+ /** Command-shaped parameter names — an unconstrained one is a command-injection precondition. */
35
+ export declare const COMMAND_PARAM_NAMES: string[];
36
+ /**
37
+ * Field names that must never be collected via MCP elicitation (the spec
38
+ * forbids it). Their presence alongside a declared elicitation capability is a
39
+ * credential-phishing signal.
40
+ */
41
+ export declare const SECRET_FIELD_NAMES: string[];
42
+ /**
43
+ * The virtual "client built-in" tools. When `includeBuiltins` is set, the
44
+ * toxic-flow graph assumes the host agent also exposes generic web-fetch (an
45
+ * untrusted-input source) and generic file/network tools (sinks), which can
46
+ * complete a trifecta together with a single-capability server.
47
+ */
48
+ export declare const CLIENT_BUILTINS: {
49
+ tool: string;
50
+ tags: CapabilityTag[];
51
+ }[];