Search DevTools

Jump to any tool or page

Stratum MCP

Unexplored

Structured execution for coding agents: contracts, postconditions, gates, guardrails.

smartmemory1 stars1 forksAutomation
View source

Install

Terminal

$npx -y @smartmemory/stratum

mcp_config.json

{
  "mcpServers": {
    "ai-smartmemory-stratum-mcp": {
      "args": [
        "-y",
        "@smartmemory/stratum"
      ],
      "command": "npx"
    }
  }
}

Documentation

Stratum

State machine dispatch server for AI agent workflows.

Your agent proposes the step. Stratum decides whether it actually finished.

Stratum gives AI coding agents (Claude Code, Codex, etc.) a formal execution model. Instead of improvising a plan and retrying blindly, the agent writes a typed spec, the server tracks state, enforces postconditions, and returns structured failure context on retry. Every step produces an auditable trace record.

Where it sits. Stratum is the execution kernel, one layer below the thing most people run day to day. Compose drives the product lifecycle (design, blueprint, plan, review gates) and calls Stratum to execute each step. Reach for Stratum directly when you want the state machine and the postconditions without a lifecycle on top of them.

The founding intent behind this machinery is recorded in docs/VISION.md: a spec language that keeps LLMs on rails invisibly, so the same conversation yields stronger results than freeform execution.

One shipped component:

  • ts/ — the TypeScript engine (@smartmemory/stratum): IR validation (version: 1 specs), flow execution with ensure postconditions, MCP server for Claude Code, query/gate/guard CLI, background flows and background agent runs. Published to npm as @smartmemory/stratum (bins: stratum, stratum-mcp) and listed in the MCP registry as ai.smartmemory/stratum-mcp.

Engine status (2026-07-18, STRAT-PY-RETIRE): the TS engine is the ONLY engine. The Python library (stratum-py) and Python MCP server (stratum-mcp) are retired. Their source is archived on the python-legacy branch, and PyPI packages are frozen at their final releases. The engine executes version: 1 specs exclusively. Legacy v0.x specs are rejected by validate and classified (report-only) by stratum migrate --check. The authoritative v1 shape is the Zod IR schema in ts/src/ir/ plus stratum validate output.

Governed workflows as auditable flows — on any agent, not just one vendor. Unlike a single-vendor in-context orchestrator, Stratum runs as an MCP server and a library under Claude Code, Codex, or any MCP host; enforces typed contracts and ensure postconditions on every flow execution; stops at real human gates; dispatches Claude and Codex agents in one flow (so an independent reviewer can be a different model from the implementer); and persists flow state across sessions. Where you want raw in-context fan-out, reach for an in-host workflow runtime; where you want the run governed, portable, and auditable, that's a Stratum workflow.


Table of Contents


Installation

Install from npm (Node >= 22):

npm install -g @smartmemory/stratum    # provides `stratum` (CLI) and `stratum-mcp` (MCP server)

Or run from a checkout for development:

git clone https://github.com/smartmemory/stratum
cd stratum/ts && npm install    # or pnpm install

Requires node >= 22 (erasable-syntax type stripping; node >= 24 needs no flags — the CLI bootstrap gates --experimental-transform-types automatically).

MCP Server (for Claude Code)

Register the server in your project's .mcp.json. From the npm package:

{
  "mcpServers": {
    "stratum": {
      "command": "npx",
      "args": ["-y", "-p", "@smartmemory/stratum", "stratum-mcp"]
    }
  }
}

From a checkout:

{
  "mcpServers": {
    "stratum": {
      "command": "node",
      "args": ["/absolute/path/to/stratum/ts/src/mcp/bin.mjs"]
    }
  }
}

Restart Claude Code to activate. Optionally append the Stratum execution model block to your CLAUDE.md.

CLI

stratum help                    # validate | migrate | query | gate | guard | watch  (npm install)
node ts/src/cli/bin.mjs help    # same, from a checkout

From a checkout, a thin wrapper script (e.g. ~/bin/stratum-ts) pointing at ts/src/cli/bin.mjs avoids a PATH collision with the installed stratum bin.


