jev-agent-tools 0.1.4 → 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.
Files changed (131) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/CONTRIBUTING.md +40 -0
  3. package/README.md +42 -9
  4. package/SECURITY.md +27 -0
  5. package/dist/adapters/analysis-context.js +75 -0
  6. package/dist/adapters/ask-files.js +189 -0
  7. package/dist/adapters/ask-proof.js +144 -0
  8. package/dist/adapters/ask-syntax.js +385 -0
  9. package/dist/adapters/canonical-path.js +17 -0
  10. package/dist/adapters/command.js +181 -0
  11. package/dist/adapters/docs.js +172 -0
  12. package/dist/adapters/exec.js +207 -0
  13. package/dist/adapters/files.js +293 -0
  14. package/dist/adapters/find.js +122 -0
  15. package/dist/adapters/git-base.js +26 -0
  16. package/dist/adapters/git-inventory.js +71 -0
  17. package/dist/adapters/git.js +439 -0
  18. package/dist/adapters/locate-file.js +159 -0
  19. package/dist/adapters/output-lines.js +46 -0
  20. package/dist/adapters/private-storage.js +98 -0
  21. package/dist/adapters/risk-callers.js +426 -0
  22. package/dist/adapters/runner-version.js +78 -0
  23. package/dist/adapters/shell.js +76 -0
  24. package/dist/adapters/syntax.js +187 -0
  25. package/dist/adapters/test-inventory.js +131 -0
  26. package/dist/adapters/usage.js +20 -0
  27. package/dist/adapters/utf8.js +47 -0
  28. package/dist/configuration.js +257 -0
  29. package/dist/constants.js +119 -0
  30. package/dist/core/ask-closure.js +282 -0
  31. package/dist/core/ask-proof.js +1 -0
  32. package/dist/core/ask-references.js +194 -0
  33. package/dist/core/asks.js +436 -0
  34. package/dist/core/batches.js +65 -0
  35. package/dist/core/command-output.js +224 -0
  36. package/dist/core/diff.js +178 -0
  37. package/dist/core/docs.js +302 -0
  38. package/dist/core/find.js +108 -0
  39. package/dist/core/git.js +1 -0
  40. package/dist/core/imports.js +550 -0
  41. package/dist/core/integrity.js +45 -0
  42. package/dist/core/lexical.js +132 -0
  43. package/dist/core/locate.js +169 -0
  44. package/dist/core/output.js +120 -0
  45. package/dist/core/pointer.js +29 -0
  46. package/dist/core/risk-callers.js +851 -0
  47. package/dist/core/runner-version.js +45 -0
  48. package/dist/core/sections.js +230 -0
  49. package/dist/core/state.js +44 -0
  50. package/dist/core/syntax.js +1 -0
  51. package/dist/core/test-commands.js +334 -0
  52. package/dist/core/test-coverage.js +74 -0
  53. package/dist/core/test-discovery.js +1382 -0
  54. package/dist/core/test-evidence.js +527 -0
  55. package/dist/core/test-state.js +81 -0
  56. package/dist/core/truncate.js +12 -0
  57. package/dist/core/units.js +349 -0
  58. package/dist/describe.js +23 -0
  59. package/dist/guide.js +33 -0
  60. package/dist/host.js +24 -0
  61. package/dist/jev/client.js +434 -0
  62. package/dist/jev/pool.js +54 -0
  63. package/dist/jev/types.js +1 -0
  64. package/dist/mcp/main.js +124 -0
  65. package/dist/mcp/protocol.js +187 -0
  66. package/dist/mcp/tools.js +116 -0
  67. package/dist/presets/docs.js +62 -0
  68. package/dist/presets/risk.js +179 -0
  69. package/dist/presets/spec.js +81 -0
  70. package/dist/presets/witnesses.js +249 -0
  71. package/dist/render.js +42 -0
  72. package/dist/result.js +3 -0
  73. package/dist/runtime.js +1 -0
  74. package/dist/session.js +147 -0
  75. package/dist/texts/ask-files.js +1 -0
  76. package/dist/texts/ask.js +2 -0
  77. package/dist/texts/check-diff.js +17 -0
  78. package/dist/texts/configuration.js +1 -0
  79. package/dist/texts/find.js +14 -0
  80. package/dist/texts/guide.js +16 -0
  81. package/dist/texts/locate.js +10 -0
  82. package/dist/texts/select-tests.js +2 -0
  83. package/dist/tools/ask-files.js +217 -0
  84. package/dist/tools/ask-schema.js +70 -0
  85. package/dist/tools/ask.js +686 -0
  86. package/dist/tools/check-diff.js +402 -0
  87. package/dist/tools/docs-check.js +299 -0
  88. package/dist/tools/find.js +389 -0
  89. package/dist/tools/locate.js +303 -0
  90. package/dist/tools/select-tests.js +567 -0
  91. package/dist/tools/spec-check.js +166 -0
  92. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
  93. package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
  94. package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
  95. package/docs/adr/0004-compiled-typed-intents.md +19 -0
  96. package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
  97. package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
  98. package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
  99. package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
  100. package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
  101. package/docs/adr/0010-mcp-server-thin-host.md +23 -0
  102. package/docs/agent-instructions.md +91 -0
  103. package/docs/design.md +3 -3
  104. package/docs/mcp.md +231 -0
  105. package/package.json +19 -4
  106. package/server.json +57 -0
  107. package/src/adapters/canonical-path.ts +18 -0
  108. package/src/adapters/command.ts +7 -4
  109. package/src/adapters/exec.ts +226 -0
  110. package/src/adapters/private-storage.ts +143 -0
  111. package/src/adapters/risk-callers.ts +4 -2
  112. package/src/adapters/shell.ts +97 -0
  113. package/src/configuration.ts +39 -12
  114. package/src/constants.ts +11 -0
  115. package/src/core/command-output.ts +17 -1
  116. package/src/host.ts +11 -0
  117. package/src/jev/client.ts +12 -0
  118. package/src/jev/types.ts +6 -0
  119. package/src/mcp/main.ts +135 -0
  120. package/src/mcp/protocol.ts +282 -0
  121. package/src/mcp/tools.ts +166 -0
  122. package/src/session.ts +59 -0
  123. package/src/setup.ts +13 -5
  124. package/src/tools/ask-files.ts +5 -7
  125. package/src/tools/ask.ts +26 -22
  126. package/src/tools/check-diff.ts +8 -5
  127. package/src/tools/docs-check.ts +1 -0
  128. package/src/tools/find.ts +5 -2
  129. package/src/tools/locate.ts +5 -8
  130. package/src/tools/select-tests.ts +7 -4
  131. package/src/tools/spec-check.ts +1 -0
