Skip to main content
Glama
velocms-mcp

velocms-mcp

@velocms/mcp

An MCP server for VeloCMS — draft, edit, publish, and manage your blog (posts, media, comments, members, site settings) from Claude Code, Claude Desktop, Cursor, or any other MCP client.

It's a thin, typed wrapper around the VeloCMS Public API (https://<your-blog>/api/v1, documented at /api/openapi on any VeloCMS site): every tool maps to one real API call, with Zod-validated inputs and readable errors — no scraping, no browser automation, just the platform's own supported REST API.

What it does

Tool

What it does

list_posts

List posts, paginated, optionally filtered by status (draft/published)

get_post

Fetch a single post by ID, including its full content and SEO fields

create_post

Create a new post (defaults to draft)

update_post

Partially update an existing post — only the fields you pass change

delete_post

Permanently delete a post

publish_post

Set a post's status to published (stamps published_at)

unpublish_post

Set a post's status back to draft

list_media

List the media library, paginated, optionally filtered by MIME type

list_comments

List comments, paginated, optionally filtered by post or moderation status

moderate_comment

Set a comment's status to approved, pending, or spam

list_members

List subscribers/readers (emails are masked, e.g. u***@example.com)

get_site_settings

Read the blog's name, description, and feature flags

Every tool ships a description an LLM can read to figure out when and how to use it — that's the point of MCP, not just a REST proxy with extra steps. See Tool reference below for full argument lists.

Related MCP server: dustinedwards-mcp

Setup

1. Get a VeloCMS API key

In your VeloCMS dashboard: Settings → API Keys → create a key with the scopes you need (posts:read, posts:write, media:read, comments:read, comments:moderate, members:read, site-settings:read, etc.). API access requires the Pro plan or higher.

2. Configure your MCP client

You don't need to clone this repo — npx fetches and runs it on demand.

Claude Code

claude mcp add velocms \
  --env VELOCMS_SITE_URL=https://myblog.velocms.org \
  --env VELOCMS_API_KEY=velo_your_64_char_hex_key_here \
  -- npx -y -p @velocms/mcp velocms-mcp

MCP servers register their tools at session start, so restart your Claude Code session after adding this.

Claude Desktop

Add to your claude_desktop_config.json (Settings → Developer → Edit Config):

{
  "mcpServers": {
    "velocms": {
      "command": "npx",
      "args": ["-y", "-p", "@velocms/mcp", "velocms-mcp"],
      "env": {
        "VELOCMS_SITE_URL": "https://myblog.velocms.org",
        "VELOCMS_API_KEY": "velo_your_64_char_hex_key_here"
      }
    }
  }
}

Restart Claude Desktop after editing.

Cursor / any other MCP client

Same shape as above — point the client's MCP config at npx -y -p @velocms/mcp velocms-mcp with VELOCMS_SITE_URL and VELOCMS_API_KEY set in its env. The server speaks standard MCP over stdio, so any client that supports stdio MCP servers works.

3. Local install (contributing / running from source)

git clone https://github.com/VeloCMS/velocms-mcp.git
cd velocms-mcp
npm install
npm run build
cp .env.example .env
# edit .env and set VELOCMS_SITE_URL + VELOCMS_API_KEY
VELOCMS_SITE_URL=... VELOCMS_API_KEY=... node dist/index.js

Config reference

Env var

Required

Description

VELOCMS_SITE_URL

yes

Your blog's base URL — a *.velocms.org subdomain or a bound custom domain. No trailing slash needed.

VELOCMS_API_KEY

yes

An API key from /admin/settings → API Keys. Requires Pro plan or higher. Never logged.

If either is missing, the server prints a clear message to stderr and exits immediately (process.exit(1)) — it never starts half-configured.

Tool reference

Argument names are camelCase; the server maps them to the API's snake_case JSON fields for you.

list_posts — page?, perPage? (max 100), status? (draft | published).

get_post — id (required).

create_post — title (required, ≤255 chars), slug?, contentHtml?, contentJson? (TipTap ProseMirror document — prefer contentHtml unless you need this), excerpt? (≤500), status? (draft default | published), tags? (string array), seoTitle? (≤60), seoDescription? (≤160).

update_post — id (required) + any of the create_post fields (all optional here). At least one field besides id is required — the tool rejects a no-op call before making any network request.

delete_post — id (required). Permanent.

publish_post / unpublish_post — id (required). Shorthand for update_post({ status: "published" }) / update_post({ status: "draft" }).

list_media — page?, perPage?, type? (MIME type prefix, e.g. "image").

list_comments — page?, perPage?, postId?, status? (approved | pending | spam).

moderate_comment — id (required), status (required: approved | pending | spam).

list_members — page?, perPage?, tier? (free | paid).

get_site_settings — no arguments.

Error handling model

The VeloCMS API returns a consistent JSON error envelope:

{ "error": { "code": "RATE_LIMITED", "message": "...", "details": {} } }

This client surfaces that message directly (never a raw stack trace), enriched with actionable hints:

  • 401 (UNAUTHORIZED) — the message tells you to check VELOCMS_API_KEY.

  • 403 (PLAN_UPGRADE_REQUIRED) — the message points at /admin/billing.

  • 403 (INVALID_SCOPE/FORBIDDEN) — the message tells you to check the key's scopes.

  • 429 (RATE_LIMITED) — the Retry-After response header (seconds) is parsed and included in the error message, and returned as retryAfterSeconds if you're calling the client library directly.

  • Any other non-2xx — the API's own code + message are surfaced as-is.

