npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@frmwrkd/snag-mcp

v0.1.3

Published

The Snag MCP server for Claude Code: five tools over .snag/.

Readme

@frmwrkd/snag-mcp

stdio MCP server exposing the Snag queue to Claude Code. Five tools, four resources.

Licence: Apache-2.0 · Spec: docs/02-BACKEND-SPEC.md §9. Locked decisions: docs/00-MASTER-BUILD-PLAN.md §7 — do not relitigate them.


The number that shapes this package

Claude Code warns at 10,000 tokens of MCP tool output and hard-caps at 25,000 (MAX_MCP_OUTPUT_TOKENS, doc 03 §4.3). Chrome DevTools MCP ships ~17,000 tokens of tool definitions before a single call, and pays that on every turn.

So: .snag/ files carry the bulk, and MCP carries actions and filtered queries. Measured on this server:

| | tokens | |---|---| | Tool definitions, every turn | ~2,900 | | snag_list on a 20-task queue | ~600 | | snag_get on one task | ~500 | | A screenshot, if you ask for one | ~1,568 visual |

The regression guards live in test/tools.test.ts; they fail if a sixth tool appears or the definitions cross 4,000 tokens.


Install

@frmwrkd/snag-mcp is not published yet, so the server runs from a local build:

npx [email protected] build
node scripts/install-claude-code.mjs "C:\code\my-app"

The installer writes .mcp.json with an absolute node command, plus the skill, hooks and settings — see integrations/claude-code/README.md:

// .mcp.json, project-scoped — machine-specific: do not commit it yet
{ "mcpServers": { "snag": { "type": "stdio", "command": "node", "args": ["L:/snag/apps/mcp/dist/index.js"] } } }

That absolute path exists only on the machine that ran the installer, which warns about it every run. Until @frmwrkd/snag-mcp is published and the entry can be npx -y @frmwrkd/snag-mcp, keep .mcp.json out of commits.

The server is spawned with the project directory as its cwd and finds .snag/ by walking up from there — skipping ~/.snag/, which is snagd's token directory, not a project. It only talks to a snagd whose /health root is this project — or, when no such daemon answers, one serving another git worktree of the same repository (same git rev-parse --git-common-dir). A snagd for some other project is never used, and a write then fails naming it:

snagd on :7654 serves L:\code\other-app, not L:\code\my-app — open Claude Code in L:\code\other-app, or start snagd with --root L:\code\my-app

Before every status change and verify request the cached daemon's /health is asked again, so a port another project's snagd took over after a restart is dropped and discovery runs again.

snag_resolve first asks POST /verify/:taskId with dry_run: true, before it moves the task at all. no_capture_url (an authored task has no page to re-open) leaves the task in_progress and says a human must resolve it; verify_disabled resolves it directly with verified: false where §5.3 allows. Otherwise it moves the task to verifying and gets 202 back from the real POST at once; Verify runs in Chrome afterwards, so the tool tells the agent to snag_get the task in ~30 s, where verify carries the last outcome and reason from GET /verify/:taskId. When no extension is connected (503 verify_unavailable) the daemon's message is passed through word for word and the task stays verifying — the only refusal that says to call snag_resolve again.


Tools

| Tool | Signature | Returns | |---|---|---| | snag_list | (status?, wave?, limit=20) | Compact list: id, title, severity, kind, wave, status, first suspect file + line range. Non-terminal tasks by default. | | snag_get | (id) | The full task via structuredContent, plus resource_links to screenshots on disk, a next_actions list, and — once Verify is involved — verify: { running, outcome, reason, last_at }. | | snag_claim | (id) | open → claimed. Returns the full task. | | snag_resolve | (id, commit?, note?) | Verify preflight, then → verifying and queues a verify run. Never resolved while Verify is enabled. | | snag_screenshot | (id, index=0) | The image inline. Only when explicitly called. |

id is SNAG-<n> or tsk_<ulid>; both work everywhere.