package/docs/mcp.md ADDED
@@ -0,0 +1,231 @@
1
+ # MCP setup guide
2
+
3
+ `jev-agent-tools-mcp` is a stdio [MCP](https://modelcontextprotocol.io) server that exposes the same six `jev_*` tools as the pi and omp extension to any MCP client. It ships in the `jev-agent-tools` npm package, has no runtime dependencies beyond that package, and runs one session per server process.
4
+
5
+ Setup takes three steps: provide the endpoint and key, register the server with your client, and add the [agent instructions](agent-instructions.md) to your project.
6
+
7
+ ## 1. Requirements
8
+
9
+ - Node.js 24 or later, and Git, on the machine that runs the client.
10
+ - Bash for optional `jev_ask` command evidence. On Windows this is Git for Windows bash, found automatically next to `git` or under Program Files; set `JEV_TOOLS_BASH` to use another. The WSL `bash.exe` launchers are never used.
11
+ - A Jev endpoint URL and API key.
12
+
13
+ The server ships in `jev-agent-tools` version 0.2.0 and later. For a development checkout, see [From a clone](#from-a-clone).
14
+
15
+ ## 2. Provide the endpoint and key
16
+
17
+ The server reads, per field, the environment first and then the configuration saved by `/jev-setup` in pi or omp. There is no interactive setup inside MCP.
18
+
19
+ | Variable | Required | Meaning |
20
+ |---|---|---|
21
+ | `JEV_TOOLS_URL` | yes | Complete endpoint URL compatible with the Jev API format. |
22
+ | `JEV_TOOLS_API_KEY` | yes | Bearer credential. Never printed in tool output. |
23
+ | `JEV_TOOLS_MODEL` | no | Requested model, default `openjev`. |
24
+ | `JEV_TOOLS_ROOT` | no | Repository directory when `--root` is not given. |
25
+ | `JEV_TOOLS_MAX_CALLS`, `JEV_TOOLS_MAX_USD` | no | Session call and cost limits for this server process. |
26
+ | `JEV_TOOLS_ALLOW_COMMAND` | no | `0` removes `command` from `jev_ask`. |
27
+ | `JEV_TOOLS_BASH` | no | Full path of the bash used for commands on Windows. |
28
+
29
+ Prefer passing secrets through your client's environment-variable interpolation (shown per client below) rather than writing the key into a committed file. Saved configuration from `/jev-setup` lives in `~/.config/jev-agent-tools/config.json` and is refused unless the directory and file are private: owner-only mode bits on Linux and macOS, an access list limited to you, SYSTEM and Administrators on Windows. Unusable saved storage is reported on the server's stderr and never stops the server.
30
+
31
+ Without an endpoint and key the server still starts, lists the tools and answers each call with what is missing.
32
+
33
+ ## 3. Register the server
34
+
35
+ Every example registers a server named `jev`. Replace the repository path. The repository the tools work in is `--root`, else `JEV_TOOLS_ROOT`, else the directory the client starts the server in.
36
+
37
+ **Windows:** most clients start the command without a shell, and `npx` is a `.cmd` script there, so `"command": "npx"` fails to start. Use `"command": "cmd"` with `"args": ["/c", "npx", ...]`, as in the Windows examples. This was checked on Windows 11 with Node.js 24: a shell-less spawn of `npx` failed with `ENOENT`, `cmd /c npx` started the server.
38
+
39
+ The shared arguments are:
40
+
41
+ ```text
42
+ npx -y -p jev-agent-tools jev-agent-tools-mcp --root <repository>
43
+ ```
44
+
45
+ `-p jev-agent-tools` is required because the binary name differs from the package name. Pin a version (`jev-agent-tools@X.Y.Z`) for reproducible setups.
46
+
47
+ ### From the MCP Registry
48
+
49
+ Releases are also published to the official [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.NomenAK/jev-agent-tools`, from [`server.json`](../server.json). Clients and catalogs that read the registry can install the server from there; they start it as `npx jev-agent-tools` (the package's only binary) and ask for `JEV_TOOLS_URL` and the secret `JEV_TOOLS_API_KEY`. The manual entries below give the same result.
50
+
51
+ ### Claude Code
52
+
53
+ For a private registration, add it from the project directory with the CLI. Your shell expands the variables, so the values are stored in your private `~/.claude.json`, not in the project:
54
+
55
+ ```sh
56
+ claude mcp add --scope local --env JEV_TOOLS_URL="$JEV_TOOLS_URL" --env JEV_TOOLS_API_KEY="$JEV_TOOLS_API_KEY" --transport stdio jev -- npx -y -p jev-agent-tools jev-agent-tools-mcp --root .
57
+ ```
58
+
59
+ To share the server with the team, write `.mcp.json` at the project root instead. Claude Code expands `${VAR}` and `${VAR:-default}` from each user's environment when it loads the file, so no key is committed:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "jev": {
65
+ "command": "npx",
66
+ "args": ["-y", "-p", "jev-agent-tools", "jev-agent-tools-mcp", "--root", "${CLAUDE_PROJECT_DIR:-.}"],
67
+ "env": {
68
+ "JEV_TOOLS_URL": "${JEV_TOOLS_URL}",
69
+ "JEV_TOOLS_API_KEY": "${JEV_TOOLS_API_KEY}"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ On Windows use `"command": "cmd"` and prepend `"/c", "npx"` to `args`. Check with `claude mcp list`. Add the instructions to `CLAUDE.md` ([template](agent-instructions.md#claudemd)).
77
+
78
+ ### Claude Desktop
79
+
80
+ Edit `claude_desktop_config.json` (Settings, Developer, Edit Config): `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Claude Desktop has no project directory, so `--root` is required. Its documentation does not describe variable interpolation, so values here are literal; keep this file private.
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "jev": {
86
+ "command": "cmd",
87
+ "args": ["/c", "npx", "-y", "-p", "jev-agent-tools", "jev-agent-tools-mcp", "--root", "C:\\path\\to\\repository"],
88
+ "env": {
89
+ "JEV_TOOLS_URL": "https://your-jev-endpoint.example/judge",
90
+ "JEV_TOOLS_API_KEY": "your-key"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ On macOS use `"command": "npx"` without `"/c", "npx"`. Restart Claude Desktop after saving. One server serves one repository; add a second entry with another name for another repository.
98
+
99
+ ### Kiro (IDE and CLI)
100
+
101
+ Workspace: `.kiro/settings/mcp.json`. User: `~/.kiro/settings/mcp.json`. The workspace file wins for a server of the same name. Kiro expands `${VAR}`.
102
+
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "jev": {
107
+ "command": "npx",
108
+ "args": ["-y", "-p", "jev-agent-tools", "jev-agent-tools-mcp", "--root", "."],
109
+ "env": {
110
+ "JEV_TOOLS_URL": "${JEV_TOOLS_URL}",
111
+ "JEV_TOOLS_API_KEY": "${JEV_TOOLS_API_KEY}"
112
+ },
113
+ "disabled": false,
114
+ "autoApprove": ["jev_ask_files", "jev_find_files", "jev_locate_in_file", "jev_check_diff", "jev_select_tests"]
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ `autoApprove` above leaves `jev_ask` on manual approval because it can run shell commands. On Windows use `"command": "cmd"` with `"/c", "npx"` first in `args`. Add the instructions as a steering file ([template](agent-instructions.md#kiro-steering)).
121
+
122
+ ### Cursor
123
+
124
+ Project: `.cursor/mcp.json`. Global: `~/.cursor/mcp.json`. Cursor expands `${env:NAME}` and `${workspaceFolder}`.
125
+
126
+ ```json
127
+ {
128
+ "mcpServers": {
129
+ "jev": {
130
+ "command": "npx",
131
+ "args": ["-y", "-p", "jev-agent-tools", "jev-agent-tools-mcp", "--root", "${workspaceFolder}"],
132
+ "env": {
133
+ "JEV_TOOLS_URL": "${env:JEV_TOOLS_URL}",
134
+ "JEV_TOOLS_API_KEY": "${env:JEV_TOOLS_API_KEY}"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### VS Code (GitHub Copilot)
142
+
143
+ Workspace: `.vscode/mcp.json`. The top-level key is `servers`, not `mcpServers`.
144
+
145
+ ```json
146
+ {
147
+ "servers": {
148
+ "jev": {
149
+ "command": "npx",
150
+ "args": ["-y", "-p", "jev-agent-tools", "jev-agent-tools-mcp", "--root", "${workspaceFolder}"],
151
+ "env": {
152
+ "JEV_TOOLS_URL": "https://your-jev-endpoint.example/judge",
153
+ "JEV_TOOLS_API_KEY": "${input:jev-api-key}"
154
+ }
155
+ }
156
+ },
157
+ "inputs": [
158
+ { "type": "promptString", "id": "jev-api-key", "description": "Jev API key", "password": true }
159
+ ]
160
+ }
161
+ ```
162
+
163
+ The `inputs` prompt follows VS Code's MCP configuration reference; check it for your VS Code version.
164
+
165
+ ### OpenAI Codex CLI
166
+
167
+ ```sh
168
+ codex mcp add jev --env JEV_TOOLS_URL=https://your-jev-endpoint.example/judge -- npx -y -p jev-agent-tools jev-agent-tools-mcp --root .
169
+ ```
170
+
171
+ Or in `~/.codex/config.toml` (or a trusted project's `.codex/config.toml`). `env_vars` forwards variables from your environment without writing them into the file:
172
+
173
+ ```toml
174
+ [mcp_servers.jev]
175
+ command = "npx"
176
+ args = ["-y", "-p", "jev-agent-tools", "jev-agent-tools-mcp", "--root", "."]
177
+ env_vars = ["JEV_TOOLS_URL", "JEV_TOOLS_API_KEY"]
178
+ ```
179
+
180
+ Add the instructions to `AGENTS.md` ([template](agent-instructions.md#agentsmd)).
181
+
182
+ ### Windsurf and other clients
183
+
184
+ Clients that use an `mcpServers` object accept the Claude Code JSON entry above. Windsurf reads `mcp_config.json` (see its MCP documentation for the current path) and expands `${env:NAME}`.
185
+
186
+ ## 4. Verify
187
+
188
+ Run the server once by hand; it reads protocol messages from stdin and writes diagnostics to stderr:
189
+
190
+ ```sh
191
+ npx -y -p jev-agent-tools jev-agent-tools-mcp --version
192
+ npx -y -p jev-agent-tools jev-agent-tools-mcp --root . < /dev/null
193
+ ```
194
+
195
+ In PowerShell, run the second as `$null | npx -y -p jev-agent-tools jev-agent-tools-mcp --root .`.
196
+
197
+ The second command prints `jev-agent-tools MCP server ready (root ...; endpoint configured)` on stderr and exits when stdin closes. In the client, the six tools should be listed: `jev_ask`, `jev_ask_files`, `jev_find_files`, `jev_locate_in_file`, `jev_check_diff`, `jev_select_tests`. Ask the agent to run `jev_ask` with a one-line note and a yes/no question; the result ends with the calls, cost and time footer.
198
+
199
+ ## From a clone
200
+
201
+ ```sh
202
+ npm ci --include=optional
203
+ npm run build
204
+ ```
205
+
206
+ Then use `"command": "node"` with `"args": ["/absolute/path/to/jev-tools/dist/mcp/main.js", "--root", "<repository>"]`. This form also avoids the Windows `npx` issue. Node does not strip TypeScript types under `node_modules`, which is why the server ships as compiled `dist/` while pi and omp load `src/`.
207
+
208
+ ## Differences from pi and omp
209
+
210
+ - **No run-end documentation check.** It is a host hook. Before finishing, ask the agent to call `jev_check_diff` with `check: "docs"`; the [instructions](agent-instructions.md) say so.
211
+ - **Approval is the client's.** `jev_ask` is annotated as not read-only and potentially destructive while it accepts `command`; the other five are read-only. All six are open-world because evidence goes to your endpoint. `JEV_TOOLS_ALLOW_COMMAND=0` removes `command` from the schema and makes `jev_ask` read-only.
212
+ - **Instructions.** The reading guide and the `jev_ask` policy are sent as the server's `instructions`. Clients may ignore them, so also add the [agent instructions](agent-instructions.md) to the project.
213
+ - **Tool names in descriptions** refer to "your text search tool" and "your file-name search tool" instead of pi or omp tool names.
214
+ - **Protocol.** Versions 2024-11-05 through 2025-11-25 via `initialize`, 2026-07-28 via `server/discover` and per-request `_meta`. Modern discovery supplies server identity in `_meta["io.modelcontextprotocol/serverInfo"]`; discovery and tool lists advertise `ttlMs: 0` and `cacheScope: "private"`, so clients must not share them across authorization contexts. Tools only; no resources, prompts or sampling. Cancelling a call aborts its Jev requests and command.
215
+ - **One process, one session.** Limits, cache and counters last as long as the connection; restart the server to reset them.
216
+ - **Command shutdown.** Cancellation, command timeout, stdin closure, SIGTERM and SIGINT wait for bounded command-tree termination. On POSIX the managed group receives SIGTERM, then SIGKILL after two seconds if it remains. On Windows, the server maps ordinary MSYS descendants through the same Bash installation's process table before running the system `taskkill /T /F`; each subprocess is bounded to five seconds. Cancelled calls receive no response. This is not a sandbox: descendants that deliberately detach from the managed group or escape the tracked tree are not contained.
217
+
218
+ ## Troubleshooting
219
+
220
+ | Symptom | Cause and fix |
221
+ |---|---|
222
+ | Client shows the server failed to start on Windows | `npx` cannot start without a shell. Use `cmd /c npx` or `node <path>/dist/mcp/main.js`. |
223
+ | `jev-tools is not configured` in every result | The server did not receive `JEV_TOOLS_URL` and `JEV_TOOLS_API_KEY`. Check the client's `env` block and that interpolated variables exist where the client was started. |
224
+ | stderr: `Cannot read or save Jev configuration` | Saved `/jev-setup` storage is not private or is malformed. Environment variables still apply. Fix permissions (on Windows, remove other accounts from the folder's Security tab) or delete the file and save again. |
225
+ | `Repository directory not found` | `--root` or `JEV_TOOLS_ROOT` points to a missing directory. |
226
+ | Command evidence reports `Command executable unavailable` | No usable bash. Install Git for Windows or set `JEV_TOOLS_BASH`. |
227
+ | Results from the wrong repository | The client started the server elsewhere. Pass an absolute `--root`. |
228
+
229
+ ## Data and safety
230
+
231
+ Repository evidence, notes and command output are sent to your configured endpoint; review its data-handling policy first. File collection is confined to the repository, but `jev_ask` commands run with your shell permissions and no sandbox. See [SECURITY.md](../SECURITY.md).
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "jev-agent-tools",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "description": "Six evidence-oriented tools for pi and omp coding agents, compatible with the Jev API format.",
6
+ "mcpName": "io.github.NomenAK/jev-agent-tools",
7
+ "description": "Six evidence-oriented tools for pi and omp coding agents and any MCP client, compatible with the Jev API format.",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/NomenAK/jev-tools.git"
@@ -19,7 +20,9 @@
19
20
  "test-selection",
20
21
  "pi",
21
22
  "omp",
22
- "jev"
23
+ "jev",
24
+ "mcp",
25
+ "model-context-protocol"
23
26
  ],
24
27
  "files": [
25
28
  "src/",
@@ -33,8 +36,18 @@
33
36
  "docs/tools/jev_find_files.md",
34
37
  "docs/tools/jev_locate_in_file.md",
35
38
  "docs/tools/jev_check_diff.md",
36
- "docs/tools/jev_select_tests.md"
39
+ "docs/tools/jev_select_tests.md",
40
+ "docs/mcp.md",
41
+ "docs/agent-instructions.md",
42
+ "dist/",
43
+ "server.json",
44
+ "SECURITY.md",
45
+ "CONTRIBUTING.md",
46
+ "docs/adr/"
37
47
  ],
48
+ "bin": {
49
+ "jev-agent-tools-mcp": "dist/mcp/main.js"
50
+ },
38
51
  "pi": {
39
52
  "extensions": [
40
53
  "./src/index.ts"
@@ -49,6 +62,8 @@
49
62
  "node": ">=24"
50
63
  },
51
64
  "scripts": {
65
+ "build": "tsc -p tsconfig.build.json",
66
+ "prepack": "npm run build",
52
67
  "typecheck": "tsc --noEmit",
53
68
  "check:imports": "node scripts/check-imports.ts",
54
69
  "lint": "biome check src test scripts",
package/server.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.NomenAK/jev-agent-tools",
4
+ "title": "Jev agent tools",
5
+ "description": "Evidence-oriented code questions, diff review and test selection via a Jev endpoint.",
6
+ "version": "0.2.0",
7
+ "repository": {
8
+ "url": "https://github.com/NomenAK/jev-tools",
9
+ "source": "github"
10
+ },
11
+ "websiteUrl": "https://github.com/NomenAK/jev-tools/blob/main/docs/mcp.md",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "jev-agent-tools",
17
+ "version": "0.2.0",
18
+ "runtimeHint": "npx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "packageArguments": [
23
+ {
24
+ "type": "named",
25
+ "name": "--root",
26
+ "description": "Repository directory the tools work in. Defaults to the directory the client starts the server in.",
27
+ "format": "filepath",
28
+ "isRequired": false
29
+ }
30
+ ],
31
+ "environmentVariables": [
32
+ {
33
+ "name": "JEV_TOOLS_URL",
34
+ "description": "Complete endpoint URL compatible with the Jev API format.",
35
+ "isRequired": true
36
+ },
37
+ {
38
+ "name": "JEV_TOOLS_API_KEY",
39
+ "description": "Bearer credential for the endpoint.",
40
+ "isRequired": true,
41
+ "isSecret": true
42
+ },
43
+ {
44
+ "name": "JEV_TOOLS_MODEL",
45
+ "description": "Requested Jev model.",
46
+ "default": "openjev",
47
+ "isRequired": false
48
+ },
49
+ {
50
+ "name": "JEV_TOOLS_ALLOW_COMMAND",
51
+ "description": "Set to 0 to remove shell commands from jev_ask.",
52
+ "isRequired": false
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
@@ -0,0 +1,18 @@
1
+ import { realpath } from "node:fs";
2
+ import { promisify } from "node:util";
3
+
4
+ const nativeRealpath = promisify(realpath.native);
5
+
6
+ /**
7
+ * Canonical form of an existing path: resolves symlinks and, on Windows,
8
+ * expands 8.3 short names (C:\Users\NAME~1) so the path relates correctly to
9
+ * `git rev-parse --show-toplevel`, which always reports the long form. Falls
10
+ * back to the input when the path cannot be resolved.
11
+ */
12
+ export async function canonicalPath(path: string): Promise<string> {
13
+ try {
14
+ return await nativeRealpath(path);
15
+ } catch {
16
+ return path;
17
+ }
18
+ }
@@ -21,6 +21,7 @@ import {
21
21
  import type { GitExec } from "../core/git.ts";
22
22
  import type { Result } from "../result.ts";
23
23
  import { outputLines } from "./output-lines.ts";
24
+ import { resolveShell } from "./shell.ts";
24
25
 
25
26
  export interface CommandOutput {
26
27
  command: string;
@@ -62,13 +63,15 @@ export async function captureCommand(
62
63
  killed: boolean;
63
64
  };
64
65
  try {
66
+ const shell = resolveShell();
67
+ // Fail closed: never spawn a bare name that PATH could resolve to WSL.
68
+ if (!shell.ok) throw new Error(shell.error);
65
69
  executed = await exec(
66
- "env",
70
+ shell.executable,
67
71
  [
68
- "CI=1",
69
- "bash",
72
+ ...shell.prefix,
70
73
  "-c",
71
- `exec >"$1" 2>"$2"; set --; (\n${command}\n)\nexit $?`,
74
+ `${shell.scriptPrefix}exec >"$1" 2>"$2"; set --; (\n${command}\n)\nexit $?`,
72
75
  "jev",
73
76
  stdoutPath,
74
77
  stderrPath,
@@ -0,0 +1,226 @@
1
+ import { type ChildProcess, execFile, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import {
5
+ PROCESS_KILL_GRACE_MS,
6
+ PROCESS_PIPE_GRACE_MS,
7
+ PROCESS_TREE_TIMEOUT_MS,
8
+ } from "../constants.ts";
9
+ import type { GitExec } from "../core/git.ts";
10
+
11
+ /** Signal the managed process group, or await Windows process-tree termination. */
12
+ export async function killTree(
13
+ child: ChildProcess,
14
+ signal: NodeJS.Signals = "SIGTERM",
15
+ platform: NodeJS.Platform = process.platform,
16
+ ): Promise<void> {
17
+ const pid = child.pid;
18
+ if (pid === undefined) return;
19
+ if (platform === "win32") {
20
+ const taskkill = join(
21
+ process.env.SystemRoot ?? "C:\\Windows",
22
+ "System32",
23
+ "taskkill.exe",
24
+ );
25
+ const pids = [pid];
26
+ // MSYS fork/exec can leave Windows parent IDs pointing at vanished helper
27
+ // processes. Use the same Bash installation's POSIX table before killing
28
+ // the root; taskkill /T alone cannot find those ordinary descendants.
29
+ if (basename(child.spawnfile).toLowerCase() === "bash.exe") {
30
+ const directory = dirname(child.spawnfile);
31
+ const ps = [
32
+ join(directory, "ps.exe"),
33
+ join(directory, "..", "usr", "bin", "ps.exe"),
34
+ ].find(existsSync);
35
+ if (ps) {
36
+ const snapshot = Promise.withResolvers<string>();
37
+ execFile(
38
+ ps,
39
+ ["-e"],
40
+ { windowsHide: true, timeout: PROCESS_TREE_TIMEOUT_MS },
41
+ (error, stdout) => snapshot.resolve(error ? "" : stdout),
42
+ );
43
+ const rows = [];
44
+ for (const line of (await snapshot.promise).split("\n")) {
45
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s/.exec(line);
46
+ if (!match) continue;
47
+ rows.push({
48
+ pid: Number(match[1]),
49
+ parent: Number(match[2]),
50
+ windows: Number(match[4]),
51
+ });
52
+ }
53
+ const root = rows.find((row) => row.windows === pid);
54
+ if (root) {
55
+ const managed = new Set([root.pid]);
56
+ let added = true;
57
+ while (added) {
58
+ added = false;
59
+ for (const row of rows) {
60
+ if (!managed.has(row.pid) && managed.has(row.parent)) {
61
+ managed.add(row.pid);
62
+ if (row.windows > 0) pids.push(row.windows);
63
+ added = true;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ const { promise, resolve } = Promise.withResolvers<void>();
71
+ execFile(
72
+ taskkill,
73
+ [...new Set(pids)]
74
+ .flatMap((target) => ["/PID", String(target)])
75
+ .concat("/T", "/F"),
76
+ { windowsHide: true, timeout: PROCESS_TREE_TIMEOUT_MS },
77
+ (error) => {
78
+ if (error) child.kill();
79
+ resolve();
80
+ },
81
+ );
82
+ await promise;
83
+ return;
84
+ }
85
+ try {
86
+ process.kill(-pid, signal);
87
+ } catch {
88
+ child.kill(signal);
89
+ }
90
+ return;
91
+ }
92
+
93
+ function groupExists(child: ChildProcess): boolean {
94
+ if (child.pid === undefined) return false;
95
+ try {
96
+ process.kill(-child.pid, 0);
97
+ return true;
98
+ } catch (error) {
99
+ return !(
100
+ error instanceof Error &&
101
+ "code" in error &&
102
+ error.code === "ESRCH"
103
+ );
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Capture a process without a shell. Timeout and abort finish tree termination
109
+ * before resolving, even when the direct child exits before its descendants.
110
+ * Deliberately detached descendants are not sandboxed or tracked.
111
+ */
112
+ export const spawnExec: GitExec = (command, args, options) => {
113
+ const { promise, resolve, reject } = Promise.withResolvers<{
114
+ stdout: string;
115
+ stderr: string;
116
+ code: number;
117
+ killed: boolean;
118
+ }>();
119
+ const posix = process.platform !== "win32";
120
+ // Git's bin/bash.exe forwards to usr/bin/bash.exe. Spawn the MSYS process
121
+ // directly so its Windows PID can be matched in the POSIX process table.
122
+ if (!posix && basename(command).toLowerCase() === "bash.exe") {
123
+ const direct = join(dirname(command), "..", "usr", "bin", "bash.exe");
124
+ if (existsSync(direct)) command = direct;
125
+ }
126
+ const child = spawn(command, args, {
127
+ cwd: options.cwd,
128
+ shell: false,
129
+ stdio: ["ignore", "pipe", "pipe"],
130
+ windowsHide: true,
131
+ detached: posix,
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ let killed = false;
136
+ let spawned = false;
137
+ let settled = false;
138
+ let finishing = false;
139
+ let force: NodeJS.Timeout | undefined;
140
+ let pipes: NodeJS.Timeout | undefined;
141
+ let termination: Promise<void> | undefined;
142
+ let terminated: (() => void) | undefined;
143
+ child.stdout.setEncoding("utf8");
144
+ child.stderr.setEncoding("utf8");
145
+ child.stdout.on("data", (chunk: string) => {
146
+ stdout += chunk;
147
+ });
148
+ child.stderr.on("data", (chunk: string) => {
149
+ stderr += chunk;
150
+ });
151
+ const kill = () => {
152
+ if (killed || settled) return;
153
+ killed = true;
154
+ if (posix) {
155
+ void killTree(child, "SIGTERM");
156
+ const completion = Promise.withResolvers<void>();
157
+ termination = completion.promise;
158
+ terminated = completion.resolve;
159
+ // Keep this timer referenced: a closing transport must not exit
160
+ // before a surviving member of the managed group receives SIGKILL.
161
+ force = setTimeout(() => {
162
+ void killTree(child, "SIGKILL");
163
+ completion.resolve();
164
+ }, PROCESS_KILL_GRACE_MS);
165
+ } else termination = killTree(child);
166
+ };
167
+ const timer =
168
+ options.timeout > 0 ? setTimeout(kill, options.timeout) : undefined;
169
+ if (options.signal?.aborted) kill();
170
+ else options.signal?.addEventListener("abort", kill, { once: true });
171
+ const cleanup = () => {
172
+ clearTimeout(timer);
173
+ clearTimeout(force);
174
+ clearTimeout(pipes);
175
+ options.signal?.removeEventListener("abort", kill);
176
+ };
177
+ const finish = async (code: number | null, signal: NodeJS.Signals | null) => {
178
+ if (settled || finishing) return;
179
+ finishing = true;
180
+ clearTimeout(pipes);
181
+ if (posix && terminated && !groupExists(child)) {
182
+ clearTimeout(force);
183
+ terminated();
184
+ }
185
+ if (termination) await termination;
186
+ settled = true;
187
+ cleanup();
188
+ resolve({
189
+ stdout,
190
+ stderr,
191
+ code: code ?? (signal ? 128 + signalNumber(signal) : 1),
192
+ killed,
193
+ });
194
+ };
195
+ child.on("spawn", () => {
196
+ spawned = true;
197
+ });
198
+ child.on("error", (error) => {
199
+ if (spawned) return;
200
+ settled = true;
201
+ cleanup();
202
+ reject(error);
203
+ });
204
+ child.on("exit", (code, signal) => {
205
+ // An escaped descendant may hold inherited pipes open indefinitely.
206
+ pipes = setTimeout(() => {
207
+ child.stdout.destroy();
208
+ child.stderr.destroy();
209
+ void finish(code, signal);
210
+ }, PROCESS_PIPE_GRACE_MS);
211
+ });
212
+ child.on("close", (code, signal) => {
213
+ void finish(code, signal);
214
+ });
215
+ return promise;
216
+ };
217
+
218
+ function signalNumber(signal: NodeJS.Signals): number {
219
+ const numbers: Partial<Record<NodeJS.Signals, number>> = {
220
+ SIGHUP: 1,
221
+ SIGINT: 2,
222
+ SIGKILL: 9,
223
+ SIGTERM: 15,
224
+ };
225
+ return numbers[signal] ?? 0;
226
+ }