Connect Claude to CMS One
Give Claude a Model Context Protocol connection and it can write, update, publish, and illustrate posts directly in your workspace - no copy-pasting required.
POST /api/mcpOverview
CMS One exposes a single MCP endpoint that speaks the Streamable HTTP transport. Any MCP-compatible client - Claude Code, Claude Desktop, or Claude.ai - can connect to it and call a fixed set of tools scoped to your workspace.
Built for Claude
Works with Claude Code, Claude Desktop, and Claude.ai custom connectors over Streamable HTTP.
Flexible Auth
Use your workspace API key (Claude Code, scripts) or sign in with OAuth (Claude.ai, Claude Desktop) - whichever fits the client.
Stateless
Every request is self-contained - there's no server-side session to manage or expire.
Tenant-Scoped
Every tool call is scoped to the workspace that owns the API key. No cross-tenant access.
Endpoint
https://your-domain.com/api/mcpReplace your-domain.com with your own domain. This is the same base host as the v1 REST API - just a different path.
Authentication
The MCP endpoint supports two ways in. For Claude Code or your own scripts, use a Bearer token - the same workspace API key as the v1 REST API, from Workspace → API Keys. For Claude.ai and Claude Desktop's graphical connector, sign in with OAuth instead - it's auto-discovered, so there's nothing to configure by hand.
Required header
Authorization: Bearer YOUR_API_KEYWhere to find your API key
Open your workspace and navigate to API Keys. If you already use the key for the REST API, reuse it here - one key works for both.
Connecting Claude
Pick the client you use. Each one just needs the endpoint URL and the Authorization header above.
Add the server once from a terminal - it's then available in every session.
claude mcp add --transport http cmsone \
https://your-domain.com/api/mcp \
--header "Authorization: Bearer YOUR_API_KEY"Tools
Every tool call is scoped to the workspace that owns your API key. Browse list_categories / list_tags first if you want Claude to reuse existing taxonomy rather than creating new tags on the fly.
Posts
list_posts- Browse postsLists posts for the workspace, optionally filtered by status, type, or search term.
| Parameter | Type | Description |
|---|---|---|
status | draft | published | scheduled | archived | Filter by status. |
type | blog | project | news | case-study | tutorial | documentation | page | Filter by article type. |
search | string | Full-text search across title and content. |
page | number | Defaults to 1. |
limit | number | Defaults to 10, max 50. |
get_post- Read one postFetches a single post's full content and metadata, by id or slug.
| Parameter | Type | Description |
|---|---|---|
id | string | The post's ObjectId. |
slug | string | The post's slug. Provide id or slug, not both. |
create_post- Write a new postCreates a post. Send the body as Markdown (easiest) or HTML, or as a raw Tiptap document object - either way it is validated and stored as 'blocks', the same Tiptap JSON the dashboard editor produces, so it opens in the editor and renders on your site exactly like a hand-written post. Headings are levels 2-4 (# becomes level 2); tables are flattened to one paragraph per row. Invalid documents are rejected with an error naming the unsupported nodes/marks. Unknown tag names are created automatically; unknown category names return an error listing valid ones.
| Parameter | Type | Description |
|---|---|---|
title* | string | Post title. |
content* | string | object | Post body - a Markdown or HTML string, or a Tiptap document object ({ type: "doc", content: [...] }). |
contentType | markdown | html | blocks | Format of a string content you are sending. Defaults to markdown. Stored content is always blocks. |
featuredImage | string (URL) | Cover image URL, e.g. from create_image. |
featuredImageAlt | string | Alt text for the cover image. |
excerpt | string | Short summary. |
type | blog | project | news | case-study | tutorial | documentation | page | Defaults to blog. |
status | draft | published | scheduled | archived | Defaults to draft. |
slug | string | Defaults to a slug generated from the title. |
tags | string[] | Tag names - created automatically if new. |
categoryNames | string[] | Existing category names (see list_categories). |
scheduledFor | ISO date-time | Required when status is scheduled. |
update_post- Edit a postUpdates only the fields you pass. Setting status to published for the first time also sets publishedAt.
| Parameter | Type | Description |
|---|---|---|
id* | string | The post's ObjectId. |
title | string | New title. |
content | string | object | Complete new body - Markdown/HTML string or Tiptap document object. Converted and stored as blocks. |
contentType | markdown | html | blocks | Format of a string content you are sending. Defaults to markdown; ignored without content. |
featuredImage | string (URL) | New cover image URL; empty string clears it. |
featuredImageAlt | string | New cover image alt text. |
type | blog | project | news | case-study | tutorial | documentation | page | New post type. |
excerpt | string | New summary. |
status | draft | published | scheduled | archived | New status. |
slug | string | New slug - must be unique. |
tags | string[] | Replaces the post's tags. |
categoryNames | string[] | Replaces the post's categories. |
scheduledFor | ISO date-time | Required if status is set to scheduled. |
publish_post- Publish a postA thin shortcut for update_post with status set to published.
| Parameter | Type | Description |
|---|---|---|
id* | string | The post's ObjectId. |
Taxonomy
list_categories- Browse categoriesLists up to 200 categories for the workspace. Use this before passing categoryNames to create_post or update_post.
Media
create_image- Add an image to the media libraryUploads an image from a URL or base64 data into the media library and returns a mediaId. JPEG/PNG/GIF/WebP only, 5 MB max. Does not generate images - bring your own URL or bytes.
| Parameter | Type | Description |
|---|---|---|
url | string | A publicly reachable image URL. Provide url or base64Data, not both. |
base64Data | string | Base64-encoded image bytes (a data: URI prefix is fine). |
mimeType | string | Required alongside base64Data, e.g. image/png. |
originalFilename | string | Optional, stored as metadata only. |
attach_image_to_post- Attach an image to a postAttaches a previously created image (by mediaId) to a post's media gallery.
| Parameter | Type | Description |
|---|---|---|
postId* | string | The post's ObjectId. |
mediaId* | string | The mediaId returned by create_image. |
alt | string | Alt text for accessibility. |
caption | string | Caption shown with the image. |
order | number | Display order. Defaults to the end of the gallery. |
Example Workflow
A single request to Claude typically becomes a short chain of tool calls. For example, asking Claude to:
Write a blog post about "remote work best practices", tag it "remote-work" and
"productivity", attach this image: https://images.example.com/remote-work.jpg, and publish it.- create_post - writes the draft with the given title, content, and tags (creating any new tags).
- create_image - downloads the image URL into the media library and returns a mediaId.
- attach_image_to_post - attaches that mediaId to the new post.
- publish_post - flips the post's status to published.
Error Handling
Connection-level problems (bad auth, wrong method) return a normal HTTP error. Problems inside a tool call - like an unknown category name - don't.
| Status | When it occurs | Response body |
|---|---|---|
| 401 | The Authorization header is missing or malformed, or the API key is invalid/expired. | { "success": false, "error": "Invalid API key" } |
| 405 | A method other than POST was used. The endpoint is stateless - there's no GET stream or session to open. | { "jsonrpc": "2.0", "error": { "code": -32000, "message": "Method not allowed..." } } |
A failed tool call (e.g. an unknown category name, or a post that doesn't exist) is not an HTTP error - it comes back as a normal MCP tool result with isError: true and a human-readable message in content, so Claude can see what went wrong and retry or ask you for clarification.
