mcp-triage 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 +21 -0
- package/README.md +111 -0
- package/dist/checks.d.ts +12 -0
- package/dist/checks.js +211 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +83 -0
- package/dist/clients.d.ts +13 -0
- package/dist/clients.js +116 -0
- package/dist/discover.d.ts +5 -0
- package/dist/discover.js +70 -0
- package/dist/fix.d.ts +24 -0
- package/dist/fix.js +131 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/parse.d.ts +31 -0
- package/dist/parse.js +643 -0
- package/dist/report.d.ts +9 -0
- package/dist/report.js +99 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +2 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +61 -0
package/dist/fix.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DiscoveredFile, FixOutcome, ParsedConfig } from './types.ts';
|
|
2
|
+
export interface FixOptions {
|
|
3
|
+
/** Compute what a fix would do, write nothing. */
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Strip `// line` comments and `/* block *\/` comments, ignoring string context
|
|
8
|
+
* (both quote styles are tracked, escapes honored). Line endings are preserved
|
|
9
|
+
* (a `// ...` on a CRLF line leaves the CRLF in place).
|
|
10
|
+
*/
|
|
11
|
+
export declare function stripJsonComments(text: string): {
|
|
12
|
+
out: string;
|
|
13
|
+
removed: number;
|
|
14
|
+
};
|
|
15
|
+
export interface RepairResult {
|
|
16
|
+
/** The repaired candidate text (equal to the input when no repair applies). */
|
|
17
|
+
out: string;
|
|
18
|
+
/** Human-readable descriptors, e.g. ['stripped 1 comment', 'removed 2 trailing commas']. */
|
|
19
|
+
changes: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Apply the mechanical repair passes to a copy of the text. Never touches the disk. */
|
|
22
|
+
export declare function repairJsonText(text: string): RepairResult;
|
|
23
|
+
/** Files that currently fail to parse get one repair attempt; everything else is untouched. */
|
|
24
|
+
export declare function applyFixes(files: DiscoveredFile[], parsed: ParsedConfig[], opts?: FixOptions): FixOutcome[];
|
package/dist/fix.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Fix engine: mechanical, verifiable repairs for JSON config files that fail to parse.
|
|
2
|
+
//
|
|
3
|
+
// v0.1 scope is deliberately small — pure-deletion repairs only, applied on a copy:
|
|
4
|
+
// 1. strip // and /* */ comments (string-aware)
|
|
5
|
+
// 2. drop trailing commas before } or ] (string-aware)
|
|
6
|
+
// The repaired copy must pass JSON.parse or nothing is written. Before the first write a
|
|
7
|
+
// `.mcp-triage.bak` backup of the original file is created (an existing backup is kept, not
|
|
8
|
+
// overwritten, so the pristine pre-fix version survives repeated runs).
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import { readFileSafe, stripTrailingCommas } from "./parse.js";
|
|
11
|
+
/**
|
|
12
|
+
* Strip `// line` comments and `/* block *\/` comments, ignoring string context
|
|
13
|
+
* (both quote styles are tracked, escapes honored). Line endings are preserved
|
|
14
|
+
* (a `// ...` on a CRLF line leaves the CRLF in place).
|
|
15
|
+
*/
|
|
16
|
+
export function stripJsonComments(text) {
|
|
17
|
+
let out = '';
|
|
18
|
+
let inString = false;
|
|
19
|
+
let quote = '"';
|
|
20
|
+
let removed = 0;
|
|
21
|
+
for (let i = 0; i < text.length; i++) {
|
|
22
|
+
const c = text[i];
|
|
23
|
+
if (inString) {
|
|
24
|
+
out += c;
|
|
25
|
+
if (c === '\\' && i + 1 < text.length) {
|
|
26
|
+
out += text[i + 1];
|
|
27
|
+
i++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (c === quote)
|
|
31
|
+
inString = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (c === '"' || c === "'") {
|
|
35
|
+
inString = true;
|
|
36
|
+
quote = c;
|
|
37
|
+
out += c;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
41
|
+
removed++;
|
|
42
|
+
while (i < text.length && text[i] !== '\n')
|
|
43
|
+
i++;
|
|
44
|
+
if (i >= text.length)
|
|
45
|
+
break; // comment runs to EOF — drop the tail
|
|
46
|
+
out += text[i - 1] === '\r' ? '\r\n' : '\n';
|
|
47
|
+
continue; // the for-loop advance moves past the '\n'
|
|
48
|
+
}
|
|
49
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
50
|
+
removed++;
|
|
51
|
+
i += 2;
|
|
52
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
53
|
+
i++;
|
|
54
|
+
i++; // position on '/': the for-loop advance moves past it
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
out += c;
|
|
58
|
+
}
|
|
59
|
+
return { out, removed };
|
|
60
|
+
}
|
|
61
|
+
/** Apply the mechanical repair passes to a copy of the text. Never touches the disk. */
|
|
62
|
+
export function repairJsonText(text) {
|
|
63
|
+
const changes = [];
|
|
64
|
+
const noComments = stripJsonComments(text);
|
|
65
|
+
if (noComments.removed > 0) {
|
|
66
|
+
changes.push(`stripped ${noComments.removed} comment${noComments.removed === 1 ? '' : 's'}`);
|
|
67
|
+
}
|
|
68
|
+
const noTrailing = stripTrailingCommas(noComments.out);
|
|
69
|
+
const dropped = noComments.out.length - noTrailing.length; // the pass only deletes ',' chars
|
|
70
|
+
if (dropped > 0) {
|
|
71
|
+
changes.push(`removed ${dropped} trailing comma${dropped === 1 ? '' : 's'}`);
|
|
72
|
+
}
|
|
73
|
+
return { out: noTrailing, changes };
|
|
74
|
+
}
|
|
75
|
+
/** Files that currently fail to parse get one repair attempt; everything else is untouched. */
|
|
76
|
+
export function applyFixes(files, parsed, opts = {}) {
|
|
77
|
+
const outcomes = [];
|
|
78
|
+
for (let i = 0; i < files.length; i++) {
|
|
79
|
+
const p = parsed[i];
|
|
80
|
+
if (!p || p.ok)
|
|
81
|
+
continue; // fix candidates are exactly the files that fail to parse
|
|
82
|
+
outcomes.push(fixOne(files[i], opts));
|
|
83
|
+
}
|
|
84
|
+
return outcomes;
|
|
85
|
+
}
|
|
86
|
+
function fixOne(f, opts) {
|
|
87
|
+
const base = { file: f.file, clientId: f.clientId, changes: [] };
|
|
88
|
+
const text = readFileSafe(f.file);
|
|
89
|
+
if (text === null) {
|
|
90
|
+
return { ...base, status: 'skipped', reason: 'file could not be read' };
|
|
91
|
+
}
|
|
92
|
+
const { out, changes } = repairJsonText(text);
|
|
93
|
+
if (changes.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
status: 'not-fixable',
|
|
97
|
+
reason: 'no mechanical repair applies (not a comments / trailing-commas problem)',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
let parseOk = false;
|
|
101
|
+
try {
|
|
102
|
+
JSON.parse(out.replace(/^\uFEFF/, '')); // same BOM handling as the parser
|
|
103
|
+
parseOk = true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// leave parseOk false
|
|
107
|
+
}
|
|
108
|
+
if (!parseOk) {
|
|
109
|
+
return {
|
|
110
|
+
...base,
|
|
111
|
+
status: 'not-fixable',
|
|
112
|
+
changes,
|
|
113
|
+
reason: 'the repaired copy still does not parse as JSON — the file has other syntax problems; nothing was written',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (opts.dryRun) {
|
|
117
|
+
return { ...base, status: 'would-fix', changes };
|
|
118
|
+
}
|
|
119
|
+
const backupPath = f.file + '.mcp-triage.bak';
|
|
120
|
+
const backupExisted = fs.existsSync(backupPath);
|
|
121
|
+
try {
|
|
122
|
+
if (!backupExisted)
|
|
123
|
+
fs.writeFileSync(backupPath, text, 'utf8');
|
|
124
|
+
fs.writeFileSync(f.file, out, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
128
|
+
return { ...base, status: 'skipped', changes, reason: `write failed: ${msg}` };
|
|
129
|
+
}
|
|
130
|
+
return { ...base, status: 'fixed', changes, backupPath, backupKept: backupExisted || undefined };
|
|
131
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { VERSION } from './version.ts';
|
|
2
|
+
export { CLIENTS, clientPathsForPlatform, defaultPathContext, expandPlaceholders } from './clients.ts';
|
|
3
|
+
export type { PathContext } from './clients.ts';
|
|
4
|
+
export { discoverFiles, expandGlob } from './discover.ts';
|
|
5
|
+
export { parseConfigFile, parseJsonConfig, parseTomlConfig, parseYamlLight, normalizeJson5, extractServersFromJson, findTrailingComma, stripTrailingCommas, readFileSafe, } from './parse.ts';
|
|
6
|
+
export { runChecks, checkCrossClientDrift, resolveCommandOnPath, findEnvRefs, DEFAULT_CHECK_CONTEXT } from './checks.ts';
|
|
7
|
+
export type { CheckContext } from './checks.ts';
|
|
8
|
+
export { applyFixes, repairJsonText, stripJsonComments } from './fix.ts';
|
|
9
|
+
export type { FixOptions, RepairResult } from './fix.ts';
|
|
10
|
+
export { renderHuman, renderJson } from './report.ts';
|
|
11
|
+
export type { ReportInput } from './report.ts';
|
|
12
|
+
export type { ClientSpec, Diagnostic, DiscoveredFile, FixOutcome, FixStatus, Format, ParsedConfig, ScanResult, ServerEntry, Severity, } from './types.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Library entry — programmatic use:
|
|
2
|
+
// import { discoverFiles, parseConfigFile, runChecks, applyFixes } from 'mcp-triage';
|
|
3
|
+
// The CLI (dist/cli.js, `bin`) is the primary interface; this entry exposes the same
|
|
4
|
+
// pipeline pieces for embedding. Zero runtime dependencies.
|
|
5
|
+
export { VERSION } from "./version.js";
|
|
6
|
+
export { CLIENTS, clientPathsForPlatform, defaultPathContext, expandPlaceholders } from "./clients.js";
|
|
7
|
+
export { discoverFiles, expandGlob } from "./discover.js";
|
|
8
|
+
export { parseConfigFile, parseJsonConfig, parseTomlConfig, parseYamlLight, normalizeJson5, extractServersFromJson, findTrailingComma, stripTrailingCommas, readFileSafe, } from "./parse.js";
|
|
9
|
+
export { runChecks, checkCrossClientDrift, resolveCommandOnPath, findEnvRefs, DEFAULT_CHECK_CONTEXT } from "./checks.js";
|
|
10
|
+
export { applyFixes, repairJsonText, stripJsonComments } from "./fix.js";
|
|
11
|
+
export { renderHuman, renderJson } from "./report.js";
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Diagnostic, DiscoveredFile, ParsedConfig, ServerEntry } from './types.ts';
|
|
2
|
+
export declare function readFileSafe(file: string): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* JSON5-light normalization: strips // and block comments and trailing commas outside strings,
|
|
5
|
+
* converts single-quoted strings to double-quoted, and quotes unquoted identifier keys.
|
|
6
|
+
* Covers what OpenClaw documents as legal JSON5 (comments + trailing commas; the parser also
|
|
7
|
+
* accepts unquoted keys). Deliberately light: exotic JSON5 beyond this (hex numbers, multiline
|
|
8
|
+
* strings, +/- Infinity) is out of scope and will still fail JSON.parse.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeJson5(text: string): string;
|
|
11
|
+
/** Pass 2: drop trailing commas before } or ] (outside strings). Also used by the fix engine. */
|
|
12
|
+
export declare function stripTrailingCommas(text: string): string;
|
|
13
|
+
export declare function findTrailingComma(text: string): {
|
|
14
|
+
line: number;
|
|
15
|
+
snippet: string;
|
|
16
|
+
} | null;
|
|
17
|
+
/** Recognized server container shapes: mcpServers | servers | mcp.servers (first match wins). */
|
|
18
|
+
export declare function extractServersFromJson(data: unknown): ServerEntry[];
|
|
19
|
+
export interface ParseOutcome {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
servers: ServerEntry[];
|
|
22
|
+
diagnostics: Diagnostic[];
|
|
23
|
+
caveat?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseJsonConfig(text: string, file: string, clientId: string, opts?: {
|
|
26
|
+
json5?: boolean;
|
|
27
|
+
json5Fallback?: boolean;
|
|
28
|
+
}): ParseOutcome;
|
|
29
|
+
export declare function parseTomlConfig(text: string, file: string, clientId: string): ParseOutcome;
|
|
30
|
+
export declare function parseYamlLight(text: string, file: string, clientId: string): ParseOutcome;
|
|
31
|
+
export declare function parseConfigFile(f: DiscoveredFile): ParsedConfig;
|