Skip to content

MCP Server

Overview

ODP includes a built-in Model Context Protocol (MCP) server that enables AI agents and LLMs to interact with ODP data and schema through a standardized tool-calling interface.

The MCP server exposes ODP's data operations (CRUD on collections, schema management, workflow operations) as typed tools that can be discovered and called by any MCP-compatible AI client (Claude, ChatGPT plugins, etc.).


Architecture

ODP implements MCP in two modes:

1. Stateless HTTP (per-request)

POST /server/mcp — A simple endpoint where each request is handled by a fresh MCP Server instance and FastifyTransport. No persistent connection required.

2. Full MCP Protocol (via OdpMCP)

Routes using OdpMCP.handleRequest() implement the full JSON-RPC message protocol per the MCP specification. Each request creates a fresh Server instance and FastifyTransport to avoid connection conflicts with concurrent requests.


Access Control

MCP is not a permission bypass. Every MCP operation runs under the caller's own access policies — exactly like the equivalent REST call. Access is enforced in two layers.

Layer 1 — Gate: who can use MCP at all

  1. Authenticated request — the request must carry a valid token resolving to accountability.user, accountability.role, or accountability.admin === true. Anonymous requests are rejected with 403 Forbidden (odp-mcp.ts).
  2. App permission mcp / access — when ENABLE_APP_PERMISSIONS !== 'false', the caller must be granted the app permission module = mcp, action = access via their access policy. Without it, no tool can be invoked (validateAppAccess).

Layer 2 — Per-operation: what they can see/do through MCP

Each tool instantiates the matching service (ItemsService, CollectionsService, UsersService, workflow services, …) with the caller's accountability — never a system/null bypass. Because services are the only layer that touches the database and they enforce permissions internally, every MCP operation passes the full permission pipeline:

  • Collection-levelvalidateAccess(read | create | update | delete) throws 403 Forbidden if the policy denies the action.
  • Row-level — permission filters are merged into the query (via processAst), so the caller only sees rows their policy allows.
  • Field-level — fields the policy hides are stripped from reads and rejected on writes.

Implication for your team

You do not configure permissions separately for MCP. MCP inherits whatever access policies the user already has. If a user cannot read collection articles over REST, the MCP items_read tool returns the same 403 / empty result for articles. To change what an MCP user can do, edit their role / access policy — not the MCP module.

Layer 0 — Module settings (further narrowing)

On top of permissions, the MCP module has its own settings (managed via GET/PATCH /server/mcp/settings, see Configuration) that constrain everyone, including admins:

  • mcp_enabled — global on/off. When false, every /server/mcp request returns 403 regardless of permissions.
  • mcp_allowed_collections — allowlist; if set, MCP tools can only touch these collections (null = all).
  • mcp_read_only_collections — collections exposed read-only through MCP even if the user's policy allows writes.
  • mcp_allow_deletes — when false, delete tools are not exposed at all.

These narrow what MCP can do; they never widen a user's permissions. Effective access = user's access policy MCP module settings.

Setup checklist

To let a team member use MCP:

  1. Enable the module — set mcp_enabled = true (and optionally scope mcp_allowed_collections / mcp_read_only_collections / mcp_allow_deletes) via PATCH /server/mcp/settings.
  2. Create a token for the user (login or a sub-token) — MCP requests authenticate with Authorization: Bearer <token>.
  3. Grant the MCP app permission — in their access policy, add module = mcp, action = access (skip only if you run with ENABLE_APP_PERMISSIONS=false, which disables Layer 1 globally — Layer 2 still applies).
  4. Grant the data permissions they need — read/create/update/delete on the specific collections, with any row filters and field restrictions. These are the same policy rules that govern REST; MCP reuses them.
User caseResult through MCP
mcp_enabled = false (module off)403 Forbidden for everyone, including admins
No token / invalid token403 Forbidden — gate rejects before any tool runs
Authenticated but no mcp / access app permission403 Forbidden — cannot invoke any tool
Has MCP access, but policy denies read on collection ABCitems_read on ABC403 / no rows (identical to REST)
Policy allows read on ABC with a row filterOnly the permitted rows returned; hidden fields stripped
Collection not in mcp_allowed_collectionsNot reachable via MCP even if the policy allows it
Collection in mcp_read_only_collectionsReads work; create/update/delete blocked through MCP
accountability.admin === trueBypasses auth gate + policy checks, but still bound by module settings (Layer 0)

Endpoints

POST /server/mcp

Simple tool execution endpoint (non-standard, simpler protocol).

