Server anatomy

The workflow-server codebase in src/: entry point, tool registrars, loaders, session machinery, and validators.

For contributors. Build and test instructions: Development guide (source).

The shape of the codebase

Startup is a short, linear path: the entry point parses configuration, a factory builds the MCP server, and two registrar functions attach all sixteen tools as closures. There is no routing layer and no dependency-injection framework — the registrars close over the ServerConfig and call shared machinery directly.

Startup and registration structure The entry point src/index.ts calls createServer in src/server.ts, which invokes three registrars: registerWorkflowTools with twelve tools, registerResourceTools with four tools, and registerSchemaResources for the schemas MCP resource. The two tool registrars sit on shared machinery: loaders, schemas, the session store, validators, delivery, and trace. src/index.ts parses config, connects stdio createServer() src/server.ts, wires the MCP surface registerWorkflowTools 12 tools: navigation, checkpoints registerResourceTools 4 tools: sessions, content registerSchemaResources schemas as MCP resources Shared machinery loaders · schemas · session store · validators · delivery · trace
Startup and registration. main() loads the configuration and connects the stdio transport; createServer() attaches every tool. Individual tools are closures inside the two registrars — workflow-tools.ts registers the twelve navigation, checkpoint, and trace tools, and resource-tools.ts registers start_session, dispatch_child, get_technique, and get_resource.

Module map

Grouped by what a request touches. Tool handlers sit on shared machinery; machinery reads definitions and reads or writes session state on disk.

How modules stack An MCP tool call enters the tools layer, delegates to shared machinery for loading, validation, and delivery, and reaches the session store when state is read or written. MCP tool call Zod-validated parameters tools/ sixteen closures in workflow-tools.ts and resource-tools.ts Shared machinery loaders · schema · validation · delivery · trace · result/errors utils/session/ → planning folders on disk
Every tool is a thin handler over the same machinery. Loaders and validators are stateless; only the session store owns mutable run state. The tables below link each group to its source files.

Primary source files by group:

Startup

ModuleRole
index.ts, config.ts, server.tsParse config, build MCP server, register tools.
tools/Sixteen tool handlers across workflow and resource registrars.

Definitions and validation

ModuleRole
loaders/Read workflow YAML, compose techniques, load resources and schemas.
schema/Zod shapes; JSON schemas in schemas/ are generated from here.
utils/validation.tsRuntime fidelity validators for transitions and manifests.

Persistence and delivery

ModuleRole
utils/session/Planning folders, atomic writes, HMAC seals, session index derivation.
utils/delivery.tsReference-not-repeat delivery and content hashing.
trace.ts, logging.tsTrace buffers, signed tokens, audit logging.
result.ts, errors.tsTyped errors loaders return across the tool boundary.

The layer every call passes through

There is no central router — each tool is a closure registered at startup — but session-bound handlers follow the same skeleton so logging, seal checks, and persistence are uniform. Bootstrap entry points (discover, list_workflows, health_check, start_session) get audit logging only. The worked example is request lifecycle.

Handler wrapper skeleton Session-bound tools pass through withAuditLog, loadSessionForTool, handler logic, and saveSessionForTool on the write path. Bootstrap tools skip session load and save. withAuditLog audit + trace on every call loadSessionForTool index → folder, seal check, parse Tool handler load definitions · validate · deliver content advanceSession → saveSessionForTool mutators only · atomic write + reseal Bootstrap path withAuditLog → handler no session_index Read-only tools may still call save when recording delivery or fetch events in history
The nesting is literal — wrappers in logging.ts and session/resolver.ts — so a failed seal check never reaches handler logic.

Stage by stage:

  1. withAuditLog (logging.ts) — outermost wrapper on every registered handler. It times the call, emits a structured audit event to stderr, and — when the parameters include a session_index — appends a trace event to the session's in-process TraceStore buffer. This runs on success and on thrown errors alike, so a failed seal check or validation still leaves an audit and trace record.
  2. loadSessionForTool (session/resolver.ts) — resolves the six-character session_index to a planning folder (how the index maps to disk), verifies the HMAC seal on session.json against .session-token, and parses the file into typed SessionFile state. Child sessions dispatched via dispatch_child live inside the parent's session.json; the loader navigates to the embedded sub-state the index addresses. If the seal does not match or the file fails schema validation, the call stops here — the handler never runs on state the server cannot vouch for.
  3. advanceSession and saveSessionForTool (session/resolver.ts) — the write path. Handlers that change state build the next snapshot with advanceSession (increments seq, refreshes ts, applies a mutator callback), then pass it to saveSessionForTool, which merges embedded child updates back into the top-level file, canonicalises the JSON, recomputes the HMAC, and atomically writes session.json and .session-token. Read-only tools such as get_activity may still call save when they record delivery or fetch events in history.

Everything cryptographic — session seals, session indexes, trace tokens — funnels to a single 32-byte server key created lazily at ~/.workflow-server/secret (file mode 0600) by session/crypto.ts. One key, three uses, one place to reason about it.

Error discipline

Loaders return Result<T, E> values rather than throwing, and failures that must cross the tool boundary are typed so handlers can map them to user-facing responses without string matching:

ErrorRaised when
WorkflowNotFoundError, ActivityNotFoundError, TechniqueNotFoundError, ResourceNotFoundErrorA definition referenced by id does not exist in the workflow directory.
WorkflowValidationErrorA definition file parses but fails schema validation; the error carries the issue list.
SessionStoreErrorSession state cannot be loaded or written. The code field distinguishes NOT_FOUND, COLLISION, SEAL_MISMATCH, INVALID_INDEX, and WORKSPACE_INVALID.
MarkdownTechniqueParseError, MigrationError, WorkspaceConfigErrorA technique file violates the technique protocol, a legacy planning folder cannot be upgraded, or the workspace path is unusable at startup.

How these surface to a calling agent — and how they differ from non-blocking validation warnings — is covered in Protocol.

Where things live on disk

Three locations, three lifetimes — definitions are read-only content, session state belongs to the workspace, and the server key is per-user infrastructure.

Three on-disk locations Workflow definitions live in the workflows worktree. Session state is written under the workspace engineering tree. The HMAC server key lives in the user home directory. workflows/ worktree orphan workflows branch workflow.yaml, activities/, techniques/, resources/ read at call time, never cached workspace/.engineering/ artifacts/planning/<slug>/ session.json .session-token workflow-trace.json written by session store ~/.workflow-server/ secret — 32-byte HMAC key session seals session indexes trace tokens outside any repository loaders read left · session store reads and writes centre · crypto.ts reads right
Definitions and session artifacts are version-controlled in their respective trees; the server key is created lazily at ~/.workflow-server/secret (mode 0600). Folder contents are detailed in session store and getting started.

Next

Follow one request through this structure in the request lifecycle, or read how state is kept honest in the session store.