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.
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/acquire/clientConfig.d.ts +47 -0
- package/dist/acquire/clientConfig.js +124 -0
- package/dist/acquire/discover.d.ts +12 -0
- package/dist/acquire/discover.js +53 -0
- package/dist/acquire/index.d.ts +24 -0
- package/dist/acquire/index.js +175 -0
- package/dist/acquire/live.d.ts +47 -0
- package/dist/acquire/live.js +357 -0
- package/dist/acquire/manifest.d.ts +10 -0
- package/dist/acquire/manifest.js +94 -0
- package/dist/acquire/npm.d.ts +10 -0
- package/dist/acquire/npm.js +55 -0
- package/dist/acquire/source.d.ts +16 -0
- package/dist/acquire/source.js +109 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.js +396 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +81 -0
- package/dist/data/capabilityLexicon.d.ts +51 -0
- package/dist/data/capabilityLexicon.js +204 -0
- package/dist/data/confusables.d.ts +17 -0
- package/dist/data/confusables.js +65 -0
- package/dist/data/injectionPatterns.d.ts +55 -0
- package/dist/data/injectionPatterns.js +216 -0
- package/dist/data/knownCves.d.ts +24 -0
- package/dist/data/knownCves.js +49 -0
- package/dist/data/protectedPackages.d.ts +30 -0
- package/dist/data/protectedPackages.js +89 -0
- package/dist/data/ruleCatalog.d.ts +16 -0
- package/dist/data/ruleCatalog.js +94 -0
- package/dist/data/sourcePatterns.d.ts +25 -0
- package/dist/data/sourcePatterns.js +92 -0
- package/dist/data/unicode.d.ts +34 -0
- package/dist/data/unicode.js +99 -0
- package/dist/detectors/capability.d.ts +14 -0
- package/dist/detectors/capability.js +281 -0
- package/dist/detectors/collision.d.ts +11 -0
- package/dist/detectors/collision.js +63 -0
- package/dist/detectors/flowGraph.d.ts +43 -0
- package/dist/detectors/flowGraph.js +100 -0
- package/dist/detectors/index.d.ts +18 -0
- package/dist/detectors/index.js +26 -0
- package/dist/detectors/injection.d.ts +9 -0
- package/dist/detectors/injection.js +178 -0
- package/dist/detectors/meta.d.ts +8 -0
- package/dist/detectors/meta.js +33 -0
- package/dist/detectors/posture.d.ts +9 -0
- package/dist/detectors/posture.js +128 -0
- package/dist/detectors/source.d.ts +12 -0
- package/dist/detectors/source.js +87 -0
- package/dist/detectors/supplyChain.d.ts +17 -0
- package/dist/detectors/supplyChain.js +243 -0
- package/dist/detectors/toxicFlow.d.ts +22 -0
- package/dist/detectors/toxicFlow.js +201 -0
- package/dist/detectors/unicode.d.ts +21 -0
- package/dist/detectors/unicode.js +244 -0
- package/dist/engine.d.ts +27 -0
- package/dist/engine.js +135 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +87 -0
- package/dist/policy.d.ts +13 -0
- package/dist/policy.js +48 -0
- package/dist/report/badge.d.ts +14 -0
- package/dist/report/badge.js +18 -0
- package/dist/report/json.d.ts +4 -0
- package/dist/report/json.js +4 -0
- package/dist/report/markdown.d.ts +6 -0
- package/dist/report/markdown.js +110 -0
- package/dist/report/sarif.d.ts +7 -0
- package/dist/report/sarif.js +105 -0
- package/dist/report/terminal.d.ts +15 -0
- package/dist/report/terminal.js +252 -0
- package/dist/scoring/capability.d.ts +10 -0
- package/dist/scoring/capability.js +48 -0
- package/dist/scoring/index.d.ts +10 -0
- package/dist/scoring/index.js +96 -0
- package/dist/scoring/model.d.ts +50 -0
- package/dist/scoring/model.js +101 -0
- package/dist/types.d.ts +361 -0
- package/dist/types.js +12 -0
- package/dist/util/ansi.d.ts +26 -0
- package/dist/util/ansi.js +42 -0
- package/dist/util/capabilities.d.ts +17 -0
- package/dist/util/capabilities.js +68 -0
- package/dist/util/distance.d.ts +23 -0
- package/dist/util/distance.js +119 -0
- package/dist/util/hash.d.ts +20 -0
- package/dist/util/hash.js +80 -0
- package/dist/util/text.d.ts +45 -0
- package/dist/util/text.js +170 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +13 -0
- package/package.json +72 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Offline acquisition: build a normalized ServerSurface from a pre-generated
|
|
4
|
+
* JSON manifest. Accepts several shapes so `mcptrustchecker scan tools.json` "just
|
|
5
|
+
* works": a raw `tools/list` result, a full surface object, or a hand-written
|
|
6
|
+
* `{ server, tools, prompts, resources }`.
|
|
7
|
+
*/
|
|
8
|
+
/** Coerce an untrusted transport object into typed, string-only fields. */
|
|
9
|
+
function normalizeTransport(raw) {
|
|
10
|
+
if (!raw || typeof raw !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
const t = raw;
|
|
13
|
+
const kind = t.kind;
|
|
14
|
+
const validKind = kind === 'stdio' || kind === 'http' || kind === 'sse' ? kind : 'unknown';
|
|
15
|
+
return {
|
|
16
|
+
kind: validKind,
|
|
17
|
+
url: typeof t.url === 'string' ? t.url : undefined,
|
|
18
|
+
command: typeof t.command === 'string' ? t.command : undefined,
|
|
19
|
+
args: Array.isArray(t.args) ? t.args.filter((x) => typeof x === 'string') : undefined,
|
|
20
|
+
userControlledCommand: typeof t.userControlledCommand === 'boolean' ? t.userControlledCommand : undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function asArray(v) {
|
|
24
|
+
return Array.isArray(v) ? v.filter((x) => x && typeof x === 'object') : [];
|
|
25
|
+
}
|
|
26
|
+
function str(v) {
|
|
27
|
+
return typeof v === 'string' ? v : undefined;
|
|
28
|
+
}
|
|
29
|
+
function normalizeTool(o) {
|
|
30
|
+
const name = str(o.name);
|
|
31
|
+
if (!name)
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
title: str(o.title),
|
|
36
|
+
description: str(o.description),
|
|
37
|
+
inputSchema: (o.inputSchema ?? o.input_schema),
|
|
38
|
+
outputSchema: (o.outputSchema ?? o.output_schema),
|
|
39
|
+
annotations: o.annotations,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function normalizePrompt(o) {
|
|
43
|
+
const name = str(o.name);
|
|
44
|
+
if (!name)
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
title: str(o.title),
|
|
49
|
+
description: str(o.description),
|
|
50
|
+
arguments: asArray(o.arguments).map((a) => ({
|
|
51
|
+
name: str(a.name) ?? '',
|
|
52
|
+
description: str(a.description),
|
|
53
|
+
required: Boolean(a.required),
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function normalizeResource(o) {
|
|
58
|
+
return {
|
|
59
|
+
uri: str(o.uri),
|
|
60
|
+
uriTemplate: str(o.uriTemplate),
|
|
61
|
+
name: str(o.name),
|
|
62
|
+
title: str(o.title),
|
|
63
|
+
description: str(o.description),
|
|
64
|
+
mimeType: str(o.mimeType),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Build a surface from an arbitrary manifest object. */
|
|
68
|
+
export function surfaceFromManifest(input, origin, id) {
|
|
69
|
+
const root = (input && typeof input === 'object' ? input : {});
|
|
70
|
+
const serverObj = (root.server ?? {});
|
|
71
|
+
const meta = (root.serverInfo ?? root.server_info ?? {});
|
|
72
|
+
const tools = asArray(root.tools).map(normalizeTool).filter((t) => t !== null);
|
|
73
|
+
const prompts = asArray(root.prompts).map(normalizePrompt).filter((p) => p !== null);
|
|
74
|
+
const resources = asArray(root.resources).map(normalizeResource);
|
|
75
|
+
return {
|
|
76
|
+
id: id ?? str(root.id) ?? origin,
|
|
77
|
+
source: { kind: 'manifest', origin },
|
|
78
|
+
server: {
|
|
79
|
+
name: str(serverObj.name) ?? str(meta.name) ?? str(root.name),
|
|
80
|
+
version: str(serverObj.version) ?? str(meta.version),
|
|
81
|
+
title: str(serverObj.title),
|
|
82
|
+
instructions: str(serverObj.instructions) ?? str(root.instructions),
|
|
83
|
+
protocolVersion: str(root.protocolVersion),
|
|
84
|
+
capabilities: serverObj.capabilities ??
|
|
85
|
+
root.capabilities ??
|
|
86
|
+
undefined,
|
|
87
|
+
},
|
|
88
|
+
tools,
|
|
89
|
+
prompts,
|
|
90
|
+
resources,
|
|
91
|
+
transport: normalizeTransport(root.transport),
|
|
92
|
+
packageMeta: root.packageMeta,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort package-metadata resolution for the supply-chain stage. Network
|
|
4
|
+
* access is opt-in (`--online`); everything degrades gracefully offline.
|
|
5
|
+
*/
|
|
6
|
+
import type { PackageMeta } from '../types.js';
|
|
7
|
+
/** Fetch npm registry metadata for a package (latest dist-tag). */
|
|
8
|
+
export declare function fetchNpmMeta(name: string): Promise<PackageMeta>;
|
|
9
|
+
/** Fetch PyPI metadata for a package. */
|
|
10
|
+
export declare function fetchPypiMeta(name: string): Promise<PackageMeta>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort package-metadata resolution for the supply-chain stage. Network
|
|
4
|
+
* access is opt-in (`--online`); everything degrades gracefully offline.
|
|
5
|
+
*/
|
|
6
|
+
async function getJson(url, timeoutMs = 8000) {
|
|
7
|
+
const ctrl = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(url, { signal: ctrl.signal, headers: { 'User-Agent': 'mcptrustchecker' } });
|
|
11
|
+
if (!res.ok)
|
|
12
|
+
return null;
|
|
13
|
+
return await res.json();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Fetch npm registry metadata for a package (latest dist-tag). */
|
|
23
|
+
export async function fetchNpmMeta(name) {
|
|
24
|
+
const meta = { registry: 'npm', name };
|
|
25
|
+
const packument = (await getJson(`https://registry.npmjs.org/${encodeURIComponent(name).replace('%40', '@')}`));
|
|
26
|
+
if (packument && typeof packument === 'object') {
|
|
27
|
+
const latest = packument['dist-tags']?.latest;
|
|
28
|
+
const version = latest ?? Object.keys(packument.versions ?? {}).pop();
|
|
29
|
+
meta.version = version;
|
|
30
|
+
const v = version ? packument.versions?.[version] : undefined;
|
|
31
|
+
if (v) {
|
|
32
|
+
meta.scripts = v.scripts ?? {};
|
|
33
|
+
meta.dependencies = Object.keys(v.dependencies ?? {});
|
|
34
|
+
meta.license = typeof v.license === 'string' ? v.license : v.license == null ? null : String(v.license);
|
|
35
|
+
const repo = v.repository;
|
|
36
|
+
meta.repositoryUrl = typeof repo === 'string' ? repo : repo?.url ?? null;
|
|
37
|
+
}
|
|
38
|
+
meta.publishedAt = version ? packument.time?.[version] ?? null : null;
|
|
39
|
+
}
|
|
40
|
+
const encoded = encodeURIComponent(name).replace('%40', '@');
|
|
41
|
+
const downloads = (await getJson(`https://api.npmjs.org/downloads/point/last-week/${encoded}`));
|
|
42
|
+
meta.weeklyDownloads = downloads?.downloads ?? null;
|
|
43
|
+
return meta;
|
|
44
|
+
}
|
|
45
|
+
/** Fetch PyPI metadata for a package. */
|
|
46
|
+
export async function fetchPypiMeta(name) {
|
|
47
|
+
const meta = { registry: 'pypi', name };
|
|
48
|
+
const data = (await getJson(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`));
|
|
49
|
+
if (data?.info) {
|
|
50
|
+
meta.version = data.info.version;
|
|
51
|
+
meta.license = data.info.license || null;
|
|
52
|
+
meta.repositoryUrl = data.info.project_urls?.Source ?? data.info.home_page ?? null;
|
|
53
|
+
}
|
|
54
|
+
return meta;
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Acquire a server's implementation source from a local package directory, so
|
|
4
|
+
* the scan can analyze what the code *does* — not only what its tools *claim*.
|
|
5
|
+
* Bounded (file count, per-file and total size) so a huge repo can't OOM the
|
|
6
|
+
* scan, and never executes any of the code it reads. Offline.
|
|
7
|
+
*/
|
|
8
|
+
import type { ServerSurface, SourceFile } from '../types.js';
|
|
9
|
+
/** Recursively collect scannable source files from a directory (bounded). */
|
|
10
|
+
export declare function readSourceFiles(dir: string): SourceFile[];
|
|
11
|
+
/**
|
|
12
|
+
* Build a scan surface from a local package directory: its implementation source
|
|
13
|
+
* (for MTC-SRC-* analysis), its package metadata (for supply-chain checks), and
|
|
14
|
+
* a tools manifest if the directory ships one (tools.json / mcp-tools.json).
|
|
15
|
+
*/
|
|
16
|
+
export declare function surfaceFromPackageDir(dir: string): ServerSurface;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*! MCP Trust Checker · https://mcptrustchecker.com · support@mcptrustchecker.com · © 2026 Illia Haidar · MIT */
|
|
2
|
+
/**
|
|
3
|
+
* Acquire a server's implementation source from a local package directory, so
|
|
4
|
+
* the scan can analyze what the code *does* — not only what its tools *claim*.
|
|
5
|
+
* Bounded (file count, per-file and total size) so a huge repo can't OOM the
|
|
6
|
+
* scan, and never executes any of the code it reads. Offline.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { join, relative, extname } from 'node:path';
|
|
10
|
+
import { SOURCE_EXTENSIONS } from '../data/sourcePatterns.js';
|
|
11
|
+
import { surfaceFromManifest } from './manifest.js';
|
|
12
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '__pycache__', 'venv', '.venv', 'vendor']);
|
|
13
|
+
const MAX_FILES = 400;
|
|
14
|
+
const MAX_FILE_BYTES = 512 * 1024; // 512 KB per file
|
|
15
|
+
const MAX_TOTAL_BYTES = 12 * 1024 * 1024; // 12 MB total
|
|
16
|
+
/** Recursively collect scannable source files from a directory (bounded). */
|
|
17
|
+
export function readSourceFiles(dir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let total = 0;
|
|
20
|
+
const walk = (d) => {
|
|
21
|
+
if (out.length >= MAX_FILES || total >= MAX_TOTAL_BYTES)
|
|
22
|
+
return;
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = readdirSync(d);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const name of entries) {
|
|
31
|
+
if (out.length >= MAX_FILES || total >= MAX_TOTAL_BYTES)
|
|
32
|
+
return;
|
|
33
|
+
const full = join(d, name);
|
|
34
|
+
let st;
|
|
35
|
+
try {
|
|
36
|
+
st = statSync(full);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (st.isDirectory()) {
|
|
42
|
+
if (!SKIP_DIRS.has(name) && !name.startsWith('.'))
|
|
43
|
+
walk(full);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!SOURCE_EXTENSIONS.has(extname(name).toLowerCase()))
|
|
47
|
+
continue;
|
|
48
|
+
if (st.size > MAX_FILE_BYTES)
|
|
49
|
+
continue;
|
|
50
|
+
try {
|
|
51
|
+
out.push({ path: relative(dir, full) || name, content: readFileSync(full, 'utf8') });
|
|
52
|
+
total += st.size;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* unreadable file — skip */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(dir);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** Read a package.json (if present) into the surface's package metadata. */
|
|
63
|
+
function readPackageMeta(dir) {
|
|
64
|
+
const pkgPath = join(dir, 'package.json');
|
|
65
|
+
if (!existsSync(pkgPath))
|
|
66
|
+
return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
69
|
+
return {
|
|
70
|
+
registry: 'npm',
|
|
71
|
+
name: typeof pkg.name === 'string' ? pkg.name : undefined,
|
|
72
|
+
version: typeof pkg.version === 'string' ? pkg.version : undefined,
|
|
73
|
+
scripts: pkg.scripts ?? undefined,
|
|
74
|
+
dependencies: pkg.dependencies ? Object.keys(pkg.dependencies) : undefined,
|
|
75
|
+
license: typeof pkg.license === 'string' ? pkg.license : null,
|
|
76
|
+
repositoryUrl: typeof pkg.repository === 'string'
|
|
77
|
+
? pkg.repository
|
|
78
|
+
: (pkg.repository?.url ?? null),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build a scan surface from a local package directory: its implementation source
|
|
87
|
+
* (for MTC-SRC-* analysis), its package metadata (for supply-chain checks), and
|
|
88
|
+
* a tools manifest if the directory ships one (tools.json / mcp-tools.json).
|
|
89
|
+
*/
|
|
90
|
+
export function surfaceFromPackageDir(dir) {
|
|
91
|
+
let manifest = {};
|
|
92
|
+
for (const name of ['tools.json', 'mcp-tools.json', 'mcp.json']) {
|
|
93
|
+
const p = join(dir, name);
|
|
94
|
+
if (existsSync(p)) {
|
|
95
|
+
try {
|
|
96
|
+
manifest = JSON.parse(readFileSync(p, 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* ignore an unparseable sidecar manifest */
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const surface = surfaceFromManifest(manifest, dir, dir);
|
|
105
|
+
surface.source = { kind: 'package', origin: dir };
|
|
106
|
+
surface.packageMeta = { ...readPackageMeta(dir), ...(surface.packageMeta ?? {}) };
|
|
107
|
+
surface.sourceFiles = readSourceFiles(dir);
|
|
108
|
+
return surface;
|
|
109
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export {};
|