Agents
MCP endpoint
- URL:
<deployment HTTP actions URL>/mcp, e.g.http://127.0.0.1:3211/mcplocally orhttps://<deployment>.convex.site/mcpon Convex Cloud. - Transport: Streamable HTTP, stateless, JSON responses (
application/json); no SSE stream. Every message is onePOST; notifications get202;GETandDELETEget405. - Protocol: 2026-07-28 and the 2025 revisions (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05) on the same URL; see Protocol versions.
- Authentication:
Authorization: Bearer <personal API key>, or an OAuth access token of an agent connection the owner approved on the phone, which is how the Claude apps connect (oauth.md). Missing, wrong, expired or revoked credentials get HTTP 401 with aWWW-Authenticateheader that points OAuth clients at the authorization server. A request with a browserOriginheader gets HTTP 403 unless the origin is listed inDAYHAND_MCP_ALLOWED_ORIGINS(deploy.md). - Every change is recorded with the key’s kind as the actor (
clifor the user at a terminal,agentfor an AI agent) and the key’s name, or asagentwith the OAuth client’s name; see Keys and actors. - Pass a
clientId(e.g. a UUID) with changes you might retry; a repeatedclientIdis not applied twice. PassdryRun: trueto check a change without saving it (Dry runs). - Each key or connection may make 120 reads and 60 changes a minute (Limits).
- Several agents can share the plan: an activity log, acceptance criteria and proof, dependencies and time-boxed claims; agents can ask the owner first, the owner can pause them or set a policy per tool, agents propose work into an inbox, and a health report shows what needs a look (agent-work.md).
- The server’s name and backend release version appear in
serverInfoofinitializeand_metaof 2026-07-28 results. The server identity is the source of truth; see Versions for release history.
Tools
The input schemas are generated from the same validators the operations enforce; the output
schemas come from schemas/, the contract dayhand --json follows too.
Every task has an id and a ref: the user’s task number (1, 2, 3, …). It is given once, kept
through the trash, the archive and a restore, and never given to another task, even after the
task is purged. Every tool that takes a taskId accepts either the id or the number written
as "#42".
tools/list returns the tools in this order. Tools marked O are the owner’s: only the app and
cli keys may call them, and tools/list leaves them out for agents (agent keys and agent
connections; since 0.11.0). Read-only tools are marked R, destructive ones D
(destructiveHint); every change except add_task is idempotent (idempotentHint: repeating
it with the same arguments changes nothing more), and no tool reaches outside Dayhand
(openWorldHint: false). Each tool also has a title.
| Tool | Title | Hints | Result (schema) | Operation |
|---|---|---|---|---|
add_task |
Add task | task |
Add a task (title; optional note, dueDate, dueTime, status, resumeNote, repeat, labels, links, checklist, reminders, acceptance, plan, dependsOn, blockedReason, proposal, source, requestedBy, returnTo, projectId) | |
update_task |
Update task | task |
Change fields; null clears an optional field |
|
set_status |
Set task status | task |
todo / doing / done |
|
set_now |
Set the now task | set-now |
Make one task the “now” task, optionally with a resume note; taskId: null clears it |
|
complete_task |
Complete task | complete |
Mark done, optionally with proof; ends the claim; repeating tasks return their next occurrence (next: null when the series has ended). An agent needs every acceptance criterion met and a proof summary (below) |
|
reschedule_task |
Reschedule task | task |
Move to another day/time; dueDate: null removes the date |
|
archive_task |
Archive task | task |
Archive | |
delete_task |
Move task to trash | D | task |
Move to the trash (purged after 30 days) |
restore_task |
Restore task | task |
Restore from the trash, or from the archive | |
list_today |
Today | R | today |
Overdue, today, done today and the now task; optional project filter |
list_all |
Find tasks | R | task-list |
Tasks not in the trash: search, filters and pages (below) |
list_trash |
Trash | R | task-list |
Tasks in the trash |
board |
Board | R | board |
Three columns: todo, doing, done; optional project filter |
get_task |
Task with history | R | task-detail |
One task with its change history |
export_plan |
Export plan | R | export |
Markdown, JSON or iCalendar export of the whole plan |
get_settings |
Settings | R | settings |
Time zone, summary time, default reminders |
update_settings |
Update settings | D | settings |
Change them |
list_events |
Activity log | R | event-list |
Task changes after a cursor (below) |
claim_task |
Claim task | task |
Claim a task for minutes (default 120); other agents get conflict (below) |
|
renew_claim |
Renew claim | task |
Extend your claim by minutes from now |
|
release_task |
Release task | task |
End your claim, optionally with a checkpoint note (saved as the resume note) |
|
request_approval |
Ask the owner | approval |
Ask the owner to decide before you act: action, 2 to 4 options, reversible, expiresInMinutes, optional taskId (below) |
|
get_approval |
Approval request | R | approval |
One request: pending, decided (with the chosen option and note) or expired |
list_approvals |
Approval requests | R | approval-list |
Requests, newest first, by status and taskId |
decide_approval |
Decide approval request | O | approval |
Pick an option of a pending request |
get_agent_controls |
Agent controls | R | agent-controls |
Whether agents are paused, and the tool policies |
pause_agents |
Pause agents | O | agent-controls |
paused: true refuses every agent’s changes until paused: false |
set_tool_policy |
Set tool policy | O | agent-controls |
apiKeyId, toolName (or "*"), rule: allow, ask or deny |
accept_proposal |
Accept proposal | O | task |
A proposal joins the plan (below) |
merge_proposal |
Merge proposal | O | merge |
Fold a proposal into a task already in the plan (intoTaskId) |
reject_proposal |
Reject proposal | O, D | task |
Move a proposal to the trash, optionally with a reason |
agent_brief |
Agent brief | R | agent-brief |
Start of a session: list_today plus claims, approvals, ready tasks and nextSteps (below) |
import_backlog |
Import Backlog.md tasks | backlog-import |
Backlog.md task files (files: [{path, content}]) as tasks, a file imported before skipped (backlog-md.md) |
|
export_backlog |
Export as Backlog.md | R | backlog-export |
The plan as Backlog.md task files and a config.yml (parameters) |
plan_health |
Plan health | R | plan-health |
What needs a look, in one read (below) |
Results
A successful call returns the result as structuredContent (a list as { "result": [...] },
null as { "result": null }) and as JSON in its text block. A Markdown export’s text block is
the Markdown itself and its structuredContent is { "format": "markdown", "content": "…" };
an iCalendar export (format: "ics") likewise has the calendar text in both.
Each tool’s outputSchema is the result’s schema from schemas/ or the
tool error (anyOf), with every referenced schema inlined
under $defs: clients never fetch a URL. The top level is an object, as 2025 clients require.
backend/convex/lib/outputSchemas.ts is generated from docs/schemas by npm run schemas in
backend/; a test fails when it is out of date.
Finding tasks (list_all)
| Argument | Meaning |
|---|---|
query |
Tasks whose title, note, resume note or labels contain this text, in any case |
status |
Only these statuses, e.g. ["todo", "doing"]; done tasks are included only when listed |
labels |
Only tasks having all of these labels |
projectId |
Optional project filter (since 0.13.0) |
ready |
true: only tasks ready to work on (not done, no blockedReason, not a proposal, every task in dependsOn done); false: only the others |
proposed |
true: only proposals waiting in the inbox; without it, or false, proposals are left out (since 0.11.0) |
dueBefore, dueAfter |
Only tasks due before / after this day (YYYY-MM-DD, "today", "tomorrow"); tasks without a day never match |
overdue |
true: only overdue tasks; false: only the others |
includeDone, includeArchived |
Done tasks (default true; ignored when status is given), archived ones (default false) |
limit |
Page size, 1 to 200 |
cursor |
nextCursor of the previous page |
Tasks come oldest due first (tasks without a day first, timed before untimed on one day). The
result is { "result": [...], "total": <all matches>, "nextCursor": <string or null> }; pass
nextCursor back as cursor with the same filters for the next page. The cursor is opaque and
stays valid when tasks are added or removed in the meantime.
Without limit, every match comes back at once and the text block holds the plain task array,
as before 0.4.0, so clients that read the text keep working; with limit or cursor the text
block holds the whole object.
Projects
Since backend 0.13.0, a task may carry projectId, the stored project id. add_task and
update_task accept a project id or its name (case-insensitive); "none" means no project.
An omitted field leaves the task unchanged on update; projectId: null also clears it.
Selecting an archived project fails with invalid_argument, unless an update names the
project the task is already in. Unknown projects return not_found.
list_all, list_today and board take the same optional projectId selector: one project’s
tasks, "none" for tasks without a project, or omit it for all projects. The filter applies to
every part of Today, including the now task and done today. Other filters and pagination still
apply. Project CRUD and client availability are described in architecture.
Export formats
export_plan exports the whole plan. Since backend 0.13.0:
- Markdown task lines show the project as
+Project-name, with whitespace replaced by hyphens, next to the#labels. This is readable output, not an import format. - JSON adds a
projectscatalog in menu order, archived and empty projects included. Each task’sprojectIdreferences an id in that catalog. The export version stays 1; older importers ignore project fields. JSON import describes restoration into another account.
For iCalendar (ics), see calendar.md.
Agent work
Since 0.10.0 a task may carry fields for agents working on it (agent-work.md has
the reasons). None of them is a new status: the board stays todo, doing, done.
| Field | Set with | Meaning |
|---|---|---|
acceptance |
add_task, update_task |
Criteria { id, text, met } in order; new ones get a1, a2, …; the list is replaced as a whole, null or [] removes it (at most 50, up to 500 characters each) |
plan |
add_task, update_task |
How the work will be done (up to 20000 characters) |
proof |
complete_task |
{ summary?, links, checks: [{ name, result: "pass" | "fail", url? }] }, kept on the task (schema) |
dependsOn |
add_task, update_task |
Ids of the tasks that must be done first (given as ids or "#42"); [] removes them; no cycles (at most 50) |
blockedReason |
add_task, update_task |
Why the task cannot go on; null clears it |
claimedBy, claimExpiresAt |
claim_task, renew_claim, release_task |
Who holds the claim (keyId or connectionId, and name) and until when (Unix ms); kept after expiry until released or claimed again |
When a task has acceptance criteria, an agent (an agent key or an agent connection) can
complete it only when every criterion is met and the proof has a summary; otherwise
complete_task (and set_status to done) fails with precondition_failed. The app and cli
keys complete tasks as before. A repeating task’s next occurrence keeps the criteria, unticked,
and the plan.
Claims
claim_task gives the caller’s key or connection the task for minutes (1 to 1440, default 120).
While the claim lasts, another agent’s claim_task, renew_claim, release_task and every change
of that task (update_task, set_status, set_now, complete_task, reschedule_task,
archive_task, delete_task, restore_task) fail with conflict, whose message names the holder
and the expiry. Claiming again extends your own claim. renew_claim works while nobody else has
claimed the task, also after your claim ran out; without a claim of yours it is
precondition_failed. release_task ends the claim and saves note as the resume note.
Completing the task ends its claim. Done, archived and trashed tasks cannot be claimed
(invalid_argument). The owner wins: changes from the app and from cli keys are never refused,
and the app can end any claim (app.release).
Activity log (list_events)
| Argument | Meaning |
|---|---|
since |
nextCursor of an earlier call ("c…"), or a time as Unix milliseconds or ISO 8601 with a zone (the changes from then on); without it, the newest changes |
by |
Only changes by app, cli, agent (any agent) or "agent: <name>" |
taskId |
Only changes of this task |
limit |
1 to 500, default 100 |
The result is { "events": [...], "nextCursor": "c…", "hasMore": <bool> }
(schema). Each event has id, at, taskId, ref and title
(the task’s now), action, actor, actorName, by, before and after, like a history
entry. Events come oldest first, in the order the changes were committed. Pass nextCursor as
since next time to get only what changed in between; hasMore: true means another page is
already there. Only task changes are listed, not changes of API keys, webhooks or devices. Changes
made before 0.10.0 appear once the deployment ran migrations:backfillEventSeqs
(deploy.md).
Approvals, pausing and policies
Since 0.11.0 (agent-work.md has the reasons):
request_approvalstores a request{ action, detail?, options: [{ label, consequence?, id? }], reversible, expiresInMinutes? (1 to 10080, default 1440), taskId?, clientId? }; options geto1,o2, … unless they carry an id. The owner decides on the phone (or with aclikey,decide_approval); the agent reads the outcome withget_approvalor gets the webhookapproval.decided(webhooks.md). A request about a task enters that task’s history and the activity log (approvalRequest,approvalDecide). Dayhand records the decision; it does not stop an agent from acting without one. The agent’s own tooling has to check it.pause_agentswithpaused: truerefuses every change by an agent key or agent connection withagents_paused, dry runs included, untilpaused: false; reads keep working, and the owner’s own changes (app,clikeys) are never refused.set_tool_policysets, for one agent key,allow,askordenyon a tool that changes the plan, or on"*"(every such tool without its own rule); no rule meansallow.denyfails the call withforbidden.askfails it withapproval_requiredand anapprovalId: Dayhand has made a request with the optionsallow(“Allow once”) anddeny, bound to this key, this tool and these arguments (withoutclientId,dryRunandapprovalId); calling again while it waits returns the same request. Once the owner allows it, the same call withapprovalIdruns once; another call, a second use, a denied or an expired request is refused. A dry run needs no approval. Policies are per API key; agent connections (OAuth) follow only the pause and their scopes.
Proposals
add_task with proposal: true adds a task with proposedAt: it waits in the owner’s inbox and
stays out of list_today, board, list_all (unless proposed: true), ready, the calendar
feed and the phone’s Today until the owner accepts it (accept_proposal), merges it into another
task (merge_proposal; merge rules) or rejects
it (reject_proposal: to the trash). A proposal cannot be claimed, made the now task, moved to
doing or completed (invalid_argument). Three fields work on any new task:
| Field | Meaning |
|---|---|
source |
{ kind, ref, url? }, e.g. { "kind": "sentry", "ref": "CHECKOUT-42" }. One source makes one task: add_task with a source that already made a task returns that task, also when it is in the trash; once the trash is purged the source is free again. A merged proposal’s source leads to the task it was merged into |
requestedBy |
Who asked for it (up to 200 characters); an agent’s proposal without it says agent: <name> |
returnTo |
Id of one of the user’s webhooks: it gets task.returned with the outcome (completed, rejected or merged) and the proof, whatever events it subscribes to (webhooks.md) |
Plan health (plan_health)
plan_health (also GET /api/v1/health, dayhand health, the prompt plan_health) returns, as
of at, in the user’s time zone: agentsPaused; overdue, dueToday, inProgress (each with
workingBy: "agent: <name>" while a claim lasts), expiredClaims, blocked (each with
reasons: the blocked reason and every dependency not done yet), awaitingApproval (tasks with a
pending request, and pendingApprovals in all), completedToday (each with proof, and
withProof, withoutProof): each { count, tasks } with the first 20 tasks; proposals.count;
and oldestOpen, the open task created longest ago with ageDays
(schema). Proposals and archived tasks are not counted as open.
Agent brief (agent_brief)
agent_brief (also GET /api/v1/brief, dayhand prime --json) is what an agent reads first in a
session, as of at and from its own side (since 0.12.0): every field of list_today, then
caller (type, name), agentsPaused and pausedReason, policies (the owner’s rules for the
caller’s own key other than allow), claims.active and claims.expired (each claim with
claimedBy, claimExpiresAt and mine), approvals.pending (every request waiting, mine when the
caller asked) and approvals.decided (the caller’s requests decided in the last 24 hours, with the
decision), ready (tasks ready to work on that no other agent holds, soonest due first), each
{ count, items } with the first 10; and nextSteps, sentences most pressing first that name the
tools and the phase guides to use (schema). While
agents are paused, nextSteps says only that.
Backlog.md (import_backlog, export_backlog)
import_backlog takes files: [{ path, content }], with paths relative to the backlog folder;
export_backlog returns the files and configuration to write. The result contracts are
backlog-import and
backlog-export. See backlog-md.md for field
mappings, source identity, limits, approvals and safe folder handling.
Dry runs
Every changing tool takes dryRun: true. The change runs with every check, then Dayhand rolls it
back: nothing is saved and nothing enters the history. The result is what the real call would
return, marked "dryRun": true (for set_now with taskId: null, { "result": null, "dryRun": true }). A dry run of add_task shows the number the task would get, but does not reserve it.
A change that would fail fails its dry run with the same error. Dry runs count as changes for
the limits.
Resources
| URI | Type | Content |
|---|---|---|
dayhand://today |
application/json |
list_today |
dayhand://activity |
application/json |
list_events without since: the newest changes and the cursor to follow from |
dayhand://activity{?since} (template) |
application/json |
list_events with since, e.g. dayhand://activity?since=c1793011200000123 |
dayhand://guide |
text/markdown |
How an agent should work with Dayhand: numbers, the now task, retries, dry runs, limits |
dayhand://workflow/intake |
text/markdown |
Phase 1 of a task: find or add it, proposal or not, acceptance criteria, plan, dependencies (since 0.12.0) |
dayhand://workflow/execution |
text/markdown |
Phase 2: choose ready work, claim it, tick criteria, ask before risky steps, stop with a checkpoint |
dayhand://workflow/finalization |
text/markdown |
Phase 3: check the criteria, complete with proof, ask the owner for the last step, or hand back |
dayhand://tasks/{ref} (template) |
application/json |
get_task of task #ref, e.g. dayhand://tasks/42 |
resources/list returns the six without a template, resources/templates/list the two templates. The helm://
URIs of the same resources, from before the rename, can still be read. Reading a
resource that does not exist (also a task number that does not) is a JSON-RPC error with
data: { "uri" }: -32602 on 2026-07-28, -32002 on 2025 revisions.
Prompts
| Prompt | Arguments | What it starts |
|---|---|---|
plan_day |
focus (optional) |
Go through overdue and today’s tasks, agree on an order and set the now task; the Today screen is attached as the resource dayhand://today |
weekly_review |
Look back at the last 7 days (board’s done column) and ahead at the next 7 (list_all with dates), then tidy the plan |
|
triage_overdue |
Decide, task by task, what happens to each overdue task; the overdue tasks are attached | |
plan_health |
Go through what needs a look, most urgent first; the plan_health report is attached (since 0.11.0) |
Each prompt tells the agent to ask before changing anything, to pass a clientId and to preview
larger changes with dryRun. An unknown prompt or argument is a JSON-RPC error -32602.
Limits
Each API key, and each agent connection (OAuth), may make 120 reads and 60 changes a minute
(convex/callLimits.ts). The minute
starts with the first call; reads and changes are counted apart. Reads are tool calls of
read-only tools, resources/read of dayhand://today, dayhand://activity… and dayhand://tasks/…, and prompts/get;
listing tools, resources and prompts, server/discover, dayhand://guide and the dayhand://workflow/… guides are not counted.
A call over the limit is refused before it runs, so nothing is applied:
- a tool call returns the tool error
rate_limitedwithretryAfterMs, HTTP 200 and aRetry-Afterheader (seconds); resources/readandprompts/getget HTTP 429 withRetry-Afterand the JSON-RPC error{ "code": 429, "message": "Too many reads with this API key: …", "data": { "retryAfterMs" } }.
npx convex run callLimits:reset '{"keyId":"<key id>"}' clears a key’s counters
('{"connectionId":"<connection id>"}' a connection’s).
Repeat rules
repeat is an object, or an RFC 5545
RRULE string (with or without RRULE:) that the server reads into the object. Every repeating
task also comes back with its rule as an RRULE in rrule, e.g. "FREQ=WEEKLY;BYDAY=MO,TH".
| Field | Meaning | RRULE |
|---|---|---|
every, interval |
every day, week, month or year; interval 2 = every other (1 to 366) |
FREQ, INTERVAL |
weekdays |
weekly rules: days 1 (Monday) to 7 (Sunday) | BYDAY=MO,TH |
monthDay |
monthly rules: day 1 to 31, or -1 for the last day | BYMONTHDAY=15, BYMONTHDAY=-1 |
weekdayOfMonth |
monthly rules: { nth, weekday }, nth 1 to 5 or -1 (last) to -5 |
BYDAY=2TU, BYDAY=-1FR (or BYDAY=TU;BYSETPOS=2) |
from |
"due" (default): the next date follows the due date; "completion": it is counted from the day the task is done |
none |
until |
last date the series may fall on, YYYY-MM-DD | UNTIL=20261231 (a UTC date-time is read in the user’s time zone) |
count |
occurrences left, this one included; 1 = the last one |
COUNT |
until and count cannot be combined (as in RFC 5545). Completing a task counted from the due
date creates the first date of the series after today, so a late completion skips the dates it
missed, and those count against count. When the series has ended, complete_task returns
next: null. The next occurrence keeps the title, note, labels, links and reminders, with the
checklist unticked.
Dayhand reads FREQ (DAILY, WEEKLY, MONTHLY, YEARLY), INTERVAL, BYDAY, BYMONTHDAY,
BYSETPOS, UNTIL, COUNT and WKST; anything else (BYHOUR, BYMONTH, …) is an
invalid_argument naming the part. Where Dayhand differs from RFC 5545:
- A day the month does not have (31 in April) falls on the month’s last day; RFC 5545 skips the
month.
rrulewrites such days asBYMONTHDAY=28,29,30,31;BYSETPOS=-1, which means the same in both, and reads that form back asmonthDay: 31. A plainBYMONTHDAY=31is read asmonthDay: 31too, with Dayhand’s meaning. - A plain monthly rule takes its day from the due date; after a shorter month the next
occurrence keeps that day as
monthDay(31 Jan, 28 Feb, 31 Mar). - A yearly rule from 29 February falls on 28 February in other years and stays there.
from: "completion"has no RRULE form;rruledescribes the dates only.- Weeks start on Monday; another
WKSTis refused only where it would change the dates (several days every two or more weeks).
The rule logic is in backend/convex/lib/repeat.ts (advance, and occurrences to list a series
ahead, e.g. for a calendar) and lib/rrule.ts (toRRule, parseRRule).
Checklists and reminders
checklist: items{ id, title, done }in the order shown.update_taskreplaces the whole list: send the items you keep with their ids, new items without one (the server givesc1,c2, …).nullor[]removes it. At most 100 items of up to 500 characters. History records the list before and after each change.reminders: the task’s own reminders instead of the default ones from settings. Each is{ minutesBefore }(before the due time; rings only for timed tasks) or{ time: "HH:MM", daysBefore? }(that time on the due day, or up to 30 days before; works for untimed tasks too). A task withoutremindersuses the defaults;[]means no reminders for this task;nullinupdate_taskgoes back to the defaults. At most 5. Reminders ring on the phone (architecture.md).
Errors
A failed tool call is a normal result with isError: true. The text block holds a message
for people; structuredContent.error holds the same message and a code for programs:
{
"content": [{ "type": "text", "text": "Task #404 not found" }],
"structuredContent": { "error": { "code": "not_found", "message": "Task #404 not found" } },
"isError": true
}
| Code | Meaning | Example message |
|---|---|---|
invalid_argument |
Wrong or missing arguments, or an operation the task’s state does not allow | dueDate must be YYYY-MM-DD, "today" or "tomorrow", got "someday"; Invalid arguments: taskId must be a task id or "#<number>", got "nope"; Task is in the trash; restore it first |
not_found |
No such task (or API key, device) for this user | Task #404 not found |
rate_limited |
Too many calls with this key or connection; retryAfterMs says when to try again |
Too many changes with this API key: at most 60 a minute. Try again in 12 s. |
unauthenticated |
Reserved for identity errors inside a tool; a missing, wrong or revoked key or token gets HTTP 401 before any tool runs | |
precondition_failed |
The task’s state does not allow the change yet: an agent completing a task with unmet acceptance criteria or without a proof summary, renew_claim without a claim of yours; through the REST API also If-Match naming a version the task is no longer at (api.md) |
Acceptance criteria not met: a2; mark them met with update_task first; The task has changed since that version (it is now "1793011200000"); read it again |
conflict |
Another agent holds the task’s claim (Claims); pick another task or wait for the time named | Task #42 is claimed by claude-code until 2026-10-24T14:30:00.000Z |
agents_paused |
The owner paused every agent; reading works, changes wait until they resume (since 0.11.0) | The owner has paused all agents (holiday): reading works, changes are refused until they resume agents |
forbidden |
Only the owner may do this, or the owner’s policy denies the tool to this key, or the owner denied the approval (since 0.11.0) | Only the owner can accept proposals (in the app or with a cli key); The owner's policy does not let this key call delete_task |
approval_required |
The owner’s policy asks first; approvalId names the request to wait for, then repeat the same call with it (since 0.11.0) |
The owner's policy asks before this key calls complete_task. Approval request … waits for the owner until … |
internal |
Anything unexpected; the message says only Internal error |
Internal error |
New codes may be added in a minor version; treat an unknown code like internal.
Errors outside tools are JSON-RPC errors:
| Code | When | HTTP |
|---|---|---|
-32602 |
Unknown tool or prompt, bad prompt arguments, a resource that does not exist (2026-07-28), a missing _meta field (2026-07-28) |
200; 400 for the _meta field |
-32002 |
A resource that does not exist (2025 revisions) | 200 |
-32601 |
Unknown method | 200; 404 on 2026-07-28 |
-32020 |
2026-07-28 header mismatch: a standard header missing or not matching the body | 400 |
-32022 |
Unsupported protocol version; data: { supported, requested } |
400 |
429 |
Too many reads outside tools (Limits) | 429 |
-32600, -32700 |
Not a JSON-RPC request, a batch, a body over 256 KB, a browser origin not allowed | 400, 413, 403 |
An OAuth access token whose scopes do not cover the request gets HTTP 403 with
WWW-Authenticate: Bearer error="insufficient_scope", scope="…" and no JSON-RPC body: a tool
of the other scope, or, without tasks:read, resources/read of the user’s data and
prompts/get (oauth.md). tools/list shows only the
tools the token’s scopes allow.
Protocol versions
One endpoint serves both eras of MCP, the way the specification describes a dual-era server:
- 2025 revisions (2025-11-25 and earlier): the client sends
initialize, and the server answers with the requested version when it knows it, otherwise2025-11-25.pingworks. Results have noresultType. Dayhand keeps no session, so every request stands alone anyway. - 2026-07-28: no
initialize; a request is served as 2026-07-28 when itsparams._metacarriesio.modelcontextprotocol/protocolVersion. It must also carryio.modelcontextprotocol/clientCapabilities(else-32602, HTTP 400) and the headersMCP-Protocol-Version(equal to that version),Mcp-Methodand, fortools/call,prompts/getandresources/read,Mcp-Name(plain or=?base64?…?=), all matching the body (else-32020, HTTP 400).server/discoverreturnssupportedVersions(both eras), the capabilities and the instructions. Every result hasresultType: "complete"and_meta["io.modelcontextprotocol/serverInfo"];server/discover,tools/list,prompts/list,resources/listandresources/templates/listcarryttlMs: 300000andcacheScope: "public"(tools/listof an agent, or of a token with fewer scopes:private, since it leaves tools out),resources/readcarriesttlMs: 0andcacheScope: "private"(dayhand://guide: public).pingno longer exists and unknown methods get HTTP 404.
Not offered in either era: subscriptions/listen (and listChanged notifications), logging,
completion, sampling, elicitation and the tasks extension. Dayhand never asks the client for
input, so no request returns input_required.
The official TypeScript SDK v2 reaches the 2026-07-28 era with
versionNegotiation: { mode: "auto" } (or a pin); without it, it speaks 2025 like SDK v1.
Both are tested (backend/tests/mcp-modern.test.ts).
Keys and actors
Every API key has a name and a kind:
cli: the user at a terminal. History shows changes ascli.agent: an AI agent. History shows changes asagent: <key name>, e.g.agent: claude-code.
Changes made in the app are app. Changes made through an agent connection (OAuth, e.g. the
Claude app) are agent with the client’s name, e.g. agent: Claude
(oauth.md). get_task returns each history
entry with actor (app, cli or agent), actorName (the key’s or client’s name, null
for the app) and by, the label above. Give each agent its own key so it can be told apart and revoked on its own.
Keys without a kind (created before 0.3.0) act as agent keys; the 0.3.0 migration records
that (deploy.md). A new key is an agent key unless created with
"kind": "cli":
npx convex run apiKeys:create '{"userId":"<issuer>|owner","actor":"app","name":"macbook","kind":"cli"}'
npx convex run apiKeys:create '{"userId":"<issuer>|owner","actor":"app","name":"claude-code"}'
The app does the same with app.createApiKey({ name, kind }); app.listApiKeys returns each
key’s kind.
Since 0.8.0 a key also has a scope and may expire:
| Scope | On /mcp |
REST /api/v1 (api.md) |
Account: keys, webhooks, devices, connections, calendar feed |
|---|---|---|---|
read |
only the reading tools (tools/list shows only them; a change gets 403 insufficient_scope) |
GET routes |
no (403) |
write |
every tool | every task and settings route | no (403) |
full |
every tool | everything | yes |
Keys without a scope (every key made before 0.8.0) are full keys; nothing changes for them.
expiresAt (Unix milliseconds) ends a key: after it the key gets 401 like a revoked one.
npx convex run apiKeys:create '{"userId":"<issuer>|owner","actor":"app","name":"n8n","scope":"read","expiresAt":1798761600000}'
Creating and revoking API keys is not an MCP tool on purpose: a key that can mint keys could
outlive its own revocation. Keys are managed by the app (app.createApiKey,
app.revokeApiKey), by the deployment admin (npx convex run apiKeys:create), or with a full
key through POST /api/v1/keys (dayhand key create). A key made that way can only have scope
read or write, expires no later than the key that made it, and works only while that key
works: revoking the parent stops it too (api.md).
Phone pairing is not an MCP tool either. The CLI pairs phones through separate endpoints
(/devices/*, see pairing.md) that accept the same API key.
Versions
serverInfo.version is the backend version (backend/package.json) and the one clients use
to check compatibility. It follows semver: a minor version only adds tools, fields, error codes
and accepted values; a major version may change or remove them. A client written for 0.3
works with any 0.x from 0.3 on.
| Version | Changes |
|---|---|
| 0.13.0 | Project selectors, export fields and Backlog.md targeting; backend project support and client availability |
| 0.12.0 | Phase guides and Backlog.md (agent-work.md, backlog-md.md): resources dayhand://workflow/intake, dayhand://workflow/execution and dayhand://workflow/finalization; agent_brief; import_backlog and export_backlog (source kind backlog-md) |
| 0.11.0 | Agents and the owner (agent-work.md): request_approval, get_approval, list_approvals, decide_approval; get_agent_controls, pause_agents, set_tool_policy, and approvalId on every change an agent makes; proposals (add_task takes proposal, source, requestedBy, returnTo; task fields proposedAt, source, requestedBy, returnTo, mergedInto; list_all takes proposed; accept_proposal, merge_proposal, reject_proposal); plan_health and the prompt plan_health; error codes agents_paused, forbidden, approval_required (with approvalId); webhook events approval.decided and task.returned; the owner’s tools (O) are left out of an agent’s tools/list |
| 0.10.0 | Agent work (agent-work.md): list_events and the resource dayhand://activity (the activity log); task fields acceptance, plan, proof, dependsOn, blockedReason, claimedBy, claimExpiresAt; complete_task takes proof; list_all takes ready; claim_task, renew_claim, release_task; error code conflict, and precondition_failed outside If-Match too |
| 0.9.0 | The rename to Dayhand: serverInfo.name dayhand; resources dayhand://today, dayhand://guide and dayhand://tasks/{ref} (the helm:// URIs are still read); new API keys start with dayhand_ (helm_ keys keep working); import format dayhand-json (helm-json still accepted); WWW-Authenticate realm dayhand; x-dayhand-scope in the OpenAPI document; problem types under https://schemas.dayhand.invalid/; deployment variables DAYHAND_* (HELM_* still read, deploy.md) |
| 0.8.0 | API keys with a scope (read, write, full) and expiresAt (a read key sees and calls only reading tools); the error code precondition_failed (REST If-Match only); the REST API /api/v1 with OpenAPI 3.1 (api.md), webhooks (webhooks.md) and import. Devices, connections and the calendar feed need a full key |
| 0.7.0 | export_plan format ics; the read-only calendar feed GET /cal/<token>.ics and its endpoints /calendar/feed* (calendar.md) |
| 0.6.0 | OAuth 2.1 for the Claude apps and other MCP clients: access tokens next to API keys, WWW-Authenticate with resource_metadata, scopes tasks:read and tasks:write (tools/list filtered, 403 insufficient_scope), limits per connection (oauth.md) |
| 0.5.0 | Repeat rules: from, monthDay, weekdayOfMonth, until, count, RRULE strings in repeat and rrule on repeating tasks; checklist and reminders on tasks; complete_task returns next: null when a series has ended |
| 0.4.0 | MCP 2026-07-28 next to the 2025 revisions; tool titles, all behaviour hints and output schemas; list_all search, filters and pages (total, nextCursor); dryRun on every change; resources helm://today, helm://guide, helm://tasks/{ref}; prompts plan_day, weekly_review, triage_overdue; limits per key and error rate_limited; structured content for a Markdown export; Origin check |
| 0.3.0 | Task numbers (ref, taskId: "#42"); error codes in structuredContent.error; key kinds and named actors in history (actor: "cli", actorName, by) |
| 0.2.0 | Phone pairing endpoints (/devices/*, /auth/*) |
| 0.1.0 | MCP endpoint with 17 tools |
Connecting a client
The Claude apps (claude.ai, Desktop, Android, iOS) add Dayhand as a custom connector with the URL
<deployment HTTP actions URL>/mcp and sign in with OAuth; the owner approves the connection
on the phone. The steps are in oauth.md.
Any MCP client with Streamable HTTP and custom headers works with an API key, for example:
claude mcp add --transport http dayhand http://127.0.0.1:3211/mcp \
--header "Authorization: Bearer $DAYHAND_API_KEY"
This page is docs/mcp.md in the repository, where
you can suggest a change.