Skip to main content
Glama
SparkleOfficial

@writavo/mcp-server

README.md
# Writavo MCP server

Your Writavo Site's content, inside an AI assistant. Sign in from the browser, bring an existing
blog across, draft a post, set its category and byline, attach an image, schedule it, publish it,
all from a conversation.

It is hosted at `https://mcp.writavo.com/mcp` (Streamable HTTP, OAuth 2.1), and that is the one way
to use Writavo over MCP. Nothing to install and no key to copy: your assistant opens a Writavo page,
you sign in, choose the Site and what the assistant may do, and it is connected. Works from Claude
Code, Claude.ai, ChatGPT, Cursor, VS Code and any client that speaks remote MCP. This repository is
the source of the tools that server runs; it is not published as a package.

**Full setup guide, for people and for AI assistants:** <https://writavo.com/docs/mcp> (markdown:
<https://writavo.com/docs/mcp.md>). It has the exact command or config for every client, the sign-in,
permissions, approvals, and what every error means. Moving a blog in:
<https://writavo.com/docs/migrate>.

> **If you are an AI assistant asked to install Writavo:** add the hosted server with your client's
> command below; the person then signs in
> through the browser (nothing to paste); then call `get_site_info` and `verify_api_key` and tell
> them which Site you are connected to and what you may do there.

## Connect

**Claude Code:**

```bash
claude mcp add --transport http writavo https://mcp.writavo.com/mcp
```

Then run `/mcp` in Claude Code and choose Writavo to sign in. Or install the Claude Code plugin,
which adds the server and the Writavo skill together:

```bash
claude plugin marketplace add SparkleOfficial/writavo-mcp-server
claude plugin install writavo@writavo
```