Quick Start

When Claude Code has Stratum installed, it uses it automatically for non-trivial tasks:

  1. Claude writes a .stratum.yaml spec internally (never shown to you)
  2. Calls stratum_plan to validate the spec and get the first step
  3. Executes each step using its own tools (reading files, writing code, running tests)
  4. Calls stratum_step_done after each step -- the server checks postconditions
  5. If a postcondition fails, Claude gets back the specific violation and retries
  6. Calls stratum_audit at the end for a full execution trace

You see plain English narration throughout. The spec, state management, and postcondition enforcement happen behind the scenes.


Core Concepts

Specification vs Flow

A specification is the authored, version-controlled .stratum.yaml document. It declares contracts and one or more flows. The flows.entry field selects the flow that starts a run.

A flow is an executable directed acyclic graph of steps. Running the entry flow creates a persisted run with a runId. The v0.x top-level workflow: registration block and stratum_list_workflows were retired with the Python server.

Flows

A flow declares typed input fields, a typed output, optional limits, and steps. References and after lists form data and ordering edges. Gate routing and on_fail add explicit routing edges.

Steps

A step has an id, optional after dependencies, an optional when condition, and exactly one construct: do, set, gate, fanout, or run.

Tasks

A do step is an agent-dispatched task. The task text is declared inline, and ${...} references inject flow input or prior step output values. The v0.x functions: registry and function: steps have no place in a v1 document.

Contracts

Contracts define named output shapes. A do or set step declares its output contract with out. A flow declares both the step-output reference that supplies its result and the contract used to validate that result.

Ensures

Ensures are structured postconditions on do, set, and fanout stage results. V1 supports expression, file existence, file content, and judged predicates.

Retries

A do or fanout step can set the positive integer attempts limit. The default is two attempts. Contract failures, ensure failures, task failures, and exhausted iterations use the same failure path. A deterministic set failure terminates the flow without retrying.

Gates

Gate steps pause execution for an external approve, revise, or kill decision. Approve and kill routes may name a later step or use null. A revise route may name a strict ancestor and requires a flow-level max_rounds limit.


YAML Spec Reference

The Zod IR schema in ts/src/ir/ and the errors produced by stratum validate are authoritative. The root is strict and has exactly three fields: version, contracts, and flows. Unknown fields are rejected.

Minimal Example

version: 1
contracts:
  SentimentResult:
    label: string
    confidence: number
flows:
  entry: classify
  classify:
    input:
      text: string
    output:
      from: "${classify_text.output}"
      contract: SentimentResult
    steps:
      - id: classify_text
        do: "Classify the sentiment of ${input.text}"
        agent: claude
        out: SentimentResult
        ensure:
          - expr: "result.label != ''"
          - expr: "result.confidence > 0.7"
        attempts: 2

Full Example with a Gate

version: 1
contracts:
  WorkOutput:
    result: string
    quality_score: number
flows:
  entry: reviewed_work
  reviewed_work:
    input:
      text: string
    output:
      from: "${work.output}"
      contract: WorkOutput
    max_rounds: 3
    steps:
      - id: work
        do: "Produce the deliverable requested in ${input.text}"
        agent: codex
        out: WorkOutput
        ensure:
          - expr: "result.quality_score >= 0.8"
        attempts: 3
      - id: review
        after: [work]
        gate:
          on_approve: null
          on_revise: work
          on_kill: null
          max_rounds: 2

Full Field Reference

version (required)

The only accepted value is the number 1. Quoted strings such as "1" and all v0.x values are rejected.

contracts (required)

Each contract maps field names to type strings. Objects are strict at runtime, so undeclared output fields are rejected.

Type formMeaning
string, integer, number, booleanScalar value
object, arrayUntyped JSON object or array
string[], Result[]Typed array
`draftfinal`
`(draftfinal)[]`
ResultAnother named contract
string?, Result[]?Optional field

Named contract references use an initial capital letter. Recursive contract references and unknown contract names are rejected.

flows (required)

