Integrations

MCP API

JSON-RPC 2.0 over HTTP. Mint a token in Settings, then point Claude Desktop, Cursor, or your own client at the endpoint below.

Endpoint
POST /api/mcp
Required headers on every request:
Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Accept: application/json, text/event-stream
The raw token is shown only once at creation time. Viewer tokens may only call read tools; editor/owner tokens may call writes. Every write snapshots a row into entity_revisions.
↓ Download Postman collection (v2.1)Import into Postman, then set base_url and mcp_token collection variables.
Try it
Run JSON-RPC requests against your live /api/mcp endpoint. Token is kept in this browser only.
Response
No request sent yet.
Handshake

MCP clients first call initialize and then tools/list. You usually don't have to do this by hand — the client does it for you — but here is the shape:

{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {}
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}

Token scopes & permissions

Every MCP token is minted against a single workspace and a single role. The role gates which JSON-RPC tools the token can call — there is no per-tool override. Tokens never escape their workspace: cross-workspace reads or writes require a separate token.

viewer

Read-only access to entities, revisions, and metadata.

editor

All viewer access plus create / update / delete entities.

owner

All editor access plus workspace, member, and token management (UI only).

Workspace access: the token's workspace is resolved server-side from the bearer; tool arguments cannot target other workspaces. Row Level Security additionally scopes every read and write to that tenant's data.

Unauthorized calls return JSON-RPC error -32001 ("forbidden") when the token's role is insufficient, or -32000 ("unauthorized") when the bearer is missing, revoked, or expired.

Member & token management (invite users, mint or revoke tokens, change roles) is owner-only and lives in the dashboard Settings page — it is not exposed over MCP.

Errors & failure cases

Errors follow the JSON-RPC 2.0 error object shape: { code, message, data? }. The HTTP status is 200 for tool-level errors and 401 only when the bearer itself is rejected before dispatch. The id in the response always matches the request id (or null when the request id could not be parsed).

CodeMeaningTypical cause
-32000unauthorizedMissing, revoked, or expired bearer token.
-32001forbiddenToken role cannot call this tool.
-32003version_conflictOptimistic concurrency check failed on upsert.
-32004entity_not_foundEntity id does not exist in this workspace.
-32601Method not foundUnknown JSON-RPC method or tool name.
-32602Invalid paramsArgument schema validation failed.
-32603Internal errorUnexpected server failure; safe to retry.
-32700Parse errorRequest body was not valid JSON.

Missing or invalid bearer token

code -32000

The Authorization header is absent, malformed, points to a revoked token, or the token has expired. The HTTP response is 401.

Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_workpackages",
    "arguments": {}
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32000,
    "message": "unauthorized",
    "data": {
      "reason": "invalid_token"
    }
  }
}

Unauthorized tool for this role

code -32001

The token is valid but its role cannot call the requested tool — e.g. a viewer token calling upsert_entity or delete_entity. See the permissions matrix above.

Request
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "upsert_entity",
    "arguments": {
      "id": "F-001",
      "kind": "Feature",
      "fields": {}
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32001,
    "message": "forbidden",
    "data": {
      "reason": "role_insufficient",
      "required": "editor",
      "actual": "viewer"
    }
  }
}

Unknown tool name

code -32601

The tool name in params.name is not registered. Use tools/list to discover valid names.

Request
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "delete_workpackage",
    "arguments": {
      "id": "W-9"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32601,
    "message": "Method not found",
    "data": {
      "tool": "delete_workpackage"
    }
  }
}

Invalid entity id (not found)

code -32004

The id passed to get_entity, delete_entity, list_revisions, or get_revision does not exist inside this workspace. Cross-workspace ids are treated as not-found.

Request
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "get_entity",
    "arguments": {
      "id": "F-9999"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 4,
  "error": {
    "code": -32004,
    "message": "entity_not_found",
    "data": {
      "id": "F-9999"
    }
  }
}

Invalid arguments / schema validation

code -32602

Required arguments are missing or the wrong type — for example upsert_entity called without id, or list_revisions called with a non-string id.

Request
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "upsert_entity",
    "arguments": {
      "kind": "Feature"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 5,
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "issues": [
        {
          "path": [
            "id"
          ],
          "message": "Required"
        }
      ]
    }
  }
}

Version conflict on upsert

code -32003

upsert_entity was called with an expected_version that no longer matches the row — another writer (UI or MCP) updated it first. Re-read with get_entity and retry.

Request
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "upsert_entity",
    "arguments": {
      "id": "F-001",
      "kind": "Feature",
      "expected_version": 3,
      "fields": {
        "title": "Renamed"
      }
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 6,
  "error": {
    "code": -32003,
    "message": "version_conflict",
    "data": {
      "id": "F-001",
      "expected_version": 3,
      "current_version": 5
    }
  }
}