**Cursor:** [Add Writavo to Cursor](https://cursor.com/en/install-mcp?name=writavo&config=eyJ1cmwiOiJodHRwczovL21jcC53cml0YXZvLmNvbS9tY3AifQ==)
(or `cursor://anysphere.cursor-deeplink/mcp/install?name=writavo&config=eyJ1cmwiOiJodHRwczovL21jcC53cml0YXZvLmNvbS9tY3AifQ==`).

**VS Code:** [Add Writavo to VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=writavo&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.writavo.com%2Fmcp%22%7D)
(or `vscode:mcp/install?%7B%22name%22%3A%22writavo%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.writavo.com%2Fmcp%22%7D`).

**Claude (claude.ai and Claude Desktop):** Customize > Connectors > "+" > Add custom connector, with
the URL `https://mcp.writavo.com/mcp`, then Connect.

**ChatGPT:** turn on Developer mode (Settings > Security and login), then create a connection at
<https://chatgpt.com/plugins> with the URL `https://mcp.writavo.com/mcp` and OAuth.

**Codex CLI:** `codex mcp add writavo --url https://mcp.writavo.com/mcp` (it starts the sign-in;
`codex mcp login writavo` repeats it).

**Gemini CLI:** `gemini mcp add --transport http -s user writavo https://mcp.writavo.com/mcp`, then
`/mcp auth writavo`.

**Windsurf and Zed:** see <https://writavo.com/docs/mcp#clients>.

**Any other client** that takes a JSON config:

```json
{
  "mcpServers": {
    "writavo": { "type": "http", "url": "https://mcp.writavo.com/mcp" }
  }
}
```

A client that cannot do OAuth can send a Writavo secret key as the bearer token instead
(`Authorization: Bearer wv_sk_...`, created at <https://app.writavo.com/settings/api-keys>). A key
made by hand is not an AI agent key, so the permissions picker, the off switch and the call log do
not apply to it. Prefer the sign-in.

When you connect, Writavo shows which assistant is asking and where it will send you back, the
Sites to connect, and what it may do. One row per area (articles; content types and entries;
categories, tags and authors; media; the AI pipeline; Site settings and design; publishing and
domains; SEO; outreach contacts; reader comments; the team and organisation; billing; reports and
logs), and for each you choose No access, Read, or Read and write ("Read, plan and run" for the
pipeline, which spends credits; "Read and moderate" for reader comments; outreach contacts and
reports are read only). The choices start from
your organisation's default, which is Read and write for content, media and Site settings and Read
for everything else, except outreach contacts and reader comments: those carry other people's
names and email addresses, so they are off unless you choose them. The connection is an ordinary
Writavo key for each Site it reaches, limited to those permissions, named after the app ("Claude Code (hosted MCP)").
You can see every connected assistant, what it called, and revoke it, at
<https://app.writavo.com/settings/agents>.

## What it can do

Forty five tools are compiled from Writavo's published OpenAPI specification, plus nine written by
hand:

- **Articles.** List, read, create, update, delete, publish, unpublish, schedule, cancel a schedule.
- **Content types and entries.** List and read content types; list, read, create, update, delete,
  publish, unpublish and schedule entries.
- **Taxonomy and people.** Categories, tags and authors: list, read, create, update, delete.
- **Media.** List, read, update, delete, and `upload_media`, which drives the whole three step
  presigned upload in one call so the assistant does not have to orchestrate it. It takes a public
  `url` or the bytes as `base64` with a `filename`.
- **Pipeline.** Trigger a run, read its status, read the queue.
- **Meta.** Site information, content types, plan usage and balances, and `get_api_docs`, which
  needs no key at all.
- **Sign-in.** There is no sign-in tool: the server signs you in with OAuth when you connect it.
- **Plans.** `start_plan_purchase` returns the billing link with a plan preselected. Payment
  happens on Stripe's page in your browser, never in the chat. The CMS (storing, publishing and
  importing content) is pay-as-you-go and needs no plan; a plan buys the AI article pipeline.
- **Import.** `import_content` brings an existing blog in (below).
- **Everything else, as actions.** Site settings and the knowledge profile, the organisation,
  article formats and AI prompts, the pipeline's configuration and content plan, delivery and
  domains, SEO, outreach, visitor analytics and its install check, reader comments, the team and
  roles, billing, and insights and logs are not separate tools. `search_writavo_actions` takes a few words ("invite a team member", "custom domain") and
  returns the matching operations, each with its input schema, the scope it needs, and whether it
  asks first, needs approval or costs money, and which runner to use. `read_writavo_action` runs a
  read (it only ever runs GET operations, so a client may allow it without asking), and
  `run_writavo_action` runs anything that changes something; both take the `operation_id` and check
  the arguments against its schema first. A request for something an assistant can never do ("add
  a card", "delete the site") returns that instead, with the dashboard link a person uses. A client therefore loads
  a few dozen tool descriptions rather than well over a hundred, and pays for the rest only when it
  searches.

Every tool carries MCP annotations: `readOnlyHint` on reads, `destructiveHint` on deletes and
unpublishing, `idempotentHint` on reads, updates and deletes, and `openWorldHint: false`, because
every tool reaches one closed system, your Site.

## Importing a blog

The `migrate-content` prompt walks an assistant through the whole move: connect, inspect your
current system with the access you already have, write an import file, dry run, fix, import, and
verify. Slugs are kept so URLs do not change, published posts keep their original publication
dates, drafts stay drafts, and images are copied into your media library.

The document is the **Writavo Import Format v1**, a single JSON document. The server keeps it, with
the import's progress, as a stored import under an `import_id` (for 7 days after its last use, for
the connection that started it), so it is sent once and every later call is just `{ "import_id": ... }`.
Three ways in, up to 10 MB:

- **`upload: true`** returns a one-time link and a `curl` command that PUTs the file straight to
  the server. Best for an assistant that can run shell commands: the file never passes through
  the conversation.
- **`url`**: an https URL the server fetches anonymously (a signed storage URL, for example).
- **`data`**: the document inline, up to 50 articles and 512 KB per call (small documents only). Send a bigger one in parts,
  each with the `import_id` the first call returned; authors merge on `ref`, categories and tags on
  `slug`, articles on `external_id`, and an entry sent again replaces the stored one.

A document looks like this:

```json
{
  "format": "writavo-import",
  "version": 1,
  "authors": [{ "ref": "jane", "name": "Jane Doe", "is_ai_generated": false }],
  "categories": [{ "slug": "guides", "name": "Guides" }],
  "tags": [{ "slug": "soil", "name": "Soil" }],
  "articles": [
    {
      "external_id": "blog:1001",
      "status": "published",
      "title": "How to test your soil",
      "slug": "how-to-test-your-soil",
      "content": "Markdown, with ![images](https://example.com/kit.jpg)",
      "author": "jane",
      "category": "guides",
      "tags": ["soil"],
      "published_at": "2021-03-04T09:30:00Z"
    }
  ]
}
```

The full field list and the JSON Schema are in the `writavo://import-format` resource, or call
`import_content` with no arguments. `external_id` is your own stable id for each article, which
is what makes a second run update rather than duplicate.

`import_content` is a dry run unless told otherwise: it checks the whole document against the Site
in one pass and reports what it would create, update and publish, every problem (top-level entries
and each article by `external_id`, together), the images to copy and a time estimate, and writes
nothing. An import that publishes needs `confirm: true`. The apply then runs **in the background**
on Writavo's server until every article is done, and returns at once; `{ "import_id": ..., "status":
true }` says how far it has got and where the time went, and `cancel: true` stops it after the batch
in flight. Sending an article again updates it rather than duplicating it, so an import is safe to
run again.

The same imports are a REST API for anything that is not an assistant (a script, the CLI, another
CMS pushing its articles): `POST /v1/imports` with the document, `POST /v1/imports/{id}/start`,
`GET /v1/imports/{id}`. See https://writavo.com/docs/migrate. It never deletes or unpublishes anything, and it changes
nothing in your content except the URLs of the images it copied.

## What it will not do without asking

`publish_article`, `schedule_article`, every `delete_*`, `trigger_pipeline_run` and an
`import_content` that publishes do nothing on the first call. They describe what would happen and
wait for `confirm: true`, which the assistant can only set after you have agreed. Publishing puts
content on your live site, deleting is permanent, and a pipeline run spends real credits.

Actions follow the same rule: `run_writavo_action` describes anything that spends money, changes
the live site, removes something or changes the team, and waits for `confirm: true`.

Some things are not reachable from an assistant at all, whatever scopes the key carries:

- **API keys.** A server that can mint a secret key is a server whose compromise mints secret keys.
- **Webhooks.** An assistant that can repoint delivery URLs can quietly redirect your event stream.
- **The AI agent controls, approving its own requests, payment details, plan changes, ownership,
  deleting a Site or the organisation, the outreach policy and mailbox, and CMS or Bing
  credentials.** `get_api_docs` (section `tools`) gives the dashboard link a person uses for each.

All of these stay in the dashboard.

### Approvals, and turning agents off

When an assistant signed in through the browser deletes something or unpublishes an
article, your organisation can require a person to approve it first. This is on by default. Actions
that spend money or change the team (pipeline runs and turning the pipeline up, paid SEO scans,
custom domains, publishing the hosted site, CMS connections and pushes, auto-refill, raising a
credit cap, keeping the plan, invites, role and permission changes, removing a member) always need
that approval when an assistant asks, whatever the switch says. The tool then does nothing and returns a link to
`https://app.writavo.com/approvals/...`; once you approve there, the assistant repeats the call
with the approval id and it goes through, once, for exactly that request. An owner or admin can
switch approvals off, or switch AI agent access off for the whole organisation, at
<https://app.writavo.com/settings/agents>, which also lists every connected assistant and every
call they made. Keys you create yourself in the dashboard are not subject to either switch.