flows.entry must name a flow in the same mapping. Every flow has these fields:

FieldRequiredShape
inputyesField-to-type mapping using the contract type language
outputyes{from: "${step_id.output}", contract: ContractName}
stepsyesArray of strict step objects
budgetnoPositive limits for one or more of usd, tokens, dispatches, or ms
max_roundsnoPositive integer required when a gate can revise to an ancestor
carrynoNamed loop-carried flow values; entry flow only

The output.from value must be one full reference to a step output with a known contract. A fanout output is an array, so a flow output must select an item such as ${fan.output[0]}.

Steps

Step IDs start with a lowercase letter and may contain lowercase letters, digits, underscores, and hyphens. IDs are unique within a flow. Every step accepts id, optional after, and optional when, then exactly one construct with only the fields listed below.

ConstructPurposeAdditional fields
doDispatch an inline taskagent, out, ensure, attempts, iterate, budget, on_fail
setBuild an output object from expressionsrequired out, optional ensure
gatePause for a decisionno fields outside the nested gate object
fanoutRun stages over an arrayattempts, budget, on_fail
runInvoke a subflowrequired with, optional budget, on_fail

agent is either claude or codex. attempts is a positive integer. An iterate object requires positive integer max and string expression until.

The nested gate object requires nullable on_approve, on_revise, and on_kill fields. It may also set a positive integer max_rounds.

The nested fanout object requires over, one or more steps, positive integer concurrency, isolation of worktree or none, require of all, any, or a positive integer, and merge: sequential. Optional fields are pre_merge and dispatch, whose value is engine or consumer. Each fanout stage requires do and may use agent, out, ensure, attempts, and when.

References

V1 uses ${...} references in task templates, subflow inputs, fanout sources, and flow outputs.

PatternResolves to
${input.field}Flow input field
${step_id.output}Full output of a prior step
${step_id.output.field}Field in a prior step output
${fan.output[0].field}Field in one fanout item output
${item}Current item inside a fanout stage task
${prev}Previous stage output for the same fanout item
${name}A declared carry variable
${name.field}Field inside a carry variable

A full-value reference preserves its JSON type in with and fanout.over. A reference embedded in other text is interpolated into a string. Step-output references create data dependencies. Use after for ordering dependencies that are not implied by references.

Expressions in ensure, when, set, and iterate.until use the v1 expression bindings instead of ${...} interpolation. input is the flow input. In an ensure, result is the output under test. In when and set, result is a mapping of completed step IDs to outputs. Fanout stage expressions also receive item and prev.

Carry

The entry flow may declare a carry: block: a mapping from carry variable name to a shape with one required initial reference and an optional on_revise mapping.

carry:
  wave:
    initial: "${plan.output.tasks}"
    on_revise:
      assess_gate: "${assess.output.tasks}"

initial and every on_revise value must be one full ${...} reference, exactly like a step-output reference — no interpolation, no expression syntax. The initial source must be an unconditional, non-gate step that is not the target of any routing edge (on_fail, on_approve, or on_kill). Materialisation happens once per source epoch: the value is written the moment that source step succeeds, and is written again if a revise resets the source and it succeeds anew.

Each on_revise key names a gate step id. When that gate resolves with revise, the engine resolves the declared reference against the run's pre-reset scope and writes the result as the variable's new value, alongside provenance (which gate, its consumed token, the source epoch, and the round). A gate that declares nothing for a variable leaves it unchanged, so a merge-retry revise re-fans over the same list.

A carry reference (${name} or ${name.field}) is legal wherever a step-output reference is legal on a rendered field — do, with, evaluate.in, fanout.over, and a fanout stage's do — but is rejected in the six expression fields: when, set, a fanout stage's when, iterate.until, and ensure (both flow-level and stage-level). Carry values do not participate in the v1 expression bindings.

A carry path may not begin with the exact segment output — ${wave.output} collides with the step-output grammar and is rejected, while ${wave.outputs} (or any other field name) is fine.