Three invariants

  1. Screenshots are never inlined by snag_get (locked decision D13). They go out as resource_links with file:// paths — Claude Code can Read a local PNG directly, which sidesteps the output cap entirely.
  2. snag_resolve never sets resolved. doc 02 §5.3: "resolved is only ever set by Verify, never by the agent directly." The single documented exception is plan.verify_on_resolve: false in snagd's .snag/config.json — reported by the verify preflight as verify_disabled — in which case the task goes straight to resolved with verified: false, and the daemon still gates that edge on the config value. Verify itself re-opens the page, re-resolves the anchor and checks the captured console errors; it does not compare pixels yet, and a capture with no console error goes to needs_review.
  3. next_actions is Jam's getDetails pattern. One cheap call tells the agent which expensive calls are worth making: it suggests snag_screenshot when kind is ux/a11y/layout, and explicitly tells the agent not to bother otherwise.

Resources

snag://queue · snag://plan · snag://task/{id} · snag://map

Surfaced in Claude Code as @snag:snag://… autocompletes.

snag://task/{id} returns the bytes of .snag/tasks/SNAG-<n> - <title>.md unchanged, so reading a task through MCP and reading it with Read produce character-identical text. The frontmatter carries edit anchors; they must not be re-rendered.

Every resource is capped: if a payload would exceed the client's warning threshold it projects itself down to a summary and names the file on disk. Losing detail loudly beats a client truncating mid-JSON.


How it gets its data

snag_list / snag_get / resources  →  snagd HTTP  →  falls back to .snag/ files
snag_claim / snag_resolve         →  /health re-checked → snagd HTTP  →  hard error if absent
snag_screenshot                   →  .snag/shots/*.png on disk

Reads prefer the daemon because the SQLite index carries live status; .snag/plan.json is a snapshot whose statuses go stale the moment a task is claimed. When the daemon is unreachable the fallback still answers, and every response is labelled source: "daemon" | "files" so a stale status is a known stale status.

Writes require the daemon. Editing .snag/tasks/*.md from here would race the daemon's file watcher and desynchronise the index, so snag_claim and snag_resolve fail with an error that names the fix:

snagd is not running (probed http://127.0.0.1:7654-7664 and /repo/.snag/.port). Start it with npx snagd in /repo. Until then the queue is still readable without me — Read/Grep .snag/tasks/*.md and .snag/plan.json.

Port discovery reads .snag/.port first, then probes 7654–7664 for a GET /health that identifies itself as snagd (doc 02 §7.3 — nothing hard-codes 7654). Auth is the token from ~/.snag/token, sent as Authorization: Bearer.


Implementation notes

Why the low-level Server and not McpServer. McpServer.registerTool generates each tool's wire schema itself, at draft-7. The MCP 2026-07-28 spec says tool schemas are JSON Schema 2020-12, and @frmwrkd/snag-core's toInlineJsonSchema emits exactly that. Going through the low-level Server keeps @frmwrkd/snag-core the single source of truth and makes the emitted bytes inspectable, which is what lets the tests assert their token cost.

One interop wrinkle, found by running it. The SDK's own client validates structuredContent against the declared outputSchema with an Ajv instance built from ajv's draft-07 entry point. Draft-07 has no prefixItems, so the spec-correct 2020-12 tuple for Task.suspect_files[].lines (prefixItems + items: false) was rejected with

MCP error -32602: … data/tasks/0/suspect_lines/0 boolean schema is false

openTupleTail in src/tools.ts drops the items: false when maxItems already pins the arity — a no-op under 2020-12, and the difference between working and not under draft-07. test/schema-interop.test.ts runs the SDK's real validator over the real schemas so it cannot regress.


Development

pnpm --filter @frmwrkd/snag-mcp build
pnpm --filter @frmwrkd/snag-mcp test        # vitest, 160 tests
pnpm --filter @frmwrkd/snag-mcp typecheck