@frmwrkd/snag-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+
17
+ Copyright 2026 Snag contributors
18
+
19
+ Full text: https://www.apache.org/licenses/LICENSE-2.0.txt
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @frmwrkd/snag-mcp
2
+
3
+ stdio MCP server exposing the Snag queue to Claude Code. **Five tools, four resources.**
4
+
5
+ **Licence:** Apache-2.0 · Spec: `docs/02-BACKEND-SPEC.md` §9. Locked decisions: `docs/00-MASTER-BUILD-PLAN.md` §7 — do not relitigate them.
6
+
7
+ ---
8
+
9
+ ## The number that shapes this package
10
+
11
+ Claude Code **warns at 10,000 tokens of MCP tool output and hard-caps at
12
+ 25,000** (`MAX_MCP_OUTPUT_TOKENS`, doc 03 §4.3). Chrome DevTools MCP ships
13
+ ~17,000 tokens of tool *definitions* before a single call, and pays that on
14
+ every turn.
15
+
16
+ So: `.snag/` files carry the bulk, and MCP carries actions and filtered
17
+ queries. Measured on this server:
18
+
19
+ | | tokens |
20
+ |---|---|
21
+ | Tool definitions, every turn | **~2,900** |
22
+ | `snag_list` on a 20-task queue | ~600 |
23
+ | `snag_get` on one task | ~500 |
24
+ | A screenshot, if you ask for one | ~1,568 visual |
25
+
26
+ The regression guards live in `test/tools.test.ts`; they fail if a sixth tool
27
+ appears or the definitions cross 4,000 tokens.
28
+
29
+ ---
30
+
31
+ ## Install
32
+
33
+ `@frmwrkd/snag-mcp` is not published yet, so the server runs from a local build:
34
+
35
+ ```powershell
36
+ npx pnpm@10.33.0 build
37
+ node scripts/install-claude-code.mjs "C:\code\my-app"
38
+ ```
39
+
40
+ The installer writes `.mcp.json` with an absolute `node` command, plus the
41
+ skill, hooks and settings — see `integrations/claude-code/README.md`:
42
+
43
+ ```json
44
+ // .mcp.json, project-scoped — machine-specific: do not commit it yet
45
+ { "mcpServers": { "snag": { "type": "stdio", "command": "node", "args": ["L:/snag/apps/mcp/dist/index.js"] } } }
46
+ ```
47
+
48
+ That absolute path exists only on the machine that ran the installer, which
49
+ warns about it every run. Until `@frmwrkd/snag-mcp` is published and the entry can be
50
+ `npx -y @frmwrkd/snag-mcp`, keep `.mcp.json` out of commits.
51
+
52
+ The server is spawned with the project directory as its cwd and finds `.snag/`
53
+ by walking up from there — skipping `~/.snag/`, which is snagd's token
54
+ directory, not a project. It only talks to a snagd whose `/health` `root` is
55
+ this project — or, when no such daemon answers, one serving another `git
56
+ worktree` of the same repository (same `git rev-parse --git-common-dir`). A
57
+ snagd for some other project is never used, and a write then fails naming it:
58
+
59
+ > snagd on :7654 serves L:\code\other-app, not L:\code\my-app — open Claude Code
60
+ > in L:\code\other-app, or start snagd with --root L:\code\my-app
61
+
62
+ Before every status change and verify request the cached daemon's `/health` is
63
+ asked again, so a port another project's snagd took over after a restart is
64
+ dropped and discovery runs again.
65
+
66
+ `snag_resolve` first asks `POST /verify/:taskId` with `dry_run: true`, before it
67
+ moves the task at all. `no_capture_url` (an authored task has no page to
68
+ re-open) leaves the task `in_progress` and says a human must resolve it;
69
+ `verify_disabled` resolves it directly with `verified: false` where §5.3
70
+ allows. Otherwise it moves the task to `verifying` and gets `202` back from the
71
+ real `POST` at once; Verify runs in Chrome afterwards, so the tool tells the
72
+ agent to `snag_get` the task in ~30 s, where `verify` carries the last
73
+ `outcome` and `reason` from `GET /verify/:taskId`. When no extension is
74
+ connected (`503 verify_unavailable`) the daemon's message is passed through
75
+ word for word and the task stays `verifying` — the only refusal that says to
76
+ call `snag_resolve` again.
77
+
78
+ ---
79
+
80
+ ## Tools
81
+
82
+ | Tool | Signature | Returns |
83
+ |---|---|---|
84
+ | `snag_list` | `(status?, wave?, limit=20)` | Compact list: id, title, severity, kind, wave, status, first suspect file + line range. Non-terminal tasks by default. |
85
+ | `snag_get` | `(id)` | The full task via `structuredContent`, plus `resource_link`s to screenshots on disk, a `next_actions` list, and — once Verify is involved — `verify: { running, outcome, reason, last_at }`. |
86
+ | `snag_claim` | `(id)` | `open → claimed`. Returns the full task. |
87
+ | `snag_resolve` | `(id, commit?, note?)` | Verify preflight, then `→ verifying` and queues a verify run. **Never `resolved`** while Verify is enabled. |
88
+ | `snag_screenshot` | `(id, index=0)` | The image inline. Only when explicitly called. |
89
+
90
+ `id` is `SNAG-<n>` or `tsk_<ulid>`; both work everywhere.
91
+
92
+ ### Three invariants
93
+
94
+ 1. **Screenshots are never inlined by `snag_get`** (locked decision D13). They
95
+ go out as `resource_link`s with `file://` paths — Claude Code can `Read` a
96
+ local PNG directly, which sidesteps the output cap entirely.
97
+ 2. **`snag_resolve` never sets `resolved`.** doc 02 §5.3: "`resolved` is only
98
+ ever set by Verify, never by the agent directly." The single documented
99
+ exception is `plan.verify_on_resolve: false` in snagd's `.snag/config.json`
100
+ — reported by the verify preflight as `verify_disabled` — in which case the
101
+ task goes straight to `resolved` with `verified: false`, and the daemon
102
+ still gates that edge on the config value. Verify itself re-opens the page,
103
+ re-resolves the anchor and checks the captured console errors; it does not
104
+ compare pixels yet, and a capture with no console error goes to
105
+ `needs_review`.
106
+ 3. **`next_actions` is Jam's `getDetails` pattern.** One cheap call tells the
107
+ agent which expensive calls are worth making: it suggests
108
+ `snag_screenshot` when `kind` is `ux`/`a11y`/`layout`, and explicitly tells
109
+ the agent not to bother otherwise.
110
+
111
+ ## Resources
112
+
113
+ `snag://queue` · `snag://plan` · `snag://task/{id}` · `snag://map`
114
+
115
+ Surfaced in Claude Code as `@snag:snag://…` autocompletes.
116
+
117
+ `snag://task/{id}` returns the **bytes of `.snag/tasks/SNAG-<n> - <title>.md`
118
+ unchanged**, so reading a task through MCP and reading it with `Read` produce
119
+ character-identical text. The frontmatter carries edit anchors; they must not
120
+ be re-rendered.
121
+
122
+ Every resource is capped: if a payload would exceed the client's warning
123
+ threshold it projects itself down to a summary and names the file on disk.
124
+ Losing detail loudly beats a client truncating mid-JSON.
125
+
126
+ ---
127
+
128
+ ## How it gets its data
129
+
130
+ ```
131
+ snag_list / snag_get / resources → snagd HTTP → falls back to .snag/ files
132
+ snag_claim / snag_resolve → /health re-checked → snagd HTTP → hard error if absent
133
+ snag_screenshot → .snag/shots/*.png on disk
134
+ ```
135
+
136
+ Reads prefer the daemon because the SQLite index carries live status;
137
+ `.snag/plan.json` is a snapshot whose statuses go stale the moment a task is
138
+ claimed. When the daemon is unreachable the fallback still answers, and every
139
+ response is labelled `source: "daemon" | "files"` so a stale status is a known
140
+ stale status.
141
+
142
+ Writes require the daemon. Editing `.snag/tasks/*.md` from here would race the
143
+ daemon's file watcher and desynchronise the index, so `snag_claim` and
144
+ `snag_resolve` fail with an error that names the fix:
145
+
146
+ > snagd is not running (probed http://127.0.0.1:7654-7664 and /repo/.snag/.port).
147
+ > Start it with `npx snagd` in /repo. Until then the queue is still readable
148
+ > without me — Read/Grep .snag/tasks/\*.md and .snag/plan.json.
149
+
150
+ **Port discovery** reads `.snag/.port` first, then probes 7654–7664 for a
151
+ `GET /health` that identifies itself as `snagd` (doc 02 §7.3 — nothing
152
+ hard-codes 7654). **Auth** is the token from `~/.snag/token`, sent as
153
+ `Authorization: Bearer`.
154
+
155
+ ---
156
+
157
+ ## Implementation notes
158
+
159
+ **Why the low-level `Server` and not `McpServer`.** `McpServer.registerTool`
160
+ generates each tool's wire schema itself, at draft-7. The MCP `2026-07-28`
161
+ spec says tool schemas are JSON Schema **2020-12**, and `@frmwrkd/snag-core`'s
162
+ `toInlineJsonSchema` emits exactly that. Going through the low-level `Server`
163
+ keeps `@frmwrkd/snag-core` the single source of truth *and* makes the emitted bytes
164
+ inspectable, which is what lets the tests assert their token cost.
165
+
166
+ **One interop wrinkle, found by running it.** The SDK's own client validates
167
+ `structuredContent` against the declared `outputSchema` with an Ajv instance
168
+ built from `ajv`'s **draft-07** entry point. Draft-07 has no `prefixItems`, so
169
+ the spec-correct 2020-12 tuple for `Task.suspect_files[].lines`
170
+ (`prefixItems` + `items: false`) was rejected with
171
+
172
+ > MCP error -32602: … data/tasks/0/suspect_lines/0 boolean schema is false
173
+
174
+ `openTupleTail` in `src/tools.ts` drops the `items: false` when `maxItems`
175
+ already pins the arity — a no-op under 2020-12, and the difference between
176
+ working and not under draft-07. `test/schema-interop.test.ts` runs the SDK's
177
+ real validator over the real schemas so it cannot regress.
178
+
179
+ ---
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ pnpm --filter @frmwrkd/snag-mcp build
185
+ pnpm --filter @frmwrkd/snag-mcp test # vitest, 160 tests
186
+ pnpm --filter @frmwrkd/snag-mcp typecheck
187
+ ```