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

@vinumcms/mcp

v0.2.0

Published

An MCP server over a Vinum install: read the content model, read and write pages, through the same sanitizers the admin uses.

Readme

@vinumcms/mcp

MCP tools over a Vinum install: read the site's content model, read and write its pages, without shelling out or reaching into the key-value store by hand.

// your MCP client's config
{
  "mcpServers": {
    "vinum": {
      "command": "npx",
      "args": ["-y", "@vinumcms/mcp", "--cwd=/path/to/your-site"]
    }
  }
}

Read-only. Add "--write" to the args to allow content changes.

Why it holds your instance rather than building one

It imports your own app/vinum.server.ts and calls the same vinum.pages.* API the admin screens call. There is no second path to the data and no privileged one — which matters because the alternative, an agent editing ["page", slug] directly, steps around every sanitizer and the first thing it stores is a block shape the editor can never produce.

That also means it reads your store — and the store has to be a persistent one, because this is a separate process. With memoryStore it opens its own empty database beside your dev server's and every tool reports an empty site.

On Node that means fsStore, which ships in @vinumcms/adapters as of 0.6.0 and takes a lock file precisely so this server and your dev server can write the same content without losing each other's edits:

import { fsStore } from "@vinumcms/adapters/fs";

store: fsStore({ path: "./data/store.json" }),

Tools

| | | |---|---| | get_site | Name, origin, locales, and whether writes are allowed | | list_block_types | Every block this site can use, with its fields. Read this first — the palette is per-site, because a host registers its own alongside Vinum's | | list_page_types | Collections, their base path and structured fields | | list_pages | Every page with route and status | | get_page | One page in full, blocks included | | scaffold_block | Generate a custom block — type, sanitizer, settings panel, renderer and a test — as source for you to write | | save_draft | Create or update a draft — needs --write | | publish_page | Publish a draft, snapshotting the outgoing version — needs --write |

list_block_types is the one that earns its place. Guessing a block's shape is the failure mode Vinum's agent-experience notes are about: agents do not get stuck, they guess confidently. This hands over the real registry, including the blocks you wrote.

Scaffolding a block

scaffold_block takes a field list and returns source. It writes no files and needs no --write — the agent holding this tool already has file tools, and giving the server the repo would overload --write, which today means exactly one thing, into meaning two.

{
  "type": "testimonial",
  "layout": "section",
  "variants": ["tone"],
  "fields": [
    { "name": "heading", "kind": "text", "placeholder": "What people say" },
    { "name": "quotes",  "kind": "list", "of": [
      { "name": "body", "kind": "longText" },
      { "name": "name", "kind": "text" }
    ]}
  ]
}

Field kinds: text, longText, image, link, boolean, number, select, list. Text is editable in place; the rest get a settings panel. Images go through safeSrc and links through safeHref, because a sanitizer is a trust boundary whether a person or an agent wrote the block.

It returns a whole app/blocks.tsx when the site has no custom blocks yet, and a definition to insert plus the imports it needs when it does — a distinction it can make because it is reading your live registry rather than guessing at your files. That is also why it refuses a type you have already registered, defaults the palette group to one your site actually uses, and rejects a field called id, type or variant (the system writes those around your sanitizer's output, so such a field is silently overwritten on every save).

The generated test asserts the block reached BLOCKS, that create() round-trips through sanitize(), and that hostile input coerces rather than throwing. Invariant 1 asks for a sanitizer and a test in the same commit as a new block type; this produces both.

Writes are a trust boundary

save_draft runs sanitizeBlocks before storing, exactly as the admin's own route does. Unknown block types are dropped and unknown fields coerced — a CLI or MCP write is untrusted like any other, and "it's a dev tool" has never been an exemption.

A sanitizer drops silently, which is right for a form post where the editor sees the result and wrong for an agent that would otherwise report success for content that was never stored. So save_draft compares what it was given against what survived and says so:

{
  "saved": "about",
  "blocks": 1,
  "warning": "1 of 2 block(s) were dropped by the sanitizer — an unknown type, or a field it could not coerce. Check list_block_types."
}

Refusals work the same way: with --write absent, a write tool returns an error explaining the server is read-only rather than vanishing from the list. A tool an agent cannot see is a tool it invents a workaround for.

Requirements

Node 18+, and a Vinum install of 0.7.0 or later — scaffold_block reads the variant vocabulary, which core gained in 0.7.0. It runs under tsx, which is a dependency of this package — Vinum ships TypeScript with no build step, and Node refuses to strip types under node_modules, so a plain node process cannot load the sanitizers this server depends on.

Licence

MIT