gavel-mcp
by newlix
README.md
# gavel-mcp
The gavel acceptance oracle as an MCP server: one tool that turns an
agent's "done" into a receipt. `gavel_acceptance` cold-runs a command
and reports the exit code. Exit code 0 is the only passing verdict.
## Setup
### 1. Build
Requires Node ≥ 20 and git.
```
cd gavel-mcp
npm install
npm run build # → dist/index.js
```
`dist/` is gitignored — every fresh clone needs this step before the
server can start.
### 2. Wire it into ZCode
Two scopes; both auto-connect at session start.
**Workspace scope** — versioned with the repo, shared with the team.
Create `<repo>/.zcode/config.json`:
```json
{
"mcp": {
"servers": {
"gavel": {
"command": "node",
"args": ["/ABS/PATH/TO/gavel-mcp/dist/index.js"]
}
}
}
}
```
**User scope** — applies to every workspace. Put the same
`mcp.servers` object in `~/.zcode/cli/config.json`, and pair it with
the acceptance rule (section 5) in `~/.zcode/AGENTS.md` so every
session knows when to call the tool, not just how.
A user-scope install pins every workspace to this machine's build:
- After changing `src/`, run `npm run build` — other sessions keep
loading the old `dist/` until you do.
- Moving or deleting the repo directory breaks every session at once.
Works from a git remote today — no registry needed. The `prepare`
script builds `dist/` on install, so `npx` handles the rest:
```json
{
"command": "npx",
"args": ["-y", "github:newlix/gavel-mcp#v0.5.0"]
}
```
Pin a tag (`#v0.5.0`) to make the npx cache stable; without one you
track the default branch and cache refresh is at npx's discretion.
First start on a machine pays a one-time clone + install + build.
Once published to npm, `["-y", "gavel-mcp"]` is equivalent and skips
the git requirement. Any other MCP host works too; only the config
shape differs.
### 3. Restart the session
MCP servers connect at session start. An already-running session will
not pick the server up.
### 4. Verify
- ZCode: **Settings → MCP** shows `gavel` connected.
- Or simply ask the agent to call `gavel_acceptance` with
`cmd: "test -d ."` — expect `verdict=pass exit=0`.
### 5. The rule (AGENTS.md)
The tool is the structure; the rule tells the agent when to use it.
Drop this into `<repo>/AGENTS.md`:
```markdown
## Acceptance
- Done = `gavel_acceptance` returned exit 0. One self-contained
command, cold from the repo root; report the verdict and the
command itself — never a paraphrase of test results.
- The command asserts intent (what should happen), not the
implementation.
- `refused` means it never ran. Report it verbatim.
```
For user-scope installs, the same block goes in `~/.zcode/AGENTS.md`
instead — user instructions load first, so a repo's own AGENTS.md can
still narrow the rule per project.
## The contract
The oracle never trusts a paraphrased result — it runs the command
itself, so a red acceptance cannot be narrated green. Two structural
layers, cheapest first:
1. **Lint** (`src/lint.ts`): a command that cannot fail (`true`,
`exit 0`, bare echo/printf, `x && true` with no real check in it)
is refused before execution — `passed: false`, `refused: <reason>`,
no receipt minted. The Go linter's syntax and destructive-pattern
checks are deliberately dropped: syntax fails identically when
executed, and policing dangerous commands is the host permission
layer's job, not the verdict layer's.
2. **Cold run** (`src/runner.ts`): the command runs via the platform
shell from the project root; exit code 0 is the only pass. Signal
deaths report 128+signal, spawn failure -1, command-not-found 127.
Receipt semantics: `refused` = never executed. Report it verbatim.
## Tools
### `gavel_acceptance(cmd, cwd?, timeout_sec?)`
→ `{ passed, exit_code, duration_ms, refused, output }`
- `output`: merged stdout+stderr, raw; head+tail with a marker when
longer than ~20 KB.
- A timeout kills the whole process tree and fails the run.
## Troubleshooting
- **Server not connected** (Settings → MCP shows an error): the dist
path is wrong or `npm run build` was skipped. The path must be
absolute and point at `dist/index.js`.
- **`exit_code: 127`**: the acceptance command itself was not found.
## Dev
```
npm install
npm test # node:test via tsx (24 tests)
npm run build # tsc → dist/
```
Layout: `src/index.ts` is the thin stdio bootstrap; the MCP surface
(`buildServer`) lives in `src/server.ts` so tests can drive it
in-process over `InMemoryTransport` plus one cold stdio smoke via
tsx. The manual smoke below is the same exchange the stdio test runs.
Manual smoke (MCP stdio is newline-delimited JSON):
```
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"gavel_acceptance","arguments":{"cmd":"test -d ."}}}' \
| node dist/index.js
```
TDQS
A4.5/5.0
Scored across 1 tool
Disambiguation5/5
There is only one tool, so there is no possibility of an agent confusing it with another. Its purpose is clearly distinct by default.
Naming Consistency4/5
With a single tool there are no conflicting conventions to penalize. The name gavel_acceptance is clear and follows snake_case, though it is noun-oriented rather than a verb_noun pattern.
Tool Count4/5
One tool is minimal, but the server appears purpose-built for a single acceptance action, so the count is slightly under the typical range yet reasonable for the stated scope.
Completeness5/5
The tool fully covers the server's apparent purpose: running an acceptance command and returning a mechanical verdict with well-specified exit-code and output handling. No obvious operations are missing for this narrow domain.
Maintenance
ActivityMaintained
ResponsivenessNo issues