@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,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSuppressions = parseSuppressions;
4
+ const types_1 = require("./types");
5
+ const ALL = new Set(types_1.ALL_PATTERN_IDS);
6
+ const DIRECTIVE_RE = /mcp-vet-disable-(file|next-line|line)\b([^\r\n]*)/g;
7
+ function parseIds(trailing) {
8
+ const ids = new Set();
9
+ for (const tok of trailing.toUpperCase().match(/[A-Z0-9_]+/g) ?? []) {
10
+ if (ALL.has(tok))
11
+ ids.add(tok);
12
+ }
13
+ return ids; // empty => all patterns
14
+ }
15
+ /**
16
+ * Parse inline suppression directives from a file's raw lines. Recognized in any
17
+ * comment style (they are matched textually):
18
+ * mcp-vet-disable-file
19
+ * mcp-vet-disable-line [PATTERN_ID ...]
20
+ * mcp-vet-disable-next-line [PATTERN_ID ...]
21
+ */
22
+ function parseSuppressions(lines) {
23
+ let fileDisabled = false;
24
+ const byLine = new Map();
25
+ const add = (line, ids) => {
26
+ const existing = byLine.get(line);
27
+ if (!existing) {
28
+ byLine.set(line, ids);
29
+ return;
30
+ }
31
+ if (existing.size === 0)
32
+ return; // already "all"
33
+ if (ids.size === 0) {
34
+ byLine.set(line, new Set()); // widen to all
35
+ return;
36
+ }
37
+ ids.forEach((i) => existing.add(i));
38
+ };
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const line = lines[i];
41
+ DIRECTIVE_RE.lastIndex = 0;
42
+ let m;
43
+ while ((m = DIRECTIVE_RE.exec(line)) !== null) {
44
+ const kind = m[1];
45
+ const ids = parseIds(m[2]);
46
+ if (kind === 'file')
47
+ fileDisabled = true;
48
+ else if (kind === 'line')
49
+ add(i + 1, ids);
50
+ else if (kind === 'next-line')
51
+ add(i + 2, ids);
52
+ }
53
+ }
54
+ return {
55
+ fileDisabled,
56
+ byLine,
57
+ isSuppressed(line, id) {
58
+ if (fileDisabled)
59
+ return true;
60
+ const ids = byLine.get(line);
61
+ if (!ids)
62
+ return false;
63
+ return ids.size === 0 || ids.has(id);
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeTs = analyzeTs;
4
+ const ts_morph_1 = require("ts-morph");
5
+ let project = null;
6
+ let counter = 0;
7
+ const CAP = new Set(['roots', 'sampling', 'logging']);
8
+ const INIT_STRINGS = new Set(['initialize', 'notifications/initialized']);
9
+ function getProject() {
10
+ if (!project) {
11
+ project = new ts_morph_1.Project({
12
+ useInMemoryFileSystem: true,
13
+ skipAddingFilesFromTsConfig: true,
14
+ compilerOptions: {
15
+ allowJs: true,
16
+ noLib: true, // pure syntactic extraction — no type checking / lib needed
17
+ },
18
+ });
19
+ }
20
+ return project;
21
+ }
22
+ function scanExtension(absPath) {
23
+ const p = absPath.toLowerCase();
24
+ if (p.endsWith('.tsx'))
25
+ return '.tsx';
26
+ if (p.endsWith('.jsx'))
27
+ return '.jsx';
28
+ if (p.endsWith('.mts') || p.endsWith('.cts'))
29
+ return '.ts';
30
+ if (p.endsWith('.js') || p.endsWith('.mjs') || p.endsWith('.cjs'))
31
+ return '.js';
32
+ return '.ts';
33
+ }
34
+ function safePropName(node) {
35
+ const pa = node.asKind(ts_morph_1.SyntaxKind.PropertyAssignment) ??
36
+ node.asKind(ts_morph_1.SyntaxKind.ShorthandPropertyAssignment);
37
+ try {
38
+ return pa?.getName();
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ /** Is `node` structurally inside a `capabilities` object / call argument? */
45
+ function isInCapabilities(node) {
46
+ let depth = 0;
47
+ for (const anc of node.getAncestors()) {
48
+ if (depth++ > 12)
49
+ break;
50
+ const k = anc.getKind();
51
+ if (k === ts_morph_1.SyntaxKind.PropertyAssignment || k === ts_morph_1.SyntaxKind.ShorthandPropertyAssignment) {
52
+ if (safePropName(anc) === 'capabilities')
53
+ return true;
54
+ }
55
+ else if (k === ts_morph_1.SyntaxKind.VariableDeclaration) {
56
+ try {
57
+ if (anc.asKind(ts_morph_1.SyntaxKind.VariableDeclaration).getName() === 'capabilities')
58
+ return true;
59
+ }
60
+ catch {
61
+ /* ignore */
62
+ }
63
+ }
64
+ else if (k === ts_morph_1.SyntaxKind.CallExpression || k === ts_morph_1.SyntaxKind.NewExpression) {
65
+ const expr = anc.getExpression?.();
66
+ if (expr && /capabilit/i.test(expr.getText()))
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ /** Is this string literal used like a registered method name? */
73
+ function isRegistrationContext(node) {
74
+ const parent = node.getParent();
75
+ if (!parent)
76
+ return false;
77
+ // switch (x) { case 'initialize': }
78
+ if (parent.getKind() === ts_morph_1.SyntaxKind.CaseClause)
79
+ return true;
80
+ // x.method === 'initialize'
81
+ if (parent.getKind() === ts_morph_1.SyntaxKind.BinaryExpression) {
82
+ const be = parent.asKind(ts_morph_1.SyntaxKind.BinaryExpression);
83
+ const op = be.getOperatorToken().getKind();
84
+ if (op === ts_morph_1.SyntaxKind.EqualsEqualsEqualsToken || op === ts_morph_1.SyntaxKind.EqualsEqualsToken) {
85
+ const left = be.getLeft();
86
+ const other = left.getStart() === node.getStart() ? be.getRight() : left;
87
+ if (/method|type/i.test(other.getText()))
88
+ return true;
89
+ }
90
+ }
91
+ // { method: 'initialize' }
92
+ if (parent.getKind() === ts_morph_1.SyntaxKind.PropertyAssignment) {
93
+ const pa = parent.asKind(ts_morph_1.SyntaxKind.PropertyAssignment);
94
+ try {
95
+ if (pa.getInitializer()?.getStart() === node.getStart() &&
96
+ /^(method|type|name)$/i.test(pa.getName())) {
97
+ return true;
98
+ }
99
+ }
100
+ catch {
101
+ /* ignore */
102
+ }
103
+ }
104
+ // server.setRequestHandler('initialize', handler)
105
+ let cur = node;
106
+ for (let i = 0; i < 4 && cur; i++) {
107
+ const p = cur.getParent();
108
+ if (!p)
109
+ break;
110
+ if (p.getKind() === ts_morph_1.SyntaxKind.CallExpression) {
111
+ const call = p.asKind(ts_morph_1.SyntaxKind.CallExpression);
112
+ const isArg = call
113
+ .getArguments()
114
+ .some((a) => a.getStart() === cur.getStart() && a.getEnd() === cur.getEnd());
115
+ if (isArg) {
116
+ const exprText = call.getExpression().getText();
117
+ if (/handler|handle|register|method|route|request|notification|(^|\.)on$/i.test(exprText)) {
118
+ return true;
119
+ }
120
+ }
121
+ break;
122
+ }
123
+ cur = p;
124
+ }
125
+ return false;
126
+ }
127
+ /**
128
+ * Parse a TypeScript/JavaScript file with ts-morph and emit normalized tokens:
129
+ * string literals, numeric literals (with sign), identifiers, and object keys —
130
+ * annotated with structural capability context and registration context.
131
+ */
132
+ function analyzeTs(absPath, text) {
133
+ const proj = getProject();
134
+ const ext = scanExtension(absPath);
135
+ const sf = proj.createSourceFile(`__scan_${counter++}${ext}`, text, { overwrite: true });
136
+ const tokens = [];
137
+ const posOf = (node) => {
138
+ try {
139
+ const { line, column } = sf.getLineAndColumnAtPos(node.getStart());
140
+ return { line, col: column };
141
+ }
142
+ catch {
143
+ return { line: node.getStartLineNumber(), col: undefined };
144
+ }
145
+ };
146
+ try {
147
+ sf.forEachDescendant((node) => {
148
+ const kind = node.getKind();
149
+ if (kind === ts_morph_1.SyntaxKind.StringLiteral ||
150
+ kind === ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
151
+ let value;
152
+ try {
153
+ value = node.getLiteralValue();
154
+ }
155
+ catch {
156
+ const raw = node.getText();
157
+ value = raw.length >= 2 ? raw.slice(1, -1) : raw;
158
+ }
159
+ const { line, col } = posOf(node);
160
+ const tok = { kind: 'string', value, line, col };
161
+ if (CAP.has(value))
162
+ tok.inCapabilities = isInCapabilities(node);
163
+ if (INIT_STRINGS.has(value))
164
+ tok.registration = isRegistrationContext(node);
165
+ tokens.push(tok);
166
+ return;
167
+ }
168
+ if (kind === ts_morph_1.SyntaxKind.NumericLiteral) {
169
+ const raw = node.getText();
170
+ let value = raw;
171
+ const parent = node.getParent();
172
+ if (parent && parent.getKind() === ts_morph_1.SyntaxKind.PrefixUnaryExpression) {
173
+ const pu = parent.asKind(ts_morph_1.SyntaxKind.PrefixUnaryExpression);
174
+ if (pu && pu.getOperatorToken() === ts_morph_1.SyntaxKind.MinusToken)
175
+ value = '-' + raw;
176
+ }
177
+ const { line, col } = posOf(node);
178
+ tokens.push({ kind: 'number', value, line, col });
179
+ return;
180
+ }
181
+ if (kind === ts_morph_1.SyntaxKind.Identifier) {
182
+ const { line, col } = posOf(node);
183
+ tokens.push({ kind: 'name', value: node.getText(), line, col });
184
+ return;
185
+ }
186
+ });
187
+ // Object literal keys (roots:, "sampling":, logging shorthand, ...)
188
+ const emitKey = (nameNode, value) => {
189
+ const { line, col } = posOf(nameNode);
190
+ const tok = { kind: 'key', value, line, col };
191
+ if (CAP.has(value))
192
+ tok.inCapabilities = isInCapabilities(nameNode);
193
+ tokens.push(tok);
194
+ };
195
+ for (const pa of sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
196
+ try {
197
+ emitKey(pa.getNameNode(), pa.getName());
198
+ }
199
+ catch {
200
+ /* computed / unusual key */
201
+ }
202
+ }
203
+ for (const sp of sf.getDescendantsOfKind(ts_morph_1.SyntaxKind.ShorthandPropertyAssignment)) {
204
+ try {
205
+ emitKey(sp.getNameNode(), sp.getName());
206
+ }
207
+ catch {
208
+ /* ignore */
209
+ }
210
+ }
211
+ }
212
+ finally {
213
+ proj.removeSourceFile(sf);
214
+ }
215
+ return tokens;
216
+ }
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_PATTERN_IDS = void 0;
4
+ exports.ALL_PATTERN_IDS = [
5
+ 'MCP_SESSION_ID',
6
+ 'INITIALIZE_HANDLER',
7
+ 'ERROR_CODE_32002',
8
+ 'TASKS_LEGACY',
9
+ 'ROOTS_CAP',
10
+ 'SAMPLING_CAP',
11
+ 'LOGGING_CAP',
12
+ ];
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@booyaka/mcp-vet",
3
+ "version": "0.2.0",
4
+ "description": "Scan MCP server source code for patterns that break under the 2026-07-28 Model Context Protocol spec release candidate.",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "mcp-vet": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "CHANGELOG.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "author": "Booyaka101 <cbosch101@gmail.com>",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Booyaka101/mcp-vet.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/Booyaka101/mcp-vet/issues"
25
+ },
26
+ "homepage": "https://github.com/Booyaka101/mcp-vet#readme",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "linter",
34
+ "static-analysis",
35
+ "migration",
36
+ "breaking-changes",
37
+ "2026-07-28",
38
+ "sarif",
39
+ "ci",
40
+ "ast",
41
+ "cli",
42
+ "typescript",
43
+ "python"
44
+ ],
45
+ "license": "MIT",
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
48
+ "prepare": "npm run build",
49
+ "test": "npm run build && node --test test/scan.test.mjs"
50
+ },
51
+ "dependencies": {
52
+ "chalk": "^4.1.2",
53
+ "commander": "^12.1.0",
54
+ "ts-morph": "^23.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^20.14.0",
58
+ "typescript": "^5.5.4"
59
+ }
60
+ }