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.
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.
Primary source files by group:
Startup
| Module | Role |
|---|---|
index.ts, config.ts, server.ts | Parse config, build MCP server, register tools. |
tools/ | Sixteen tool handlers across workflow and resource registrars. |
Definitions and validation
| Module | Role |
|---|---|
loaders/ | Read workflow YAML, compose techniques, load resources and schemas. |
schema/ | Zod shapes; JSON schemas in schemas/ are generated from here. |
utils/validation.ts | Runtime fidelity validators for transitions and manifests. |
Persistence and delivery
| Module | Role |
|---|---|
utils/session/ | Planning folders, atomic writes, HMAC seals, session index derivation. |
utils/delivery.ts | Reference-not-repeat delivery and content hashing. |
trace.ts, logging.ts | Trace buffers, signed tokens, audit logging. |
result.ts, errors.ts | Typed 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.
logging.ts and session/resolver.ts — so a failed seal check never reaches handler logic.Stage by stage:
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 asession_index— appends a trace event to the session's in-processTraceStorebuffer. This runs on success and on thrown errors alike, so a failed seal check or validation still leaves an audit and trace record.loadSessionForTool(session/resolver.ts) — resolves the six-charactersession_indexto a planning folder (how the index maps to disk), verifies the HMAC seal onsession.jsonagainst.session-token, and parses the file into typedSessionFilestate. Child sessions dispatched viadispatch_childlive inside the parent'ssession.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.advanceSessionandsaveSessionForTool(session/resolver.ts) — the write path. Handlers that change state build the next snapshot withadvanceSession(incrementsseq, refreshests, applies a mutator callback), then pass it tosaveSessionForTool, which merges embedded child updates back into the top-level file, canonicalises the JSON, recomputes the HMAC, and atomically writessession.jsonand.session-token. Read-only tools such asget_activitymay 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:
| Error | Raised when |
|---|---|
WorkflowNotFoundError, ActivityNotFoundError, TechniqueNotFoundError, ResourceNotFoundError | A definition referenced by id does not exist in the workflow directory. |
WorkflowValidationError | A definition file parses but fails schema validation; the error carries the issue list. |
SessionStoreError | Session state cannot be loaded or written. The code field distinguishes NOT_FOUND, COLLISION, SEAL_MISMATCH, INVALID_INDEX, and WORKSPACE_INVALID. |
MarkdownTechniqueParseError, MigrationError, WorkspaceConfigError | A 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.
~/.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.