All docs

MCP execution tools

MCP execution tools

Module: apps/mcp/src/tools/execution/ Scopes: tasks:read for get_next_actionable, tasks:write for the rest.

The agent loop

[ get_next_actionable ] ──▶ list of ready tasks + knowledge digests
        │
        ▼
[ claim_task(taskId) ]    ──▶ CAS UPDATE: state ready→claimed, lease 900s
        │
        ▼
[ update_task_status(taskId, 'in_progress') ]
        │  ↻ heartbeat extends lease (default heartbeat=true)
        ▼
[ complete_task(taskId, output, learnings) ]
        │  ─ verification.kind=none      → state=done
        │  ─ verification.kind=manual    → state=needs_review (human gate)
        │  ─ verification.kind=github_pr → state=needs_review + BullMQ verify job
        └─ learnings → knowledge_entries (source='task_completion') + task_knowledge edge

get_next_actionable

Returns up to N state='ready' tasks for the agent's org, sorted by (priority ASC, created_at ASC). Each result is enriched with a knowledge digest (top-3 hybrid search hits, min score 0.5) and the repository row when the task is wired to one. Search failures degrade gracefully — the task is still returned with knowledge.references=[].

The :read scope makes this safe for read-only agents (UI introspection, dashboards).

claim_task

Atomic CAS — racing agents can never both succeed (the UPDATE … WHERE state='ready' filter is the chokepoint). Idempotent on (taskId, agentId): re-claim by the same agent returns the current lease.

Task-29a integration: if isBlockedByReview(taskId) reports an open blocker comment the claim is rejected with task.not_actionable. Today the helper is a stub that always returns blocked: false; comments land in task 29a.

update_task_status

Only the claim holder can call this — non-holder → task.already_claimed. Transitions are enforced by the core state machine; an invalid combination (e.g. done → in_progress) surfaces as task.invariant_violated.

complete_task

Dispatches on verificationContract.kind:

verificationEtaSec is a non-binding hint (60s for CI-bound kinds, 30s for commands, 0 for none, null for manual).

Each learning becomes a knowledge_entries row with source='task_completion', source_task_id=taskId. A task_knowledge edge of kind 'produced' is inserted, and an embedding job is enqueued (fire-and-forget). Learnings count against the knowledge_stored quota.

Quota touch-points

  • complete_task calls checkQuota(orgId, 'knowledge_stored') only when

learnings.length > 0, before the transaction. On success the counter is bumped after commit (best-effort — under-count is preferable to rolling back a successful business write).

  • claim_task does not count toward quotas.