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

@vncsleal/quillby

v0.4.2

Published

MCP server for multi-client content operations: brand voice memory, daily research briefs, and drafts. Agent-first, no server-side LLM.

Readme

Quillby MCP

MCP server for multi-client content operations: brand voice memory, daily research briefs, and social media drafts.

Spec: MCP 2026-07-28 (stateless) SDK: @modelcontextprotocol/server v2

Architecture: agent-first

The server is the agent's durable memory. It stores per-client knowledge (voice, examples, lessons, sources), daily brief records (harvested items + ranked ideas), and drafts with lifecycle status. It does not generate anything — the host agent (opencode, Claude, etc.) reads the state, thinks in conversation, and writes decisions back. No server-side LLM, no LLM API keys (Sampling was deprecated in the spec, SEP-2577): relevance ranking is deterministic and corpus-relative (inverse document frequency over each run's own harvest, blended with the client's persisted, time-decayed harvest history — a word that shows up in half the news wire earns near-zero credit, a rare term earns full credit; no hardcoded keyword lists anywhere), so the server runs keyless from first boot.

The tools are deliberately few and order-agnostic: no call depends on a prior call, every write is an upsert, and reads default to the selected client. Onboarding can happen in any order — drafts before voice, sources after, corrections anytime.

Protocol-native human decisions (2026-07-28 MRTR + elicitation). The gates that encode human decisions — draft approve / draft reject, brief save, workspace delete — demand in-band consent when the client declares the elicitation capability: the tool returns an input_required result carrying a form-mode elicitation, the client renders it as a native UI, and only an explicit accept completes the call; decline/cancel store nothing. An agent can no longer record a decision the human never made. Clients without the capability keep the agent-mediated flow (confirm flags, conversation-asked reasons) unchanged.