Auth required: Authenticated user with the mcp / access app permission (see Access Control). Each tool then runs under the caller's own access policies.

Request Body:

json
{
  "tool": "items_read",
  "params": {
    "collection": "articles",
    "query": { "limit": 10 }
  }
}

Response:

json
{
  "data": {
    "items": [...]
  }
}

Error response:

json
{
  "error": "Unknown tool: nonexistent_tool"
}

GET /server/mcp/tools

List all available MCP tools with their schemas.

Auth required: Authenticated user with the mcp / access app permission (see Access Control).

Response:

json
{
  "data": [
    {
      "name": "items_read",
      "description": "Read items from a collection",
      "parameters": {
        "type": "object",
        "properties": {
          "collection": { "type": "string" },
          "query": { "type": "object" }
        },
        "required": ["collection"]
      }
    }
  ]
}

Available Tools

Items

ToolDescription
items_readRead items from a collection with optional query
items_createCreate a single item
items_updateUpdate a single item by ID
items_deleteDelete a single item
items_create_manyCreate multiple items
items_update_manyUpdate multiple items
items_delete_manyDelete multiple items

Schema

ToolDescription
schema_listList all collections with their fields
schema_readRead schema for a specific collection

Collections

ToolDescription
collections_createCreate a new collection
collections_listList all collections
collections_readRead a collection's metadata
collections_updateUpdate a collection's metadata
collections_deleteDelete a collection

Fields

ToolDescription
fields_createAdd a field to a collection
fields_updateUpdate a field's configuration
fields_deleteRemove a field from a collection

Relations

ToolDescription
relations_createCreate a relation between collections
relations_listList all relations
relations_readRead a specific relation
relations_updateUpdate a relation
relations_deleteDelete a relation

Files

ToolDescription
files_listList files with optional filters
files_readRead a file's metadata by ID

Users

ToolDescription
users_listList users
users_readRead a user by ID

Versions

ToolDescription
versions_listList content versions
versions_readRead a version by ID
versions_promotePromote a version to live content

Activity

ToolDescription
activity_listQuery activity log

Workflow (Definition)

ToolDescription
workflow_createCreate a workflow definition
workflow_listList workflow definitions
workflow_readRead a workflow
workflow_updateUpdate a workflow
workflow_deleteDelete a workflow
workflow_duplicateDuplicate a workflow
workflow_activateActivate a workflow
workflow_deactivateDeactivate a workflow

Workflow Steps & Transitions

ToolDescription
workflow_step_addAdd a step to a workflow
workflow_step_updateUpdate a step
workflow_step_removeRemove a step
workflow_transition_addAdd a transition between steps
workflow_transition_updateUpdate a transition
workflow_transition_removeRemove a transition

Workflow Instances

ToolDescription
workflow_instance_startStart a workflow instance
workflow_instance_listList instances
workflow_instance_readRead an instance
workflow_instance_cancelCancel an instance
workflow_instance_approveApprove a step
workflow_instance_rejectReject a step
workflow_instance_delegateDelegate a step to another user
workflow_instance_commentAdd a comment to an instance

Workflow Tasks

ToolDescription
workflow_tasks_mineGet tasks assigned to current user
workflow_tasks_allGet all pending tasks
workflow_tasks_countCount pending tasks

OdpMCP Configuration

The OdpMCP class accepts an allowDeletes option:

typescript
const mcp = new OdpMCP({ allowDeletes: false });

When allowDeletes = false (default), items_delete and items_delete_many tools are excluded from the tool list. This provides a safety guard for production MCP deployments.


MCPContext

Each tool handler receives a context object:

typescript
interface MCPContext {
  knex: Knex;               // Database connection
  accountability: Accountability | null;  // Caller's permissions
  schema: SchemaOverview;   // Current database schema
  settings: MCPSettings;    // Collection access settings
}

interface MCPSettings {
  allowedCollections: string[] | null;  // null = all collections
  readOnlyCollections: string[] | null; // null = none read-only
}

Example: Using MCP with Claude

json
{
  "mcpServers": {
    "odp": {
      "url": "https://api.example.com/api/mcp",
      "headers": {
        "Authorization": "Bearer <access_token>"
      }
    }
  }
}

Then in Claude:

Use the items_read tool to get the latest 5 published articles from the "articles" collection.

Configuration

VariableDefaultDescription
ENABLE_APP_PERMISSIONStrueSet to false to disable app permission checks

MCP settings are also stored in odp_extension_settings with key system_mcp_settings and can include allowedCollections and readOnlyCollections lists.

ODP Internal API Documentation