Content MCP
How to connect an AI agent (Claude or any Model Context Protocol client) to a Meridian site: turning the endpoint on, OAuth sign-in and the consent choices, the mcp configuration block, registering clients, personal access tokens and engine-dmo mcp call for scripts, engine-dmo mcp over stdio for local development, the tools, resources and prompts, limits, and what to tell a data protection officer.
The content MCP lets a member of staff connect their own AI agent to a site, through the Model Context Protocol, so the agent can read content and write drafts as that person and never as more: it sees what they may see, writes where they may write, and publishes only where they could publish in the admin, through the same review workflow and checks. Everything it writes is recorded as written through an agent, for whom and by which client.
Meridian does not choose the model, send content to one through this channel, or see the agent’s reasoning: the person chooses the client, and the client its model. Meridian’s own AI assistance features are separate (see AI assistance).
There are two ways in:
| Remote endpoint | engine-dmo mcp over stdio | |
|---|---|---|
| For | Staff and their agents, on a running install | A coding agent working on a project on your machine |
| Address | https://<admin host><admin_path>/mcp | A child process the agent starts |
| Sign-in | OAuth 2.1 with PKCE through the admin’s sign-in, or a personal access token | None: every call acts as the staff user --as names |
| Allowed in | Every environment, when mcp.enabled | Development; in staging only with dev.authoring: true; never in production |
engine-cms serves the same MCP, with the toolsets of its own modules.
Turning it on
# config/engine.yaml
mcp:
enabled: true
personal_data: refuse # the default Then decide who may connect. The verb mcp.connect, judged per site, is held by the built-in Administrator and Editor roles and not by Translator or Contributor; move it in your roles like any permission. A person without it on a site cannot connect an agent for that site, and losing it takes the site out of their connections on the next call.
Before deciding, list what an agent can do on your bundle:
engine-dmo mcp tools --project . # tools, resources and prompts, with their toolsets
engine-dmo mcp tools --json --project . # with every tool's input and output schema Connecting a client
The endpoint is a standard remote MCP server with OAuth discovery, so a client finds the sign-in by itself. Give it the endpoint’s address, such as https://studio.example.org/studio/mcp.
- Claude Code:
claude mcp add --transport http meridian https://studio.example.org/studio/mcp, then/mcpin a session to sign in. The browser opens the admin’s sign-in, then the consent screen. - Claude on the web or the desktop app: add a custom connector with the endpoint’s address; sign-in and consent open in the browser.
- Any other client supporting the MCP authorisation flow (protocol revision 2025-06-18 or later; the endpoint answers 2026-07-28, 2025-11-25 and 2025-06-18).
The endpoint publishes its OAuth metadata at /.well-known/oauth-protected-resource<admin_path>/mcp and /.well-known/oauth-authorization-server<admin_path>/oauth.
The client does the whole sign-in itself, from the first refused call to the first tool call; the person sees only the admin’s sign-in and the consent screen. The diagram uses the default admin path, /studio:
sequenceDiagram participant C as MCP client participant B as Browser participant E as Engine C->>E: POST /studio/mcp (no token) E-->>C: 401, WWW-Authenticate: resource_metadata C->>E: GET the protected resource and server metadata E-->>C: authorize and token endpoints, S256 only C->>B: open /studio/oauth/authorize<br/>with code_challenge B->>E: authorize request E->>E: identify the client (metadata document,<br/>configured, or registered) E-->>B: admin sign-in and second factor, if needed E-->>B: consent screen B->>E: approve choices E-->>B: redirect to redirect_uri with a code B-->>C: code C->>E: POST /studio/oauth/token<br/>code and code_verifier E-->>C: access token (1 hour) and refresh token C->>E: POST /studio/mcp, Bearer token E-->>C: tool results, as the person
When the access token lapses the client exchanges its refresh token at the same token endpoint; refresh tokens rotate, and one used twice ends the connection.
How clients register
| Way | Configuration | Consent screen shows |
|---|---|---|
Client metadata document (the client’s client_id is a URL of a JSON document describing it) | mcp.registration.metadata_documents: true, the default | The host of the metadata document |
| A client an administrator configured | mcp.clients | Its configured name |
| Dynamic registration (RFC 7591) | mcp.registration.dynamic: true; off by default | The name it registered |
Configure a client when your organisation’s agent platform cannot register itself:
# config/engine.yaml
mcp:
enabled: true
clients:
- id: acme-agents # the client_id the client sends
name: Acme agent platform # shown on the consent screen
redirect_uris:
- https://agents.acme.example/oauth/callback
- http://127.0.0.1:33418/callback A client’s id must not be a URL (a URL client id is a metadata document, which needs no configuration) and must be unique; name and at least one redirect URI are required; each redirect URI is https, or http on a loopback address, with no fragment.
The consent screen
Every choice only takes away from what the person may already do; none grants anything.
| Choice | What it means | Default |
|---|---|---|
| Sites | Which sites the agent may work on, among those where the person holds mcp.connect | The site they were on |
| Locales | Which languages it may edit | All the person may edit |
| What it may do | Toolsets. Read is always on. Create and edit drafts (content), Media, Notes, Tags, smart lists and navigation (structure), Bulk changes, and one for each module the bundle has | Read, content, media and notes |
| Typed tools | Up to eight content types given their own create_<type> and update_<type> tools, which some models use more reliably | None |
| Publishing | As my permissions allow: when asked, it publishes where the person could and sends to review where their changes go to review. Drafts only: it never publishes | As my permissions allow; every write is still a draft unless a call asks to publish |
| Expires | 1, 7, 30 or 90 days, at most mcp.tokens.connection_max | 30 days |
The screen also says, in plain sentences, what the agent will be able to do, that it acts as the person and is recorded under their name, and that what it reads leaves the install for the client and its model. A connection that is Drafts only and asked to publish is refused in a way the client can show (“connect again with publishing allowed”).
The People toolset (CRM personal data, engine-dmo only) is offered only when mcp.personal_data: allow.
Seeing and ending connections
- You, Agents and tokens (
<admin_path>/you/agents): your own connections and tokens, with the client, the sites and toolsets chosen, when each was last used and how many writes it made this week. End one, or all. - Settings, Agent connections (
<admin_path>/settings/agents, for people withusers.manage): every connection on the install, filterable by person, client and site. End any, or block a client. - The editor and the revision history name the agent on every revision it wrote, the approvals queue marks agent-written changes, and content lists can be filtered to “written by an agent”.
Suspending a person ends their connections at once.
Personal access tokens for scripts
A script that moves content (a nightly sync, a one-off bulk tag) uses a personal access token, made on You, Agents and tokens with the same choices as consent plus a kind: agent (a model drives it) or script (deterministic code, whose writes are not counted as AI assisted). It is sent as Authorization: Bearer mrd_pat_... to the same endpoint. A token belongs to a person and ends with them: there are no service accounts.
From a shell:
export MERIDIAN_TOKEN=mrd_pat_...
engine-dmo mcp call documents_search \
--url https://studio.example.org/studio/mcp \
--args '{"site": "main", "query": "summer fair"}' | Flag | Meaning |
|---|---|
--url | The endpoint’s address (required). |
--token-env | The environment variable holding the token, MERIDIAN_TOKEN by default. The token is never a command line argument. |
--args | The tool’s arguments as a JSON object. |
It prints the tool’s structured result as JSON; a tool error exits non-zero with its JSON on standard output. Read a tool’s arguments with engine-dmo mcp tools --json.
engine-dmo mcp over stdio
For a coding agent working on a project locally, the engine serves the same tools over standard input and output (newline-delimited JSON-RPC), every call acting as one staff user with exactly their grants:
engine-dmo mcp --as editor@example.org --project /path/to/project claude mcp add meridian -- engine-dmo mcp --as editor@example.org --project /path/to/project The process needs the project’s environment (DATABASE_URL and the rest) and a migrated database.
| Flag | Meaning |
|---|---|
--as | The staff user every call acts as, by email (required). |
--site | The site a call that names none is for; the project’s default site when absent, the first of --sites when that is set. |
--sites | Narrow to these sites, comma separated. |
--toolsets | Narrow to these toolsets, comma separated; read is always included. |
--types | Give these types typed tools, comma separated, at most eight. |
--drafts-only | Never publish, as a Drafts only connection. |
It is refused when engine.environment is production, and in staging unless dev.authoring: true. The narrowing flags only remove, as consent does.
What an agent can do
engine-dmo mcp tools lists exactly what your bundle offers. On engine-dmo:
| Toolset | Tools |
|---|---|
read (always) | sites_list, types_list, type_describe, documents_search, document_get, document_history, singleton_get, translation_status, notes_list, picker_search, media_search, media_get, tags_list, smart_lists_list, smart_list_get, content_check, preview_link, bulk_status |
content | document_create, document_update, document_clone, document_trash, document_restore, document_sites_set, document_override_set, document_variant_set, publish, unpublish, review_request, schedule_cancel, landing_content_get, landing_content_set |
media | media_upload, media_upload_url, media_upload_page, media_upload_page_get, media_text_set, media_rights_set |
notes | note_add |
structure | tag_create, smart_list_save, navigation_get, navigation_edit |
bulk | bulk_plan, bulk_apply |
crm | organisations_search, organisation_get, organisation_update |
people | people_search, person_get (only with mcp.personal_data: allow) |
newsletter | newsletter_campaigns_list, newsletter_campaign_get, newsletter_campaign_draft |
affiliates | affiliate_programmes_list, affiliate_links_list, affiliate_link_create |
claims | claims_list, claim_get (read only) |
promotions | promotions_list, promotion_request_get, promotion_slots_list (read only) |
paidlistings | paid_orders_list, paid_order_get (read only) |
Resources: meridian://rules (rules for agents), meridian://sites, meridian://sites/{site}/schema, meridian://sites/{site}/guidance and meridian://sites/{site}/guidance/{type} (the site’s editorial guidance), meridian://types/{type} and meridian://documents/{id}. Prompts: draft_article (site, type, topic, sources), fix_content_health (site, type) and translate_document (document, target locale, source locale), each in the content toolset.
An agent cannot purge or export content, change users, settings or permissions, read the audit log, act as a member, call Meridian’s own AI features, send a newsletter or broadcast, change a price, create an order, or decide a claim, a promotion or a membership. Those stay with a person in the admin.
Photos from a phone
An agent in a chat app cannot pass a photo’s file to the install, so media_upload_page gives the person a one-time link instead. They open it on their phone, pick photos (20 unless the agent asked for another number, never more than 30), and go back to the conversation; the agent then sees the photos with media_upload_page_get, writes their alt text and puts them where asked. The link needs no sign-in (the link is the key), works for an hour, and ends when the connection ends. The photos are stripped of location and camera metadata like any upload, and are not published until a published page uses them; a page with a photo still missing alt text cannot be published. The agent needs the Media toolset and the person must be allowed to upload.
Configuration
Key in engine.yaml | Default | Limits | Meaning |
|---|---|---|---|
mcp.enabled | false | Serves <admin_path>/mcp and its OAuth endpoints. | |
mcp.personal_data | refuse | refuse or allow | Whether a toolset that returns CRM personal data may be chosen and called. |
mcp.registration.metadata_documents | true | Accept clients identified by a metadata document URL. | |
mcp.registration.dynamic | false | Accept dynamic client registration. | |
mcp.clients | none | id, name, redirect_uris each required | Configured clients. |
mcp.tokens.access_ttl | 1h | 5m to 24h | An access token’s life. |
mcp.tokens.refresh_idle | 30d | 1d to 180d | How long a refresh token may go unused. |
mcp.tokens.connection_max | 90d | 1d to 180d | The longest a connection or personal token may live. |
mcp.media.fetch | true | Whether media_upload_url may fetch a file from an address. | |
mcp.media.fetch_hosts | any | Host names | When set, only these hosts and their subdomains may be fetched from. |
mcp.limits.* | See below | See below | The operational limits. |
# config/engine.yaml
mcp:
enabled: true
personal_data: refuse
registration: { metadata_documents: true, dynamic: false }
tokens: { access_ttl: 1h, refresh_idle: 30d, connection_max: 90d }
media: { fetch: true, fetch_hosts: [images.example.org] }
limits: { calls_per_minute: 600, writes_per_minute: 60, writes_per_day: 2000, bulk_items: 200, media_by_url_per_hour: 20 } Limits
| Limit | Default | Ceiling | Key |
|---|---|---|---|
Weighted calls per connection a minute (a read 1, a search 2, content_check 3, a write 5, bulk_plan 1 an item) | 600 | 1,200 | calls_per_minute |
| Writes per person a minute, across every connection | 60 | 120 | writes_per_minute |
| Writes per person a day, across every connection | 2,000 | 5,000 | writes_per_day |
| Operations in one bulk plan | 200 | 500 | bulk_items |
| Uploads by address per connection an hour | 20 | 100 | media_by_url_per_hour |
A value above its ceiling, or zero or below, is a check error; leave a key out for its default. The weighted call limit is counted in each engine process’s memory, so with several replicas behind a load balancer a connection can make up to calls_per_minute on each replica: with three, up to three times the figure. The write limits are counted in the database and hold across replicas. A call past a limit is refused rate_limited with the seconds to wait. The arguments of one write are at most 1 MB. Sign-in, token and registration attempts have fixed limits of their own. The operational limits fail open if they cannot be counted; the security limits fail closed.
What to tell your data protection officer
This section describes the install as shipped; your configuration and choice of clients decide the rest.
- Who decides where content goes. The person who connects an agent chooses the client, and the client chooses its model provider. Meridian sends nothing to any model through this channel and proxies nothing: content an agent reads goes from your install to that person’s client, under their own account with it. Treat each client your organisation approves as a processor you have assessed, and tell staff which clients are approved.
- What can leave. By default only what the person can already see in the admin: the site’s content, drafts included, its media and the notes on its documents. CRM personal data about people is refused by default (
mcp.personal_data: refuse) and no tool returns it. Organisations’ public profile fields are reachable through the CRM toolset. A claim’s claimant contact details are left out while personal data is refused. Fields marked sensitive in the CRM are never returned. - If you allow personal data (
mcp.personal_data: allow), people with the CRM permissions may choose the People toolset, and every read through it that returns records is written to the audit log. Add “CRM personal data to the person’s MCP client” to your record of processing and your DPIA, naming the approved clients and their providers. - What is recorded. Every write is in the audit log as the person via the client, with the connection, the tool and the call’s key; every revision it made says it came through an agent and for whom; consent, token issue, token reuse and every ending of a connection are audited. Reads are counted per connection and day rather than logged one by one, except personal data reads. The audit log’s retention is
retention.audit_log. - Tokens and sign-in. Connections use OAuth 2.1 with PKCE through the admin’s own sign-in and second factor. Tokens are random, held only as hashes, and access tokens live an hour by default; refresh tokens rotate, and one used twice ends the whole connection. Tokens are never put in a URL or a log.
- Prompt injection. Content written by members or imported from feeds may contain text addressed to a model. The engine never treats content as an instruction, and marks untrusted content it hands a prompt. An agent misled by such text can still do only what its connection allows, publishing only as its person’s workflow allows, and anything it trashes stays in the trash for
content.trash_retention(30 days by default).
Sources and importers
How to bring another service's data into a site without code: sources.yaml for cached, allowlisted upstream JSON read by templates and endpoints (with caching, failure modes and the circuit breaker), and importers.yaml for scheduled feed imports that create and update documents by external id, with every option, default and limit.
Member API
How to build your own member portal (a single-page app or a headless site's account area) on the member API at /api/member/v1: what it requires, signing members in through the engine's sign-in screens, the same-origin session cookie and CSRF token, a worked request sequence, the form description, errors, rate limits and the OpenAPI contract.