Tools

Each tool is invoked via tools/call with { name, arguments }.

search_entities

viewer+

Full-text search across entities in the tenant. All filters optional; combine freely.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_entities",
    "arguments": {
      "query": "rescue",
      "domain": "ba",
      "kind": "Feature",
      "workpackage": "w-7-rescue-flow",
      "repo": "spec-catalog",
      "limit": 25
    }
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_entities",
    "arguments": {
      "query": "rescue",
      "domain": "ba",
      "kind": "Feature",
      "workpackage": "w-7-rescue-flow",
      "repo": "spec-catalog",
      "limit": 25
    }
  }
}'
result → { entities: [{ external_id, name, title, lifecycle, ... }] }

get_entity

viewer+

Fetch a single entity (with full `fields` JSON) by external_id.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_entity",
    "arguments": {
      "externalId": "F-001"
    }
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_entity",
    "arguments": {
      "externalId": "F-001"
    }
  }
}'
result → Full entity row including fields and linked repositories.

list_workpackages

viewer+

Every Workpackage in the tenant, with lifecycle and fields.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_workpackages",
    "arguments": {}
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_workpackages",
    "arguments": {}
  }
}'
result → { workpackages: [{ external_id, name, title, lifecycle, fields }] }

list_repositories

viewer+

Repositories registered in the tenant.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_repositories",
    "arguments": {}
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_repositories",
    "arguments": {}
  }
}'
result → { repositories: [{ slug, name, color, git_url }] }

upsert_entity

editor / owner

Create or update an entity. Use `expectedVersion` for optimistic concurrency. `repos` is an array of repository slugs.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "upsert_entity",
    "arguments": {
      "externalId": "F-001",
      "name": "F-001",
      "title": "Operator can publish a rescue",
      "description": "Allow the operator to publish a rescue offer to nearby partners.",
      "domain": "ba",
      "kind": "Feature",
      "fkind": "Feature",
      "lifecycle": "in-progress",
      "owner": "product",
      "workpackage": "w-7-rescue-flow",
      "fields": {
        "priority": "must",
        "acceptance": [
          "partner notified",
          "audit log written"
        ]
      },
      "repos": [
        "spec-catalog",
        "rescue-app"
      ],
      "expectedVersion": 3
    }
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "upsert_entity",
    "arguments": {
      "externalId": "F-001",
      "name": "F-001",
      "title": "Operator can publish a rescue",
      "description": "Allow the operator to publish a rescue offer to nearby partners.",
      "domain": "ba",
      "kind": "Feature",
      "fkind": "Feature",
      "lifecycle": "in-progress",
      "owner": "product",
      "workpackage": "w-7-rescue-flow",
      "fields": {
        "priority": "must",
        "acceptance": [
          "partner notified",
          "audit log written"
        ]
      },
      "repos": [
        "spec-catalog",
        "rescue-app"
      ],
      "expectedVersion": 3
    }
  }
}'
result → { entity: {...}, version: 4 } // 409 on version mismatch

delete_entity

editor / owner

Delete an entity by external_id. Snapshots a revision row first.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "delete_entity",
    "arguments": {
      "externalId": "F-001"
    }
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "delete_entity",
    "arguments": {
      "externalId": "F-001"
    }
  }
}'
result → { ok: true }

list_revisions

viewer+

List the revision history of an entity (newest first). Each row records the author, source (ui/mcp), and an optional change summary.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_revisions",
    "arguments": {
      "externalId": "F-001",
      "limit": 50
    }
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_revisions",
    "arguments": {
      "externalId": "F-001",
      "limit": 50
    }
  }
}'
result → { currentVersion: 4, revisions: [{ version, changed_by, changed_via, change_summary, created_at }] }

get_revision

viewer+

Fetch one or two specific revisions for diffing. Pass `version` for a single snapshot, or `from`+`to` for both sides.

JSON-RPC request body
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_revision",
    "arguments": {
      "externalId": "F-001",
      "from": 2,
      "to": 4
    }
  }
}
curl
curl -sS /api/mcp \
  -H 'Authorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_revision",
    "arguments": {
      "externalId": "F-001",
      "from": 2,
      "to": 4
    }
  }
}'
result → { revisions: [{ version, fields, repos, change_summary, ... }] }
Client config

Claude Desktop / Cursor (mcp.json):

{
  "mcpServers": {
    "spec-catalog": {
      "transport": "http",
      "url": "/api/mcp",
      "headers": {
        "Authorization": "Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
Errors
  • 401 Unauthorized — missing or revoked token.
  • -32000 — viewer token attempted a write.
  • -32601 — unknown method or tool name.
  • 409 on upsert — expectedVersion didn't match current row; re-fetch and retry.