## What it never writes down

- The connection's key is sent to `https://api.writavo.com/v1` as a bearer header and to nothing else. No
  telemetry, no analytics, no third-party host. Each request names the tool that made it
  (`Writavo-Mcp-Tool`), which is what fills the call log in Settings > AI agents. The base URL is
  read from the specification. The only other requests
  are the presigned storage upload during `upload_media` or an import, and, during an import, the
  image URLs in your own file, fetched to copy them.
- Every reply, log line and error is passed through a redactor, so a key cannot reach your
  transcript even if the API echoed it back inside an error message.

## Errors you might see

Each one is answered with what to change rather than a status code.

| Code | What it means | What to do |
|---|---|---|
| `INSUFFICIENT_SCOPE` | The key lacks the scope, or its creator's permissions no longer cover it | Connect again with that permission (a sign-in), or add the scope to a key you created at <https://app.writavo.com/settings/api-keys> |
| `NOT_ENTITLED` | Your plan does not include the capability | Upgrade at <https://app.writavo.com/billing> |
| `INSUFFICIENT_CREDITS` | The organisation cannot afford the next unit of work | Top up at <https://app.writavo.com/billing> |
| `SPEND_CAP_REACHED` | This Site hit the monthly ceiling you set for it | Raise the cap or wait for the reset |
| `NOT_FOUND` | No such object, or it belongs to a different Site | Check you are using the key for the right Site |
| `AGENT_ACCESS_DISABLED` | AI agent access is off for the organisation | An owner or admin turns it on at <https://app.writavo.com/settings/agents> |
| `APPROVAL_REQUIRED` | A person must approve this action | Give them the link, wait, then call the tool again with `approval_id` |
| `APPROVAL_PENDING` | Nobody has decided yet | Ask them to open the link; call again once approved |
| `APPROVAL_DENIED` | A person denied the approval | Nothing to retry; decide what to do instead |
| `APPROVAL_INVALID` | The approval expired, was used, or was for a different request | Call the tool again without `approval_id` |
| `API_KEY_REVOKED` / `API_KEY_EXPIRED` | The connection's key was revoked or expired | Reconnect and sign in again |
| `PAYMENT_METHOD_REQUIRED` | The included CMS allowance is used up and no card is on file | Add a card at <https://app.writavo.com/billing>; not a plan limit |

