@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 +19 -0
- package/README.md +187 -0
- package/dist/client.d.ts +544 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1074 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/resources.d.ts +19 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +281 -0
- package/dist/resources.js.map +1 -0
- package/dist/tokens.d.ts +69 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +90 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tools.d.ts +115 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +868 -0
- package/dist/tools.js.map +1 -0
- package/package.json +44 -0
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
|
+
```
|