Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

MCP plugin

One endpoint, one credential, and the same authority an API token already has.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

The Model Context Protocol endpoint. An external agent connects with an API token and gets the CMS’s content tools.

POST /api/v1/mcp        stateless Streamable HTTP, JSON responses

The plugin owns the protocol only — endpoint, bearer auth, workspace resolution, JSON-RPC. The tools live in the plugins that own the data and in the shared registry (@orthacms/tools-server) the copilot also uses.

Install and turn on

npm install @orthacms/mcp-server
// apps/server/src/plugins.ts — register LAST: it serves what the plugins above contributed
import { McpPlugin } from '@orthacms/mcp-server';

McpPlugin({ config: config.plugins.mcp });
MCP_ENABLED=true               # default false — off, /api/v1/mcp is a 404
MCP_CALL_TIMEOUT_MS=30000      # ceiling on one tools/call
MCP_MAX_RESULT_BYTES=4194304   # ceiling on one tool result

apps/server/config/mcp.ts also sets name: 'orthacms' and version, the identity clients display. In create-orthacms-app, MCP is an opt-in protocol, unticked by default.

On, it hands an agent what the token can do

A full token lets an agent create, publish and delete content across its workspaces. Give retrieval-only agents a read token.

Connect a client

Header / paramWhen
Authorization: Bearer orthacms_…Always. A session cookie is not accepted
X-Workspace-Id: <uuid> or ?workspaceId=<uuid>When the token covers several workspaces. The header wins
X-Reader-Tags: premiumTo read as a segment reader

A client that speaks Streamable HTTP (exact keys vary by client):

{
    "mcpServers": {
        "orthacms": {
            "type": "http",
            "url": "https://cms.example.com/api/v1/mcp",
            "headers": {
                "Authorization": "Bearer orthacms_…",
                "X-Workspace-Id": "6b1f0c2e-8a4d-4f7b-9c3e-2d5a7e9f1b40"
            }
        }
    }
}

A stdio-only client, through mcp-remote:

{
    "mcpServers": {
        "orthacms": {
            "command": "npx",
            "args": [
                "-y",
                "mcp-remote",
                "https://cms.example.com/api/v1/mcp?workspaceId=6b1f0c2e-8a4d-4f7b-9c3e-2d5a7e9f1b40",
                "--header",
                "Authorization: Bearer orthacms_…"
            ]
        }
    }
}

Call it by hand

No session and no initialize needed — every request authenticates on its own. Send Accept: application/json, text/event-stream.

Example language
curl -X POST https://cms.example.com/api/v1/mcp \
-H 'Authorization: Bearer orthacms_…' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -X POST https://cms.example.com/api/v1/mcp \
-H 'Authorization: Bearer orthacms_…' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"content_list","arguments":{"typeName":"article","fields":"title,slug","pageSize":5,"locale":"de"}}}'
async function callTool(name, args) {
  const res = await fetch('https://cms.example.com/api/v1/mcp', {
      method: 'POST',
      headers: {
          Authorization: 'Bearer ' + process.env.ORTHACMS_TOKEN,
          Accept: 'application/json, text/event-stream',
          'Content-Type': 'application/json'
      },
      body: JSON.stringify({
          jsonrpc: '2.0',
          id: 1,
          method: 'tools/call',
          params: { name, arguments: args }
      })
  });
  const { result } = await res.json();
  const data = JSON.parse(result.content[0].text);
  if (result.isError) throw new Error(data.message);
  return data;
}

const draft = await callTool('content_create', {
  typeName: 'article',
  values: { title: 'Autumn catalogue' }
});
await callTool('content_publish', { typeName: 'article', id: draft.id });

A result carries the value twice — as JSON text and as structuredContent:

{
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
        "content": [{ "type": "text", "text": "{\"items\":[…],\"total\":42,\"page\":1,\"pageSize\":5}" }],
        "structuredContent": { "items": [], "total": 42, "page": 1, "pageSize": 5 }
    }
}

A tool failure is a result the model can read, not a transport error:

{
    "result": {
        "isError": true,
        "content": [{ "type": "text", "text": "{\"status\":504,\"code\":\"timeout\",\"message\":\"\\\"content_list\\\" did not finish within 30000ms and was abandoned. Retry, or narrow the request.\"}" }]
    }
}
SituationAnswer
Bad or missing tokenHTTP 401 — before any JSON-RPC
GET or DELETEHTTP 405 (after auth) — stateless, no stream, no session
Workspace outside the tokenHTTP 403
?workspaceId repeatedHTTP 400
Body over 1 MBHTTP 413
Call past MCP_CALL_TIMEOUT_MSisError, code: "timeout"
Result over MCP_MAX_RESULT_BYTESisError, code: "result_too_large"

The timeout abandons the call; a tool that ignores the abort still runs to the end.

Tools

Twenty-six. tools/list shows only those whose permissions the token’s scope holds.

PluginTools
Content — 16content_types_list, content_type_get, content_list, content_get, content_relations, content_media, content_translations, content_create, content_update, content_publish, content_unpublish, content_delete, content_bulk_save, content_bulk_publish, content_bulk_unpublish, content_bulk_delete
i18ni18n_locales_list
Mediamedia_assets_search, media_folders_list, media_asset_read
Segmentssegments_list, content_access_get, content_access_set
Protectionprotection_review_status, protection_review_diff, protection_request_review
  • Content reads serve published entries. status: "draft" or "any" needs a full token. A create is a draft — read it back with status: "any".
  • Since 0.7.0, content_list takes source: "own" | "shared" | "all" (default own). Items from a shared workspace carry source: { workspaceId, workspaceName }; content_get marks them readOnly: true. Link them by id — writing one is a 403.
  • The content tools and content_access_set are MCP-only. The locale, media, segment-read and protection tools are shared with the copilot.
  • No tool approves a review or sends mail.

Each content type’s schema is also an MCP resource, orthacms://content-type/<name> (resources/list, resources/read).

See MCP for the agent-side guide and agent tools for adding a tool.