MCP Task Orchestrator · Field guidePlate 5 of 5

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 caller is always the credential's principal, on REST and on /mcp alike, and an actor in the body is refused as unknown_parameter. A REST advance is bound by claim ownership and by leases, as on MCP. 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. advanceRecords 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 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 claim 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

claimed: true; a claim_held error (pick another item); a select with no match (claimed: false, no reason: stop or wait); and a select with a reason (claim_held or dependency_unmet: matches exist but none can be claimed now, retry after retryAfterMs). An actor holds one claim at a time, and each element may carry an idempotencyKey.

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. Nothing has to clear it: a background sweep only records the expiry event.

Who holds it stays private

Lists and searches show only whether an item is claimed. The holder's identity appears in one place: the claimDetail of items_read context for that item.

How the server knows the caller

Local no credential is checked every caller is "local" Token a token-file credential the holder is api:<id> Verified a signed JWT, checked against a key set the holder is api:<sub> Fail-closed bad token: 401 key set unavailable: 503 never a guessed identity
The caller is the principal its transport established, never a field in the request. The same credential works on REST and on /mcp. The recorded actor is the principal's audited id (api:<id> or local) with kind external, and a verification of api-jwks, api-bearer or local.
API_ENABLED=true
API_AUTH_MODE=jwks
API_JWKS_URL=https://idp.example/.well-known/jwks.json
API_JWKS_ISSUER=https://idp.example
API_JWKS_AUDIENCE=task-orchestrator
API_JWKS_ALGORITHMS=EdDSA,RS256
SetupThe caller of /mcp is
stdiolocal
HTTP, REST API off, MCP_ALLOW_UNAUTHENTICATED=truelocal
HTTP, REST API off, no opt-inThe server refuses to start.
API_AUTH_MODE=nonelocal
API_AUTH_MODE=bearerapi: and the token's id
API_AUTH_MODE=jwksapi: and the JWT's sub

Local is one principal

Every stdio or local-mode session is the same local caller, so they share claims and note ownership and never contend. Telling agents apart takes a credential or a run seat token for each.

Sessions stick to a caller

An MCP session is bound to the principal that opened it, and another principal on that session gets a 401. A director credential reaches only the director endpoint. A wrong token is the JSON error envelope, not a JSON-RPC error.

Old identity settings do nothing

No request carries an actor or a proof any more, so nothing authenticates or attributes a call by identity. The actor_authentication block is removed: a leftover block now stops the server from starting, and the info page (/api/v1/info) no longer reports it.

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 the claim op of advance. The server then fences the item to its holder: a transition, note write, item update or dependency write by anyone else is not_claim_holder.

Credentials

Opt-in and separate from claims. With a bearer or JWKS mode, REST and /mcp both require a credential, so attribution is proven and no longer shared.