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.
- package/CHANGELOG.md +70 -1
- package/CONTRIBUTING.md +40 -0
- package/README.md +42 -9
- package/SECURITY.md +27 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +189 -0
- package/dist/adapters/ask-proof.js +144 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +181 -0
- package/dist/adapters/docs.js +172 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +293 -0
- package/dist/adapters/find.js +122 -0
- package/dist/adapters/git-base.js +26 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +439 -0
- package/dist/adapters/locate-file.js +159 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +98 -0
- package/dist/adapters/risk-callers.js +426 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +76 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +131 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +257 -0
- package/dist/constants.js +119 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +194 -0
- package/dist/core/asks.js +436 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +120 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +44 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +434 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +187 -0
- package/dist/mcp/tools.js +116 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +42 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +1 -0
- package/dist/texts/ask.js +2 -0
- package/dist/texts/check-diff.js +17 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +14 -0
- package/dist/texts/guide.js +16 -0
- package/dist/texts/locate.js +10 -0
- package/dist/texts/select-tests.js +2 -0
- package/dist/tools/ask-files.js +217 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +686 -0
- package/dist/tools/check-diff.js +402 -0
- package/dist/tools/docs-check.js +299 -0
- package/dist/tools/find.js +389 -0
- package/dist/tools/locate.js +303 -0
- package/dist/tools/select-tests.js +567 -0
- package/dist/tools/spec-check.js +166 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +91 -0
- package/docs/design.md +3 -3
- package/docs/mcp.md +231 -0
- package/package.json +19 -4
- package/server.json +57 -0
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +7 -4
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/private-storage.ts +143 -0
- package/src/adapters/risk-callers.ts +4 -2
- package/src/adapters/shell.ts +97 -0
- package/src/configuration.ts +39 -12
- package/src/constants.ts +11 -0
- package/src/core/command-output.ts +17 -1
- package/src/host.ts +11 -0
- package/src/jev/client.ts +12 -0
- package/src/jev/types.ts +6 -0
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +282 -0
- package/src/mcp/tools.ts +166 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/tools/ask-files.ts +5 -7
- package/src/tools/ask.ts +26 -22
- package/src/tools/check-diff.ts +8 -5
- package/src/tools/docs-check.ts +1 -0
- package/src/tools/find.ts +5 -2
- package/src/tools/locate.ts +5 -8
- package/src/tools/select-tests.ts +7 -4
- 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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
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
|
+
}
|
package/src/adapters/command.ts
CHANGED
|
@@ -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
|
-
|
|
70
|
+
shell.executable,
|
|
67
71
|
[
|
|
68
|
-
|
|
69
|
-
"bash",
|
|
72
|
+
...shell.prefix,
|
|
70
73
|
"-c",
|
|
71
|
-
|
|
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
|
+
}
|