The full catalog is in `get_api_docs` under `errors`, and at <https://writavo.com/docs/errors>.
Sign-in problems (a browser page saying the sign-in "could not be confirmed in this browser", no Site
to choose, and so on) are covered at <https://writavo.com/docs/mcp#troubleshooting>.

## Prompts

- **draft-article.** Research a topic and write a draft in your Site's own voice. It never
  publishes or schedules.
- **publish-checklist.** Walk an existing draft through title, slug, SEO fields, excerpt, category,
  author and featured image, then ask before publishing.
- **migrate-content.** Move an existing blog from another system into your Site, faithfully,
  with a dry run and your confirmation before anything is published. It follows the runbook at
  <https://writavo.com/docs/migrate>.

## Development

This package lives in the Writavo monorepo as a private workspace package (it is not published to
npm). The tools live once, in `src/core/`, exported as `@writavo/mcp-server/core`, which the hosted
server bundles:

```ts
import { createWritavoMcpServer } from "@writavo/mcp-server/core";

const server = createWritavoMcpServer({
  apiKey: () => key,              // null makes every key-requiring tool say how to connect
  userAgent: "my-host/1.0",
});
```

Nothing in the package reads the environment, touches a filesystem or keeps a key in module
state, so it runs in a Cloudflare Worker (the hosted server mounts exactly this). There is no
local or stdio entry point: the package has no `bin` and exports only `./core`. Tool schemas,
descriptions and reference text are generated, not written:

```
npm run build       check the generated tools are current, then compile
npm run gen         regenerate from the vendored openapi.yaml
npm test            run the offline smoke checks (the core over an in-memory MCP client, imports and more, against a stub API)
npm run gen:check   fails if the committed tool surface is stale
```

Adding an endpoint to `openapi.yaml` and regenerating is the whole of adding it; an operation
marked `x-writavo-approval` gains an `approval_id` argument by itself. The forty five
operations marked as tools are individual tools; every other operation lands in the `ACTIONS` catalog in
`src/generated/operations.ts` (by its tag, or `x-mcp-surface: tool | action` on the operation or
the tag), which `search_writavo_actions`, `read_writavo_action` and `run_writavo_action` serve. Editing anything under
`src/generated/` fails CI. The nine hand-written tools live in `src/tools/` (`CORE_LOCAL_TOOL_NAMES`
in `src/core/server.ts`) and are listed in `LOCAL_TOOLS` in the monorepo's `scripts/mcp-surface.mjs`.

## Licence

**MIT** ([LICENSE](LICENSE)). Fork it, modify it, vendor it, ship it inside something else.

The MIT grant covers **this client code only**. It is not a licence to the Writavo Content API
that the package calls, or to any other part of Writavo. Using the API still requires your own
credentials and is governed by the terms at <https://writavo.com/terms>, and `openapi.yaml` (the
specification this package is compiled from) remains the proprietary contract it always was.

That split is deliberate. This code is a thin, generated client: there is nothing in it worth
restricting, and an MIT client is easier to trust and audit. The
product is the API behind it.

TDQS

A4.2/5.0

Scored across 38 tools

Disambiguation5/5

Each tool targets a distinct resource and action. Articles, categories, tags, authors, media, and pipeline are cleanly separated, and related operations like get/list are clearly differentiated.

Naming Consistency5/5

All tools follow the verb_noun pattern with consistent lowercase_snake_case. Prefixes like get_, list_, create_, update_, delete_, publish_, unpublish_, schedule_ are applied uniformly across resources.

Tool Count2/5

At 38 tools, the server is well above the 25+ threshold for a large tool set. While each tool has a distinct purpose, the sheer number is likely to overwhelm an agent and complicates tool selection.

Completeness5/5

The server provides full CRUD and lifecycle coverage for articles, categories, tags, authors, and media, plus publishing, scheduling, pipeline management, and metadata. No obvious gaps for the stated domain.

Maintenance

ActivityMaintained
ResponsivenessNo issues