@vantageos/mcp-doctor 0.1.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 ADDED
@@ -0,0 +1,110 @@
1
+ Functional Source License, Version 1.1, Apache 2.0 Future License
2
+
3
+ Abbreviation
4
+
5
+ FSL-1.1-Apache-2.0
6
+
7
+ Notice
8
+
9
+ Copyright 2026 VantageOS (ElPi Corp)
10
+
11
+ Terms and Conditions
12
+
13
+ Licensor: VantageOS (ElPi Corp)
14
+
15
+ Licensed Work: VantagePeers
16
+ The Licensed Work is (c) 2026 VantageOS (ElPi Corp)
17
+
18
+ Additional Use Grant: You may make production use of the Licensed Work,
19
+ provided Your use does not include offering the Licensed
20
+ Work to third parties as a hosted or managed service
21
+ where the service provides users with access to any
22
+ substantial set of the features or functionality of the
23
+ Licensed Work.
24
+
25
+ Change Date: 2028-04-03 (two years from publication)
26
+
27
+ Change License: Apache License, Version 2.0
28
+
29
+ For information about alternative licensing arrangements for the Licensed Work,
30
+ please contact: contact@vantageos.com
31
+
32
+ License text below is provided for informational purposes only.
33
+
34
+ ---
35
+
36
+ Functional Source License, Version 1.1, Apache 2.0 Future License
37
+
38
+ 1. Purpose
39
+
40
+ This license gives you broad permission to use, modify, and share this
41
+ software, with one important condition: you may not use it to compete with us.
42
+
43
+ 2. Acceptance
44
+
45
+ To use the software, you must agree to these terms. If you do not agree, you
46
+ may not use the software.
47
+
48
+ 3. Copyright License
49
+
50
+ The licensor grants you a non-exclusive, royalty-free, worldwide, non-
51
+ sublicensable, non-transferable license to use, copy, distribute, make
52
+ available, and prepare derivative works of the software, in each case subject
53
+ to the limitations below.
54
+
55
+ 4. Limitations
56
+
57
+ You may not make the functionality of the software available to third parties
58
+ as a service, or otherwise use the software to provide a service to third
59
+ parties that competes with the licensor.
60
+
61
+ 5. Patents
62
+
63
+ The licensor grants you a license, under any patent claims the licensor can
64
+ license or becomes able to license, to make, have made, use, sell, offer for
65
+ sale, import, and have imported the software, in each case subject to the
66
+ limitations and conditions in this license.
67
+
68
+ 6. Fair Use
69
+
70
+ This license is not intended to limit any rights you may have under applicable
71
+ fair use or other laws.
72
+
73
+ 7. No Other Rights
74
+
75
+ These terms do not grant any other rights. The licensor reserves all rights not
76
+ expressly granted.
77
+
78
+ 8. Termination
79
+
80
+ Your license is automatically terminated if you violate the terms. However, if
81
+ you cure the violation within 30 days of discovering it, your license is
82
+ reinstated retroactively.
83
+
84
+ 9. Disclaimer
85
+
86
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
87
+ IMPLIED.
88
+
89
+ 10. Limitation of Liability
90
+
91
+ TO THE EXTENT PERMITTED BY LAW, THE LICENSOR IS NOT LIABLE FOR ANY DAMAGES
92
+ ARISING FROM THE USE OF THE SOFTWARE.
93
+
94
+ 11. Change Date and License
95
+
96
+ After the Change Date, the licensor grants you the rights under the Change
97
+ License.
98
+
99
+ 12. Definitions
100
+
101
+ "Compete" means providing a product or service that is substantially similar
102
+ to, or a replacement for, all or a significant portion of the Licensed Work.
103
+
104
+ ---
105
+
106
+ Apache License, Version 2.0
107
+
108
+ After the Change Date (2028-04-03), this software will be available under the
109
+ Apache License, Version 2.0. See https://www.apache.org/licenses/LICENSE-2.0
110
+ for full text.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @vantageos/mcp-doctor
2
+
3
+ Static **MCP conformance checker**. Point it at an MCP server source tree and it emits a
4
+ **conformance-diff table** against the MCP **2026-07-28** standard — one row per checked item:
5
+
6
+ ```
7
+ item | file:line | present | absent | non-conforming | verdict
8
+ ```
9
+
10
+ No narrative, no LLM, no network: a deterministic static scan you can wire into CI. It reproduces,
11
+ unaided, the manual conformance audits the fleet ran by hand on its own MCP servers.
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ npx @vantageos/mcp-doctor <path-to-mcp-server-source>
17
+ ```
18
+
19
+ Example:
20
+
21
+ ```bash
22
+ npx @vantageos/mcp-doctor ./mcp-server
23
+ ```
24
+
25
+ ```
26
+ surface: ./mcp-server
27
+ item | file:line | state | verdict
28
+ -----------------------------------------------------------------------------
29
+ D2 stateless | - | present | stateless (no mcp-session-id session-keying)
30
+ D3 resultType | server.ts:589 | absent | list/read builder returns {content} without resultType -> add via protocol-kit
31
+ D4 CacheableResult | server.ts:579,595 | absent | list/read builders lack ttlMs/cacheScope -> add CacheableResult
32
+ D5 server/discover | - | absent | no server/discover handler -> add via protocol-kit
33
+ D6 mcp-apps ui-key | - | absent | no MCP Apps UI key (no _meta ui)
34
+ D7 protocol-kit | package.json | absent | hand-rolled builders, protocol-kit not a dependency
35
+ ```
36
+
37
+ The path is the root of the MCP server sources (the folder holding `server.ts` / `transport/` /
38
+ `package.json`). `node_modules`, `dist`, `build`, `.git` and test folders are skipped automatically.
39
+
40
+ ## Programmatic use
41
+
42
+ ```ts
43
+ import { runDoctor, renderTable } from "@vantageos/mcp-doctor";
44
+
45
+ const results = runDoctor("./mcp-server");
46
+ console.log(renderTable("./mcp-server", results));
47
+ ```
48
+
49
+ ## What it checks — the 6 Tier-A detectors (MCP 2026-07-28)
50
+
51
+ Each detector maps one standard item and is proven **bipolar** on real material: a genuine defect
52
+ (MUST_FLAG) and a genuine conformant site (MUST_PASS).
53
+
54
+ | ID | Standard item | Verdict shape |
55
+ |----|---------------|---------------|
56
+ | **D2** stateless | `sessionIdGenerator: undefined`, no `Mcp-Session-Id` session-keying, no hand-rolled `initialize` gate | conforming (stateless) / non-conforming (session-keyed) |
57
+ | **D3** resultType | `resultType` present on list/read result builders | present / absent |
58
+ | **D4** CacheableResult | `ttlMs` / `cacheScope` on the list/read result types | present / absent |
59
+ | **D5** server/discover | a `server/discover` handler is registered (absence-based, with a positive control) | present / absent |
60
+ | **D6** mcp-apps ui-key | MCP Apps UI key is the GA nested `_meta.ui.resourceUri` — not flat `ui/resourceUri`, not community `io.modelcontextprotocol/ui` | GA-nested / flat-deprecated / community-nonGA / none |
61
+ | **D7** protocol-kit | `@vantageos/mcp-protocol-kit` listed in `package.json` **and actually called** (builder invoked, not merely declared) | listed+called / listed-not-called / hand-rolled |
62
+
63
+ ## Positioning
64
+
65
+ `mcp-doctor` is both an **internal fleet upgrade tool** — it drives MCP servers toward the current
66
+ standard by naming the exact `file:line` to fix — and a **vendable product** for any team shipping
67
+ MCP servers who needs a reproducible conformance gate. It is deterministic: the same tree yields the
68
+ same diff on any machine, so it belongs in CI, not in a review checklist.
69
+
70
+ ## License
71
+
72
+ FSL-1.1-Apache-2.0 (Functional Source License, Apache 2.0 future license). See [LICENSE](./LICENSE).
73
+
74
+ ---
75
+
76
+ Orchestrator: Gamma — VantageOS Team | 2026-08-03
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ type State = "present" | "absent" | "non-conforming";
3
+ interface DetectorResult {
4
+ id: string;
5
+ /** display anchor(s) file:line — the site the verdict is about. */
6
+ anchor: string;
7
+ state: State;
8
+ verdict: string;
9
+ }
10
+
11
+ declare function runDoctor(target: string): DetectorResult[];
12
+ declare function renderTable(surface: string, results: DetectorResult[]): string;
13
+
14
+ export { renderTable, runDoctor };
package/dist/doctor.js ADDED
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/scan.ts
4
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
5
+ import { join, dirname, relative } from "path";
6
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", "tests", "__tests__", "coverage"]);
7
+ function isCommentLine(raw) {
8
+ const t = raw.trim();
9
+ if (t === "") return false;
10
+ return t.startsWith("*") || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*/");
11
+ }
12
+ function walk(dir, out) {
13
+ let entries;
14
+ try {
15
+ entries = readdirSync(dir);
16
+ } catch {
17
+ return;
18
+ }
19
+ for (const name of entries) {
20
+ const full = join(dir, name);
21
+ let st;
22
+ try {
23
+ st = statSync(full);
24
+ } catch {
25
+ continue;
26
+ }
27
+ if (st.isDirectory()) {
28
+ if (SKIP_DIRS.has(name)) continue;
29
+ walk(full, out);
30
+ } else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
31
+ if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
32
+ out.push(full);
33
+ }
34
+ }
35
+ }
36
+ function loadFile(path, root) {
37
+ const raw = readFileSync(path, "utf8");
38
+ const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
39
+ return { path, rel: relative(root, path) || path, lines };
40
+ }
41
+ function findPackageJson(startDir) {
42
+ let dir = startDir;
43
+ for (let depth = 0; depth < 8; depth++) {
44
+ const candidate = join(dir, "package.json");
45
+ if (existsSync(candidate)) {
46
+ try {
47
+ return { path: candidate, json: JSON.parse(readFileSync(candidate, "utf8")) };
48
+ } catch {
49
+ return { path: candidate, json: {} };
50
+ }
51
+ }
52
+ const parent = dirname(dir);
53
+ if (parent === dir) break;
54
+ dir = parent;
55
+ }
56
+ return null;
57
+ }
58
+ function loadTree(root) {
59
+ const st = statSync(root);
60
+ const baseDir = st.isDirectory() ? root : dirname(root);
61
+ const filePaths = [];
62
+ if (st.isDirectory()) walk(root, filePaths);
63
+ else if (root.endsWith(".ts") || root.endsWith(".tsx")) filePaths.push(root);
64
+ const files = filePaths.map((p) => loadFile(p, baseDir));
65
+ return { root, files, packageJson: findPackageJson(baseDir) };
66
+ }
67
+
68
+ // src/detectors/d2.ts
69
+ var SESSION_RE = /mcp-session-id/i;
70
+ var d2 = {
71
+ id: "D2 stateless",
72
+ run(tree) {
73
+ const hits = [];
74
+ for (const file of tree.files) {
75
+ for (const line of file.lines) {
76
+ if (line.comment) continue;
77
+ if (SESSION_RE.test(line.raw)) hits.push({ file: file.rel, line: line.n });
78
+ }
79
+ }
80
+ if (hits.length > 0) {
81
+ const anchor = `${hits[0].file}:${hits.map((h) => h.line).join(",")}`;
82
+ return {
83
+ id: this.id,
84
+ anchor,
85
+ state: "non-conforming",
86
+ verdict: "session-keyed (mcp-session-id header) -> migrate to stateless"
87
+ };
88
+ }
89
+ return {
90
+ id: this.id,
91
+ anchor: "-",
92
+ state: "present",
93
+ verdict: "stateless (no mcp-session-id session-keying)"
94
+ };
95
+ }
96
+ };
97
+
98
+ // src/detectors/toolContext.ts
99
+ var LIST_READ_RE = /^(list|read|get|search)/;
100
+ var INLINE_NAME_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*["'`]([a-z][a-z0-9_]*)["'`]/;
101
+ var OPENER_RE = /\b(?:s\.tool|\.tool|registerTool)\s*\(\s*$/;
102
+ var BARE_NAME_RE = /^\s*["'`]([a-z][a-z0-9_]*)["'`]\s*,?/;
103
+ var DESCRIPTOR_NAME_RE = /\bname:\s*["'`]([a-z][a-z0-9_]*)["'`]/;
104
+ var CONTENT_RE = /content:\s*\[/;
105
+ function findListReadBuilders(tree) {
106
+ const out = [];
107
+ for (const file of tree.files) {
108
+ let currentTool = null;
109
+ let currentToolLine = 0;
110
+ let awaitingName = false;
111
+ for (const line of file.lines) {
112
+ if (line.comment) continue;
113
+ const raw = line.raw;
114
+ const inline = raw.match(INLINE_NAME_RE);
115
+ if (inline) {
116
+ currentTool = inline[1];
117
+ currentToolLine = line.n;
118
+ awaitingName = false;
119
+ } else if (OPENER_RE.test(raw)) {
120
+ awaitingName = true;
121
+ } else if (awaitingName) {
122
+ const bare = raw.match(BARE_NAME_RE);
123
+ if (bare) {
124
+ currentTool = bare[1];
125
+ currentToolLine = line.n;
126
+ }
127
+ awaitingName = false;
128
+ }
129
+ const desc = raw.match(DESCRIPTOR_NAME_RE);
130
+ if (desc) {
131
+ currentTool = desc[1];
132
+ currentToolLine = line.n;
133
+ }
134
+ if (CONTENT_RE.test(raw) && currentTool && LIST_READ_RE.test(currentTool)) {
135
+ out.push({
136
+ file,
137
+ toolName: currentTool,
138
+ toolNameLine: currentToolLine,
139
+ contentLine: line.n
140
+ });
141
+ }
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+ function countKeywordInCode(tree, re) {
147
+ let count = 0;
148
+ let firstFile;
149
+ let firstLine;
150
+ for (const file of tree.files) {
151
+ for (const line of file.lines) {
152
+ if (line.comment) continue;
153
+ if (re.test(line.raw)) {
154
+ count += 1;
155
+ if (firstFile === void 0) {
156
+ firstFile = file;
157
+ firstLine = line.n;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ return { count, firstFile, firstLine };
163
+ }
164
+
165
+ // src/detectors/d3.ts
166
+ var RESULT_TYPE_RE = /\bresultType\b/;
167
+ var KIT_RESULT_CALL_RE = /\b(buildToolsListResult|buildListResult|buildToolCallResult)\s*\(/;
168
+ var d3 = {
169
+ id: "D3 resultType",
170
+ run(tree) {
171
+ const builders = findListReadBuilders(tree);
172
+ const rt = countKeywordInCode(tree, RESULT_TYPE_RE);
173
+ const kit = countKeywordInCode(tree, KIT_RESULT_CALL_RE);
174
+ if (rt.count > 0 || kit.count > 0) {
175
+ const src = rt.count > 0 ? rt : kit;
176
+ return {
177
+ id: this.id,
178
+ anchor: src.firstFile ? `${src.firstFile.rel}:${src.firstLine}` : "-",
179
+ state: "present",
180
+ verdict: rt.count > 0 ? "resultType present" : "resultType via kit result-builder call"
181
+ };
182
+ }
183
+ if (builders.length === 0) {
184
+ return { id: this.id, anchor: "-", state: "absent", verdict: "no list/read builders found; resultType absent" };
185
+ }
186
+ const b = builders[0];
187
+ return {
188
+ id: this.id,
189
+ anchor: `${b.file.rel}:${b.contentLine}`,
190
+ state: "absent",
191
+ verdict: `list/read builder returns {content} without resultType (grep resultType=0) -> add via protocol-kit`
192
+ };
193
+ }
194
+ };
195
+
196
+ // src/detectors/d4.ts
197
+ var CACHEABLE_RE = /\b(ttlMs|cacheScope)\b/;
198
+ var d4 = {
199
+ id: "D4 CacheableResult",
200
+ run(tree) {
201
+ const builders = findListReadBuilders(tree);
202
+ const cache = countKeywordInCode(tree, CACHEABLE_RE);
203
+ if (cache.count > 0) {
204
+ return {
205
+ id: this.id,
206
+ anchor: cache.firstFile ? `${cache.firstFile.rel}:${cache.firstLine}` : "-",
207
+ state: "present",
208
+ verdict: "CacheableResult present (ttlMs/cacheScope)"
209
+ };
210
+ }
211
+ if (builders.length === 0) {
212
+ return { id: this.id, anchor: "-", state: "absent", verdict: "no list/read builders found; CacheableResult absent" };
213
+ }
214
+ const anchorLines = builders.slice(0, 2).map((b) => b.toolNameLine);
215
+ return {
216
+ id: this.id,
217
+ anchor: `${builders[0].file.rel}:${anchorLines.join(",")}`,
218
+ state: "absent",
219
+ verdict: "list/read builders lack ttlMs/cacheScope (grep=0) -> add CacheableResult"
220
+ };
221
+ }
222
+ };
223
+
224
+ // src/detectors/d5.ts
225
+ var DISCOVER_RE = /server\/discover/;
226
+ var POSITIVE_CONTROL = 'method: zv4.literal("server/discover"),';
227
+ function d5PositiveControl() {
228
+ return { pattern: DISCOVER_RE.source, sample: POSITIVE_CONTROL, matched: DISCOVER_RE.test(POSITIVE_CONTROL) };
229
+ }
230
+ var d5 = {
231
+ id: "D5 server/discover",
232
+ run(tree) {
233
+ const control = d5PositiveControl();
234
+ if (!control.matched) {
235
+ throw new Error(
236
+ `D5 positive control FAILED: pattern /${control.pattern}/ did not match sample \`${control.sample}\` \u2014 matcher broken, absence verdict untrustworthy`
237
+ );
238
+ }
239
+ for (const file of tree.files) {
240
+ for (const line of file.lines) {
241
+ if (DISCOVER_RE.test(line.raw)) {
242
+ return {
243
+ id: this.id,
244
+ anchor: `${file.rel}:${line.n}`,
245
+ state: "present",
246
+ verdict: "server/discover handler present"
247
+ };
248
+ }
249
+ }
250
+ }
251
+ return {
252
+ id: this.id,
253
+ anchor: "-",
254
+ state: "absent",
255
+ verdict: "no server/discover handler (grep=0, positive-control OK) -> add via protocol-kit"
256
+ };
257
+ }
258
+ };
259
+
260
+ // src/detectors/d6.ts
261
+ var COMMUNITY_RE = /["'`]io\.modelcontextprotocol\/ui["'`]\s*:|_meta\s*\[\s*["'`]io\.modelcontextprotocol\/ui["'`]\s*\]/;
262
+ var FLAT_RE = /["'`]ui\/resourceUri["'`]\s*:|_meta\s*\[\s*["'`]ui\/resourceUri["'`]\s*\]/;
263
+ var NESTED_UI_RE = /_meta\s*:\s*\{\s*ui\b/;
264
+ var RESOURCE_URI_RE = /\bresourceUri\b/;
265
+ var d6 = {
266
+ id: "D6 mcp-apps ui-key",
267
+ run(tree) {
268
+ let community = null;
269
+ let flat = null;
270
+ let nested = null;
271
+ let sawResourceUri = false;
272
+ for (const file of tree.files) {
273
+ for (const line of file.lines) {
274
+ if (line.comment) continue;
275
+ if (!community && COMMUNITY_RE.test(line.raw)) community = { file: file.rel, line: line.n };
276
+ if (!flat && FLAT_RE.test(line.raw)) flat = { file: file.rel, line: line.n };
277
+ if (!nested && NESTED_UI_RE.test(line.raw)) nested = { file: file.rel, line: line.n };
278
+ if (RESOURCE_URI_RE.test(line.raw)) sawResourceUri = true;
279
+ }
280
+ }
281
+ if (community) {
282
+ return {
283
+ id: this.id,
284
+ anchor: `${community.file}:${community.line}`,
285
+ state: "non-conforming",
286
+ verdict: "community io.modelcontextprotocol/ui key (non-GA) -> _meta.ui.resourceUri"
287
+ };
288
+ }
289
+ if (flat) {
290
+ return {
291
+ id: this.id,
292
+ anchor: `${flat.file}:${flat.line}`,
293
+ state: "non-conforming",
294
+ verdict: "flat ui/resourceUri key (deprecated) -> _meta.ui.resourceUri"
295
+ };
296
+ }
297
+ if (nested && sawResourceUri) {
298
+ return {
299
+ id: this.id,
300
+ anchor: `${nested.file}:${nested.line}`,
301
+ state: "present",
302
+ verdict: "GA nested _meta.ui.resourceUri"
303
+ };
304
+ }
305
+ return { id: this.id, anchor: "-", state: "absent", verdict: "no MCP Apps UI key (no _meta ui)" };
306
+ }
307
+ };
308
+
309
+ // src/detectors/d7.ts
310
+ var KIT_PKG = "@vantageos/mcp-protocol-kit";
311
+ var KIT_CALL_RE = /\b(buildToolsListResult|buildListResult|buildToolCallResult|handleServerDiscover)\s*\(/;
312
+ var KIT_IMPORT_RE = /from\s+["'`]@vantageos\/mcp-protocol-kit["'`]/;
313
+ function depListed(pkgJson) {
314
+ if (!pkgJson) return false;
315
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
316
+ const deps = pkgJson[key];
317
+ if (deps && typeof deps === "object" && KIT_PKG in deps) return true;
318
+ }
319
+ return false;
320
+ }
321
+ var d7 = {
322
+ id: "D7 protocol-kit",
323
+ run(tree) {
324
+ const listed = depListed(tree.packageJson?.json);
325
+ let importLine = null;
326
+ let callLine = null;
327
+ for (const file of tree.files) {
328
+ for (const line of file.lines) {
329
+ if (line.comment) continue;
330
+ if (!importLine && KIT_IMPORT_RE.test(line.raw)) importLine = { file: file.rel, line: line.n };
331
+ if (!callLine && KIT_CALL_RE.test(line.raw)) callLine = { file: file.rel, line: line.n };
332
+ }
333
+ }
334
+ const called = importLine !== null && callLine !== null;
335
+ if (listed && called) {
336
+ return {
337
+ id: this.id,
338
+ anchor: `${callLine.file}:${callLine.line}`,
339
+ state: "present",
340
+ verdict: "protocol-kit listed + builder called"
341
+ };
342
+ }
343
+ if (listed && !called) {
344
+ return {
345
+ id: this.id,
346
+ anchor: tree.packageJson ? tree.packageJson.path.split("/").slice(-2).join("/") : "-",
347
+ state: "non-conforming",
348
+ verdict: "protocol-kit listed but no builder called -> wire buildToolsListResult/handleServerDiscover"
349
+ };
350
+ }
351
+ return {
352
+ id: this.id,
353
+ anchor: tree.packageJson ? tree.packageJson.path.split("/").slice(-2).join("/") : "-",
354
+ state: "absent",
355
+ verdict: "hand-rolled builders, protocol-kit not a dependency -> add @vantageos/mcp-protocol-kit"
356
+ };
357
+ }
358
+ };
359
+
360
+ // src/detectors/index.ts
361
+ var detectors = [d2, d3, d4, d5, d6, d7];
362
+
363
+ // src/doctor.ts
364
+ function pad(s, w) {
365
+ return s.length >= w ? s : s + " ".repeat(w - s.length);
366
+ }
367
+ function runDoctor(target) {
368
+ const tree = loadTree(target);
369
+ return detectors.map((d) => d.run(tree));
370
+ }
371
+ function renderTable(surface, results) {
372
+ const cols = { item: 24, anchor: 26, state: 15 };
373
+ const header = `${pad("item", cols.item)} | ${pad("file:line", cols.anchor)} | ${pad("state", cols.state)} | verdict`;
374
+ const sep = "-".repeat(header.length);
375
+ const rows = results.map(
376
+ (r) => `${pad(r.id, cols.item)} | ${pad(r.anchor, cols.anchor)} | ${pad(r.state, cols.state)} | ${r.verdict}`
377
+ );
378
+ return [`surface: ${surface}`, header, sep, ...rows].join("\n");
379
+ }
380
+ function main() {
381
+ const target = process.argv[2];
382
+ if (!target) {
383
+ console.error("usage: tsx src/doctor.ts <source-tree-path>");
384
+ process.exit(1);
385
+ }
386
+ const results = runDoctor(target);
387
+ console.log(renderTable(target, results));
388
+ }
389
+ if (import.meta.url === `file://${process.argv[1]}`) main();
390
+ export {
391
+ renderTable,
392
+ runDoctor
393
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@vantageos/mcp-doctor",
3
+ "version": "0.1.0",
4
+ "description": "Static MCP conformance checker: audits an MCP server source tree against the MCP 2026-07-28 standard and emits a conformance-diff table (file:line | present | absent | non-conforming | verdict).",
5
+ "type": "module",
6
+ "main": "./dist/doctor.js",
7
+ "types": "./dist/doctor.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/doctor.d.ts",
11
+ "import": "./dist/doctor.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "bin": {
16
+ "mcp-doctor": "./dist/doctor.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/elpiarthera/mcp-doctor.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/elpiarthera/mcp-doctor/issues"
32
+ },
33
+ "homepage": "https://github.com/elpiarthera/mcp-doctor#readme",
34
+ "author": "ElPi Corp / Laurent Perello",
35
+ "keywords": [
36
+ "mcp",
37
+ "model-context-protocol",
38
+ "conformance",
39
+ "audit",
40
+ "linter",
41
+ "static-analysis"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "doctor": "tsx src/doctor.ts",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "typecheck": "tsc --noEmit",
49
+ "lint": "biome check src tests",
50
+ "lint:fix": "biome check --write src tests",
51
+ "prepack": "npm run build",
52
+ "prepublishOnly": "npm run build && npm test"
53
+ },
54
+ "license": "FSL-1.1-Apache-2.0",
55
+ "engines": {
56
+ "node": ">=20"
57
+ },
58
+ "devDependencies": {
59
+ "@biomejs/biome": "1.9.4",
60
+ "@types/node": "^26.1.2",
61
+ "tsup": "^8.5.1",
62
+ "tsx": "^4.19.2",
63
+ "typescript": "^5.7.2",
64
+ "vitest": "^2.1.8"
65
+ }
66
+ }