@booyaka/mcp-vet 0.2.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.
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CHANGELOG_URL = exports.SPEC_URL = exports.SPEC_DATE = void 0;
37
+ exports.getVersion = getVersion;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ exports.SPEC_DATE = 'July 28, 2026';
41
+ exports.SPEC_URL = 'https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/';
42
+ exports.CHANGELOG_URL = 'https://tokenmix.ai/blog/mcp-updates-changelog-every-protocol-change-2026';
43
+ /** Resolve the package version from package.json, tolerating layout differences. */
44
+ function getVersion() {
45
+ const candidates = [
46
+ path.join(__dirname, '..', 'package.json'), // dist/ -> package.json
47
+ path.join(__dirname, '..', '..', 'package.json'),
48
+ ];
49
+ for (const c of candidates) {
50
+ try {
51
+ const pkg = JSON.parse(fs.readFileSync(c, 'utf8'));
52
+ if (pkg && typeof pkg.version === 'string')
53
+ return pkg.version;
54
+ }
55
+ catch {
56
+ /* try next */
57
+ }
58
+ }
59
+ return '0.0.0';
60
+ }
package/dist/ignore.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IgnoreMatcher = void 0;
4
+ /**
5
+ * Minimal gitignore-flavored glob matcher (no external deps). Supports `*`,
6
+ * `**`, `?`, leading `/` anchoring, trailing `/`, and bare basename patterns.
7
+ * Patterns and paths are matched in posix form.
8
+ */
9
+ function compile(pattern) {
10
+ let p = pattern.trim();
11
+ if (!p || p.startsWith('#'))
12
+ return null;
13
+ const anchored = p.startsWith('/');
14
+ if (anchored)
15
+ p = p.slice(1);
16
+ if (p.endsWith('/'))
17
+ p = p.slice(0, -1);
18
+ if (!p)
19
+ return null;
20
+ let re = '';
21
+ for (let i = 0; i < p.length; i++) {
22
+ const c = p[i];
23
+ if (c === '*' && p[i + 1] === '*') {
24
+ if (p[i + 2] === '/') {
25
+ re += '(?:.*/)?';
26
+ i += 2;
27
+ }
28
+ else {
29
+ re += '.*';
30
+ i += 1;
31
+ }
32
+ }
33
+ else if (c === '*') {
34
+ re += '[^/]*';
35
+ }
36
+ else if (c === '?') {
37
+ re += '[^/]';
38
+ }
39
+ else if ('\\^$+.()|{}[]'.includes(c)) {
40
+ re += '\\' + c;
41
+ }
42
+ else {
43
+ re += c;
44
+ }
45
+ }
46
+ const hasSlash = p.includes('/');
47
+ const full = anchored || hasSlash ? `^${re}(?:/.*)?$` : `(?:^|/)${re}(?:/.*)?$`;
48
+ try {
49
+ return new RegExp(full);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ class IgnoreMatcher {
56
+ constructor(patterns) {
57
+ this.regexes = patterns.map(compile).filter((r) => r !== null);
58
+ }
59
+ get size() {
60
+ return this.regexes.length;
61
+ }
62
+ matches(relPosixPath) {
63
+ return this.regexes.some((r) => r.test(relPosixPath));
64
+ }
65
+ }
66
+ exports.IgnoreMatcher = IgnoreMatcher;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findPython = findPython;
37
+ exports.pythonAvailable = pythonAvailable;
38
+ exports.analyzePyBatch = analyzePyBatch;
39
+ const node_child_process_1 = require("node:child_process");
40
+ const path = __importStar(require("node:path"));
41
+ let cachedPython = undefined;
42
+ /** Locate a usable Python 3 interpreter, trying platform-appropriate names. */
43
+ function findPython() {
44
+ if (cachedPython !== undefined)
45
+ return cachedPython;
46
+ if (process.env.MCP_VET_NO_PYTHON) {
47
+ // Escape hatch: force the regex fallback (also used to test that path).
48
+ cachedPython = null;
49
+ return null;
50
+ }
51
+ if (process.env.MCP_VET_PYTHON) {
52
+ cachedPython = process.env.MCP_VET_PYTHON;
53
+ return cachedPython;
54
+ }
55
+ const candidates = process.platform === 'win32'
56
+ ? ['python', 'py', 'python3']
57
+ : ['python3', 'python'];
58
+ for (const c of candidates) {
59
+ try {
60
+ const r = (0, node_child_process_1.spawnSync)(c, ['--version'], { encoding: 'utf8' });
61
+ if (!r.error &&
62
+ (r.status === 0 || `${r.stdout}${r.stderr}`.toLowerCase().includes('python'))) {
63
+ cachedPython = c;
64
+ return c;
65
+ }
66
+ }
67
+ catch {
68
+ /* try next */
69
+ }
70
+ }
71
+ cachedPython = null;
72
+ return null;
73
+ }
74
+ const SCRIPT = path.join(__dirname, 'python', 'mcp_ast_scan.py');
75
+ const CHUNK_SIZE = 300;
76
+ function pythonAvailable() {
77
+ return findPython() !== null;
78
+ }
79
+ function runChunk(py, files) {
80
+ const out = {};
81
+ const r = (0, node_child_process_1.spawnSync)(py, [SCRIPT], {
82
+ input: files.join('\n'),
83
+ encoding: 'utf8',
84
+ maxBuffer: 256 * 1024 * 1024,
85
+ });
86
+ if (r.error || r.status !== 0 || !r.stdout)
87
+ return out;
88
+ try {
89
+ const parsed = JSON.parse(r.stdout);
90
+ for (const [f, toks] of Object.entries(parsed))
91
+ out[f] = toks;
92
+ }
93
+ catch {
94
+ /* malformed output — treat chunk as no findings */
95
+ }
96
+ return out;
97
+ }
98
+ /**
99
+ * Analyze a batch of Python files via the bundled subprocess script. Files are
100
+ * processed in chunks so a single pathological file can only lose its own chunk,
101
+ * and so stdin/stdout stay well under buffer limits on large repos.
102
+ * Returns a map of absolute-file-path -> tokens.
103
+ */
104
+ function analyzePyBatch(absFiles) {
105
+ const out = {};
106
+ const py = findPython();
107
+ if (!py || absFiles.length === 0)
108
+ return out;
109
+ for (let i = 0; i < absFiles.length; i += CHUNK_SIZE) {
110
+ const chunk = absFiles.slice(i, i + CHUNK_SIZE);
111
+ Object.assign(out, runChunk(py, chunk));
112
+ }
113
+ return out;
114
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.regexFallbackTokens = regexFallbackTokens;
4
+ /**
5
+ * Degraded, regex-based tokenizer for Python source, used ONLY when no Python
6
+ * interpreter is available. It cannot verify AST structure, so capability
7
+ * findings fall back to the line-proximity heuristic (medium confidence) and it
8
+ * may over-report inside comments/docstrings. Deterministic string/number rules
9
+ * (session id, initialize, -32002, tasks/*) remain reliable.
10
+ */
11
+ function regexFallbackTokens(text) {
12
+ const tokens = [];
13
+ const lines = text.split(/\r?\n/);
14
+ const strRe = /(['"])((?:\\.|(?!\1).)*)\1/g;
15
+ const numRe = /-?\b\d+\b/g;
16
+ const idRe = /[A-Za-z_][A-Za-z0-9_]*/g;
17
+ const capKeyRe = /\b(roots|sampling|logging)\s*[:=]/g;
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+ const ln = i + 1;
21
+ let m;
22
+ strRe.lastIndex = 0;
23
+ while ((m = strRe.exec(line)) !== null) {
24
+ tokens.push({ kind: 'string', value: m[2], line: ln, col: m.index + 1 });
25
+ }
26
+ numRe.lastIndex = 0;
27
+ while ((m = numRe.exec(line)) !== null) {
28
+ tokens.push({ kind: 'number', value: m[0], line: ln, col: m.index + 1 });
29
+ }
30
+ idRe.lastIndex = 0;
31
+ while ((m = idRe.exec(line)) !== null) {
32
+ const v = m[0];
33
+ // Only names that can matter to a rule (keeps the token list small).
34
+ if (/mcpsessionid/i.test(v)) {
35
+ tokens.push({ kind: 'name', value: v, line: ln, col: m.index + 1 });
36
+ }
37
+ }
38
+ capKeyRe.lastIndex = 0;
39
+ while ((m = capKeyRe.exec(line)) !== null) {
40
+ tokens.push({ kind: 'key', value: m[1], line: ln, col: m.index + 1 });
41
+ }
42
+ }
43
+ return tokens;
44
+ }
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """Bundled Python AST scanner for mcp-vet.
3
+
4
+ Reads a newline-separated list of file paths on stdin, parses each with
5
+ ``ast.parse`` and emits normalized tokens the Node rule engine consumes. Output
6
+ is a single JSON object mapping each file path to its list of tokens:
7
+
8
+ {"/abs/path.py": [{"kind": "string", "value": "...", "line": 12, "col": 5}, ...]}
9
+
10
+ Token kinds match the TypeScript analyzer: "string", "number", "name", "key".
11
+ Capability keys carry ``inCapabilities`` (structurally inside a ``capabilities``
12
+ object/kwarg) and ``initialize`` strings carry ``registration`` (used like a
13
+ registered method name), so the engine can assign confidence uniformly.
14
+ """
15
+ import ast
16
+ import json
17
+ import re
18
+ import sys
19
+
20
+ sys.setrecursionlimit(20000)
21
+
22
+ CAP = {"roots", "sampling", "logging"}
23
+ INIT_STRINGS = {"initialize", "notifications/initialized"}
24
+ HANDLERISH = re.compile(r"handler|handle|register|route|request|notification|method|^on$", re.I)
25
+ METHODISH = ("method", "type")
26
+
27
+
28
+ def _is_int_constant(node):
29
+ return (
30
+ isinstance(node, ast.Constant)
31
+ and isinstance(node.value, int)
32
+ and not isinstance(node.value, bool)
33
+ )
34
+
35
+
36
+ def _col(node):
37
+ c = getattr(node, "col_offset", None)
38
+ return (c + 1) if isinstance(c, int) else None
39
+
40
+
41
+ def _mentions_method(o):
42
+ if isinstance(o, ast.Attribute):
43
+ return o.attr.lower() in METHODISH
44
+ if isinstance(o, ast.Name):
45
+ return o.id.lower() in METHODISH
46
+ if isinstance(o, ast.Subscript):
47
+ s = o.slice
48
+ if isinstance(s, ast.Index): # py<3.9 compatibility
49
+ s = s.value
50
+ if isinstance(s, ast.Constant) and isinstance(s.value, str):
51
+ return s.value.lower() in METHODISH
52
+ return False
53
+
54
+
55
+ def _func_is_handlerish(func):
56
+ name = ""
57
+ if isinstance(func, ast.Attribute):
58
+ name = func.attr
59
+ elif isinstance(func, ast.Name):
60
+ name = func.id
61
+ return bool(HANDLERISH.search(name))
62
+
63
+
64
+ def _is_registration(node):
65
+ p = getattr(node, "parent", None)
66
+ if p is None:
67
+ return False
68
+ if isinstance(p, ast.Compare):
69
+ for o in [p.left, *p.comparators]:
70
+ if o is not node and _mentions_method(o):
71
+ return True
72
+ if isinstance(p, ast.Call) and node in p.args and _func_is_handlerish(p.func):
73
+ return True
74
+ if isinstance(p, ast.Dict):
75
+ try:
76
+ idx = p.values.index(node)
77
+ except ValueError:
78
+ idx = -1
79
+ if idx >= 0:
80
+ k = p.keys[idx]
81
+ if isinstance(k, ast.Constant) and isinstance(k.value, str):
82
+ if k.value.lower() in ("method", "type", "name"):
83
+ return True
84
+ return False
85
+
86
+
87
+ class Scanner:
88
+ def __init__(self):
89
+ self.tokens = []
90
+
91
+ def emit_for(self, node, in_caps):
92
+ if isinstance(node, ast.Constant):
93
+ if isinstance(node.value, str):
94
+ tok = {"kind": "string", "value": node.value, "line": node.lineno, "col": _col(node)}
95
+ if node.value in CAP:
96
+ tok["inCapabilities"] = in_caps
97
+ if node.value in INIT_STRINGS:
98
+ tok["registration"] = _is_registration(node)
99
+ self.tokens.append(tok)
100
+ elif _is_int_constant(node):
101
+ self.tokens.append(
102
+ {"kind": "number", "value": str(node.value), "line": node.lineno, "col": _col(node)}
103
+ )
104
+ elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and _is_int_constant(node.operand):
105
+ op = node.operand
106
+ self.tokens.append(
107
+ {"kind": "number", "value": str(-op.value), "line": op.lineno, "col": _col(node)}
108
+ )
109
+ elif isinstance(node, ast.Name):
110
+ self.tokens.append({"kind": "name", "value": node.id, "line": node.lineno, "col": _col(node)})
111
+ elif isinstance(node, ast.Attribute):
112
+ self.tokens.append({"kind": "name", "value": node.attr, "line": node.lineno, "col": _col(node)})
113
+
114
+ def visit(self, node, in_caps):
115
+ self.emit_for(node, in_caps)
116
+
117
+ if isinstance(node, ast.Dict):
118
+ for k, v in zip(node.keys, node.values):
119
+ if k is not None:
120
+ self.visit(k, in_caps)
121
+ if isinstance(k, ast.Constant) and isinstance(k.value, str):
122
+ tok = {"kind": "key", "value": k.value, "line": k.lineno, "col": _col(k)}
123
+ if k.value in CAP:
124
+ tok["inCapabilities"] = in_caps
125
+ self.tokens.append(tok)
126
+ child_caps = in_caps or (isinstance(k, ast.Constant) and k.value == "capabilities")
127
+ self.visit(v, child_caps)
128
+ return
129
+
130
+ if isinstance(node, ast.Call):
131
+ self.visit(node.func, in_caps)
132
+ for a in node.args:
133
+ self.visit(a, in_caps)
134
+ for kw in node.keywords:
135
+ if kw.arg is not None:
136
+ line = getattr(kw, "lineno", None) or getattr(kw.value, "lineno", 0)
137
+ tok = {"kind": "key", "value": kw.arg, "line": line, "col": _col(kw)}
138
+ if kw.arg in CAP:
139
+ tok["inCapabilities"] = in_caps
140
+ self.tokens.append(tok)
141
+ child_caps = in_caps or (kw.arg == "capabilities")
142
+ self.visit(kw.value, child_caps)
143
+ return
144
+
145
+ for child in ast.iter_child_nodes(node):
146
+ self.visit(child, in_caps)
147
+
148
+
149
+ def scan_source(src):
150
+ try:
151
+ tree = ast.parse(src)
152
+ except (SyntaxError, ValueError):
153
+ return []
154
+ for n in ast.walk(tree):
155
+ for c in ast.iter_child_nodes(n):
156
+ c.parent = n
157
+ scanner = Scanner()
158
+ try:
159
+ scanner.visit(tree, False)
160
+ except RecursionError:
161
+ pass
162
+ return scanner.tokens
163
+
164
+
165
+ def main():
166
+ data = sys.stdin.read()
167
+ files = [line for line in data.splitlines() if line.strip()]
168
+ result = {}
169
+ for f in files:
170
+ try:
171
+ # utf-8-sig strips a leading BOM (common on Windows-authored files);
172
+ # a BOM left in the source makes ast.parse raise SyntaxError.
173
+ with open(f, "r", encoding="utf-8-sig", errors="replace") as fh:
174
+ src = fh.read()
175
+ except OSError:
176
+ result[f] = []
177
+ continue
178
+ result[f] = scan_source(src)
179
+ sys.stdout.write(json.dumps(result))
180
+
181
+
182
+ if __name__ == "__main__":
183
+ main()