@editpink/mcp
v0.11.0
Published
Pink Editor MCP server — gives your AI client editorial framework + content graph + recall over your published content
Downloads
53
Maintainers
Readme
@editpink/mcp — Pink Editor MCP server
Local stdio MCP server that gives your AI client (Claude Code, Claude Desktop, Cursor, Windsurf) the editorial framework + content graph + recall + publishing flow over your Pink-Editor-managed content.
AI client ──stdio──► editpink-mcp ──HTTPS──► app.edit.pink /api/cm/mcp/* ──► executorWhy: drive content work from the client you're already in, using your own Pro/Max subscription quota instead of paying API tokens — and have the host model adopt your editorial framework automatically on every session.
What it does
Three mechanisms converge on "playbook loaded" so the host model behaves like your editorial team, not generic Claude:
/pinkslash prompt — type/pinkin any MCP client; the prompt server-side hydrates the editorial playbook + the current site's foundation (brand, audiences, goals, CTAs) + the list of available sites. First call loads everything; later calls in the same session are lightweight reminders.get_foundationtool — embeds the same playbook in its response, so models that skip/pinkstill learn the rules the moment they call it.- Tool description breadcrumbs —
list_sites→set_site→get_foundationwalks any first-time model through the bootstrap.
Install — pick the path that matches your client
There are two transports. If your client accepts a URL, use the remote URL —
it's the simplest and most reliable path and needs no local install. The
local stdio bridge is for clients that only run a command (Claude Code,
Claude Desktop).
| Your client | Use |
|---|---|
| ChatGPT, Cursor, Perplexity, Codex, Claude.ai, any "add a URL" connector | Remote URL (below) |
| Claude Code, Claude Desktop, Windsurf (local command) | Stdio bridge (below) |
First, mint a key at https://app.edit.pink/dashboard/api-keys (cm_live_…).
Remote URL — simplest, no OAuth (recommended)
Paste your key into the URL. No header config, no OAuth round-trip:
https://app.edit.pink/api/mcp/cm_live_<your-key>Or, if your client prefers a header:
{
"mcpServers": {
"editpink": {
"url": "https://app.edit.pink/api/mcp",
"headers": { "Authorization": "Bearer cm_live_<your-key>" }
}
}
}OAuth (only if your client insists on it): point it at
https://app.edit.pink/api/mcpwith no key and it'll discover the OAuth flow viaWWW-Authenticate//.well-known/oauth-protected-resource. This works, but the URL-embedded key above sidesteps OAuth entirely and is the path we recommend when a client supports it.
Codex CLI
Codex's remote-MCP + OAuth support is experimental and must be enabled.
Easiest is the URL-embedded key, which avoids OAuth altogether — add to
~/.codex/config.toml:
[mcp_servers.editpink]
url = "https://app.edit.pink/api/mcp/cm_live_<your-key>"If you want the header form with a bearer token instead:
[mcp_servers.editpink]
url = "https://app.edit.pink/api/mcp"
bearer_token_env_var = "EDITPINK_API_KEY" # export EDITPINK_API_KEY=cm_live_…To use the OAuth flow specifically, enable the experimental client and log in:
[features]
experimental_use_rmcp_client = truecodex mcp login editpinkStdio bridge — Claude Code / Claude Desktop
Add to ~/.claude.json (Claude Code) or
~/Library/Application Support/Claude/claude_desktop_config.json (Claude
Desktop, macOS — %APPDATA%/Claude/claude_desktop_config.json on Windows):
{
"mcpServers": {
"editpink": {
"command": "npx",
"args": ["-y", "@editpink/mcp"],
"env": {
"EDITPINK_API_URL": "https://app.edit.pink"
}
}
}
}Or, while developing against this repo, point at the local file:
{
"mcpServers": {
"editpink": {
"command": "node",
"args": ["/Users/dk/Desktop/main/edit.pink/editor/mcp-servers/pink/index.mjs"],
"env": {
"EDITPINK_API_URL": "http://localhost:3000"
}
}
}
}Then restart the client. Tools appear as mcp__editpink__<tool> (e.g.
mcp__editpink__list_sites). The /pink prompt appears in the slash menu.
Windsurf: same JSON block in ~/.codeium/windsurf/mcp_config.json.
Auth — stdio bridge device flow
The remote URL paths above carry the key directly, so there's nothing to authorize. The stdio bridge authorizes on the first tool call via the RFC 8628 device-authorization flow:
- The first tool call requests a
user_code+ verification URL and opens your browser (also printed to stderr). - The bridge does not block: it waits ~25s for approval, then returns the
verification URL + code as the tool result so your AI client shows you
exactly where to approve. Approve, then ask it to run the tool again — the
next call picks up the approval automatically and persists the token to
~/.editpink-mcp-state.json. - Subsequent tool calls use that token; no re-auth until you revoke it.
Skip the flow entirely by setting EDITPINK_API_KEY (a cm_live_… key) in the
env block — useful for headless / CI usage. The env var always wins over the
on-disk token and is never written to the state file.
First run
/pink— loads playbook, foundation (if a site is already pinned), and the list of your sites.- If no site is pinned, the prompt tells the model to call
list_sitesthenset_site({site_id: <id>}). The pinned site survives across restarts. get_foundation— pulls the site's brand, audiences, goals, CTAs, playbook,writable_cols, skills/memory registries, today's date, and current selection in one call.
Tool surface
Everything below is exposed by the stdio bridge and dispatches to
/api/cm/mcp/call (or a dedicated bridge route for the lifecycle tools).
Site lifecycle (bootstrap)
| Tool | What |
|---|---|
| list_sites | Every site you can read. Each row carries is_member and read_only. |
| set_site({ site_id }) | Pin a site for the rest of the session. Returns the foundation immediately so you don't need a second call. |
| get_site | The pinned site row only (light). |
| get_foundation | Full foundation refresh — same shape set_site returned. |
| get_selection | What the user has open in the Pink Editor UI right now (page/topic with keywords, pains, questions, notes pre-joined). Call before acting on "this" / "the current page". |
Reads & planning
| Tool | What |
|---|---|
| db_select | Site-scoped read of any allowed cm_* table. |
| graph({ predicate }) | Dense CSV map of the content graph. Predicates: inventory, cannibals, orphans, stale, underlinked, missing_primary, coverage, intent_mix, neighbors. |
| recall | Vector recall across the site's cm_chunks. Cheap (one embedding call); run before research. |
| research | Serper SERP + URL scrape + LLM synthesis. Paid. |
| fetch_keywords | Google Keyword Planner volume + CPC for a list of keywords (paid). |
| query_stats | Stats summary across the site's plan. |
Writes (CRUD)
| Tool | What |
|---|---|
| db_insert | Insert into allowed cm_* tables. site_id auto-stamped. |
| db_update | Patch existing rows by id (or composite PK). |
| db_delete | Soft-delete on tables that support it; hard-delete elsewhere. |
| apply_link_delta | Bulk-edit cm_page_links with a soft floor warning when deletes shrink the link count. |
| merge_topics | Merges only — creates and renames go through db_insert / db_update. |
Publishing — the ONLY MCP path to take a page live
Works for every page_type — editorial (Article / Pillar / FAQ / Landing) and Utility (privacy / terms / about). The publish flow is uniform; Utility pages just use a smaller block subset and skip the AUDIENCE: line.
| Tool | What |
|---|---|
| get_article_brief({ page_id }) | Foundation + page + questions + research + recall + block vocab in one read. Free. |
| generate_article({ page_id }) | The only publish entry point. State machine: every response carries { run_token, step, instruction, payload, next_call } — follow it until step: "done". Your only job is the write_* step(s): execute payload.prompts[*].prompt verbatim and return plain markdown prose. The server does research, structuring into Block[], link + citation resolution, and the push. |
| get_pipeline_status({ page_id }) | Poll run progress. Don't tight-loop in one turn — check on the user's next message. |
Submit ONE page per call. payload is { copies: { "<page_id>": "<prose>" }, append?: true, more?: true }.
The server banks each copy and re-asks for whatever is still missing (a partial
submission is a normal response, not an error), and only publishes once every id
in payload.expected_keys is in. Returning every child of a pillar in one payload
is what silently stalls runs — hosts cap or mangle very large tool arguments. For a
single long article, split on blank lines and send each continuation with
append: true plus more: true ("not finished"), dropping more on the last chunk.
The older two-phase save_article_copy / save_article_blocks tools are gone —
generate_article drives both phases internally.
cm_pages.published and cm_questions.published are READ-ONLY through
db_update — flipping them directly bypasses block validation and the
live-site push (the reader never sees a "published" row that wasn't pushed). Use
generate_article instead.
There is no MCP path to UNPUBLISH or move a page to DRAFT yet — flag to the user if asked.
GSC analytics (read-only)
| Tool | What |
|---|---|
| gsc_query_stats | Aggregated impressions / clicks / position per query, with filters. |
| gsc_striking_distance | Queries ranking 8–20 — close to page-1 wins. |
| gsc_content_decay | Pages losing clicks vs trailing 4-week baseline. |
| gsc_cannibalization | Multiple URLs competing for the same query. |
| gsc_hidden_winners | High-impression, low-CTR rows worth retitling. |
| gsc_forecast | Projected clicks for a planned page. |
Skills, memory, recovery
| Tool | What |
|---|---|
| load_skill({ name }) | Pull a per-site editorial playbook on demand. Discovered via get_foundation. |
| load_memory({ name }) | Pull a per-site memory blob. |
| remember({ name, body }) | Persist future-conversation facts (not drafts/deliverables). |
| undo({ scope: 'last_call' \| 'last_turn' }) | Roll back via the journal. No approval gate — every write commits and is journaled. |
Env vars
| Var | Default | Notes |
|---|---|---|
| EDITPINK_API_URL | http://localhost:3000 | Pink Editor backend origin. Use https://app.edit.pink in production. |
| EDITPINK_API_KEY | — | Optional. If set, skips the device flow. Useful for headless / CI usage. |
State persists at ~/.editpink-mcp-state.json (pinned site_id + issued token)
so you don't have to re-pick a site or re-auth every session.
Troubleshooting
- Browser opened to a
/devicepage asking for a code — that's the device flow on first call. Sign in to Pink Editor and approve the code shown on stderr. Tool "X" failed: HTTP 401— token revoked orEDITPINK_API_KEYmismatch. Delete~/.editpink-mcp-state.jsonto re-trigger the device flow, or fix the env var.Tool "X" failed: HTTP 404 cm_sites N not found—set_sitepointed at a non-existent site. Runlist_sitesand pick a real id.No current site— callset_sitebefore invoking any site-scoped tool.- Connection refused — the backend at
EDITPINK_API_URLisn't reachable. - State file corruption — delete
~/.editpink-mcp-state.jsonand let the device flow run again.
