sentinel-ir-mcp
by jakcmencz
README.md
# Sentinel-IR-MCP
MCP server for [`sentinel-ir`](https://www.npmjs.com/package/sentinel-ir):
it lets AI agents in Claude Desktop, Cursor, Windsurf or any other
[Model Context Protocol](https://modelcontextprotocol.io) client read a
compact **structural IR** of JavaScript, TypeScript and Python files instead of the raw source.
- npm: <https://www.npmjs.com/package/sentinel-ir-mcp>
- GitLab: <https://gitlab.com/jakubrojicek11/sentinel-ir-mcp>
- Library behind it: <https://gitlab.com/jakubrojicek11/sentinel-ir>
The IR tells the agent what a file *does* (risk level, dangerous
capabilities such as `eval` or `child_process`, routes, exports, imports,
environment reads, network / filesystem / process / crypto calls) in a few
hundred characters. The agent can triage a folder first and then read the raw
source only of the files that matter.
**Supported:** JavaScript (`.js`, `.mjs`, `.cjs`), TypeScript (`.ts`, `.mts`,
`.cts`, `.tsx`; since 0.2.0) and Python (`.py`, `.pyi`; since 0.3.0). The
language is picked from the file extension; `analyze_code` also takes `target`
or an explicit `language` (`javascript` | `typescript` | `tsx` | `python`). Every result carries `language` and
`analysisStatus`; a file that does not parse cleanly (including JSX in a `.ts`
file) is `"incomplete"`, and a low risk on an incomplete result is not evidence
of safety. Other languages are not supported yet. What is detected in Python
(and what is not) is listed in the
[sentinel-ir README](https://gitlab.com/jakubrojicek11/sentinel-ir#python).
**0.4.5** adds to that log the SHA-256 of every file the server read, so you
can check later which version of a file the agent was shown (see
[Audit log](#audit-log)).
**0.4.4** can keep a log of what the agent asked for (off unless you turn it on):
- **One question in `setup`:** *"Keep a log of what the agent asks for (file
names and results, never your code)? [y/N]"*. Or `setup --audit-log`;
`--no-audit-log` turns it off. Running `setup` again keeps the setting.
- **One command to read it:** `npx -y sentinel-ir-mcp@0.4 log` prints the last
50 entries as plain lines, `--all` everything, `--json` for scripts:
```text
2026-10-18 10:32:05 [cursor] file src/app.js: critical
2026-10-18 10:32:09 [cursor] REFUSED file secrets/aws.js: excluded by your settings
2026-10-18 10:33:40 [cursor] folder .: 42 file(s) (1 critical, 6 medium, 35 low), 3 excluded
critical src/app.js
```
- `doctor` says whether the log is on, where it is and how big, and proves the
log can be written. Details: [Audit log](#audit-log).
Also uses `sentinel-ir` 0.4.5 (Python parse errors point where Python does).
**0.4.3** lets you exclude files and folders:
- **One question in `setup`:** *"Files or folders the agent must not read"*,
e.g. `secrets, dist, *.min.js` (Enter = none). Or `setup --exclude secrets
--exclude dist`. `setup` writes them into the client config, so there is no
JSON to edit; running it again keeps them, `--no-exclude` removes them.
- **Set by you only.** The agent cannot change them, and the analyzed project
cannot either: there is no `.sentinelignore`, because a repository could use
it to hide its own files.
- `analyze_file` refuses an excluded path whether or not it exists;
`analyze_directory` only counts excluded entries in `skipped.excluded`,
because their names can be private too. `doctor` lists the exclusions and
how many files and folders they skip in the project.
**0.4.2** makes `analyze_directory` short by default:
- **Short entries:** each file is `{path, risk, analysisStatus, language,
lines, operations, riskSignals}`, plus `moreRiskSignals` and `parseError`
when they apply. `riskSignals` are up to 3 `kind:code@line` signals that set
the file's risk level (code cut to 120 characters). `analyze_file` still
returns the full IR; `detail: "full"` returns the 0.4.1 entries.
| folder (all files) | 0.4.1 | 0.4.2 default |
|---|---:|---:|
| a 190-file Node project | 96,550 chars (~24,100 tokens) | 35,856 chars (~9,000 tokens) |
| MCP SDK `dist/cjs` (174 files) | 79,945 chars (~20,000 tokens) | 29,046 chars (~7,300 tokens) |
Tokens estimated as chars / 4. The tool list the client loads once grows
from 4,103 to 5,322 chars.
- **Risk explained to the agent:** tool descriptions say that risk is about
capabilities, not vulnerabilities; that `medium` (file writes, network,
crypto) is normal in backend code; and that values are not tracked, so
`readFileSync(req.query.f)` is `low`.
**0.4.1** uses `sentinel-ir` 0.4.3:
- **Faster on large JavaScript:** a valid 1 MiB file takes about 5 s instead
of about 170 s, so `analyze_directory` no longer runs past the 60 s request
timeout of MCP SDK clients on one big file.
- **Read-time check:** files are opened first and then checked, so a file
swapped for a symlink between listing and reading is refused instead of read
from outside the allowed folders. A file removed during a scan no longer
fails the whole scan; it is listed in `skipped.unreadable`. Files with more
than one hard link are refused (`skipped.hardlinks`).
- **Finite limits:** `--max-file-bytes` is capped at 2 MiB. Valid JavaScript
with `sentinel-ir` 0.4.3:
| size | Linux | Windows CI (Node 20/22/24) |
|---|---|---|
| 1 MiB | 4–5 s, 0.9 GB | 13–23 s, 0.8 GB |
| 2 MiB | 10–14 s, 1.6 GB | 29–40 s, 1.5 GB |
| 4 MiB | 25–29 s, 3 GB | process aborts |
A file above 2 MiB can take longer than the 60 s request timeout on its own.
- **Time budget:** `analyze_directory` starts no new file after 30 s
(`--time-budget-ms`). Then `timedOut` is `true` and the remaining files are
listed in `notAnalyzed`. A file already being analyzed is not interrupted.
- **Parser errors:** incomplete files carry `facts.parseError` with the line,
column and kind of the first parser error (from `sentinel-ir` 0.4.3).
**0.4.0** uses `sentinel-ir` 0.4.2:
- **Native code:** `ctypes` / `cffi`, `process.dlopen`, `*.node` addons and
FFI packages (`koffi`, `ffi-napi`, `bun:ffi`, ...) are `critical`, with
`native_code` in `forbidden` and `nativeCode` in `facts.operations`.
- **Long sessions:** older versions never freed the WebAssembly parser
memory. After roughly 87 MB of analyzed source in one server process every
later result became `"incomplete"` (text fallback) without an error. With
`sentinel-ir-mcp` 0.3.0 + `sentinel-ir` 0.3.1 over stdio, `analyze_code` on
an 18 KB file went `incomplete` from call 4,918 on; 0.4.0 stays `complete`
(measurement below).
## Tools
| tool | input | returns |
|---|---|---|
| `analyze_code` | `code` (string), optional `target` (file name) | compact IR of the given source |
| `analyze_file` | `path` | compact IR of one file inside the allowed folders |
| `analyze_directory` | optional `path`, `max_files` (default 50, max 500), `recursive` (default `true`), `detail` (`"summary"` default, or `"full"`) | one short entry per file (path, risk, status, language, lines, operations, up to 3 risk signals; `"full"`: all facts), 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; excluded files and folders are only counted in `skipped.excluded`; `truncated` and `skipped` |
Every tool returns the result twice: as JSON text (for clients that only read
text) and as `structuredContent`. All three are annotated
`readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false`.
Example `analyze_file` result for a three-line file that calls `eval` and
`child_process` (real output, wrapped for reading):
```json
{"analysisStatus":"complete",
"mission":{"target":"danger.js","objective":"general_optimization"},
"world":{"pressure":0.5,"confidence":0.75,"budget":0.015,"risk":"critical"},
"constraints":["preserve_api","avoid_breaking_changes"],
"forbidden":["eval","child_process","process_spawn"],
"summary":{"size":93,"lines":3,"hasCrypto":false,"hasFilesystem":false,"hasNetwork":false},
"facts":{"ast":true,"exports":["default"],"imports":["child_process"],
"operations":["dynamicEval","processSpawn","childProcess"],
"calls":{"process":["cp.exec","require(child_process)"],"dynamic":["eval"]},
"dangerous":["eval","child_process","process_spawn"],
"riskSignals":["child_process_import:require(child_process)@1","dynamic_code:eval@2","process_spawn:cp.exec@2"]}}
```
Rely on `world.risk`, `forbidden` and `facts`. `mission.objective` is a coarse
hint from `sentinel-ir` and can stay `general_optimization` even for risky
files, as above.
## Setup
Requires Node.js 20 or newer. `npx` downloads and runs the server; there is
nothing to compile (the parser is WebAssembly).
### Quick setup (since 0.2.2)
Run it in your project folder:
```bash
npx -y sentinel-ir-mcp@0.4 setup --client cursor # or windsurf, claude-desktop, vscode, claude-code
npx -y sentinel-ir-mcp@0.4 doctor --client cursor # after restarting the client
npx -y sentinel-ir-mcp@0.4 guide # optional: text for AGENTS.md / client rules
```
`setup` (without `--client` it asks which client):
- shows the change first and writes only after you confirm (or with `--yes`);
`--dry-run` shows the change and writes nothing;
- adds or replaces only the `"sentinel-ir"` entry; other servers and other
fields in the file stay as they are, and running it again changes nothing;
- copies the old file to `<file>.bak-<timestamp>` before writing and writes
through a temporary file plus rename; a file with invalid JSON is never
touched;
- gives the agent one folder (`--project`, default: the current folder) and
refuses your home folder or a whole disk;
- writes `sentinel-ir-mcp@0.4`, so clients get 0.4.x fixes but no unexpected
new features;
- asks which files or folders the agent must not read (`--exclude <name>`,
repeatable, skips the question; see [Excluding files](#excluding-files));
- for VS Code and Claude Code it writes nothing and prints their own install
command (`code --add-mcp ...`, `claude mcp add ...`) instead.
| `--client` | file written by `setup` |
|---|---|
| `cursor` | `<project>/.cursor/mcp.json` with `${workspaceFolder}`; `--global`: `~/.cursor/mcp.json` with the absolute path |
| `windsurf` | `~/.config/devin/mcp_config.json` (or `$XDG_CONFIG_HOME/devin/`), Windows `%APPDATA%\devin\mcp_config.json`. Servers from an older `~/.codeium/windsurf/mcp_config.json` are copied over; that file stays unchanged |
| `claude-desktop` | macOS `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows `%APPDATA%\Claude\claude_desktop_config.json`; elsewhere pass `--config <file>` |
| `vscode`, `claude-code` | nothing; prints the command to run |
On Windows the entry is `"command": "cmd", "args": ["/c", "npx", "-y",
"sentinel-ir-mcp@0.4", "<folder>"]`: there `npx` is `npx.cmd`, and starting it
without a shell failed with `ENOENT` on a real Windows runner. For VS Code and
Claude Code, `setup` prints a PowerShell and a Command Prompt version of the
command. What was measured on which system: [COMPATIBILITY.md](COMPATIBILITY.md).
`doctor` checks Node.js, the package version, the client config (valid JSON, a
`"sentinel-ir"` entry that starts `sentinel-ir-mcp`, existing folders), then
starts **its own** server with the same folders, performs a real MCP
handshake, expects exactly the three tools and calls `analyze_directory` with
`max_files: 1`. It lists your exclusions, how many files and folders they skip
in the project right now, and warns about one that matches nothing (a typo).
It never runs the command stored in the client config. Exit
code 0 when everything passes, 1 otherwise; `--json` gives a machine-readable
report.
Only `setup` writes files, and only the client config. The MCP server itself
reads only; the one exception is the [audit log](#audit-log), if you turn it on.
**Cursor:** a server from a project `.cursor/mcp.json` starts switched off.
Open Cursor Settings → Customize → MCPs and turn on the switch next to
`sentinel-ir` (its source is the project's `.cursor/mcp.json`). It then shows
*Connected* and 3 tools. A server installed with the link below (user-level
`~/.cursor/mcp.json`) connected without this step.
### Getting a newer 0.2.x
`npx` keeps the package it downloaded for `sentinel-ir-mcp@0.2` and keeps
starting that copy, even when a newer 0.2.x is on npm (measured with npm
10.8.2, 10.8.3 and 10.9.9: a cached 0.2.2 kept running after 0.2.3 was
released; `--prefer-online` and `npm cache clean --force` did not change it).
To get the newest 0.2.x:
1. Check what runs: `npx -y sentinel-ir-mcp@0.2 doctor` prints
`sentinel-ir-mcp <version>`.
2. Close the client app and delete the `_npx` folder in npm's cache
(`npm config get cache` shows where it is; usually `~/.npm/_npx`, on
Windows `%LocalAppData%\npm-cache\_npx`). This removes all packages cached
by `npx`, not only this one; they are downloaded again when needed.
3. Start the client again. The next start downloads the newest 0.2.x.
A folder that is literally named `setup`, `doctor` or `guide` must now be
passed as `./setup` (or an absolute path); the bare word is the command.
### Upgrading from 0.3.x or older
Configs written by older versions pin `sentinel-ir-mcp@0.3` (or `@0.2`) and
keep running that line. 0.3.x has no native-code detection, and its parser
stops working in long sessions (see 0.4.0 below); 0.2.x also has no Python
support. Run `setup` again for your client (it replaces only the
`"sentinel-ir"` entry); `doctor` warns about an entry that still pins 0.3 or
older. If the client still starts an old version, see
[Getting a newer version](#getting-a-newer-version).
### One-click install links
These links follow the Cursor and VS Code documentation. They were opened in
VS Code 1.143 and Cursor 3.26 (see [COMPATIBILITY.md](COMPATIBILITY.md)). A link cannot know your
project path, so it uses `${workspaceFolder}`; if your client does not expand
it, use `setup` instead.
- Cursor: [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=sentinel-ir&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNlbnRpbmVsLWlyLW1jcEAwLjMiLCIke3dvcmtzcGFjZUZvbGRlcn0iXX0=)
- VS Code: [Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22sentinel-ir%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22sentinel-ir-mcp%400.3%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D)
Some sites (including GitLab and npm) may not open `cursor://` or `vscode:`
links; copy the link into the browser address bar or use `setup`.
### Manual setup
Pass the folder(s) the agent may read as arguments. Use absolute paths.
#### Claude Desktop
Settings → Developer → Edit Config opens `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`):
```json
{
"mcpServers": {
"sentinel-ir": {
"command": "npx",
"args": ["-y", "sentinel-ir-mcp@0.4", "/Users/you/projects/my-app"]
}
}
}
```
Restart Claude Desktop afterwards.
#### Cursor
Project `.cursor/mcp.json` (or `~/.cursor/mcp.json` for all projects).
`${workspaceFolder}` is the folder that contains `.cursor/mcp.json`. A
project entry has to be switched on once in Cursor Settings → Customize → MCPs:
```json
{
"mcpServers": {
"sentinel-ir": {
"command": "npx",
"args": ["-y", "sentinel-ir-mcp@0.4", "${workspaceFolder}"]
}
}
}
```
#### Windsurf
Windsurf 3 (Devin Desktop) reads `~/.config/devin/mcp_config.json`
(Windows: `%APPDATA%\devin\mcp_config.json`); older versions read
`~/.codeium/windsurf/mcp_config.json`. Add the same entry as for Claude Desktop
with your project's absolute path.
#### Other clients
Any MCP client that starts stdio servers works with
`command: "npx"`, `args: ["-y", "sentinel-ir-mcp@0.4", "<folder>"]`
(on Windows: `command: "cmd"`, `args: ["/c", "npx", "-y", "sentinel-ir-mcp@0.4", "<folder>"]`).
Without folder arguments the server uses the client's
[MCP roots](https://modelcontextprotocol.io/specification/latest/client/roots)
if the client offers them; if it offers none, only `analyze_code` works.
### Excluding files
```bash
npx -y sentinel-ir-mcp@0.4 setup --client cursor --exclude secrets --exclude "*.min.js"
```
| you write | excludes |
|---|---|
| `secrets` | every file or folder named `secrets`, anywhere in the project, with everything inside |
| `config/prod.js` | only this path, relative to the project folder |
| `*.min.js` | files by mask; `*` and `?` stay within one name |
| `generated/**/out` | `**` stands for any number of folders |
- Only additions: there is no `!` to re-include something. Absolute paths,
`..` and the characters `" & | < > ^ % !` are refused.
- Windows `\` works like `/`. On Windows and macOS letter case does not
matter (`SECRETS/a.js` is excluded by `secrets`), on Linux it does.
- The check uses the real path, so `lib/../secrets/a.js`, a symlink to an
excluded file and a Windows short name (`SECRE~1`) are refused too. It is
repeated when the file is opened.
- `analyze_code` is not affected: it analyzes text the agent already has.
The exclusions only bind this server. An agent that can read files with
other tools of its client is told about them, but this server cannot stop it.
### Audit log
```bash
npx -y sentinel-ir-mcp@0.4 setup --client cursor --audit-log # or answer "y" in setup
npx -y sentinel-ir-mcp@0.4 log # what the agent asked for
```
| system | folder |
|---|---|
| Windows | `%LOCALAPPDATA%\sentinel-ir-mcp\audit-log\` |
| macOS | `~/Library/Logs/sentinel-ir-mcp/` |
| Linux | `~/.local/state/sentinel-ir-mcp/audit-log/` (or `$XDG_STATE_HOME`) |
`--audit-log=<folder>` uses another folder: a full path outside the project
(the characters `" & | < > ^ % !` are refused because Windows starts the
server through `cmd`). Then read it with `log --dir <folder>`.
- **What is written:** one JSON line per tool call in `audit-YYYY-MM-DD.jsonl`
(local date): time, client name, tool, the path the agent asked for, the
result (risk, `analysisStatus`, file counts, up to 20 `critical`/`high` file
paths of a folder) or why it was refused (`excluded`, `outside`,
`not-found`, `too-large`), and the duration. A line when the server starts.
- **Which version of a file (0.4.5):** for every file the server read, the
SHA-256 of the exact bytes it analyzed: `sha256` for `analyze_file`; for
`analyze_directory` the folder line gets a `call` id and `hashed` count,
and the hashes follow in `"event": "hashes"` lines (40 files each, same
`call` id), so no line gets long. `log` shows the first 12 digits for
files and the risky files of a folder; `log --json` has all of them. To check whether a file is still the
one the agent saw, compare with `certutil -hashfile <file> SHA256`
(Windows), `shasum -a 256 <file>` (macOS) or `sha256sum <file>` (Linux).
Pasted code (`analyze_code`) gets no hash: a very short snippet could be
guessed from it. The tool answers to the agent are unchanged.
- **What is not written:** source code. For `analyze_code` only the size in
bytes, the language and the risk. Names of excluded files are not written
either, only their count. Long values are cut to 300 characters.
- **Cleanup:** day files older than 30 days are deleted; a day stops 1 MB
below 50 MB with one line saying so, so parallel servers stay under 50 MB
too. Other files in the folder are never touched.
- **Set by you only,** like exclusions: the agent and the project cannot turn
it on, off or move it. The log is never written inside the project.
- **Safe to read:** a path with a newline cannot add a fake line (JSON
escapes it), and `log` shows control and bidi characters as `\u{1b}`, so a
path cannot recolor or rewrite your terminal.
- **Never breaks the tools:** when the log cannot be written (disk full, no
permission) the tools answer as usual and the server says so once on stderr;
`doctor` reports it as `[FAIL]`. Several servers can share the folder: each
line is one append, so lines do not mix.
- `doctor` adds one line, `doctor checked that the log can be written`, and
none of its own tool calls.
- A day file that is a symlink, junction or hard link is not written to, on
Windows too, so the log cannot be used to overwrite another file.
- It records only what the agent asked **this server** for. Other file tools
of your AI app are not in it.
- Files are created readable by you only (`0600`, folder `0700`; on Windows
the folder in `%LOCALAPPDATA%` is private to your account).
### Options
```text
sentinel-ir-mcp [--exclude <name>]... [--audit-log[=<folder>]] [--max-file-bytes <n>] [--max-files <n>] [--time-budget-ms <n>] [folder ...]
--exclude <name> file or folder the file tools refuse (repeatable, max 100); see Excluding files
--audit-log[=<folder>] keep a log of the tool calls (never source code); see Audit log
--max-file-bytes <n> largest file to analyze (default 1048576 = 1 MiB, max 2097152 = 2 MiB)
--time-budget-ms <n> analyze_directory starts no new file after this (default 30000, max 600000)
--max-files <n> default file limit for analyze_directory (default 50, max 500)
```
## What the server can and cannot do
- **Reads only.** The MCP server never writes project files, runs commands or
opens network connections. It writes only the [audit log](#audit-log) when
you turn it on (outside the project). The separate `setup` command writes
only the client config file you confirm; `doctor` starts this package's own server. The only files it opens are `.js`/`.mjs`/`.cjs`/`.ts`/`.mts`/`.cts`/`.tsx`/`.py`/`.pyi` files up to
`--max-file-bytes`.
- **Only inside the allowed folders.** Every path is resolved to its real path
(symlinks followed) and must lie inside a folder given on the command line,
or, without folder arguments, inside one of the client's MCP roots. `../`,
absolute paths elsewhere and symlinks pointing outside are refused.
- **Exclusions are yours.** They come only from the command line the client
starts (written by `setup`), never from the agent or from files in the
project. Excluded paths are refused by `analyze_file` and counted, not
named, in `skipped.excluded`.
- **`analyze_directory`** skips `node_modules`, `.git`, hidden folders and
every symlink, and stops after `max_files` files (`truncated: true`) or
when the time budget is spent (`timedOut: true`, rest in `notAnalyzed`).
Each file is checked again when it is opened: it must still be a regular
file inside the allowed folders.
- **Hard links are refused.** A hard link inside the folder can point to a
file outside it, and the other names of a file cannot be found. So files
with more than one hard link are refused (`analyze_file` error,
`skipped.hardlinks` in `analyze_directory`). This also refuses harmless hard
links, e.g. pnpm's in `node_modules`.
- **The IR is still untrusted text.** Comments, string bodies and function
bodies are not copied into it, so instructions hidden there do not reach the
agent through the IR. It does contain identifiers and some literals from the
code (route paths, module names, export names, env variable names). That
*reduces* the prompt-injection surface; it does not remove it.
- **It is not a security scanner.** The risk level comes from a fixed set of
AST patterns in `sentinel-ir`; code that hides its behaviour (e.g. building
a module name at runtime) can look harmless.
## Measurements
The library's own published sources (`sentinel-ir@0.1.3`), read through
`analyze_file` / `analyze_directory` over stdio. Tokens counted with
`gpt-tokenizer@4.0.4` (`o200k_base` encoding); other models' tokenizers give
different numbers.
| file | raw chars | raw tokens | IR chars | IR tokens |
|---|---:|---:|---:|---:|
| `src/ast-facts.js` | 39,877 | 8,439 | 450 | 120 |
| `src/sentinel-ir.js` | 9,123 | 2,084 | 515 | 133 |
| `src/tree-parser.js` | 4,393 | 891 | 465 | 121 |
| `examples/basic.js` | 600 | 150 | 461 | 120 |
| all 4 via `analyze_directory` | | 11,564 | | 317 |
The IR has a floor of roughly 120 tokens per file, so it does not pay off for
very small files. `analyze_file` on `src/ast-facts.js` took about 160 ms per
call (average of 10, Node 24, Linux cloud VM); the first call also loads the
WebAssembly parser.
Long session (one server process over stdio, 6,000 `analyze_code` calls on
the same 17.8 KB JavaScript file, about 107 MB in total, Node 24):
| version | calls with a different `analysisStatus` than the first | first such call |
|---|---:|---:|
| 0.3.0 + `sentinel-ir` 0.3.1 | 1,083 | 4,918 |
| 0.4.0 + `sentinel-ir` 0.4.2 | 0 | — |
## Dependencies
Three runtime dependencies: the official
[`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk),
`zod` (its schema peer dependency) and `sentinel-ir`. With their own
dependencies that is about 100 packages in `node_modules`; `npm audit`
reported 0 vulnerabilities at release time (0.4.0: after updating
`body-parser` to 2.4.0 in the lock file). Unlike `sentinel-ir` itself,
this package is not dependency-free.
## Development
```sh
npm ci
npm test # starts the real server over stdio and talks to it with the MCP SDK client
```
Tests cover the tool list and annotations, all three tools, refusals for
`../`, absolute paths, symlinks out of the folder, unsupported file types and
size limits, MCP roots from the client, and the command-line options.
`setup`/`doctor` tests run against temporary `HOME`/`APPDATA` folders: new
file, keeping other servers and fields, backup, idempotent second run, invalid
JSON left untouched, `--dry-run`, refused home/root folders, symlinked config,
Claude Desktop/Windsurf macOS/Windows paths (simulated), a real MCP
handshake, failing configs and old Node.js.
A second CI job runs on a real Windows runner (Node 20/22/24): the full test
suite, the packed package with `setup`/`doctor` against the real `%APPDATA%`,
starting the published package through `npx`, and the printed VS Code/Claude
Code commands pasted into PowerShell 5.1, PowerShell 7 and cmd.exe. See
[COMPATIBILITY.md](COMPATIBILITY.md).
Release: bump `version` in `package.json`, push a protected tag `vX.Y.Z`;
GitLab CI tests and publishes to npm with provenance.
## License
[Apache-2.0](LICENSE). See [NOTICE](NOTICE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues