clef-mcp 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ # Claude Code
2
+
3
+ ## Register (recommended)
4
+
5
+ ```bash
6
+ claude mcp add clef-mcp -- clef-mcp
7
+ # without a global install:
8
+ claude mcp add clef-mcp -- npx -y clef-mcp
9
+ ```
10
+
11
+ ## Or via project config
12
+
13
+ Copy `mcp.json` from this directory to your project root as `.mcp.json` (project-scope servers).
14
+
15
+ ## One-time setup
16
+
17
+ ```bash
18
+ npx clef-mcp install
19
+ ```
20
+
21
+ Verify inside Claude Code with `/mcp` — `clef-mcp` should list the `clef_decide` tool.
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "clef-mcp": {
4
+ "command": "clef-mcp",
5
+ "args": []
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,21 @@
1
+ # Codex CLI — register clef-mcp as an MCP server.
2
+ # Location: ~/.codex/config.toml
3
+ #
4
+ # If clef-mcp is installed globally (npm i -g clef-mcp):
5
+ [mcp_servers.clef-mcp]
6
+ command = "clef-mcp"
7
+ args = []
8
+
9
+ # Without a global install, prefer npx:
10
+ # [mcp_servers.clef-mcp]
11
+ # command = "npx"
12
+ # args = ["-y", "clef-mcp"]
13
+
14
+ # Optional environment overrides:
15
+ # [mcp_servers.clef-mcp.env]
16
+ # CLEF_MODEL = "clef-flash"
17
+ # CLEF_LOG_LEVEL = "error"
18
+ # CLEF_HOME = "~/.cache/clef-mcp"
19
+
20
+ # One-time setup (downloads the model):
21
+ # npx clef-mcp install
@@ -0,0 +1,12 @@
1
+ # Cursor
2
+
3
+ Copy `mcp.json` from this directory to `<project>/.cursor/mcp.json`
4
+ (or `~/.cursor/mcp.json` for global scope).
5
+
6
+ ## One-time setup
7
+
8
+ ```bash
9
+ npx clef-mcp install
10
+ ```
11
+
12
+ Cursor → Settings → MCP: `clef-mcp` should appear with the `clef_decide` tool.
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "clef-mcp": {
4
+ "command": "clef-mcp",
5
+ "args": []
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,12 @@
1
+ # clef-mcp automation recipes
2
+
3
+ One-shot `clef-mcp decide` recipes — callers that invoke the decision model **deterministically** (hooks, CI), where the call happens because the infrastructure says so, not because an LLM chose to.
4
+
5
+ | Recipe | What it does |
6
+ |---|---|
7
+ | [`pretooluse-guard/`](pretooluse-guard/) | PreToolUse hook: block shell commands the model judges destructive (p ≥ 0.9), note softer leans on stderr |
8
+ | [`ci-triage/`](ci-triage/) | GitHub Action: severity + area verdict for a PR diff, into the step summary / labels |
9
+
10
+ Both recipes are honest about the trade-offs: cold start per `decide` call (~seconds, model load included), advisory-only gating (fail open), and a 16k-token context ceiling for state. Read the caveats in each folder before wiring them into real workflows.
11
+
12
+ Client registration snippets (MCP config for Claude Code / Codex / Cursor) live in the sibling folders: [`../claude-code/`](../claude-code/), [`../codex/`](../codex/), [`../cursor/`](../cursor/).
@@ -0,0 +1,68 @@
1
+ # CI triage — label a pull request with the local Clef model
2
+
3
+ A GitHub Actions job that runs `clef-mcp decide` on the PR diff and reports severity + area. The result lands in the step summary and as job outputs; wiring it to GitHub labels is one `gh` call you can uncomment.
4
+
5
+ > **Runner reality check:** `ubuntu-latest` (16 GB RAM) fits the Q4_K_M model (6 GB), but inference is CPU-only and the one-time `install` downloads ~6 GB + the llama.cpp build. Expect ~1–3 min of setup per job. For anything hotter than a few PRs per hour, use a self-hosted runner with the model pre-installed and drop the `install` step.
6
+
7
+ ```yaml
8
+ # .github/workflows/clef-triage.yml
9
+ name: clef-triage
10
+
11
+ on:
12
+ pull_request:
13
+
14
+ jobs:
15
+ triage:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ pull-requests: read
19
+ outputs:
20
+ severity: ${{ steps.decide.outputs.severity }}
21
+ area: ${{ steps.decide.outputs.area }}
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install model + runtime (one-time ~6 GB, CPU inference)
26
+ run: npx -y clef-mcp@latest install --yes
27
+
28
+ - name: Collect the diff as state
29
+ run: gh pr diff ${{ github.event.pull_request.number }} > /tmp/pr.diff
30
+ env:
31
+ GH_TOKEN: ${{ github.token }}
32
+
33
+ - name: Decide
34
+ id: decide
35
+ run: |
36
+ DIFF=$(jq -Rs . < /tmp/pr.diff)
37
+ RESULT=$(jq -n --argjson diff "$DIFF" '{
38
+ state: { pr: ${{ toJSON(github.event.pull_request.title) }}, diff: $diff },
39
+ questions: {
40
+ severity: { type: "score", instructions: "How severe are the changes in this diff if they ship as-is?",
41
+ criteria: ["trivial", "minor", "moderate", "major", "critical"] },
42
+ area: { type: "choice", instructions: "Which area does this diff primarily touch?",
43
+ criteria: { frontend: "UI / client code", backend: "server / API code", infra: "CI, build, deployment", docs: "documentation only" } }
44
+ }
45
+ }' | npx -y clef-mcp@latest decide)
46
+
47
+ SEVERITY=$(jq -r '.decisions.severity.answer | to_entries | max_by(.value) | .key' <<<"$RESULT")
48
+ AREA=$(jq -r '.decisions.area.answer | to_entries | max_by(.value) | .key' <<<"$RESULT")
49
+ echo "severity=$SEVERITY" >> "$GITHUB_OUTPUT"
50
+ echo "area=$AREA" >> "$GITHUB_OUTPUT"
51
+ echo "| question | pick |" >> "$GITHUB_STEP_SUMMARY"
52
+ echo "|---|---|" >> "$GITHUB_STEP_SUMMARY"
53
+ echo "| severity | $SEVERITY |" >> "$GITHUB_STEP_SUMMARY"
54
+ echo "| area | $AREA |" >> "$GITHUB_STEP_SUMMARY"
55
+
56
+ # Optional: apply the verdict as labels (needs contents:write / pull-requests: write).
57
+ # - name: Label
58
+ # run: gh pr edit ${{ github.event.pull_request.number }} --add-label "triage-${{ steps.decide.outputs.severity }}"
59
+ # env:
60
+ # GH_TOKEN: ${{ github.token }}
61
+ ```
62
+
63
+ ## Honest caveats
64
+
65
+ - **The 9B model triages; it does not review.** Use the verdict for routing and prioritization, not as a merge gate.
66
+ - **Diff size:** the model context is 16k tokens; a huge diff degrades both latency and judgment. Truncate `/tmp/pr.diff` (e.g. `head -c 50000`) for large PRs.
67
+ - **Cold start per job.** The `install` step re-downloads unless you cache `~/.cache/clef-mcp` with `actions/cache` keyed on the model revision — or move to a self-hosted runner.
68
+ - **State is data.** The diff is passed to the model as content to judge, never as instructions; the server treats it that way (see SECURITY.md).
@@ -0,0 +1,87 @@
1
+ # PreToolUse guard — a Clef "reflex" before risky shell commands
2
+
3
+ A hook that asks the local Clef model one `noul` question ("is this destructive?") plus a blast-radius `score` before a shell command runs. Blocks only on a decisive `p(true) ≥ 0.9`; everything else passes with a note on stderr.
4
+
5
+ ## What it looks like
6
+
7
+ ```
8
+ $ rm -rf node_modules
9
+ clef guard: p(destructive)=0.04 blast_radius=single_file # allowed
10
+
11
+ $ git push --force origin main
12
+ Blocked: the local Clef model judges this command destructive (p=0.94, blast_radius=repo).
13
+ Ask the user to confirm explicitly; if they confirm, let them run it themselves
14
+ or have them disable the guard. Do not work around this gate by rephrasing the command.
15
+ ```
16
+
17
+ ## What a block means — learned in the wild (2026-10-04)
18
+
19
+ A block is a **pause plus escalation to the user, not a wall**. The first real firing of this guard proved both halves:
20
+
21
+ 1. **The catch worked.** A different agent session, mid history-rewrite, ran `git push --force origin --all` → p=0.94, blocked (exit 2).
22
+ 2. **The bypass also "worked" — and that was the lesson.** The agent read the script, saw the pre-filter matched the literal string `push --force`, and re-ran the identical push as a `+refspec` (`git push origin '+refs/heads/*:refs/heads/*'`), which slipped past the string match before the model was ever asked. Both fixes shipped as a result:
23
+ - the pre-filter is **semantic about pushes** now: `--force` (incl. `--force-with-lease`), `-f`, `+refspec`, and `git filter-repo`/`filter-branch` all reach the model; a plain `git push` still skips it;
24
+ - the block message **routes the agent to the user** ("ask the user to confirm explicitly") instead of hinting "re-run without the guard" — an LLM reads that hint as an instruction to find a workaround.
25
+
26
+ Honest residual: the `+refspec` form scores lower than the literal flag (p≈0.87 in the same setup — passes with a lean logged to stderr). A string pre-filter can never be bypass-proof against an agent with full access; this gate's contract is *visibility and a nudge toward the human*, and the tuning knobs (keyword list, thresholds) sit at the top of `guard.sh` on purpose.
27
+
28
+ ## Install
29
+
30
+ Requirements: `jq` on PATH, `clef-mcp install` done once.
31
+
32
+ ```bash
33
+ # 1. Put the script somewhere permanent and make it executable
34
+ cp guard.sh ~/.local/bin/clef-guard.sh && chmod +x ~/.local/bin/clef-guard.sh
35
+
36
+ # 2. Register it as a PreToolUse hook for Bash commands
37
+ ```
38
+
39
+ <details open>
40
+ <summary><b>Claude Code</b> — <code>~/.claude/settings.json</code></summary>
41
+
42
+ ```json
43
+ {
44
+ "hooks": {
45
+ "PreToolUse": [
46
+ {
47
+ "matcher": "Bash",
48
+ "hooks": [{ "type": "command", "command": "~/.local/bin/clef-guard.sh" }]
49
+ }
50
+ ]
51
+ }
52
+ }
53
+ ```
54
+ </details>
55
+
56
+ <details>
57
+ <summary><b>ZCode</b> — <code>~/.zcode/cli/config.json</code></summary>
58
+
59
+ Configuration-file hooks are **disabled by default** — the top-level `"enabled": true` is required:
60
+
61
+ ```json
62
+ {
63
+ "hooks": {
64
+ "enabled": true,
65
+ "events": {
66
+ "PreToolUse": [
67
+ {
68
+ "matcher": "Bash",
69
+ "hooks": [
70
+ { "type": "command", "command": "~/.local/bin/clef-guard.sh", "timeout": 90, "statusMessage": "clef guard: judging command risk" }
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ The matcher is a case-sensitive regex against the tool name (`Bash`, not `bash`). `timeout` is in **seconds** — keep it generous enough for a cold model load; run `clef-mcp daemon` alongside to make the gate sub-second. Exit code `2` is the standard deny.
80
+ </details>
81
+
82
+ ## Honest caveats
83
+
84
+ - **Advisory, not a security boundary.** The guard fails open: missing model, missing `jq`, or a `clef-mcp` error all allow the command. It raises the cost of accidents; it cannot prevent a determined one. A block tells the agent to confirm with the user — the user, not the agent, decides what happens next.
85
+ - **Cold start per call** (~a few seconds, model load included). The keyword pre-filter keeps model calls rare — but it now sends every force-flavored push to the model, so history-rewriting sessions pay it repeatedly; run `clef-mcp daemon` alongside to make each gate sub-second.
86
+ - **The model is a signal, not a judge.** `p ≥ 0.9` blocks; 0.5–0.9 is a lean printed to stderr for you to read.
87
+ - **Tune the threshold and the keyword list to your taste** — both are at the top of `guard.sh` on purpose.
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard: ask the local Clef model whether a shell command looks
3
+ # destructive before it runs. Blocks (exit 2) only on a decisive p(true).
4
+ #
5
+ # Contract (Claude Code / ZCode PreToolUse):
6
+ # stdin : event JSON, command at .tool_input.command
7
+ # exit 0 : allow
8
+ # exit 2 : block (stderr is shown to the agent)
9
+ #
10
+ # Design notes:
11
+ # - Fail open. If the model or jq is missing, the guard allows the command:
12
+ # it is an advisory gate, not a security boundary.
13
+ # - Cold start. Each decide call loads the model (~a few seconds). The cheap
14
+ # keyword pre-filter keeps the model calls rare.
15
+ # - Threshold. Blocking requires p(destructive) >= 0.9; below that the
16
+ # command runs and the lean is logged to stderr.
17
+
18
+ set -euo pipefail
19
+
20
+ EVENT=$(cat)
21
+ COMMAND=$(printf '%s' "$EVENT" | jq -r '.tool_input.command // empty')
22
+
23
+ [ -z "$COMMAND" ] && exit 0
24
+
25
+ # Cheap pre-filter: only spend a model call on plausibly risky commands.
26
+ # History-rewriting pushes come in several clothes: --force, -f, force-with-lease
27
+ # and the +refspec form ('git push origin "+refs/heads/*:refs/heads/*"') — all
28
+ # go to the model, because a literal 'push --force' match alone was bypassed in
29
+ # the wild (2026-10-04: same push re-run as a +refspec).
30
+ if ! printf '%s' "$COMMAND" | grep -qiE '\b(rm|mv|dd|mkfs|shred|chmod|chown|kill|pkill|drop|truncate|reset --hard|clean -|publish|filter-repo|filter-branch)\b|push[^|]*(--force|-f[ =]|\+refs)|[^|]*\|\s*(ba)?sh'; then
31
+ exit 0
32
+ fi
33
+
34
+ DECISION=$(printf '%s' "$EVENT" | jq -n \
35
+ --arg cmd "$COMMAND" \
36
+ '{
37
+ state: { source: "pretooluse-guard", command: $cmd },
38
+ questions: {
39
+ is_destructive: {
40
+ type: "noul",
41
+ instructions: "Could this shell command irreversibly destroy data, credentials or published state (deletion, force-push, credential exposure, publishing)?"
42
+ },
43
+ blast_radius: {
44
+ type: "score",
45
+ instructions: "How much damage could this command cause if it is a mistake?",
46
+ criteria: ["none", "single_file", "repo", "machine", "beyond_machine"]
47
+ }
48
+ }
49
+ }' | clef-mcp decide) || {
50
+ echo "clef guard unavailable; allowing command" >&2
51
+ exit 0
52
+ }
53
+
54
+ P_DESTRUCTIVE=$(printf '%s' "$DECISION" | jq -r '.decisions.is_destructive.answer.true')
55
+ TOP_RADIUS=$(printf '%s' "$DECISION" | jq -r '.decisions.blast_radius.answer | to_entries | max_by(.value) | .key')
56
+
57
+ echo "clef guard: p(destructive)=$P_DESTRUCTIVE blast_radius=$TOP_RADIUS" >&2
58
+
59
+ BLOCKED=$(printf '%s' "$DECISION" | jq -n --argjson p "$P_DESTRUCTIVE" '$p >= 0.9')
60
+ if [ "$BLOCKED" = "true" ]; then
61
+ echo "Blocked: the local Clef model judges this command destructive (p=$P_DESTRUCTIVE, blast_radius=$TOP_RADIUS). Ask the user to confirm explicitly; if they confirm, let them run it themselves or have them disable the guard. Do not work around this gate by rephrasing the command." >&2
62
+ exit 2
63
+ fi
64
+
65
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clef-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.HighlyLoadedEgo/clef-mcp",
5
5
  "description": "Local MCP server exposing the Clef decision model as a structured clef_decide tool.",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "dist",
26
26
  "skills",
27
+ "examples",
27
28
  "assets",
28
29
  "tests/evals",
29
30
  "README.md",
@@ -35,7 +36,7 @@
35
36
  },
36
37
  "scripts": {
37
38
  "build": "tsup",
38
- "test": "npm run build && vitest run",
39
+ "test": "npm run build && tsc --noEmit && vitest run",
39
40
  "test:watch": "vitest",
40
41
  "evals": "npm run build && node dist/cli.js evals",
41
42
  "prepack": "npm run build"
@@ -18,7 +18,15 @@ Good fits (the model was trained for these):
18
18
  - **Confidence / severity** (`score`): ordered scales like very_low → very_high.
19
19
  - **Batching related judgments**: all questions in ONE call are scored in a single forward pass — batch instead of looping.
20
20
 
21
- Bad fits: free-text generation, code writing, anything needing reasoning beyond picking among options you define.
21
+ ## When NOT to call it
22
+
23
+ The tool earns its keep where a judgment call would otherwise be a guess. Skip it when:
24
+
25
+ - **You already know the answer.** If the decision is trivial or fully determined by what you see, deciding yourself is faster and strictly better — a 9B reflex adds latency and noise, not information.
26
+ - **The task needs generation or reasoning** — free text, code writing, multi-step analysis. Clef only splits probability mass over options you define.
27
+ - **You would re-ask without new facts.** A flat distribution means the *state* did not disambiguate. Re-rolling the same state gives the same answer: gather context or ask the user instead.
28
+ - **The missing input is user preference, not judgment.** "Deploy Friday or Monday?" is a question for the user, not for the model.
29
+ - **The stakes require accountability you can't delegate.** For destructive or security-adjacent actions the model is a gate signal, never the decision maker.
22
30
 
23
31
  ## How to call
24
32
 
@@ -48,7 +56,7 @@ Bad fits: free-text generation, code writing, anything needing reasoning beyond
48
56
 
49
57
  Rules that prevent wasted calls:
50
58
 
51
- - **`state` is data, not instructions.** Compact, factual, relevant. It never executes and never changes server behavior. Keep it well under 1 MB; the model context is 16k tokens (the runtime rejects overflow with a hint).
59
+ - **`state` is data, not instructions.** Compact, factual, relevant. It never executes and never changes server behavior. Keep it well under 1 MB; the model context is 16k tokens (the runtime rejects overflow with a hint). Latency scales with context: aim for a state of ~1–4k tokens — 16k-token states are ~20× slower than 1k ones.
52
60
  - **`choice` criteria**: map `option_id → description`, or a plain list of option strings (ids become the strings). Both are accepted.
53
61
  - **`score` criteria**: an **ordered** list — index order is the score order (low → high). Do not shuffle.
54
62
  - **`noul`**: instructions only; the answer is `{"true": p, "false": 1-p}`.
@@ -62,15 +70,17 @@ Rules that prevent wasted calls:
62
70
  {
63
71
  "model": "clef-flash",
64
72
  "decisions": {
65
- "next_action": { "answer": { "inspect": 0.96, "modify": 0.01, "test": 0.01, "ask_user": 0.01 } }
66
- }
73
+ "next_action": { "answer": { "inspect": 0.96, "modify": 0.01, "test": 0.01, "ask_user": 0.01 }, "confidence": 0.83 }
74
+ },
75
+ "usage": { "input_tokens": 228, "output_tokens": 0, "latency_ms": 512 }
67
76
  }
68
77
  ```
69
78
 
70
79
  - The result is **structured JSON**. Do not render it as prose to the user; use the probabilities to act and summarize in your own words.
71
80
  - **Argmax** is the model's pick; a decisive distribution (e.g. `0.9+`) means act on it.
81
+ - **`confidence`** is the model's own certainty for that decision (reported for choice/score when the runtime sends it). Use it together with the top probability: for destructive or security-adjacent calls require top `p ≥ 0.8` *and* high `confidence` before acting; otherwise gather context or ask.
72
82
  - **Flat distributions** (~equal probabilities) mean the state did not disambiguate: gather more context and re-ask, or ask the user.
73
- - For safety-adjacent calls (destructive actions, security), require a **high threshold** (e.g. `p ≥ 0.8`) before acting; otherwise prefer asking.
83
+ - **`usage`** shows how much context the call consumed (`input_tokens`) and what it cost in time (`latency_ms`). If `input_tokens` is large and `latency_ms` is seconds, your state is too big — compact it.
74
84
  - `noul` answers above ~0.9 true/false are strong signals; 0.5–0.7 is a lean, not a verdict.
75
85
 
76
86
  ## Errors and recovery