POST /api/mcpAuthorization: Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Accept: application/json, text/event-streamentity_revisions.base_url and mcp_token collection variables./api/mcp endpoint. Token is kept in this browser only.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.
Read-only access to entities, revisions, and metadata.
All viewer access plus create / update / delete entities.
All editor access plus workspace, member, and token management (UI only).
| Tool | Access | viewer | editor | owner |
|---|---|---|---|---|
| search_entities | read | |||
| get_entity | read | |||
| list_workpackages | read | |||
| list_repositories | read | |||
| upsert_entity | write | — | ||
| delete_entity | write | — | ||
| list_revisions | read | |||
| get_revision | read |
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).
| Code | Meaning | Typical cause |
|---|---|---|
| -32000 | unauthorized | Missing, revoked, or expired bearer token. |
| -32001 | forbidden | Token role cannot call this tool. |
| -32003 | version_conflict | Optimistic concurrency check failed on upsert. |
| -32004 | entity_not_found | Entity id does not exist in this workspace. |
| -32601 | Method not found | Unknown JSON-RPC method or tool name. |
| -32602 | Invalid params | Argument schema validation failed. |
| -32603 | Internal error | Unexpected server failure; safe to retry. |
| -32700 | Parse error | Request body was not valid JSON. |
Missing or invalid bearer token
The Authorization header is absent, malformed, points to a revoked token, or the token has expired. The HTTP response is 401.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_workpackages",
"arguments": {}
}
}{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "unauthorized",
"data": {
"reason": "invalid_token"
}
}
}Unauthorized tool for this role
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.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "upsert_entity",
"arguments": {
"id": "F-001",
"kind": "Feature",
"fields": {}
}
}
}{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32001,
"message": "forbidden",
"data": {
"reason": "role_insufficient",
"required": "editor",
"actual": "viewer"
}
}
}Unknown tool name
The tool name in params.name is not registered. Use tools/list to discover valid names.
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "delete_workpackage",
"arguments": {
"id": "W-9"
}
}
}{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32601,
"message": "Method not found",
"data": {
"tool": "delete_workpackage"
}
}
}Invalid entity id (not found)
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.
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "get_entity",
"arguments": {
"id": "F-9999"
}
}
}{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32004,
"message": "entity_not_found",
"data": {
"id": "F-9999"
}
}
}Invalid arguments / schema validation
Required arguments are missing or the wrong type — for example upsert_entity called without id, or list_revisions called with a non-string id.
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "upsert_entity",
"arguments": {
"kind": "Feature"
}
}
}{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"issues": [
{
"path": [
"id"
],
"message": "Required"
}
]
}
}
}Version conflict on upsert
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.
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "upsert_entity",
"arguments": {
"id": "F-001",
"kind": "Feature",
"expected_version": 3,
"fields": {
"title": "Renamed"
}
}
}
}{
"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
Full-text search across entities in the tenant. All filters optional; combine freely.
{
"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 -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
}
}
}'get_entity
Fetch a single entity (with full `fields` JSON) by external_id.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_entity",
"arguments": {
"externalId": "F-001"
}
}
}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"
}
}
}'list_workpackages
Every Workpackage in the tenant, with lifecycle and fields.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_workpackages",
"arguments": {}
}
}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": {}
}
}'list_repositories
Repositories registered in the tenant.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_repositories",
"arguments": {}
}
}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": {}
}
}'upsert_entity
Create or update an entity. Use `expectedVersion` for optimistic concurrency. `repos` is an array of repository slugs.
{
"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 -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
}
}
}'delete_entity
Delete an entity by external_id. Snapshots a revision row first.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "delete_entity",
"arguments": {
"externalId": "F-001"
}
}
}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"
}
}
}'list_revisions
List the revision history of an entity (newest first). Each row records the author, source (ui/mcp), and an optional change summary.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_revisions",
"arguments": {
"externalId": "F-001",
"limit": 50
}
}
}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
}
}
}'get_revision
Fetch one or two specific revisions for diffing. Pass `version` for a single snapshot, or `from`+`to` for both sides.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_revision",
"arguments": {
"externalId": "F-001",
"from": 2,
"to": 4
}
}
}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
}
}
}'Claude Desktop / Cursor (mcp.json):
{
"mcpServers": {
"spec-catalog": {
"transport": "http",
"url": "/api/mcp",
"headers": {
"Authorization": "Bearer mcp_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}401 Unauthorized— missing or revoked token.-32000— viewer token attempted a write.-32601— unknown method or tool name.409on upsert —expectedVersiondidn't match current row; re-fetch and retry.