Results, errors and limits
These rules hold for every Drum MCP tool. They match the REST API wherever both have the same idea, so a figure or a reason code means the same thing on both.
Results
| Kind | Format |
|---|---|
| Ids | Integers. Get them from search_records or a get_ tool, never guess one |
| Money | Integer cents, tax exclusive, shown as {"cents": 25000, "currency": "AUD", "formatted": "$250.00"}. When you send money, send integer cents. A field that includes tax says so in its name |
| Percentages | Numbers from 0 to 100. 10 means 10 per cent |
| Durations | Whole minutes, in fields such as duration_minutes |
| Dates | yyyy-mm-dd. Fields ending _at are ISO 8601 timestamps |
| Updates | An update changes only the fields you send. Leave a field out to keep it |
| Inputs | Each tool’s input schema refuses keys it does not know |
Results come back as structuredContent, with the same data as text for clients that read only text. structuredContent matches the tool’s outputSchema.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{"type": "text", "text": "{\"rows\":[...],\"count\":1,...}"}],
"structuredContent": {
"rows": [
{
"kind": "task",
"id": 88121,
"name": "Site inspection",
"parent": {"kind": "deliverable", "id": 7301, "name": "Stage 1", "parent": {"kind": "project", "id": 20488, "name": "Riverside fit-out"}},
"status": "In progress",
"date": "2026-10-20",
"match": "exact"
}
],
"count": 1,
"limit": 25,
"offset": 0,
"has_more": false
}
}
}
Concise and detailed rows
list_projects, list_opportunities, list_tracked_times and get_receivables_report return concise rows, with linked records as {id, name} references. get_project, get_opportunity, get_proposal, get_deliverable_template and get_proposal_template return concise deliverable and task rows. Send response_format: "detailed" for the full rows. A page of detailed rows has 25 rows or fewer.
Pages
search_records and each list_ tool return one page.
| Input or field | Meaning |
|---|---|
limit |
Rows to return. Default 25, at most 100 |
offset |
Rows to skip. search_records allows at most 500 |
count, limit, offset |
The page that came back |
has_more |
true when more rows exist. Call again with offset plus count |
get_receivables_report also returns total_count. get_project, get_opportunity and get_proposal return deliverables in pages, with deliverables_page.has_more.
Idempotency keys
These tools take an optional idempotency_key (1 to 255 characters): create_tracked_time, create_cost, create_expense_claim, create_upload, create_opportunity, create_contact, create_note and create_beat. send_proposal requires one.
| Rule | Detail |
|---|---|
| New record | Send a new key, such as a UUID |
| Retry after a timeout | Send the same key and the same input. You get the first result with replayed: true, and no second record, email or bill |
| Same key, different input | conflict, reason idempotency_key_reused |
| The first record was deleted | conflict, reason idempotent_record_deleted |
| The user can no longer read the first record | not_found, reason idempotent_record_not_found |
| Two calls with the same expired key at once | conflict, reason retry_later. Wait a moment and retry |
| Lifetime | 24 hours for each user. A failed call stores no key |
The keys are shared with the REST Idempotency-Key header where both exist (see Idempotency keys).
The setup creates (save_ tools with no id, create_ template tools and the add_*_row tools) take no key. Names need not be unique, so before you retry one after a timeout, check with search_records or the get_ tool whether the first call worked.
Errors
A tool that refuses a call returns a tool error: a normal JSON-RPC result with isError: true. Its structuredContent is:
{
"error": {
"code": "not_found",
"reason": "task_not_found",
"message": "task_not_found: task_id was not found. Use search_records to find the id",
"field": "task_id"
}
}
| Key | Meaning |
|---|---|
code |
One of the codes below. Use it for program logic |
reason |
The Drum reason, the same code the REST API sends where both exist. The message starts with it |
message |
For people and the AI. It often says what to do next. Do not match on it |
field |
On input and lookup errors, the input concerned, such as row.resources[0].resource_type_id |
errors |
On a validation failure, the list of messages |
toolsets |
On tool_not_selected, the toolsets that have the tool |
| Code | Meaning | What to do |
|---|---|---|
not_authorized |
The user’s role does not permit this. Reason permission_missing when the tool itself is not permitted |
Do not retry. Change the role in Drum if the user should have access |
not_found |
The record does not exist, or the user cannot read it | Find the id again with search_records or a get_ tool |
not_found with reason tool_not_selected |
The user may use the tool, but it is not in the connection’s toolsets | Add one of the toolsets in toolsets, then reconnect if the client does not see it |
not_found with reason beats_not_enabled |
Beats are off for the account | Do not retry |
invalid |
The input breaks the schema or a business rule. Reason invalid_input, or missing_input for a required input |
Correct the input |
conflict |
The record changed, another request holds it, an idempotency key was reused, or a limit was reached (reason rate_limited) |
Read the record again, or wait, then retry |
internal_error |
Drum hit an unexpected error and has a report of it | Read the record again before you try again |
The role check comes before the toolset check, so a tool the role cannot use answers not_authorized whether it is selected or not.
Only an unknown tool name or a malformed request is a JSON-RPC error.
Limits
| Limit | Detail |
|---|---|
| Requests | MCP requests count towards the API limit of 300 requests a minute for each account, shared with REST calls. Over it, the endpoint returns HTTP 429 (see Rate limits) |
| Converting opportunities | 30 a minute for each account. Over it, conflict with reason rate_limited |
| One message | One JSON-RPC message in each request |
| Search | search_records reads the first 8 different words of query (200 characters or fewer) and stops after 10 seconds with invalid |
| Page size | 100 rows, or 25 detailed rows |