Skip to content

AI integration: MCP server and kcms CLI

View markdown

KDIGI CMS can be operated by AI agents (Claude Code, Codex, Cursor, VS Code Copilot, OMP, …) and from the terminal. Two pieces:

  • MCP endpoint POST /api/mcp: a Model Context Protocol server built into the CMS. Agents call its tools to search, write, schedule, publish and audit content.
  • kcms CLI (@kdigi/kcms-cli, source in packages/kcms-cli): a thin client of the same endpoint. It adds Markdown-file workflows, local image upload, one-command client setup, and a stdio bridge for clients that cannot call remote servers.

Both act as the user who owns the API key: that user's role and sites apply, exactly as in the admin.

flowchart LR
A[AI tool / kcms CLI] -- "Authorization: Bearer key" --> M["POST /api/mcp"]
M --> O[agent operations<br/>src/agent-api]
O -- "Local API, overrideAccess:false" --> P[(Payload collections)]

Use a dedicated user per agent or person, with the lowest role that does the job:

RoleCan do through MCP
authorCreate and edit own posts as drafts; cannot publish
editorCreate, edit, publish, schedule and trash posts and pages of their sites
adminSame as editor (account and site settings are not exposed as tools)
super-adminEvery site — avoid; whoami and the server log warn on every use
  1. Admin → Users → create the user (e.g. ai-writer@example.com), assign role and site(s).
  2. Open the user → tick Enable API Key → copy the key → Save.
  3. Store it as an environment variable where the agent runs: export KCMS_API_KEY=change-me.

Key sessions are restricted on every REST/GraphQL route, not only MCP: they cannot change email, password, API key, role or site membership, cannot create users, and cannot permanently delete anything (content can only be moved to the trash). Disable a leaked key by unticking Enable API Key.

The quickest path is the CLI, which writes the right file for each client and never puts the key in it:

Terminal window
npm install -g @kdigi/kcms-cli
export KCMS_API_KEY=change-me
kcms setup <claude-code|codex|cursor|vscode|omp|json> --url https://cms.example.com --write --with-skill

--with-skill also installs the agent skill, which teaches the editorial workflow (draft first, expectedUpdatedAt, SEO targets). Without --write the snippet is printed. Manual configuration per client:

Project file .mcp.json (docs, "Environment variable expansion in .mcp.json"):

{
"mcpServers": {
"kdigi": {
"type": "http",
"url": "https://cms.example.com/api/mcp",
"headers": { "Authorization": "Bearer ${KCMS_API_KEY}" }
}
}
}

User-wide: claude mcp add --transport http --scope user kdigi https://cms.example.com/api/mcp --header "Authorization: Bearer $KCMS_API_KEY" — your shell expands the variable, so the literal key ends up in ~/.claude.json. Skill folder: .claude/skills/kcms/.

~/.codex/config.toml (docs):

[mcp_servers.kdigi]
url = "https://cms.example.com/api/mcp"
bearer_token_env_var = "KCMS_API_KEY"

.cursor/mcp.json or ~/.cursor/mcp.json (docs):

{
"mcpServers": {
"kdigi": {
"url": "https://cms.example.com/api/mcp",
"headers": { "Authorization": "Bearer ${env:KCMS_API_KEY}" }
}
}
}

.vscode/mcp.json (docs) — VS Code prompts for the key once and keeps it in its secret storage:

{
"inputs": [{ "type": "promptString", "id": "kcms-api-key", "description": "KDIGI CMS API key", "password": true }],
"servers": {
"kdigi": {
"type": "http",
"url": "https://cms.example.com/api/mcp",
"headers": { "Authorization": "Bearer ${input:kcms-api-key}" }
}
}
}

.omp/mcp.json (project) or ~/.omp/agent/mcp.json (user); OMP expands ${…} when it discovers servers. Skill folder: .omp/skills/kcms/ or ~/.omp/agent/skills/kcms/.

{
"mcpServers": {
"kdigi": {
"type": "http",
"url": "https://cms.example.com/api/mcp",
"headers": { "Authorization": "Bearer ${KCMS_API_KEY}" }
}
}
}

Run the CLI as a local stdio server that forwards to the CMS:

