sentinel-ir-mcp 0.2.0 → 0.2.1
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/README.md +1 -1
- package/package.json +1 -1
- package/src/analyze.js +16 -2
- package/src/server.js +5 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ of safety. Python and other languages are not supported yet.
|
|
|
29
29
|
|---|---|---|
|
|
30
30
|
| `analyze_code` | `code` (string), optional `target` (file name) | compact IR of the given source |
|
|
31
31
|
| `analyze_file` | `path` | compact IR of one file inside the allowed folders |
|
|
32
|
-
| `analyze_directory` | optional `path`, `max_files` (default 50, max 500), `recursive` (default `true`) | one IR summary per file, sorted from highest to lowest risk, plus `riskCounts`, `truncated` and `skipped` |
|
|
32
|
+
| `analyze_directory` | optional `path`, `max_files` (default 50, max 500), `recursive` (default `true`) | one IR summary per file, sorted from highest to lowest risk, plus `riskCounts`, `statusCounts` and `incomplete` (paths whose analysis did not finish; check them separately, their low risk is not evidence of safety); files that could not be read are listed in `skipped.unreadable` and are not counted as analyzed, `truncated` and `skipped` |
|
|
33
33
|
|
|
34
34
|
Every tool returns the result twice: as JSON text (for clients that only read
|
|
35
35
|
text) and as `structuredContent`. All three are annotated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinel-ir-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server for sentinel-ir: lets AI agents (Claude Desktop, Cursor, Windsurf, ...) read a compact structural IR of JavaScript and TypeScript files instead of the raw source. Read-only, sandboxed to allowed folders.",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
package/src/analyze.js
CHANGED
|
@@ -34,7 +34,7 @@ async function analyzeFile(requested, roots, { maxFileBytes }) {
|
|
|
34
34
|
// and hidden folders are skipped.
|
|
35
35
|
async function collectFiles(dir, root, { maxFiles, maxFileBytes, recursive }) {
|
|
36
36
|
const files = [];
|
|
37
|
-
const skipped = { tooLarge: [], symlinks: 0 };
|
|
37
|
+
const skipped = { tooLarge: [], symlinks: 0, unreadable: [] };
|
|
38
38
|
let truncated = false;
|
|
39
39
|
const queue = [dir];
|
|
40
40
|
while (queue.length && !truncated) {
|
|
@@ -72,7 +72,14 @@ async function analyzeDirectory(requested, roots, { maxFiles, maxFileBytes, recu
|
|
|
72
72
|
const { files, skipped, truncated } = await collectFiles(real, root, { maxFiles, maxFileBytes, recursive });
|
|
73
73
|
const results = [];
|
|
74
74
|
for (const { full, rel } of files) {
|
|
75
|
-
|
|
75
|
+
// One file that cannot be read or analyzed must not hide the others.
|
|
76
|
+
let ir;
|
|
77
|
+
try {
|
|
78
|
+
ir = await analyzeCode(await fs.readFile(full, "utf8"), rel);
|
|
79
|
+
} catch {
|
|
80
|
+
skipped.unreadable.push(rel);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
76
83
|
results.push({
|
|
77
84
|
path: rel,
|
|
78
85
|
analysisStatus: ir.analysisStatus,
|
|
@@ -90,6 +97,11 @@ async function analyzeDirectory(requested, roots, { maxFiles, maxFileBytes, recu
|
|
|
90
97
|
const riskCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
91
98
|
for (const r of results) if (r.risk in riskCounts) riskCounts[r.risk]++;
|
|
92
99
|
|
|
100
|
+
// Files whose analysis did not finish are listed on their own: their risk
|
|
101
|
+
// is not reliable, so sorting alone would bury them under riskier files.
|
|
102
|
+
const incomplete = results.filter((r) => r.analysisStatus !== "complete").map((r) => r.path).sort();
|
|
103
|
+
const statusCounts = { complete: results.length - incomplete.length, incomplete: incomplete.length };
|
|
104
|
+
|
|
93
105
|
return {
|
|
94
106
|
folder: display,
|
|
95
107
|
analyzed: results.length,
|
|
@@ -97,6 +109,8 @@ async function analyzeDirectory(requested, roots, { maxFiles, maxFileBytes, recu
|
|
|
97
109
|
maxFiles,
|
|
98
110
|
skipped,
|
|
99
111
|
riskCounts,
|
|
112
|
+
statusCounts,
|
|
113
|
+
incomplete,
|
|
100
114
|
files: results
|
|
101
115
|
};
|
|
102
116
|
}
|
package/src/server.js
CHANGED
|
@@ -29,6 +29,7 @@ const INSTRUCTIONS =
|
|
|
29
29
|
"Prefer analyze_file or analyze_directory over reading raw source when you only need structure " +
|
|
30
30
|
"or risk, then read the raw source of the files that matter. Supported: JavaScript (.js, .mjs, .cjs) and TypeScript (.ts, .mts, .cts, .tsx); the language is picked from the file extension. " +
|
|
31
31
|
"Rely on analysisStatus, world.risk, forbidden and facts; analysisStatus \"incomplete\" means the file did not parse cleanly. " +
|
|
32
|
+
"analyze_directory lists such files separately in 'incomplete'; their low risk is not evidence of safety. " +
|
|
32
33
|
"Read-only: nothing is written, executed or sent over the network. " + UNTRUSTED_NOTE;
|
|
33
34
|
|
|
34
35
|
function ok(value) {
|
|
@@ -106,7 +107,10 @@ function createServer(options = {}) {
|
|
|
106
107
|
"Scan a folder inside the allowed folders and return one compact IR summary per JavaScript or TypeScript file, " +
|
|
107
108
|
"sorted from highest to lowest risk, plus risk counts. Skips node_modules, .git, hidden folders " +
|
|
108
109
|
`and symlinks. Analyzes at most max_files files (default ${limits.maxFiles}); 'truncated' tells ` +
|
|
109
|
-
"you when there were more.
|
|
110
|
+
"you when there were more. 'incomplete' lists files whose analysis did not finish (e.g. parse errors); " +
|
|
111
|
+
"their risk is not reliable, check them separately. statusCounts counts complete vs incomplete. " +
|
|
112
|
+
"Files that could not be read are listed in skipped.unreadable and are not counted as analyzed. " +
|
|
113
|
+
"Use analyze_file for the full IR of a single file. " + UNTRUSTED_NOTE,
|
|
110
114
|
inputSchema: {
|
|
111
115
|
path: z.string().min(1).optional().describe("Folder path, absolute or relative to the first allowed folder (default: the first allowed folder)"),
|
|
112
116
|
max_files: z.number().int().min(1).max(DEFAULT_LIMITS.hardMaxFiles).optional()
|