Durable async execution (io.modelcontextprotocol/tasks). When the client declares the io.modelcontextprotocol/tasks extension, the long-running tools — brief run, web fetch, web ingest — run detached and their response carries a durable task handle (taskId, status, ttl, pollInterval) instead of blocking. The client polls tasks/get (resumable across disconnects — handles live in SQLite), cancels cooperatively via tasks/cancel, pages tasks/list, and re-issues the original call via tasks/getPayload. Everyone else keeps the current blocking behavior. The tasks RPCs ride the 2025-era method registry (where today's hosts still live); the 2026-07-28 era's closed method registry has no tasks entries in the current SDK yet, so modern-era clients get the handle but poll through their own extension plumbing.

Quick Start

npm install -g @vncsleal/quillby
quillby-mcp
{ "mcpServers": { "quillby": { "command": "quillby-mcp" } } }

Tools (7)

workspace — the client envelope

| Action | What it does | |---|---| | list | all clients | | get | voice, examples, lessons, sources, last brief date (default action) | | create | name (required), optional segment + initial voice/radar; auto-selects and reports selected: true plus a note naming it the active client — subsequent calls without an id target it | | update | patch name/segment/voice; reason records a lesson | | voice_lint | deterministic check of a draft against the client's voice — banned terms (Unicode-aware boundaries; inflections like unleashing catch banned unleash), preferred terminology, passive voice, sentence lengths, structure (no LLM). Returns structured score/passed/stats/issues plus a human-readable report string; vacuous: true when no voice rules are configured, so a green report is never mistaken for an actual pass | | delete | needs confirm: true (or in-band consent when the client declares the elicitation capability) | | select | set the active client (all other calls default to it) | | sources_add | urls + type (rss) — http(s) format-validated only; feed reachability is checked at brief run time and reported per source; duplicates come back as {url, reason} in skippedUrls | | sources_remove / sources_list | by id or url / list |

Voice fields: pillars, banned (words), vocabulary {preferred, banned}, bannedConstructions, aesthetic, styleRules, preferredTerms {term → preferred}, lint {maxSentenceWords, minSentenceWords, maxSentenceChars}, examples (approved content refs), lessons (auto-appended when updating with a reason).

Radar profile fields (what the brief run's intelligence layer scores against): coreOffer, goals, topics, avoid, location, directUrls, freshnessDays (default 120), locale (en | pt — localizes Google News queries/params, categories, formats, angles, and generated prompts), audience (consumer | business | mixed, default mixed — a deterministic flavor detector penalizes B2B-flavored content for consumer clients and vice-versa; mixed never penalizes).

brief — the daily record

One record per date: radar-ranked items + curated ideas.

| Action | What it does | |---|---| | run | the content radar: collects from the client's sources + Google News (keyless) + key-gated providers (Exa/Tavily/Firecrawl), scores every item against the radar profile with corpus-relative IDF weighting (each matched term credited by its inverse document frequency over the run's own harvest blended with the client's persisted, time-decayed harvest history — generic wire words earn near-zero credit, rare terms full credit, and a small harvest still gets corpus-relative weights from the client's history; multi-word profile entries like "fresh flowers" match as contiguous phrases; no hardcoded keyword lists) plus location matches, freshness, authority, avoid penalties and audience-flavor penalties — with a human-readable why, clusters related items, categorizes (Seasonal / Customer Education / Local Opportunity / Operations / Trend), and returns a ranked digest plus prompt-ready ideas (headline, hook, brief with guardrails, ready-to-paste prompt for the operating LLM). Press releases, product listings, and PT-BR job/class announcements are filtered out and counted in filtered. Location-only matches (no domain hits, no qualifying semantic overlap) are capped at 3, sort after domain-relevant items, and score-penalized — the location lives in its own match channel and never inflates semantic similarity, so a story that merely mentions the city can't outrank a genuine domain match. refresh: true bypasses the ~4h fetch cache for a fresh harvest. The cache is per provider: fresh, error-free rows are served as-is while stale or previously-failed providers are re-fetched, so a transient failure heals on the next run instead of poisoning the cache; the whole cache is dropped automatically when the radar profile (topics/avoid/location/…) or the source list changes, so an edit is never silently served stale items. Items already seen in the last seenWithinDays (default 7) are flagged seen; newItems counts the digest's unseen links and surfaced its already-seen ones. The voice guide is emitted once at the top level (voice) instead of inside every idea. Each idea's angle and headline are specialized on its own evidence (its rarest matched domain term, else its location) so same-category items read as distinct takes, not copies of one category template. Each provider contributes at most 10 digest items, so Google News can't crowd out niche RSS feeds; Local Opportunity categorization requires real location evidence. A run with sources but no topics/coreOffer returns a warning instead of scoring generic noise, a digest that is entirely location-only despite domain terms in the profile returns a warning pointing at the profile, and an all-seen rerun reports lastBriefDate so "when did anything fresh last appear?" is answered in the same call. Backdated runs (date before today) describe that date, not now: items published after the run date are excluded (and items with no provable publish date), age/freshness/decay are scored as of the run date, and the exclusion count lands in the note — harvested keeps the raw count. Location evidence is always surfaced with its original accents and case ("São Paulo", never "sao paulo") in why lines, idea headlines, angles, and prompts. Generated ideas are suggestions — the run persists them on the record as suggestions (view with get; a saved marker flags one the human later curated via save); persist curated ones with save. Every completed run stores its record and says so machine-readably (recordStored: true, stored = items persisted); a no-op run on an unconfigured client stores nothing (recordStored: false). The response defaults to a token-light compact digest (title/url/source/score/category/seen per item, headline/format/angle/reason/source per idea); pass verbose: true for the full shape (snippets, why, prompt-ready briefs — compact: true forces compact even alongside verbose). While a fresh harvest runs, per-provider notifications/progress keep the agent informed, and freshly-fetched providers report their elapsedMs in sources (cache-served rows skip it). Every response carries the workspaceId it operated on; all scores are rounded to 2 decimals | | save | record ranked ideas for a date — ideas is required (pass [] to clear); replaces that date's ideas; stored ideas carry stable idea:<n> ids echoed back in the response and on get, so an agent can reference a curated idea; only after the user confirms the curation (clients with the elicitation capability confirm in-band via input_required) | | get | full record for a date (default today) — radar-ranked items + curated ideas + the run's suggestions (each marked saved when a curated idea references its source link), or a range with from/to (from required with to, from ≤ to); range dedupes items by link keeping the newest occurrence (its date and seen flag); range reads default to the compact shape — pass verbose: true for full items; flags expired ideas; compact: true forces the title/url digest on single-date reads; returns totalItems + truncated when the item cap (30 single / 100 ranged, limit raises it) is hit. Dates are validated as real calendar dates (e.g. 2026-02-30 is rejected) | | list | recent dates with counts — item/idea/suggestion per day — {workspaceId, total, days, truncated} (default 20 per page, max 500, offset pages past the cap) | | delete | remove a date's record — errors when no record exists for the date; echoes the removed date as deletedDate. The seen-memory survives: previously surfaced links are never re-flagged as new |

Idea fields: thesis, angle, platform, source, link, expiresAt (null = evergreen). The weekly view is brief get {from: monday, to: today}.

draft — the post decision log

Nothing is stored as uncommitted scratch. Content enters the record only as a human decision — the agent writes in conversation; the user decides; only then does it hit the disk.

| Action | What it does | |---|---| | approve | content + format (format is required — a decision without one fails at the schema layer; format is trimmed, capped at 40 chars — instagram, linkedin, twitter/x, blog, email, newsletter, tiktok, carousel…) + optional reason, briefDate/ideaIndex provenance — creates an approved record (clients with the elicitation capability confirm the decision in-band first) | | reject | same — creates a rejected record with reason (the anti-example that powers learning); the reason is required, and with elicitation it comes from the user's in-band decision | | list | filters: status, format, from/to date range (from ≤ to), q — free-text search over content and reject reason (the anti-examples that power learning; LIKE wildcards are escaped, so literal %/_ match literally); compact: true omits content — returns {total, items, truncated} (default 50 per page, max 500, offset pages past the cap) | | get / update / delete | by id / content correction or status flip (a flip to rejected needs a fresh reason; a flip to approved clears the stale one) / remove |

note — the editorial memory

Structured notes (research | idea | feedback | decision) with tags and free-text search. The reusable material that briefings fold in.

| Action | What it does | |---|---| | save | title, body, kind, tags, optional source URL (titles are trimmed; whitespace-only titles are rejected) | | update / get / delete | by id | | list | filter by kind, tag, or free-text q — returns {total, items, truncated} (default 50 per page, max 500, offset pages past the cap); pass includeBody: true to include full note bodies (omitted by default) |

briefing — the editorial brief

A structured control sheet: kind-specific template sections (tech-spec, article, update, social-post) each with per-section writing guidance, plus a Research Material section folded from notes. The writing agent completes it in conversation — nothing is generated server-side.

| Action | What it does | |---|---| | create | title (trimmed, non-empty, unique per client), kind, optional noteIds + notesText + sources; nonexistent or foreign noteIds are dropped and reported as skippedNoteIds with per-id reasons in skippedReasons (not found vs belongs to another client) | | get | full control-sheet markdown with the client's voice | | list | optional kind filter — returns {total, items, truncated} (default 50 per page, max 500, offset pages past the cap) | | status | id + status draft|ready | | delete | by id — also removes the briefing's rendered artifacts; the count returns as deletedArtifacts |

artifact — the scaffold store

Deterministic structural scaffolds rendered from briefings (headings + guidance + voice requirements). Only the skeleton is stored — finished content enters the record via draft approve/reject.

| Action | What it does | |---|---| | render | briefingId → stored skeleton (works from any briefing status); marks the briefing ready | | get / list / delete | by id / per briefing ({total, items, truncated}, default 50 per page, max 500, offset pages past the cap) / remove |

web — live web access

Grounds the pipeline in live data without leaving the assistant. No API keys.

| Action | What it does | |---|---| | search | keyless engine chain — DuckDuckGo → Bing → Mojeek (query, maxResults); results are not stored. A bot-block (DuckDuckGo's HTTP 202 anomaly page) or engine failure moves the chain on instead of returning a silent empty list; the response reports which engine served (engine: ddg|bing|mojeek), and engine: none marks a genuine dry query. Multi-token queries keep only results sharing two query tokens (a lone "paulo" overlap must not float an Apostle-Paul biography for a São Paulo query); single-token queries trust the engine | | fetch | fetch a URL, strip chrome, convert the main content to markdown (not stored) — Mozilla Readability (the Firefox Reader Mode algorithm) extracts the article, falling back to the article/main/body heuristics for non-article pages; RSS/Atom feeds parse to a markdown headline survey; PDFs and other non-HTML content return an actionable hint instead of a dead-end error. With the tasks extension declared, runs detached and returns a task handle | | ingest | fetch + persist as a research note (tags, source = URL) that briefings fold in; with the tasks extension declared, runs detached and returns a task handle |

Tasks RPC surface (io.modelcontextprotocol/tasks)

Durable handles for long-running tool calls, served on the 2025-era method registry (where today's hosts live):

| Method | What it does | |---|---| | tasks/get | current task state — {taskId, status, ttl, createdAt, lastUpdatedAt, pollInterval, statusMessage, result?, error?}; terminal tasks carry the digest/note result or the failure | | tasks/cancel | cooperative cancel — the runner observes it at its next phase boundary; the record is not stored after a cancel | | tasks/list | page of tasks (cursor, newest first) | | tasks/getPayload | the original tool call (tool, workspaceId, args) so a client can re-issue it |

Prompts & resources

  • Prompts: draft-from-brief (briefing + notes + voice requirements embedded for the operating LLM), voice-review (editorial critique against the voice), editorial-workflow (end-to-end research → brief → artifact → lint → decide)
  • Resources: quillby://voice/{workspaceId}, quillby://notes/{workspaceId}/{noteId}, quillby://briefings/{workspaceId}/{briefingId}, quillby://artifacts/{workspaceId}/{artifactId} (enumerable via resources/list)

The daily loop (agent-side)

workspace get        → voice, examples, lessons, sources
brief run            → today's items (auto-stored; seen flags show what's new)
(agent ranks ideas in conversation — nothing stored)
brief save           → today's ideas, only after the user confirms
(agent drafts in chat when the user asks — nothing stored)
draft approve/reject → the user's decision, with reason
workspace update     → deliberate voice learning, with reason (after repeated feedback themes)

Continuity costs nothing: workspace get returns lessons and last brief date; brief get with a range is the weekly view; expired ideas are flagged on read; brief run flags items you've already seen. Feedback only becomes voice knowledge deliberately — after repeated themes, the agent proposes a rule and the user confirms it via workspace update.

The editorial loop (for specs, articles, and long-form)

note save            → research/ideas from the web, conversations, anywhere
briefing create      → control sheet: kind template + per-section guidance + folded notes
artifact render      → deterministic scaffold (headings + guidance + voice requirements)
draft-from-brief     → prompt embeds briefing + notes + voice for the operating LLM
workspace voice_lint → deterministic check of the draft against the voice (no LLM)
draft approve/reject → the human decision records the finished content

Persistence = decision. The only bytes that reach the disk: harvested facts (brief run), curation the user confirmed (brief save), scaffolds the agent rendered (artifact render), and posts the user decided on (draft approve / draft reject). The agent's scratch — every draft it writes — lives in the conversation and nowhere else.

Config

~/.quillby/config.json:

{ "activeWorkspace": null, "dbPath": "~/.quillby/quillby.db" }

activeWorkspace is a process-global, last-write-wins selection shared by every MCP client on the same machine/home. Two MCP servers running concurrently (e.g. separate agent configs pointed at the same QUILLBY_HOME) each mutate the same file, so one client's select can silently change what the other one resolves to. Ids are stable per workspace, so the robust pattern under concurrency is to pass workspaceId (or the object's id) explicitly per call instead of relying on the process-level selection — every tool already accepts both.

Storage: SQLite via better-sqlite3 (the industry-standard driver — WAL mode, foreign keys, a 5s busy_timeout so concurrent access waits instead of failing, synchronous API, no experimental-warning noise; a native module with prebuilt binaries for macOS/Linux/Windows, falling back to a source compile when none match). Requires Node ≥ 24. The DB migrates in place from earlier versions (including databases written by the previous node:sqlite driver — same file format). Set QUILLBY_HOME to relocate config + DB elsewhere (useful for testing or multiple installations).

Logging: stdout is reserved for the JSON-RPC protocol; operational logs go to stderr. Set QUILLBY_LOG_LEVEL=debug for per-run timing and error traces (default info, which logs startup/shutdown and unexpected errors only). Clients may also set a per-request level via io.modelcontextprotocol/logLevel in the request's _meta (2026-07-28 spec) — that request's handlers log at the declared level without affecting concurrent requests.

Robustness: every non-workspace tool validates the target client exists and returns client not found instead of leaking raw SQL errors; all business errors return the same {error: message} envelope (schema violations surface as tool errors from the MCP SDK); every list tool returns {total, items, truncated} so page caps are never silent, and an offset param pages past the cap; dates are validated as real YYYY-MM-DD calendar dates and inverted ranges (from > to) are rejected; limits are bounded (1..500); all titles are trimmed and capped at 200 chars; draft formats are trimmed and capped at 40 chars; curated ideas require a non-empty thesis and an ISO expiresAt (or null — evergreen). Calendar days follow the local timezone (a brief run after midnight files under the new day, not UTC's). Harvested fetches are cached ~4h per provider (one row per feed URL / provider name): fresh, error-free rows are served as-is on rerun while stale or previously-failed providers are re-fetched, so a transient failure heals on the next run instead of poisoning the cache — and serving cached rows never extends their TTL. The whole cache is dropped on radar/source edits, and stale cache rows are pruned after 3 days on every fresh run. Key-gated providers are each timeboxed (45–60s) so a dead provider can never hold a run open past its budget. web fetch accepts http(s) only and surfaces the underlying cause of network failures instead of undici's generic fetch failed. sources_remove accepts ids or urls (urls resolve to the client's own sources) and reports unmatched inputs (missing, unmatchedUrls) instead of a silent {removed: 0}, and every resource timestamp reads back as ISO 8601 (including briefs). voice_lint returns structured findings (score/passed/stats/issues) alongside its human-readable report. Relevance scoring additionally leans on a persistent IDF corpus: each fresh harvest folds its term/phrase document frequencies into the client's history (30-day half-life, faded rows reaped), and every run — even a 3-item one — blends that history into its IDF weights, so genericity stays data-driven and stable across runs instead of flipping with a single small harvest.

web search gets the same resilience treatment as the radar: DuckDuckGo's reverse-engineered html endpoint fingerprint-blocks bots with an HTTP 202 anomaly page that parses to zero results — the server detects the block (202 status or anomaly-page body) and fails that engine instead of reporting a silent empty list, then tries the next keyless engine (Bing, then Mojeek), reporting which engine served in engine. web fetch runs Mozilla Readability before markdown conversion, so nav/ads/sidebars stay out of ingested research notes (the heuristics remain as fallback for non-article pages). Cacheable results carry 2026-07-28 ttlMs/cacheScope hints: tools/list and server/discover at server level, per-resource TTLs at registration (30s, private). Every tool declares an outputSchema (JSON Schema 2020-12, SEP-2106 — zod v4's native dialect) and every success result carries structuredContent alongside the text, so schema-aware clients validate the contract instead of guessing.

Key-gated providers (all optional — RSS, Google News and DDG search work with zero configuration):

| Var | Provider | |---|---| | QUILLBY_EXA_API_KEY | Exa semantic news search | | QUILLBY_TAVILY_API_KEY | Tavily news search | | QUILLBY_FIRECRAWL_API_KEY | Firecrawl direct-URL extraction (radar.directUrls) |

Development

npm install
npm run typecheck       # typecheck
npm run build           # clean + compile to dist/
npx tsx src/index.ts    # dev
npm test                # unit tests
npm run test:e2e        # end-to-end over real stdio JSON-RPC (isolated QUILLBY_HOME)
npm run test:all        # the full gate: typecheck → build → unit → e2e

Publishing

prepublishOnly runs the full test gate. Verify the tarball before publishing:

npm pack --dry-run      # inspect dist/, README.md, CHANGELOG.md, LICENSE
npm publish --access public