{
"mcpServers": {
"kdigi": {
"command": "npx",
"args": ["-y", "@kdigi/kcms-cli", "mcp"],
"env": { "KCMS_URL": "https://cms.example.com", "KCMS_API_KEY": "${KCMS_API_KEY}" }
}
}
}

ChatGPT (web) connectors and Claude.ai (web) custom connectors require OAuth 2.1 authorization; the endpoint only accepts API keys today. Use the desktop/CLI agents above, or the stdio bridge.

ToolPurpose
whoami, list_sites, get_siteIdentity, sites, content counts
search_content, get_contentFind and read posts/pages (Markdown body, SEO, updatedAt)
create_post, create_pageCreate from Markdown (draft by default)
update_post, update_pagePartial update; requires expectedUpdatedAt
set_statusPublish, unpublish, schedule (future publishedAt)
trash_content, restore_from_trashReversible removal
list_versions, restore_versionHistory and undo
list_taxonomies, create_taxonomyCategories and tags
search_media, import_media_from_url, update_media_altImages (raster formats only)
list_shortcodesReusable snippets for bodies
seo_audit, get_seo_reportSEO checks (same scorer as the admin)
get_content_urlsAdmin URL for review; public URL when live

Plus the resource kcms://guide/content (Markdown/shortcode/SEO rules) and the prompts write-post and refresh-seo.

Behaviour worth knowing:

  • Optimistic concurrency. Every write to an existing document needs the updatedAt from the last read as expectedUpdatedAt. If anyone saved in between, the write fails with conflict and changes nothing.
  • Edit locks. If an editor has the document open in the admin, writes fail with locked and name the editor; the lock is never stolen.
  • Locales. Reads and writes default to vi; pass locale: "en" for English. A read of an untranslated locale returns translated: false instead of falling back to Vietnamese.
  • Legacy bodies. Documents whose body is still old rich text are returned converted to Markdown with bodySource: "legacy-converted"; writing a new body needs acknowledgeLegacyConversion: true.
  • Drafts have no public URL. Share adminUrl for review.
  • Errors come back as code: message (invalid, conflict, locked, forbidden, not_found, unauthorized, internal) so the agent can correct itself.
Terminal window
kcms login --url https://cms.example.com # prompts for the key, validates it, saves a profile
kcms posts get my-post -o my-post.md # frontmatter + Markdown
$EDITOR my-post.md
kcms posts update my-post.md # only changed fields are sent
kcms media upload ./cover.webp --alt "Cover"
kcms seo audit

Full command list and the file format: packages/kcms-cli/README.md and skills/kcms/references/frontmatter.md.

Terminal window
curl -s https://cms.example.com/api/mcp \
-H "Authorization: Bearer $KCMS_API_KEY" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
npx @modelcontextprotocol/inspector --cli https://cms.example.com/api/mcp --transport http \
--header "Authorization: Bearer $KCMS_API_KEY" --method tools/list
SettingNotes
MCP_ENABLEDDefault true; false turns /api/mcp into a 404. See configuration.md.
TransportStreamable HTTP, stateless, JSON responses. GET/DELETE answer 405; JSON-RPC batches 400; bodies over 2 MB 413.
Reverse proxyAllow at least 30 s per request (media imports download up to 25 MB). Rate-limit /api/mcp there — the CMS has no built-in rate limiting. See deploy.md.
AuditEvery tool call logs one agent-op line (tool, user id, site, document id, ok/error code, duration) — never bodies or keys.
TLSUse HTTPS: the key travels in a header. The CLI warns about plain http:// to non-local hosts.
SymptomCause
401 / unauthorizedKey missing, wrong, or Enable API Key unticked. Cookies and JWTs are not accepted on /api/mcp.
404 on /api/mcpMCP_ENABLED=false, or the URL is not the CMS base URL.
invalid: Pass "site"The key's user belongs to several sites; pass site (or --site).
forbiddenThe role does not allow it (e.g. an author publishing).
not_foundThe site or document does not exist or belongs to a site this key is not a member of — other sites are invisible, not merely read-only.
conflictSomeone saved after your read. Re-read and re-apply.
lockedAn editor has the document open in the admin.
Client shows no toolsCheck the client log; most clients need a restart or an approval after the config changes.