This document is for an LLM-based agent that holds a TaskOH token and must act for a user.
1. Purpose and mental model
- TaskOH is a project management app. Hierarchy: workspace > projects > tasks (> subtasks, comments).
- A token belongs to one user. Every call runs as that user, with that user's role in each project. There are no token scopes.
- Project roles:
admin,project_manager,guest.adminandproject_managerare "the project team".guestis a client: sees onlyclient_visibletasks, cannot edit or delete tasks, cannot set labels. - Writes that produce activity events (
task_created,status_changed,scope_changed,billing_changed,billing_visibility_changed,comment_added,task_deleted) are recorded in the TaskOH activity log withmetadata.via = "api"; the web app shows them as "via API". Other field edits (title, description, priority, due date, assignee, labels, parent,client_visible) are not logged — see §5.6. - Fixed vocabularies:
| Field | Values |
|---|---|
| status | inbox, planned, in_progress, in_review, needs_client, done |
| type | request, bug, task |
| priority | 1 low, 2 medium, 3 high (or null) |
| scope | unclassified, in_scope, out_of_scope |
- "Open" means status is not
done.search_taskshas no open/closed switch: pass"status":["inbox","planned","in_progress","in_review","needs_client"]for open tasks (as in §7.5) or["done"]for closed ones.
1.1 Minimum successful session
get_context(no arguments) → learnme, your projects and roles.- If you will name a member or label:
get_contextwithproject. - Read:
get_my_tasks(own open work) orsearch_tasks(needsproject) orget_task. - Write:
create_task/update_task/add_comment;delete_taskonly after explicit user consent (§7.8). - Report the returned
url. Full rules: §8. Recipes: §7.
2. Authentication
2.1 Header
Authorization: Bearer toh_pat_<64 lowercase hex characters>
- Scheme is case-insensitive; the token is 72 characters, prefix
toh_pat_. - The same header works on
POST /mcpand on the REST operations listed in §9; the session-only endpoints in §2.3 return 403.
2.2 Failure responses
Authentication is checked before the controller on POST /mcp and on every route in the protected /api group (the seven REST operations in §9 are all in it); GET/DELETE /mcp answer 405 without checking the token (see §3.1). The body is always the REST envelope, even on /mcp.
| HTTP | Header | Body | Cause |
|---|---|---|---|
| 401 | WWW-Authenticate: Bearer | {"success":false,"error":{"type":"unauthorized","message":"Missing Authorization header","code":401}} | no header |
| 401 | WWW-Authenticate: Bearer | same shape, message Invalid Authorization header format | header does not match Bearer <token> |
| 401 | WWW-Authenticate: Bearer | same shape, message Invalid or revoked token | bearer starts with toh_ but the token is unknown, revoked, or expired |
| 401 | WWW-Authenticate: Bearer | same shape, message Invalid token | bearer does not start with toh_ (the token was pasted without its toh_pat_ prefix) |
| 403 | none | {"success":false,"error":{"type":"forbidden","message":"This route requires an interactive session","code":403}} | valid token on a session-only route |
| 403 | server: cloudflare, content-type: text/plain | error code: 1010 (plain text, no JSON envelope) | answered by the CDN edge, not by TaskOH: a zone security rule rejected the request. Since 2026-09-07 the API host no longer applies browser-signature checks (Python's default Python-urllib/3.x works), so this should not occur in normal operation. Not an authentication problem. |
| 429 | Retry-After: 60 | {"success":false,"error":{"type":"rate_limited","message":"Too many requests. Please try again later.","code":429}} | more than 120 requests in 60 s with this token — one bucket per token, shared by every /api route and POST /mcp; a JSON-RPC batch of n messages costs n requests and is refused as a whole when it does not fit. Not an authentication problem. |
- A 401 with
Invalid or revoked tokenis final for that token. Do not retry. Tell the user to create a new token at https://app.taskoh.app/integrations (sign in first; the page is reachable by every role, including clients). - Revocation and expiry take effect on the next request (no caching).
- A plain-text 403 with the body
error code: 1010never comes from TaskOH: it is Cloudflare answering before the request reaches the API. Until 2026-09-07 the edge rejected Python's defaultPython-urllib/3.xUser-Agent this way; the API host (api.taskoh.app) no longer applies that check, while the browser-facing hoststaskoh.appandapp.taskoh.appstill do. If you ever see it on the API host, retry after a short wait and report thecf-rayresponse header to the TaskOH team — the token is fine. Sending a descriptiveUser-Agentsuch asmy-assistant/1.0 (+https://example.com)remains good practice. - A 429 is not a token problem: the token is neither revoked nor otherwise affected. Wait the number of seconds given in
Retry-After(60), then retry, and reduce call volume: paginatesearch_taskswithpage/per_page(max 100), send oneupdate_taskwith several fields instead of several calls, and callget_contextonce per session. The quota is 120 requests per rolling 60-second window per token (not per IP address), counted across every/apiroute andPOST /mcpcombined; a JSON-RPC batch of n messages (max 20) costs n requests, and a batch that does not fit in the remaining quota is refused as a whole with the same HTTP 429 envelope andRetry-After: 60— none of its messages is executed. On/mcpthe 429 is a plain HTTP response with the JSON envelope above, not a JSON-RPC error object. Browser sessions (the web app's session JWT) callingPOST /mcpshare the same 120-per-60 s bucket, keyed per user; sessions on the/apiREST routes are not limited, and the sign-in endpoints keep their separate limit of 10 per 15 minutes per IP address.
2.3 Session-only endpoints (403 for tokens)
A token receives the 403 above on endpoints that mint credentials, grant durable access to other people, or take irreversible or financial actions: token management (listing, creating, revoking), profile and password changes, adding or removing project members, workspace team management (removing a person, resending an invitation), deleting a project, and Stripe checkout or billing-portal sessions. Read-only lists (GET /api/team, GET /api/subscription) stay available. None of these are part of the documented agent operations (§5, §9). Project payloads returned to a token (GET /api/projects, GET /api/projects/{id}, POST /api/projects) omit the project's public share-link fields.
2.4 Token hygiene
- Never log, echo, quote, or store the token. Never include it in a tool result, comment body, or task text.
- Rejected tokens are logged server-side with their last 4 characters, the client IP address and the request path — never the token.
- Never try to create tokens: the account owner is e-mailed on every token creation and at most 20 active tokens exist per user (token management is session-only anyway, §2.3).
3. Choosing a transport
| MCP (preferred) | REST | |
|---|---|---|
| URL | POST https://api.taskoh.app/mcp | https://api.taskoh.app/api/..., schema at https://api.taskoh.app/openapi.json |
| Format | JSON-RPC 2.0, JSON-only Streamable HTTP | JSON, envelope {"success":true,"data":...} |
| Name resolution | project, assignee and label names accepted | numeric ids only |
| Operations | 8 tools | 7 operations |
| Use when | the client supports MCP (Claude Code, Claude Desktop via mcp-remote, Claude API, OpenAI Responses/Agents SDK) | the client is a ChatGPT Custom GPT Action or a plain HTTP script |
3.1 MCP transport facts
| Fact | Value |
|---|---|
| Method | POST only. GET and DELETE return HTTP 405 with Allow: POST and an empty body, before and without authentication; other verbs are answered by the app's JSON error handler with HTTP 405 (body shape not part of the contract) |
| Headers to send | Authorization, Content-Type: application/json. MCP-Protocol-Version is optional |
| Supported protocol versions | 2025-03-26, 2025-06-18, 2025-11-25 (default 2025-11-25) |
MCP-Protocol-Version header | if present it must match YYYY-MM-DD, otherwise HTTP 400 with JSON-RPC error -32600 Malformed MCP-Protocol-Version header. Any date-shaped value is accepted |
| Sessions | none. No Mcp-Session-Id header is ever emitted or required. initialize is optional; tools/list and tools/call work without it |
| Server-sent events | not supported. Use --transport http (Claude Code) or --transport http-only (mcp-remote) |
| Batch | a non-empty JSON array of up to 20 requests returns a JSON array of replies (HTTP 200). Empty array [] returns -32600 Invalid Request; more than 20 elements returns -32600 Batch too large (max 20). Each message costs one request of the rate-limit quota (§2.2): a batch that does not fit in the remaining quota is refused as a whole with HTTP 429, and none of its messages is executed |
Request id | a string, a number or null; any other type answers -32600 Invalid Request |
| Notifications | a message without id, or whose method starts with notifications/, gets no reply. If the whole request was notifications the response is HTTP 202 with an empty body |
| Unknown method | -32601 Method not found (HTTP 200). server/discover is not implemented; fall back to initialize |
| Methods implemented | initialize, ping, tools/list, tools/call |
Response Content-Type | always application/json |
| HTTP status of JSON-RPC errors | 200 except the 400 above and the 405 |
A complete request:
POST /mcp HTTP/1.1
Host: api.taskoh.app
Authorization: Bearer toh_pat_<64 lowercase hex characters>
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_context","arguments":{}}}
The reply is HTTP 200 with Content-Type: application/json and a JSON-RPC response whose result has the tool result shape shown in §3.2.
initialize result:
{"protocolVersion":"2025-11-25","capabilities":{"tools":{}},"serverInfo":{"name":"taskoh","version":"1.0"},"instructions":"Call get_context once before resolving project, member or label names. Tasks live in projects; statuses are inbox, planned, in_progress, in_review, needs_client, done. Use get_my_tasks for the user's own open work and search_tasks for everything else. Writes act as the signed-in user and appear in TaskOH's activity log. Full reference: https://api.taskoh.app/llms.txt"}
protocolVersion echoes params.protocolVersion if it is in the supported list, otherwise 2025-11-25.
3.2 Tool result shape
- Success:
{"content":[{"type":"text","text":"<JSON string of the result>"}],"structuredContent":<the same result object>}. ReadstructuredContent. - Business or permission failure:
{"content":[{"type":"text","text":"<message>"}],"isError":true}. The message is meant for you; correct the arguments and retry. - Argument or schema failure: JSON-RPC error
-32602with the message inerror.message.
4. Data model reference
4.1 Compact task (list rows)
Returned by get_my_tasks, search_tasks, and as subtasks in get_task. Key order is fixed.
{"id":42,"project_id":3,"project_name":"Acme Portal","title":"Login fails on Safari","status":"in_progress","type":"bug","priority":3,"due_date":"2026-09-12","assignee_name":"Maya Okafor","label_names":["frontend"],"subtask_count":2,"parent_id":null,"client_visible":true,"url":"https://app.taskoh.app/projects/3/tasks/42"}
| Key | Type |
|---|---|
id | integer |
project_id | integer |
project_name | string or null (null if the project is not in your project list) |
title | string |
status | status enum |
type | type enum |
priority | integer 1–3 or null |
due_date | YYYY-MM-DD string or null |
assignee_name | string or null |
label_names | string[] |
subtask_count | integer; omitted for guests (§4.5) |
parent_id | integer or null |
client_visible | boolean |
url | string, https://app.taskoh.app/projects/{project_id}/tasks/{id} |
4.2 Full task
Returned by get_task, create_task, update_task.
| Key | Type | Notes |
|---|---|---|
id | integer | |
project_id | integer | |
parent_id | integer or null | null = top-level task |
title | string | |
description | string or null | |
details | string or null | |
status | status enum | |
type | type enum | |
scope | scope enum | |
is_billable | boolean | only true when scope is out_of_scope |
required_minutes | integer or null | positive multiple of 15; hidden from guests unless billing is shared |
fee_amount | decimal string or null | e.g. "1500.00"; hidden from guests unless billing is shared |
fee_currency | string or null | 3 uppercase letters; hidden from guests unless billing is shared |
billing_client_visible | boolean | whether the client may see minutes and fee |
client_visible | boolean | whether guests see the task at all |
created_by_user_id | integer or null | |
assignee_user_id | integer or null | |
priority | integer 1–3 or null | |
due_date | YYYY-MM-DD or null | |
metadata | object, or [] when empty | an empty metadata is serialised as [], not {} |
position | integer | board position |
created_at | ISO 8601 string | |
updated_at | ISO 8601 string | |
label_ids | integer[] | |
subtask_count | integer | omitted for guests (§4.5) |
assignee_name | string or null | |
assignee_email | string or null | omitted for guests (§4.5) |
assignee_last_activity_at | ISO 8601 string or null | omitted for guests (§4.5) |
label_names | string[] | |
project_name | string or null | |
url | string |
Scope and billing rules (enforced by update_task; each violation is an isError whose text is listed in §11):
is_billable: truerequiresscope: out_of_scope.required_minutesmust be a positive multiple of 15 (at most 4294967295).fee_amountis a positive decimal string, integer part 1–10 digits without leading zeros, at most 2 decimals; the server stores and returns it normalised to two decimals ("1500"becomes"1500.00").fee_amountrequiresfee_currency(^[A-Z]{3}$).fee_currencywithoutfee_amountis rejected.- minutes and fee must be null when not billable.
4.3 Project, member, label (from get_context)
{"me":{"id":7,"name":"Sam Rivera","email":"me@example.com"},
"statuses":["inbox","planned","in_progress","in_review","needs_client","done"],
"types":["request","bug","task"],
"scopes":["unclassified","in_scope","out_of_scope"],
"projects":[{"id":3,"name":"Acme Portal","my_role":"admin","client_portal_slug":"acme"}],
"members":[{"id":7,"name":"Sam Rivera","email":"me@example.com","role":"admin"}],
"labels":[{"id":11,"name":"frontend","color":"#f00"}]}
projects[]:idinteger,namestring,my_roleone ofadmin,project_manager,guest,client_portal_slugstring or null.members[]andlabels[]are present only whenprojectwas passed.members[].namemay be null. When the caller is a guest in that project,members[]entries carry noemailkey at all (the caller's ownme.emailis still present).me.nameandme.emailare string or null; fall back to the id when null.
4.4 Comment and activity shapes (from get_task)
{"comments":[{"id":5,"author_name":"Maya Okafor","body":"Looks good","created_at":"2026-09-06T14:10:00+00:00"}],
"activity":[{"type":"task_created","type_label":"Task created","actor_name":"Sam Rivera","via":"api","created_at":"2026-09-06T14:10:00+00:00","metadata":{"title":"Login fails on Safari","via":"api"}}]}
author_name: the author's name, or their email when no name is set.viais"api"for token-made events, otherwise null.- Activity
typevalues:task_created,status_changed,approval_requested,approved,changes_requested,comment_added,attachment_added,visibility_changed,quote_sent,quote_approved,quote_rejected,scope_changed,billing_changed,billing_visibility_changed,task_deleted.
4.5 What guests see
| Aspect | Guest behaviour |
|---|---|
Task lists and get_task | only tasks with client_visible: true; hidden tasks return Task <id> not found |
Subtasks in get_task | only client-visible subtasks |
required_minutes, fee_amount, fee_currency | removed unless the task is both client_visible and billing_client_visible |
Activity in get_task | scope_changed, billing_changed, billing_visibility_changed events removed unless billing_client_visible |
create_task | allowed, following the client portal's request rules: client_visible forced to true; type defaults to request; status, priority and due_date are ignored (the task starts in inbox without priority or due date); labels rejected with Only the project team can set labels; parent_task_id must be a client-visible task (a hidden, foreign or missing parent answers Task <id> not found); refused with Client requests are disabled for this project when the project does not accept client requests; the project team is notified of a new client request |
update_task, delete_task | Permission denied: edit_task |
add_comment | allowed on client-visible tasks |
get_context members | members[] entries carry no email key; the caller's own me.email is still present |
| Assignee errors | candidates listed as <name> (id <n>), without e-mail addresses |
| Assignee resolution | an e-mail address is not matched for a guest (use me, a member id or a name), so a guess cannot be confirmed |
| Task shapes | subtask_count, assignee_email and assignee_last_activity_at are absent for guests (team members keep them) |
parent_task_id in create_task | must be a client-visible task; a hidden, foreign or missing parent answers Task <id> not found, so hidden tasks cannot be probed through the parent field |
| Task shapes | subtask_count (compact and full task), assignee_email and assignee_last_activity_at (full task) are omitted |
5. Tool reference
tools/list always returns all 8 tools in this order, regardless of role. Annotation sets:
| Set | Value |
|---|---|
| READ_ONLY | {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} |
| WRITE | {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false} |
| WRITE_IDEMPOTENT | {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} |
| DESTRUCTIVE | {"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false} |
The schemas use one non-standard keyword, maxBytes: the maximum size of a string in UTF-8 bytes (maxLength counts characters). Both are enforced with a -32602 (§11).
Common isError texts (all tools that take a task or project):
| Text | Meaning |
|---|---|
Task <id> not found | task missing, in a project you are not a member of, or hidden from a guest — also for parent_task_id in create_task |
No project named '<ref>'. Your projects: <name> (id <n>), ... | unknown project (or Your projects: none) |
Project name '<ref>' is ambiguous. Candidates: <name> (id <n>), ... | two projects share the name; use the id |
5.1 get_context
- Title:
Get workspace context. Annotations: READ_ONLY. - When: first call of every session. Also with
projectbefore any name-based create, update, or search. - inputSchema:
{"type":"object","additionalProperties":false,"properties":{"project":{"type":"string","description":"Project id or name. When set, also returns that project's members and labels."}}}
- Returns: the object in §4.3.
membersandlabelsonly whenprojectis given. - Errors: -32602 for schema failures (unknown keys), isError for an unknown or ambiguous
project— texts in §11. - Side effects: none. Permission: all roles.
5.2 get_my_tasks
- Title:
Get my open tasks. Annotations: READ_ONLY. - When: "what is on my plate", "my open tasks", "what is overdue for me".
- inputSchema:
{"type":"object","additionalProperties":false,"properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":50}}}
- Returns
{"tasks":[<compact task>...],"total":2}. Order: overdue first, then priority descending, then due date ascending. Excludesdone. Guests see only client-visible tasks.totalis the number of rows returned. Thelimitis applied before role filtering, so a result shorter thanlimitdoes not prove there are no more tasks; to be exhaustive call again withlimit: 100. Not paginated. Overdue =due_dateearlier than the server's current date (UTC). - Errors: -32602 for schema failures (
limittype and 1–100 bounds) — texts in §11. - Side effects: none. Permission: all roles.
5.3 search_tasks
- Title:
Search tasks in a project. Annotations: READ_ONLY. - When: any task listing that is not "my open tasks": by project, status, assignee, label, text, unassigned, done, subtasks.
- inputSchema:
{"type":"object","additionalProperties":false,"required":["project"],"properties":{
"project":{"type":"string","description":"Project id or name"},
"query":{"type":"string","description":"Text in title or description"},
"status":{"type":"array","items":{"type":"string","enum":["inbox","planned","in_progress","in_review","needs_client","done"]}},
"type":{"type":"array","items":{"type":"string","enum":["request","bug","task"]}},
"assignee":{"type":"string","description":"\"me\", \"unassigned\", user id, name or email"},
"labels":{"type":"array","items":{"type":"string"},"description":"Label names; all must match"},
"priority":{"type":"array","items":{"type":"integer","minimum":1,"maximum":3}},
"scope":{"type":"array","items":{"type":"string","enum":["unclassified","in_scope","out_of_scope"]}},
"is_billable":{"type":"boolean"},
"parent_task_id":{"type":"integer","description":"Only subtasks of this task; 0 = top-level tasks only"},
"sort":{"type":"string","enum":["position","priority","due_date","created_at","title","status"],"default":"position"},
"order":{"type":"string","enum":["asc","desc"],"default":"asc"},
"page":{"type":"integer","minimum":1,"default":1},
"per_page":{"type":"integer","minimum":1,"maximum":100,"default":25}}}
- Semantics:
querymatches title or description (substring, trimmed, empty ignored).status,type,priority,scopeare OR within the array.labelsis AND (all must be on the task).assignee: "unassigned"= no assignee.parent_task_idomitted = all tasks;0= top-level only;>0= subtasks of that task.sort: "position"ignoresorder. - Returns
{"tasks":[<compact task>...],"total":31,"page":2,"per_page":10,"total_pages":4}. - Errors: -32602 for schema failures (a missing
projecthas its own message; array items report asstatus[0]), isError for an unknown or ambiguous project, assignee, or label — texts in §11. - Side effects: none. Permission: all roles; guests get only client-visible tasks.
5.4 get_task
- Title:
Get a task. Annotations: READ_ONLY. - When: the user asks about one task, before updating or deleting, or to read comments/activity.
- inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id"],"properties":{"task_id":{"type":"integer"},"include":{"type":"array","items":{"type":"string","enum":["comments","subtasks","activity"]},"default":["comments","subtasks"]}}}
- Returns the full task (§4.2) plus, per
include:comments(oldest first, max 50),subtasks(compact rows, position order, uncapped),activity(newest first, max 50).include: []returns only the task. - Errors: -32602 for schema failures (
task_id,include[n]), isError for task lookup — texts in §11. - Side effects: none. Permission: any member of the task's project; guest rules in §4.5.
5.5 create_task
- Title:
Create a task. Annotations: WRITE. - When: the user asks to add, create, log, or file a task, bug, or request, or to create a subtask.
- inputSchema:
{"type":"object","additionalProperties":false,"required":["project","title"],"properties":{
"project":{"type":"string","description":"Project id or name"},
"title":{"type":"string","maxLength":255},
"description":{"type":"string"},
"type":{"type":"string","enum":["request","bug","task"],"default":"task"},
"status":{"type":"string","enum":["inbox","planned","in_progress","in_review","needs_client","done"],"default":"inbox"},
"priority":{"type":"integer","minimum":1,"maximum":3,"description":"1 low, 2 medium, 3 high"},
"due_date":{"type":"string","format":"date","description":"YYYY-MM-DD"},
"assignee":{"type":"string","description":"\"me\", user id, name or email; must be a project member"},
"labels":{"type":"array","items":{"type":"string"},"description":"Existing label names; unknown names are rejected, never created. Project team only."},
"parent_task_id":{"type":"integer","description":"Create as a subtask of this task (same project)"},
"client_visible":{"type":"boolean","default":false}}}
- Returns the full task (§4.2). Example (abbreviated):
{"id":42,"project_id":3,"parent_id":null,"title":"Login fails on Safari","description":null,"details":null,"status":"inbox","type":"bug","scope":"unclassified","is_billable":false,"required_minutes":null,"fee_amount":null,"fee_currency":null,"billing_client_visible":false,"client_visible":false,"created_by_user_id":7,"assignee_user_id":9,"priority":3,"due_date":"2026-09-12","metadata":[],"position":0,"created_at":"2026-09-06T14:10:00+00:00","updated_at":"2026-09-06T14:10:00+00:00","label_ids":[11],"subtask_count":0,"assignee_name":"Maya Okafor","assignee_email":"maya@example.com","assignee_last_activity_at":null,"label_names":["frontend"],"project_name":"Acme Portal","url":"https://app.taskoh.app/projects/3/tasks/42"}
- Semantics:
titleis trimmed server-side; an empty or whitespace-onlytitle(and the literal"0") is rejected withTitle is required.due_datemust be a valid calendar date inYYYY-MM-DD. - Errors: -32602 for schema failures (
descriptionmust be a string; null is not accepted on create), isError for project/member/label lookup, guest label permission, title, due date, and parent-task failures — texts in §11. - Order of checks: project, labels, label permission, assignee, then the write. A bad name creates nothing.
- Side effects: activity
task_created(metadata.title,via: "api"). No assignment notification on create. - Permission: all roles. Guest:
client_visibleforced totrue,typedefaults torequest, labels rejected.
5.6 update_task
- Title:
Update a task. Annotations: WRITE_IDEMPOTENT. - When: change status, assignee, due date, priority, title, description, labels, parent, visibility, scope, or billing of an existing task.
- inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id"],"properties":{
"task_id":{"type":"integer"},
"title":{"type":"string","maxLength":255},
"description":{"type":["string","null"]},
"details":{"type":["string","null"]},
"status":{"type":"string","enum":["inbox","planned","in_progress","in_review","needs_client","done"]},
"type":{"type":"string","enum":["request","bug","task"]},
"priority":{"type":["integer","null"],"minimum":1,"maximum":3},
"due_date":{"type":["string","null"],"format":"date"},
"assignee":{"type":["string","null"],"description":"\"me\", id, name or email; null unassigns"},
"parent_task_id":{"type":["integer","null"],"description":"null promotes to top level"},
"add_labels":{"type":"array","items":{"type":"string"}},
"remove_labels":{"type":"array","items":{"type":"string"}},
"client_visible":{"type":"boolean"},
"scope":{"type":"string","enum":["unclassified","in_scope","out_of_scope"]},
"is_billable":{"type":"boolean","description":"Only allowed when scope is out_of_scope"},
"required_minutes":{"type":["integer","null"],"description":"Multiple of 15"},
"fee_amount":{"type":["string","null"],"description":"Decimal string, e.g. \"1500.00\"; requires fee_currency"},
"fee_currency":{"type":["string","null"],"pattern":"^[A-Z]{3}$"},
"billing_client_visible":{"type":"boolean"}}}
- Only the given keys change.
due_date: nullclears the date;assignee: nullunassigns;parent_task_id: nullpromotes to top level;priority: nullclears priority.statusandtypemust be valid enum values; an empty string is rejected with -32602.titleis trimmed; an empty or whitespace-onlytitleis rejected withTitle is required(isError) — omit the key to leave the title unchanged.due_datemust be a valid calendar date inYYYY-MM-DD(§5.5). - Returns the full task (§4.2) after the update.
- Errors: -32602 for schema failures (
labelsis not accepted here; useadd_labels/remove_labels), isError for task access and permission, member/label lookup, due date, parent (self, missing, circular), and the six billing rules in §4.2 — texts in §11. - Order of checks: task access,
add_labels,remove_labels, then the write (which is where the edit_task permission is enforced — a guest with an unknown label sees the label error first). Unknown labels change nothing. - Side effects: activity
status_changed{from,to},scope_changed{from,to},billing_changed,billing_visibility_changedonly when the value actually changed. Assigning another user sends them an assignment notification (none when you assign yourself). No activity event for title, description, priority, due date, labels, parent, orclient_visiblechanges (thevisibility_changedevent is emitted only by the web app's toggle). - Permission:
adminandproject_manageronly.
5.7 add_comment
- Title:
Add a comment. Annotations: WRITE. - When: the user wants to leave a note, reply, or notify a project member on a task.
- inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id","body"],"properties":{"task_id":{"type":"integer"},"body":{"type":"string","minLength":1,"description":"Plain text. Mention a project member as @their.email@example.com to notify them."}}}
- Returns:
{"id":77,"task_id":42,"author_name":"Sam Rivera","body":"Ship it @maya@example.com","created_at":"2026-09-06T14:10:00+00:00"}
author_name: the author's name, or their email when no name is set.- Errors: -32602 for schema failures (
bodymust be a non-blank string), isError for task lookup — texts in §11. - Side effects: activity
comment_added; every other project member gets a new-comment notification; each@emailthat belongs to a project member gets a mention notification (a mention of your own email is ignored). Mentions must be the full email, prefixed with@; names do not work. - Permission: all roles; guests only on client-visible tasks.
5.8 delete_task
- Title:
Delete a task. Annotations: DESTRUCTIVE (the only tool withdestructiveHint: true). - When: only after the user has explicitly confirmed deletion of a specific task. Follow the steps in §7.8; rule in §8.
- inputSchema:
{"type":"object","additionalProperties":false,"required":["task_id","confirm"],"properties":{"task_id":{"type":"integer"},"confirm":{"type":"boolean","const":true,"description":"Must be true. Deletes the task and all of its subtasks and comments permanently."}}}
- Returns:
{"deleted_task_id":42,"deleted_subtask_ids":[43,44]}
- Errors: -32602 for a missing, non-boolean, or false
confirm(checked before any lookup), isError for task lookup and permission — texts in §11. - Side effects: the task, its subtasks, and its comments are removed permanently (no undo). Activity
task_deletedwithmetadata{"task_id":42,"title":"...","subtask_count":2,"via":"api"}remains in the project log. - Permission:
adminandproject_manageronly.
6. Name resolution rules
Always call get_context (with project when you will name members or labels) before relying on a name. Matching is exact and case-insensitive; there is no fuzzy matching.
| Argument | Accepted forms | Resolution | Failure |
|---|---|---|---|
project | all-digit id (as a JSON string, e.g. "3") or project name | id matched first among your memberships; else case-insensitive exact name | No project named '<ref>'. Your projects: ... / Project name '<ref>' is ambiguous. Candidates: ... |
assignee | me, all-digit user id, email, name | me = token owner (no membership check); others among project members by id, email, or name. **Guests: id, name and me only** — an e-mail address never resolves for a guest, so it cannot be used to confirm who is a member | No project member matching '<ref>'. Members: <name> <email> (id <n>), ... (guests: <name> (id <n>)) / Assignee '<ref>' is ambiguous. Candidates: ... |
assignee in search_tasks | also unassigned | tasks with no assignee | unassigned in create_task/update_task is looked up as a name and fails |
labels, add_labels, remove_labels | label names | case-insensitive exact name among the project's labels; duplicates collapsed | Unknown label '<name>' in <project>. Available labels: <a>, <b>. Labels are not created automatically. |
task_id, parent_task_id | integer id only | Task <id> not found / Parent task not found in this project |
- Ambiguity: when a name matches more than one project or member, the error lists the candidates with ids. Use the id, or ask the user which one they mean.
- Two members can share a display name; prefer email when the user gives one.
- Labels are never created by the API. If a label does not exist, tell the user and proceed without it or ask.
7. Recipes
All calls are tools/call; only params is shown.
7.1 What is on my plate
{"name":"get_my_tasks","arguments":{}}
Report each task as title, project, status, due date, and url. Overdue tasks (as defined in §5.2) come first.
7.2 Create a bug in a project assigned to a person, due on a date
{"name":"get_context","arguments":{"project":"Acme Portal"}}
Confirm the project exists and that the person is in members (match name or email). Then:
{"name":"create_task","arguments":{"project":"3","title":"Login fails on Safari","type":"bug","priority":3,"assignee":"maya@example.com","due_date":"2026-09-12"}}
Report the returned id and url.
7.3 Move task N to done
{"name":"update_task","arguments":{"task_id":42,"status":"done"}}
If the reply is isError Permission denied: edit_task, the user is a guest in that project; say so.
7.4 Add a comment
{"name":"add_comment","arguments":{"task_id":42,"body":"Reproduced on Safari 18. @maya@example.com can you take a look?"}}
Use the member's email from get_context for mentions.
7.5 List unassigned high-priority tasks in a project
{"name":"search_tasks","arguments":{"project":"Acme Portal","assignee":"unassigned","priority":[3],"status":["inbox","planned","in_progress","in_review","needs_client"],"per_page":50}}
If total_pages > 1, repeat with page: 2 and so on.
7.6 Create a subtask
{"name":"create_task","arguments":{"project":"3","title":"Add Safari to the browser matrix","parent_task_id":42}}
The parent must be in the same project.
7.7 Mark a task out-of-scope, billable, with hours and fee
Hours must convert to a multiple of 15 minutes (2.5 h = 150). Send all fields in one call:
{"name":"update_task","arguments":{"task_id":42,"scope":"out_of_scope","is_billable":true,"required_minutes":150,"fee_amount":"1500.00","fee_currency":"EUR"}}
Add "billing_client_visible":true only if the user wants the client to see minutes and fee.
7.8 Delete a task
get_taskwithinclude: ["subtasks"]to learn the title andsubtask_count.- Tell the user exactly what will be deleted: the title, the number of subtasks, and that comments are removed permanently.
- Wait for an explicit yes for that task. Do not treat the original request as consent.
- Only then:
{"name":"delete_task","arguments":{"task_id":42,"confirm":true}}
- Report
deleted_task_idanddeleted_subtask_ids.
8. Behaviour rules for agents
Do:
- Call
get_contextonce at the start of a session; call it again withprojectbefore resolving members or labels in that project. - After a name resolves, use ids (
task_id, project id) for later calls. - Ask the user before
delete_task; setconfirm: trueonly after explicit consent for that specific task. - Report the task
urlafter every create or update. - Paginate
search_taskswithpagewhentotal_pages> 1; do not claim a list is complete otherwise. - On
isError, read the message, correct the arguments (use the listed candidates, ids, or labels), and retry once. On-32602, fix the argument named in the message. - Prefer one
update_taskcall with several fields over several calls. - On HTTP 429, wait the
Retry-Afterperiod (60 s) before retrying and reduce call volume (fewer, larger calls:per_pageup to 100, oneupdate_taskwith several fields,get_contextonce per session); never retry in a tight loop. The token is unaffected (§2.2). - Tell the user when a result was filtered by their role (guest), when a permission error occurs, or when a project or member does not exist.
- Send a descriptive
User-Agentheader (for examplemy-assistant/1.0) on every request. It is no longer required on the API host (§2.2), but it identifies your client in logs and keeps working if edge rules change.
Do not:
- Invent, guess, or auto-create labels, members, or projects. Names must come from
get_context. - Retry a write blindly after a
-32603Internal erroror a transport failure; check withget_taskorsearch_tasksfirst to see whether the write landed. - Expose, log, or repeat the token.
- Assume a task exists because the user remembers it:
Task <id> not foundalso covers tasks hidden from guests and tasks in projects the user is not a member of. - Use
search_taskswithoutproject; useget_my_tasksfor the user's own work across projects. - Send
"42"for an integer field ornullwhere the schema does not list"null". - Send a JSON number for
project— it is always a string, even when it is an id:"project":"3", not"project":3.
9. REST reference for GPT Actions
Schema: https://api.taskoh.app/openapi.json (OpenAPI 3.1.0, title TaskOH, version 1.0.0, server https://api.taskoh.app, security scheme bearerAuth). Import it with Authentication: API Key, Auth Type: Bearer.
| operationId | Method and path | isConsequential | Input | Success |
|---|---|---|---|---|
list_projects | GET /api/projects | false | none | 200 data: {count, projects[], workspace_role} |
list_my_tasks | GET /api/dashboard/my-tasks | false | query limit (1–100, default 50; out-of-range values are clamped into that range, never a validation error) | 200 data: Task[] |
list_project_tasks | GET /api/projects/{id}/tasks | false | path id; query search, status[], type[], assignee_user_id[], label_ids[], priority[], scope[], is_billable[] ("1"/"0"), parent_id, sort, order, page, per_page (1–100, default 50; out-of-range values are clamped into that range, never a validation error) | 200 data: {project_id, tasks[], total, page, per_page, total_pages} |
create_task | POST /api/projects/{id}/tasks | true | path id; body TaskWrite, title required | 201 data: Task |
get_task | GET /api/tasks/{id} | false | path id | 200 data: Task |
update_task | PATCH /api/tasks/{id} | true | path id; body TaskWrite | 200 data: Task |
add_comment | POST /api/tasks/{taskId}/comments | true | path taskId; body {"body": "..."} | 201 data: Comment |
- Array query parameters use the
[]suffix and are repeated for several values:?status[]=inbox&status[]=planned. list_project_taskssortvalues:position,priority,due_date,created_at,title,status,type,scope,is_billable,assignee,client_visible.TaskWritefields:title(max 255),description,details(update only),status,type,priority(1–3 or null),due_date(YYYY-MM-DD),assignee_user_id,parent_id,client_visible,position(0–100000),scope,is_billable,required_minutes,fee_amount,fee_currency,billing_client_visible.- REST uses numeric ids only. Call
list_projectsfirst to map names to ids. Members and labels are not part of the REST operations; use MCPget_context(or ask the user) to obtainassignee_user_idandlabel_ids[]. - The OpenAPI schema exposes no delete operation, so a GPT Action cannot delete tasks. Of the documented operations, only MCP
delete_taskdeletes, and it requiresconfirm: true. Note that the token is not scoped to the documented operations: it acts as the user on the rest of the REST API as well (see the people guide, §9 and §10). - Success envelope:
{"success":true,"data":...}. Error envelope:{"success":false,"error":{"type":"<type>","message":"<text>","code":<http status>}}, withdetailsadded on 422 validation errors. Types includeunauthorized(401),forbidden(403),not_found(404),validation_error(422), andplan_limit_exceeded(422, when the workspace plan blocks the action;details.limit_typeismax_projectsormax_members_per_projectand the message names the plan and limit). - Writes that produce activity events (see §1) are recorded in the activity log as "via API"; plain field edits are not logged.
10. Limits
| Limit | Value |
|---|---|
search_tasks.per_page | max 100, default 25 |
get_my_tasks.limit | max 100, default 50 |
get_task comments and activity | first 50 comments, newest 50 events |
get_task subtasks | uncapped |
REST list_my_tasks.limit and list_project_tasks.per_page | 1–100, default 50; out-of-range values are clamped (see §9) |
title | 255 characters |
| Request execution time | Long-running requests are terminated server-side. |
| Result size | no server limit; the LLM context window is the constraint. Use per_page and include to trim |
| Rate limiting | 120 requests per rolling 60-second window per token: one bucket per token (not per IP address), shared by every /api route and POST /mcp combined. A JSON-RPC batch costs one request per message (max 20) and is refused as a whole when it does not fit. Browser sessions calling POST /mcp share the same bucket, keyed per user; sessions on /api are not limited. Above the quota: HTTP 429 with Retry-After: 60 and the body {"success":false,"error":{"type":"rate_limited","message":"Too many requests. Please try again later.","code":429}} (on /mcp too a plain HTTP 429, not a JSON-RPC error). The token is not affected; wait 60 s, then retry with fewer, larger calls. Browser sessions are not limited by this rule (§2.2). |
| Batch | Max 20 requests per batch (-32600 Batch too large (max 20) above that). Each message costs one request of the quota, and a batch that does not fit is refused as a whole with HTTP 429 without executing anything. Keep batches small (a handful of requests); batches are dispatched sequentially and are subject to the same execution limit as a single request. |
| Argument sizes | title 255 characters; description, details and body 65535 bytes; query 1000 bytes; page at most 100000; array filters at most 20 items (duplicates ignored). Over the limit: -32602 (§11). |
11. Error catalogue
| Layer | Code / status | Message | Action |
|---|---|---|---|
| HTTP | 401 | Missing Authorization header | send the header |
| HTTP | 401 | Invalid Authorization header format | use Bearer <token> |
| HTTP | 401 | Invalid or revoked token | stop; user must create a new token at https://app.taskoh.app/integrations |
| HTTP | 401 | Invalid token | bearer does not start with toh_; the token was pasted incompletely — resend the full toh_pat_… token |
| HTTP | 403 | This route requires an interactive session | route is token-blocked; not available to agents |
| HTTP | 403 | plain text error code: 1010, server: cloudflare, no JSON | answered by the CDN edge, not TaskOH (a zone security rule); should not occur on the API host since 2026-09-07 — retry after a short wait and report the cf-ray header if it persists; not an auth error |
| HTTP | 405 | empty body, Allow: POST | use POST on /mcp; no SSE |
| HTTP | 429 | Too many requests. Please try again later. (Retry-After: 60) | more than 120 requests in 60 s with this token; wait 60 s, retry, reduce volume |
| HTTP | 400 | JSON-RPC -32600 Malformed MCP-Protocol-Version header | send a YYYY-MM-DD value or omit the header |
| JSON-RPC | -32700 | Parse error | body is not valid JSON |
| JSON-RPC | -32600 | Invalid Request | message is not an object, method missing, batch is [], or id is not a string, number or null |
| JSON-RPC | -32601 | Method not found | only initialize, ping, tools/list, tools/call exist |
| JSON-RPC | -32602 | Missing tool name | set params.name |
| JSON-RPC | -32602 | arguments must be an object | set params.arguments to an object |
| JSON-RPC | -32602 | Unknown tool '<name>'. Available: get_context, get_my_tasks, search_tasks, get_task, create_task, update_task, add_comment, delete_task | fix the name |
| JSON-RPC | -32602 | Missing required argument '<key>' | add the key |
| JSON-RPC | -32602 | Unknown argument '<key>' for <tool> | remove the key |
| JSON-RPC | -32602 | Argument '<path>' must be of type <t1> or <t2> | fix the JSON type ("42" is not an integer) |
| JSON-RPC | -32602 | Argument '<path>' must be one of: ... | use an enum value; array items report as status[0] |
| JSON-RPC | -32602 | Argument '<path>' must be true | delete_task.confirm |
| JSON-RPC | -32602 | Argument '<path>' must be at least <n> / must be at most <n> | integer bounds |
| JSON-RPC | -32602 | Argument '<path>' must not be empty | whitespace-only string |
| JSON-RPC | -32602 | Argument '<path>' must be at most 255 characters | shorten (title) |
| JSON-RPC | -32602 | Argument '<path>' must be at most 65535 bytes / must be at most 1000 bytes | description, details and body are capped at 65535 bytes, query at 1000 bytes; shorten |
| JSON-RPC | -32602 | Argument '<path>' must have at most 20 items | array filters (status, type, labels, priority, scope, add_labels, remove_labels) take at most 20 items; duplicates are ignored |
| JSON-RPC | -32602 | Argument 'page' must be at most 100000 | page is capped at 100000 |
| JSON-RPC | -32602 | Argument 'fee_currency' must match ^[A-Z]{3}$ | e.g. EUR |
| JSON-RPC | -32602 | project is required — call get_context to list projects, or get_my_tasks for your own open work. | add project to search_tasks |
| JSON-RPC | -32603 | Internal error | do not retry writes blindly; verify state, then report |
| isError | — | Task <id> not found | wrong id, foreign project, or hidden from guest |
| isError | — | No project named '<ref>'. Your projects: ... | pick from the list |
| isError | — | Project name '<ref>' is ambiguous. Candidates: ... | use the id |
| isError | — | No project member matching '<ref>'. Members: ... | pick from the list |
| isError | — | Assignee '<ref>' is ambiguous. Candidates: ... | use id or email |
| isError | — | Unknown label '<name>' in <project>. Available labels: ... Labels are not created automatically. | use an existing label or drop it |
| isError | — | Only the project team can set labels | guest; create without labels |
| isError | — | Permission denied: edit_task | guest cannot update or delete |
| isError | — | Title is required | empty or whitespace-only title on create or update (§5.5, §5.6) |
| isError | — | Invalid due_date format. Use YYYY-MM-DD | send a valid calendar date in YYYY-MM-DD; see §5.5 |
| isError | — | Parent task not found in this project | parent must be in the same project |
| isError | — | A task cannot be its own parent | change parent_task_id |
| isError | — | Cannot set a descendant as the parent (circular reference) | change parent_task_id |
| isError | — | Only out-of-scope tasks can be billed separately | set scope: out_of_scope first or in the same call |
| isError | — | Required minutes must be a positive multiple of 15 (quarter-hour increments) | round to 15 |
| isError | — | Fee amount must be a positive decimal with at most 2 decimal places | e.g. "1500.00"; integer part 1–10 digits, no leading zeros, at most 2 decimals (see §4.2) |
| isError | — | Fee amount must be greater than zero | positive amount |
| isError | — | Fee currency must be a 3-letter uppercase code when a fee is provided | add fee_currency |
| isError | — | Fee currency can only be provided with a fee amount | add fee_amount or drop the currency |
| isError | — | Clear the required hours and fee when a task is not billable | send required_minutes: null, fee_amount: null, fee_currency: null |
12. Example JSON-RPC exchanges
12.1 initialize
Request:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
Response (HTTP 200):
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"taskoh","version":"1.0"},"instructions":"Call get_context once before resolving project, member or label names. Tasks live in projects; statuses are inbox, planned, in_progress, in_review, needs_client, done. Use get_my_tasks for the user's own open work and search_tasks for everything else. Writes act as the signed-in user and appear in TaskOH's activity log. Full reference: https://api.taskoh.app/llms.txt"}}
The client's notifications/initialized message gets HTTP 202 with an empty body.
12.2 tools/list (abbreviated)
Request:
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
Response:
{"jsonrpc":"2.0","id":2,"result":{"tools":[
{"name":"get_context","title":"Get workspace context","description":"Returns the signed-in user, projects with your role, and the fixed status list. Pass a project to also get its members and labels.","inputSchema":{"type":"object","additionalProperties":false,"properties":{"project":{"type":"string","description":"Project id or name. When set, also returns that project's members and labels."}}},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}},
{"name":"delete_task","title":"Delete a task","description":"Deletes a task permanently, including its subtasks and comments. Requires confirm: true.","inputSchema":{"type":"object","additionalProperties":false,"required":["task_id","confirm"],"properties":{"task_id":{"type":"integer"},"confirm":{"type":"boolean","const":true,"description":"Must be true. Deletes the task and all of its subtasks and comments permanently."}}},"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false}}
]}}
Only the first and last of the 8 entries are shown; the other six (get_my_tasks, search_tasks, get_task, create_task, update_task, add_comment, in that order between them) follow the same shape, and their exact inputSchema values are the ones printed in §5.2–§5.7. There is no nextCursor; the list is complete.
12.3 create_task
Request:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_task","arguments":{"project":"Acme Portal","title":"Login fails on Safari","type":"bug","priority":3,"assignee":"Maya Okafor","due_date":"2026-09-12","labels":["frontend"]}}}
Response:
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"id\":42,\"project_id\":3,\"parent_id\":null,\"title\":\"Login fails on Safari\",\"description\":null,\"details\":null,\"status\":\"inbox\",\"type\":\"bug\",\"scope\":\"unclassified\",\"is_billable\":false,\"required_minutes\":null,\"fee_amount\":null,\"fee_currency\":null,\"billing_client_visible\":false,\"client_visible\":false,\"created_by_user_id\":7,\"assignee_user_id\":9,\"priority\":3,\"due_date\":\"2026-09-12\",\"metadata\":[],\"position\":0,\"created_at\":\"2026-09-06T14:10:00+00:00\",\"updated_at\":\"2026-09-06T14:10:00+00:00\",\"label_ids\":[11],\"subtask_count\":0,\"assignee_name\":\"Maya Okafor\",\"assignee_email\":\"maya@example.com\",\"assignee_last_activity_at\":null,\"label_names\":[\"frontend\"],\"project_name\":\"Acme Portal\",\"url\":\"https://app.taskoh.app/projects/3/tasks/42\"}"}],"structuredContent":{"id":42,"project_id":3,"parent_id":null,"title":"Login fails on Safari","description":null,"details":null,"status":"inbox","type":"bug","scope":"unclassified","is_billable":false,"required_minutes":null,"fee_amount":null,"fee_currency":null,"billing_client_visible":false,"client_visible":false,"created_by_user_id":7,"assignee_user_id":9,"priority":3,"due_date":"2026-09-12","metadata":[],"position":0,"created_at":"2026-09-06T14:10:00+00:00","updated_at":"2026-09-06T14:10:00+00:00","label_ids":[11],"subtask_count":0,"assignee_name":"Maya Okafor","assignee_email":"maya@example.com","assignee_last_activity_at":null,"label_names":["frontend"],"project_name":"Acme Portal","url":"https://app.taskoh.app/projects/3/tasks/42"}}}
12.4 update_task
Request:
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"update_task","arguments":{"task_id":42,"status":"in_progress","assignee":"me","add_labels":["backend"],"remove_labels":["frontend"]}}}
Response: same shape as §12.3 with status: "in_progress", assignee_user_id: 7, label_names: ["backend"], and a newer updated_at.
12.5 delete_task with confirm
Request:
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"delete_task","arguments":{"task_id":42,"confirm":true}}}
Response:
{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"deleted_task_id\":42,\"deleted_subtask_ids\":[43,44]}"}],"structuredContent":{"deleted_task_id":42,"deleted_subtask_ids":[43,44]}}}
12.6 A -32602 error
Request (confirm omitted):
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"delete_task","arguments":{"task_id":42}}}
Response (HTTP 200):
{"jsonrpc":"2.0","id":6,"error":{"code":-32602,"message":"Missing required argument 'confirm'"}}
Request (wrong type):
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"get_task","arguments":{"task_id":"42"}}}
Response:
{"jsonrpc":"2.0","id":7,"error":{"code":-32602,"message":"Argument 'task_id' must be of type integer"}}
12.7 An isError result
Request:
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"create_task","arguments":{"project":"Acme Portal","title":"Fix header","labels":["front-end"]}}}
Response (HTTP 200, JSON-RPC success, tool error):
{"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"text","text":"Unknown label 'front-end' in Acme Portal. Available labels: frontend, backend. Labels are not created automatically."}],"isError":true}}
Correct call: "labels":["frontend"].
13. Changelog
| Version | Date | Change |
|---|---|---|
| 1.5 | 2026-09-07 | Security hardening: a JSON-RPC batch of n messages costs n requests and is refused whole when it does not fit; browser sessions on POST /mcp share the per-user 120/60 s bucket; token-only endpoints now also cover member and team management, project deletion and Stripe routes; project payloads omit share-link fields for tokens; guest create_task follows the portal request rules (status/priority/due_date ignored, disabled requests refused, team notified); guests receive no member e-mails, subtask_count or assignee e-mails; validation caps for page, title, text fields and array filters; JSON-RPC id type check; the owner is e-mailed on token creation and holds at most 20 active tokens. |
| 1.4 | 2026-09-07 | The CDN's browser-signature check was switched off for api.taskoh.app: Python's default Python-urllib/3.x User-Agent now works on the API host (taskoh.app and app.taskoh.app still apply the check). The error code: 1010 entries in §2.2 and §11 are kept as a diagnostic. No API change. |
| 1.3 | 2026-09-07 | Per-token rate limiting introduced: 120 requests per rolling 60 s per token across /api and POST /mcp; HTTP 429 with Retry-After: 60 (batch charging and the session rule on /mcp arrived with 1.5). |
| 1.2 | 2026-09-07 | Documented the CDN edge block: a request whose User-Agent is Python's default Python-urllib/3.x receives a plain-text 403 error code: 1010 from Cloudflare before reaching TaskOH. Send a descriptive User-Agent (§2.2, §8, §11). No API change. |
| 1.1 | 2026-09-06 | title is trimmed server-side and an empty or whitespace-only title is rejected with Title is required on create and update. due_date must be a valid calendar date (calendar-invalid dates such as 2026-02-30 are rejected). REST list_my_tasks.limit and list_project_tasks.per_page are clamped to 1–100 (default 50) and openapi.json declares the same bounds. |
| 1.0 | 2026-09-06 | First release. Authentication is by personal access token only (toh_pat_); OAuth is on the roadmap. 8 MCP tools, 7 REST operations, no token scopes. |