Rate limits are plan-based (Pro: 30/min, 1,000/hr · Business: 120/min, 5,000/hr · Agency: 300/min, 20,000/hr) — this server does not retry automatically; it fails fast with the wait time so an interactive tool call never blocks silently.

Security

  • Your API key is a tenant-scoped credential — it can only reach the one blog it was issued for, and only the endpoints its scopes allow.

  • The key is read once from the environment and never logged, echoed, or included in any tool output.

  • Member emails returned by list_members are masked by the API itself (e.g. u***@example.com) — this server never sees or handles unmasked member PII.

  • Encrypted tenant settings (Stripe keys, AI provider keys) are excluded from get_site_settings by the API — there is no way to read them through this server.

Notes on this MCP server

  • moderate_comment's body field is status, not action — PATCH /api/v1/comments/{id}/moderate takes { "status": "approved" | "pending" | "spam" } per the API's own schema, so the tool's input is named to match.

  • get_post and get_site_settings return the record directly (not wrapped in { "data": ... }) — only the write endpoints (create_post, update_post, moderate_comment) wrap their response, matching the API's own inconsistency here (documented in openapi.yaml).

  • There is currently no upload_media tool — POST /api/v1/media takes multipart/form-data, which doesn't map cleanly onto typical MCP client transports. list_media is available for referencing media already uploaded through the dashboard. Contributions welcome if you need this.

Development

npm install
npm run typecheck   # tsc --noEmit
npm test             # vitest — all tests run against a mocked fetch, no live API calls
npm run build        # emits dist/

The test suite covers: the Authorization: Bearer header on every request, each tool's exact method/URL/body mapping, error-code mapping for 401/403/429/500 responses, and the missing-env-var fail-fast path. No test in this repository makes a real network call.

License

MIT — see LICENSE.

Available Tools

12 tools
create_postCreate a postA

Creates a new blog post. Defaults to status=draft — pass status=published to publish immediately (stamps published_at automatically), or create as a draft and call publish_post once you're ready. Requires the posts:write scope.

Inputs: title (required), slug, tags, status, +5 more.

delete_postDelete a postA

Permanently deletes a post by ID. Requires the posts:write scope.

Inputs: id (required).

get_postGet a postA

Fetches a single post by its PocketBase record ID, including content_html/content_json and SEO fields. Requires the posts:read scope.

Inputs: id (required).

get_site_settingsGet site settingsA

Fetches the tenant's site configuration (name, description, logo, favicon, members/comments enabled flags). Encrypted fields (Stripe keys, AI API key) are always excluded by the API. Requires the site-settings:read scope.

Inputs: no named parameters.

list_commentsList commentsA

Lists comments, paginated. Optionally filter to a specific post (postId) and/or by moderation status (approved, pending, spam). Requires the comments:read scope.

Inputs: page, postId, status, perPage.

list_mediaList mediaA

Lists items in the media library, paginated. Optionally filter by MIME type prefix (e.g. 'image'). Requires the media:read scope.

Inputs: page, type, perPage.

list_membersList membersA

Lists reader/subscriber records, paginated. Email local parts are masked by the API (e.g. u***@example.com). Optionally filter by tier (free or paid). Requires the members:read scope.

Inputs: page, tier, perPage.

list_postsList postsA

Lists blog posts for the authenticated tenant, paginated (default 20/page, max 100). Optionally filter by status (draft or published). Requires the posts:read API key scope.

Inputs: page, status, perPage.

moderate_commentModerate a commentA

Sets a comment's moderation status (approved, pending, or spam). Requires the comments:moderate scope.

Inputs: id (required), status (required).

publish_postPublish a postA

Sets a post's status to published (stamps published_at). Shorthand for update_post({ status: 'published' }). Requires the posts:write scope.

Inputs: id (required).

unpublish_postUnpublish a postA

Sets a post's status back to draft, taking it off the public site. Shorthand for update_post({ status: 'draft' }). Requires the posts:write scope.

Inputs: id (required).

update_postUpdate a postA

Partially updates an existing post — only the fields you pass are changed. Requires at least one field besides id. Requires the posts:write scope.

Inputs: id (required), slug, tags, title, +6 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 12 tools

    Disambiguation5/5

    Each tool targets a distinct resource and action: post CRUD, publish state transitions, media listing, comment moderation, member listing, and site settings retrieval. No two tools overlap in purpose.

    Naming Consistency5/5

    All tools follow a consistent snake_case verb_noun pattern (get_, create_, list_, update_, delete_, publish_, unpublish_, moderate_). The consistent use of singular/plural nouns is conventional and readable.

    Tool Count5/5

    12 tools is well-scoped for a CMS. Post lifecycle is fully represented, and the addition of media, comments, members, and settings covers the main auxiliary resources without unnecessary bloat.

    Completeness3/5

    Post CRUD and publishing workflow are complete, but media only supports listing, site settings only get, and comments lack deletion. These read-only surfaces create dead ends for agents trying to manage those resources.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers