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 data object 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 Task object directly (middleware wraps as {ok:true,data:Task})
  • No domain errors possible (repository throws only on CHECK violation or disk errors — propagated as HANDLER_ERROR by 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 Task directly
  • 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 to ListTasksFilter
  • 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.ts bootstrap() after registerSkillTools(ctx)
  • Lazy-resolves getDb() inside each handler closure
  • Uses registerColibriTool for all 5 tools
  • Throws on duplicate registration (via registerColibriTool guard)

5. Imports required

From src/domains/tasks/repository.ts:

  • createTask, getTask, updateTask, listTasks — CRUD functions
  • TaskNotFoundError — error class
  • TASK_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_cancel tool (not in P0.3.4 scope per task-breakdown.md)
  • No task_transition tool (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_actions uses direct SQL (not listTasks) — requires both migrations applied in tests

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.