Contract: p0-3-4-task-tools — β Task MCP Tool Surface
Task: P0.3.4
Author: T3 Executor (Claude Sonnet 4.6)
Date: 2026-04-17
Status: APPROVED (self-approved, no blocking ambiguity)
1. Purpose
This contract defines the behavioral specification for the 5 MCP tools comprising the β Task Pipeline tool surface. These tools expose src/domains/tasks/repository.ts CRUD functions to MCP clients via the registerColibriTool middleware chain.
2. Envelope design decision
The task brief requires: “DO NOT throw from handlers, return the envelope”. This is a deliberate deviation from the trail/skills pattern (which throws and lets middleware produce HANDLER_ERROR). Task tools must:
- On success: return
dataobject directly — middleware wraps as{ ok: true, data: ... } - On domain error: return
{ ok: false, error: { code: 'ERR_X', ... } }— middleware wraps as{ ok: true, data: { ok: false, error: { code: 'ERR_X', ... } } }
This double-wrapping is intentional for Phase 0 — it provides typed error codes visible in structured content. The outer ok: true signals “the tool call succeeded (no crash)” while the inner ok: false signals “the operation failed”. MCP clients should read data.ok not ok for task tools.
Note: This matches the s17 §6 spirit — the envelope is in the data field and clients consume it. A future cleanup may flatten this in Phase 1.
3. Tool specifications
3.1 task_create
Input schema:
z.object({
title: z.string().min(1),
status: z.enum(TASK_STATES).optional(),
project_id: z.string().nullable().optional(),
description: z.string().nullable().optional(),
priority: z.string().nullable().optional(),
assignee: z.string().nullable().optional(),
})
Behavior:
- Calls
createTask(getDb(), input) - Returns
Taskobject directly (middleware wraps as{ok:true,data:Task}) - No domain errors possible (repository throws only on CHECK violation or disk errors — propagated as
HANDLER_ERRORby middleware)
Success output: Task — the full persisted task object
Error: Propagated as {ok:false,error:{code:'HANDLER_ERROR',...}} by middleware (disk full, constraint violation)
3.2 task_get
Input schema:
z.object({
id: z.string().min(1),
})
Behavior:
- Calls
getTask(getDb(), input.id) - If result is
Task: return it directly (middleware wraps as{ok:true,data:Task}) - If result is
null: handler returns{ok:false,error:{code:'ERR_NOT_FOUND',message:'Task not found',taskId:input.id}}
Success output: Task
Not-found output (from handler, wrapped by middleware):
{ "ok": false, "error": { "code": "ERR_NOT_FOUND", "message": "Task not found: <id>", "taskId": "<id>" } }
3.3 task_update
Input schema:
z.object({
id: z.string().min(1),
patch: z.object({
title: z.string().optional(),
status: z.enum(TASK_STATES).optional(),
project_id: z.string().nullable().optional(),
description: z.string().nullable().optional(),
priority: z.string().nullable().optional(),
assignee: z.string().nullable().optional(),
}),
})
Behavior:
- Calls
updateTask(getDb(), input.id, input.patch) - On success: return the updated
Taskdirectly - On
TaskNotFoundError: return{ok:false,error:{code:'ERR_NOT_FOUND',message:...,taskId:input.id}} - On
WritebackRequiredError: return{ok:false,error:{code:'ERR_WRITEBACK_REQUIRED',message:...,missing_fields:[...],taskId:input.id}} - On any other error: re-throw (middleware produces
HANDLER_ERROR)
Success output: Task
Not-found output:
{ "ok": false, "error": { "code": "ERR_NOT_FOUND", "message": "Task not found: <id> (operation: update)", "taskId": "<id>" } }
Writeback-required output:
{ "ok": false, "error": { "code": "ERR_WRITEBACK_REQUIRED", "message": "Writeback required for task <id>: missing thought_record", "missing_fields": ["thought_record"], "taskId": "<id>" } }
3.4 task_list
Input schema:
z.object({
status: z.enum(TASK_STATES).optional(),
project_id: z.string().nullable().optional(),
limit: z.number().int().positive().max(500).optional(),
offset: z.number().int().min(0).optional(),
include_deleted: z.boolean().optional(),
})
Behavior:
- Calls
listTasks(getDb(), input)— maps directly toListTasksFilter - Returns
{tasks: Task[], total_count: number} - No domain errors possible (returns empty array on no-match)
Success output:
{ "tasks": [...], "total_count": N }
3.5 task_next_actions
Input schema:
z.object({}) // no inputs
Behavior:
- Returns tasks where
status NOT IN ('DONE', 'CANCELLED')— i.e. the non-terminal states - Uses
ORDER BY rowid ASC(insertion order — deterministic per Wave C lesson) - Returns
{tasks: Task[], total_count: number}
SQL:
SELECT * FROM tasks
WHERE deleted_at IS NULL
AND status NOT IN ('DONE','CANCELLED')
ORDER BY rowid ASC
Note: Does NOT use listTasks filter since ListTasksFilter.status only supports a single status value. Uses getDb() directly with a prepared statement.
Success output:
{ "tasks": [...], "total_count": N }
4. registerTaskTools(ctx) function
export function registerTaskTools(ctx: ColibriServerContext): void {
// Register task_create, task_get, task_update, task_list, task_next_actions
}
- Called from
src/server.tsbootstrap()afterregisterSkillTools(ctx) - Lazy-resolves
getDb()inside each handler closure - Uses
registerColibriToolfor all 5 tools - Throws on duplicate registration (via
registerColibriToolguard)
5. Imports required
From src/domains/tasks/repository.ts:
createTask,getTask,updateTask,listTasks— CRUD functionsTaskNotFoundError— error classTASK_STATES— for Zod enum
From src/domains/tasks/writeback.ts:
WritebackRequiredError— error class
From ../../db/index.js:
getDb— lazy DB resolver
From ../../server.js:
registerColibriTool,ColibriServerContext(type)
From zod:
z
6. Non-goals (explicit)
- No FSM transition enforcement in tool layer (P0.3.1 FSM is available but not invoked)
- No
task_delete/task_canceltool (not in P0.3.4 scope per task-breakdown.md) - No
task_transitiontool (not in P0.3.4 scope) - No pagination metadata beyond
total_count(no cursor tokens)
7. Files to create/modify
| File | Action |
|---|---|
src/domains/tasks/repository.ts |
Add registerTaskTools function at bottom |
src/server.ts |
Add import { registerTaskTools } + 1-line call in bootstrap() |
src/__tests__/domains/tasks/tools.test.ts |
New test file — ≥15 tests |
8. Test coverage requirements
Minimum 15 tests in src/__tests__/domains/tasks/tools.test.ts:
| Test | Coverage |
|---|---|
registerTaskTools registers 5 tools |
Registration |
registerTaskTools throws on duplicate |
Duplicate guard |
task_create happy path |
Create |
task_create Zod rejects missing title |
Invalid input |
task_get found |
Get found |
task_get not found → ERR_NOT_FOUND |
Get not-found |
task_get Zod rejects missing id |
Invalid input |
task_update happy path |
Update |
task_update not found → ERR_NOT_FOUND |
Update not-found |
task_update status→DONE without thought_record → ERR_WRITEBACK_REQUIRED |
Writeback integration |
task_update status→DONE WITH thought_record → success |
Writeback satisfied |
task_update Zod rejects invalid status |
Invalid input |
task_list happy path |
List |
task_list with status filter |
List filtered |
task_next_actions returns only non-terminal tasks |
Next actions |
task_next_actions excludes DONE and CANCELLED |
Next actions exclusion |
9. Risks
- Double-envelope for error paths (see §2) — accepted design for Phase 0
task_next_actionsuses direct SQL (notlistTasks) — requires both migrations applied in tests