MCP Task Orchestrator · Field guidePlate 5 of 9

Where it runs and who talks to it

One Docker image, one SQLite file. It runs as a throwaway container per session or as a persistent local server shared by every project, and the same gate logic answers both the MCP tools and the REST API.

Inside the server

Claude Code + plugin Any MCP client Dashboards · CI · hooks MCP over STDIO or Streamable HTTP REST + SSE Interfaces · MCP tools items, notes, dependencies, workflow, config Interfaces · REST API optional Infrastructure SQLite + FTS5 · Exposed ORM · Flyway migrations · YAML config · JWKS Application gate checks, cascades, schema and trait resolution, claims Domain WorkItem · Note · Dependency · roles · dispatch profiles
Each layer may depend only on the layers below it, and an architecture test fails the build on a new violation. The gate logic sits in the application layer and is built in one place, so an MCP call and a REST call to advance an item get the same answer.

Two ways to run it

Per-session container (STDIO)

MCP client one session stdin / stdout Container docker run --rm -i reads, writes mcp-task-data SQLite volume .taskorchestrator/ optional read-only mount No port, no daemon. The data outlives the container.
The simplest setup. Schemas come from the mounted folder, or the server runs schema-free with no gates.

Persistent server (HTTP + config-sync)

project A session project B session project C session /mcp for tools /api/v1 for config-sync One container 127.0.0.1:3001 per-root config store mcp-task-data Each session's hook pushes its project's config.yaml on start. Unauthenticated mode is for loopback only.
One server for every repository, each with its own schemas, reloaded without a restart. Bearer tokens or JWKS protect the REST API on shared hosts. The /mcp endpoint itself is never authenticated, so keep it on loopback or behind your own network controls.

The REST API, for everything that is not an agent

Dashboards, CI jobs and the plugin's own hooks use HTTP under /api/v1, switched on with API_ENABLED=true. An advance over REST runs the same gate logic as the MCP tool.

Auth modeHow a caller proves itself
bearerStatic tokens from a secrets file. Each has its own capabilities, scope and expiry. Loaded at startup.
jwksSigned JWTs checked against a key set, issuer and audience. Capabilities and scope travel as claims.
noneAn explicit double opt-in. Every request acts as admin. For a loopback port only.
CapabilityAllows
readEvery GET, and the event stream
write-items, write-notesCreating, editing and deleting items and notes
advanceRole transitions
manage-dependenciesAdding and removing edges
write-configPushing per-project config, plans and rules
adminAll of the above, plus attribution fields and lease overrides

A token can also be limited to certain project roots and tags. A single item outside that scope returns 403, and lists are filtered.

To do thisRoute under /api/v1
Read or change itemsGET /items/{id} · POST /items · PATCH /items/{id}
Write a notePUT /items/{id}/notes/{key}
Move an item, or ask what blocks itPOST /items/{id}/advance · GET /items/{id}/gate
SearchGET /search · GET /notes/search
Push or inspect project configPUT /roots/{rootId}/config · GET /roots/{rootId}/config/effective
Audit who did whatGET /transitions?since=…
Inspect or release leasesGET /resources/leases · DELETE /resources/leases/{key}
Check liveness, with no authGET /health
Follow changes liveGET /events
Dashboard Server GET /api/v1/events?types=item.advanced,note.upserted an event for each matching change, each with an id connection drops reconnect with Last-Event-ID: 4812 the missed events, replayed from a buffer If the buffer no longer holds them, the first event is sync.lost and the client refetches.
Event types: item.created, item.updated, item.deleted, item.advanced, note.upserted, note.deleted, dependency.added, dependency.removed, scope.entered, scope.left. Two control events always arrive: sync.lost and auth.expired.

Writes are guarded

Editing an item requires If-Match with its current ETag, so a stale client gets a 412 and cannot overwrite. Edits are JSON merge-patch. An Idempotency-Key makes a retried write replay its first result.

Errors say what to do

Every error is { error, message, details }. A failed gate is 422 gate_blocked with the missing notes. A contended lease is 409 resource_unavailable with a Retry-After header.

Where REST differs from MCP

The actor is always the token's identity, and one supplied in the body is dropped. A REST advance is not bound by claim ownership, though it is logged, and is still bound by leases. Lease administration and the event stream exist only here.

What the Claude Code plugin adds

The server works with any MCP client. The plugin wires it into a Claude Code session through hooks, in the order a feature normally unfolds.

  1. SessionStartInjects workflow conventions and project scope. Syncs the project config to the server.
  2. EnterPlanModeReads existing items and schema gates first, so the plan covers what the gates will ask for.
  3. ExitPlanModeTurns the approved plan into items, dependency edges and queue notes before any code is written.
  4. SubagentStartHands an implementer or reviewer its protocol: enter your phase, fill its notes, return.
  5. advance_itemCan require an actor on every write. Records which phase a sub-agent entered. Can trigger a retrospective.
  6. SubagentStopSends a sub-agent back if the phase it entered still has required notes missing.