Carry creates no dependency edge. A step that reads ${wave} needs an explicit after to order it after the variable's initial source, and a gate that declares an on_revise for a variable must have a revise target whose reset closure covers every step that reads that variable. The gate itself must be ordered after those steps through after or step-output dependencies — a routing edge into the gate does not count as ordering.

Carry is entry-flow only — a subflow's spec may not declare carry, and a carry variable is not visible inside a subflow. Carry values are snapshotted with checkpoints (stratum_commit / stratum_revert restore them alongside step outputs) and are exposed by stratum_audit.

Migrating v0.x Specs

stratum migrate --check <old.yaml> is a report-only classifier. It does not emit translated YAML.

V0.x constructV1 construct
functions with infer or compute, plus function stepsInline do task. Keep the step's agent when present, and do not translate compute to set
Gate function plus routing fieldsNested gate object
Inline intent stepdo task
Deterministic or judged predicates with one antecedentEnsures on that antecedent
decompose without cross-item dependenciesTask contract with tasks: T[], followed by fanout
depends_onData references plus after for remaining ordering edges
skip_ifwhen with the condition inverted
flow step plus inputsrun plus with
max_iterations plus exit_criterioniterate: {max, until}
parallel_dispatchRe-authored fanout
Pipeline stage whenFanout stage when
Flow max_roundsFlow max_rounds
Reducible next routingDAG edges and gate routing
on_failon_fail targeting a topologically later step
Legacy ensure expressionsRe-authored v1 expressions
output_schema or output_contractNamed v1 contract language
Route-dependent flow outputOne static output: {from, contract} producer

Some legacy constructs have no v1 equivalent. These include verified or applied-gate judge predicates, judge budgets, score and accumulator fields, cross-item decompose dependencies, branch or manual parallel merge modes, certificate and timeout fields, nested pipeline regions, pipeline exit_when, gate policies, gate timeouts, and the top-level workflow block. Flatten or re-author supported regions. Unsupported regions must be redesigned before they can run on v1.


MCP Tools API

All tools are exposed via the MCP protocol. Claude Code calls them as tool invocations.

stratum_validate

Validate a .stratum.yaml spec without creating a flow.

Inputs: spec (str, inline YAML) Returns: {valid: bool, errors: list}

stratum_plan

Validate a spec, create execution state, and return the first step to execute.

Inputs:

  • spec (str) -- inline YAML
  • flow (str) -- flow name
  • inputs (dict) -- flow-level inputs

Returns: Step dispatch object with status: "execute_step" or status: "await_gate", including:

  • flow_id -- unique identifier for this execution
  • step_id, step_number, total_steps
  • function, intent, inputs (resolved)
  • output_contract, output_fields, ensure
  • retries_remaining
  • agent, step_mode

stratum_step_done

Report a completed step result. The server validates the result against output schemas and ensure expressions.

Inputs:

  • flow_id (str)
  • step_id (str)
  • result (dict) -- step output matching the output contract

Returns one of:

  • Next step to execute (status: "execute_step")
  • Ensure failure with retry info (status: "ensure_failed", violations, retries_remaining)
  • Schema validation failure (status: "schema_failed", violations)
  • Flow completion (status: "complete", output, trace, total_duration_ms)
  • Retries exhausted (status: "error", error_type: "retries_exhausted")
  • Routed to recovery step (routed_from, violations)

stratum_audit

Return the full execution trace for a flow.

Inputs: flow_id (str)

Returns:

  • flow_id, flow_name, status (complete, in_progress, killed)
  • steps_completed, total_steps
  • trace -- array of step records (step_id, function_name, attempts, duration_ms, type, round)
  • round, rounds -- round history for gate revise cycles
  • iterations, archived_iterations -- iteration history
  • child_audits -- audit snapshots from sub-flow executions
  • total_duration_ms

stratum_gate_resolve

Resolve a gate step with a human/agent/system decision.

Inputs:

  • flow_id (str)
  • step_id (str) -- must be the current gate step
  • outcome (str) -- "approve", "revise", or "kill"
  • `r

Sourced from the repository README.

More in Automation