Three more hooksWhat each does
Before a note is writtenIf the note's schema names a skill and the body looks thin, suggests running that skill first. Advisory only.
When config.yaml changesRe-syncs the project config to the server mid-session.
When a turn endsNudges a retrospective if an item finished on its own and nothing else prompted one.
Skills, by purposeSlash commands under /task-orchestrator:
Get set upquick-start (guided onboarding) · init (project or personal root) · configure-server (transport, REST, config-sync) · adopt-project-scope (migrate an existing database)
Shape the rulesmanage-schemas (create, edit and validate schemas, traits and identity policy)
Day-to-day workwork-summary (dashboard) · create-item · status-progression (which trigger, what is missing) · dependency-manager · batch-complete
Run many itemsrun-wave (attended) · ralph (unattended queue drain)
Improve the processsession-retrospective · review-proposals

Also shipped: agent definitions for implementer, reviewer, planner and test author, the orchestrate skill and four workflow scripts. Plate 8 covers how they run a feature.

When a schema declares seats (plate 4), run-wave reads them to build each item's stages and picks the agent and model for each seat. The stop guard is seat-aware too: it only sends a sub-agent back for notes its own seat owns.

Push or pull

Orchestration, the default

Orchestrator advance_item item 1sub-agent item 2sub-agent item 3sub-agent One dispatcher, so no two agents race for an item.
The orchestrator pushes items through phases. No identity setup. Fits a single developer or a sequential pipeline.

Claim at the parent, opt-in

Agent 1 Agent 2 claim_item claim_item Feature X · TTL 900 s Feature Y · TTL 900 s child child child child hidden Children are advanced by the agent holding the parent claim.
Many agents pull work. A claim is a lease that expires on its own if its holder crashes, and a heartbeat at half the TTL keeps it alive. The recommended fleet shape.
TopologyClaimsFits
Pure orchestrationNoneSingle developer, sequential pipelines
Pure claim, flat itemsEvery itemIndependent parallel work with no hierarchy
Pure claim, chainsEvery itemRarely. When a blocker completes, every agent races for the next link.
Hybrid: claim the parent, orchestrate the sub-treeFeature level onlyFeature-based development with many agents

Claims in practice

Four answers to a claim

success, already_claimed (pick another item), queue_empty (nothing matches, stop or wait) and none_eligible (matches exist but none can be claimed now, retry later). One claim per call, and a requestId is required.

Claims clean up after themselves

Any transition into terminal clears the claim, and a reopened item starts unclaimed. A crashed agent's claim simply expires. There is no background sweeper.

Who holds it stays private

Lists and searches show only whether an item is claimed. The holder's identity appears in one place: get_context for that item.

How much the server trusts an actor

No actor nothing is recorded the default Self-reported an actor on each write a plugin hook can require it Verified a signed proof with each call, checked against a key set Fail-closed policy: reject unverified actors cannot claim or advance claimed items
Each step is a config change, and none is required. The recorded actor is the same shape at every level: { id, kind, parent }. What changes is whether anyone checked it.
actor_authentication:
  enabled: true                  # the hook requires an actor
  degraded_mode_policy: reject   # what to do when unverified
  verifier:
    type: jwks
    oidc_discovery: "https://idp.example/.well-known/openid-configuration"
    audience: "task-orchestrator"
    algorithms: ["EdDSA", "RS256"]
Policy when a proof cannot be verifiedIdentity the server uses
accept-cached, the defaultThe verified id when only the key fetch failed and cached keys still validate it. Otherwise the self-reported id, with a warning logged.
accept-self-reportedAlways the id the caller sent. For local development.
rejectNone. Claims return rejected_by_policy.

Two separate settings

enabled makes the plugin's hook refuse a write with no actor. verifier makes the server validate the proof. A call can pass the first and fail the second.

Where keys come from

An OIDC discovery URL, a JWKS URL, a local key file for air-gapped hosts, or per-agent decentralized identifiers from an allowlist. Proofs must expire, within 24 hours by default.

Set once, server-wide

Identity settings live only in the global config. A project cannot loosen them. The DEGRADED_MODE_POLICY environment variable overrides the file, and an unknown value stops the server from starting.

Three independent switches

Orchestration

Always on. Transitions, gates, dependencies and cascades work with no further setup.

Claims

Opt-in. Agents take time-limited ownership of an item with claim_item, and the server then requires the holder's identity on its transitions.

Actor authentication

Opt-in and separate from claims. Verifies the actor on each write against a JWKS key set, so attribution is proven and no longer self-reported.