@automatalabs/mcp-server
v0.11.0
Published
A **stdio [MCP](https://modelcontextprotocol.io) server** for foreground/background execution, bounded await, and safe inspection of dynamic multi-agent workflows. Its whole tool surface is the single **`workflow`** tool, with run/resume/inspect/await bra
Readme
@automatalabs/mcp-server
A stdio MCP server for foreground/background execution, bounded await, and safe inspection of dynamic multi-agent workflows. Its whole tool surface is the single workflow tool, with run/resume/inspect/await branches: agent backends authenticate from their own CLI credential stores (claude /login, codex login, opencode auth login), so there is nothing auth-shaped for a host to manage here. A run that genuinely hits an expired/missing login pauses with authContext and resumes (resumeFromRunId) after you log the backend's CLI in. Auth and provider management APIs live in the @automatalabs/workflows SDK for embedding hosts.
This package is a thin MCP adapter. All of the real work — parsing the workflow script, running the deterministic engine, fanning agent() calls out to real coding agents over ACP, journaling, resume, token budgets — lives in @automatalabs/workflows. The MCP server is the composition root: it builds the ACP-backed agent runner, injects it into the workflow engine, registers the workflow tool, and serves it over stdin/stdout.
Embedding in your own program? Don't reach for this package — use
@automatalabs/workflowsdirectly (runDynamicWorkflow(script, …)). This server exists to put that same engine behind the MCP protocol. See Programmatic use below.
Published on npm as
@automatalabs/mcp-server(bin:agentprism-workflow) — see Install.
What it is
MCP host (Claude Code / Zed / Cursor / …)
│ tools/call → "workflow" (JSON-RPC over stdio)
▼
┌────────────────────────────────────────────────────┐
│ agentprism-workflow (this package) │
│ • registers the single "workflow" tool │
│ • createAcpRunner() → injected into the engine │
│ • WorkflowManager.runSync/startInBackground │
└────────────────────────────────────────────────────┘
│ session/new, session/prompt … (ACP over stdio)
▼
claude-agent-acp / codex-acp / opencode acp
│ → real Claude / Codex / OpenCode agentsForeground is the default; background:true durably admits work and returns its run ID without
awaiting agent completion. action:"await" collects it in bounded calls (see Run model).
stdout is reserved for JSON-RPC framing — every diagnostic the server emits goes to stderr.
Install
# global (exposes the `agentprism-workflow` bin on your PATH)
npm i -g @automatalabs/mcp-server
# or per-project
npm i @automatalabs/mcp-serverInstalling the package provides the executable agentprism-workflow (declared as the package's bin, pointing at the built dist/cli.js). You usually don't run it by hand — your MCP host launches it (see Register it in an MCP host).
You also need a backend used by your scripts: Claude and Codex adapters are installed transitively; OpenCode is resolved from an opencode-ai installation or an opencode executable on PATH. Authenticate only the backends you route to. See Backends & auth.
The agentprism-workflow bin
The package ships one executable:
| bin | entry |
| --- | --- |
| agentprism-workflow | dist/cli.js |
Running it starts the MCP server on stdio: it builds an ACP-backed AgentRunner, injects it into a WorkflowManager, registers the workflow tool, and connects a StdioServerTransport. It speaks the MCP protocol — it is not an interactive CLI. Launch it from an MCP host, or pipe JSON-RPC to it yourself for testing.
Register it in an MCP host
Add the server to your host's mcpServers config. The host spawns the bin and talks MCP to it over stdio:
{
"mcpServers": {
"agentprism-workflow": {
"command": "agentprism-workflow",
"args": [],
"env": {
"AGENTPRISM_DEFAULT_BACKEND": "claude"
}
}
}
}If the bin isn't on the host's PATH, launch it through npx instead:
{
"mcpServers": {
"agentprism-workflow": {
"command": "npx",
"args": ["-y", "@automatalabs/mcp-server"],
"env": {
"AGENTPRISM_DEFAULT_BACKEND": "claude"
}
}
}
}env here is inherited by the server process and by every agent subprocess it spawns (see Backends & auth), so it is where you put AGENTPRISM_* settings and any credentials the agent CLIs need. Set AGENTPRISM_DEFAULT_BACKEND to claude (the default), codex, opencode, or a registered custom backend name to choose the backend used when an agent() call's model/tier does not pin a provider.
After your host reloads, the workflow tool appears in its tool list.
The workflow tool
Input parameters
The tool uses a run/inspect/await union. Execution resource maxima remain runtime clamps;
inspection/await limits are contract bounds and invalid values are MCP Invalid Params (-32602).
| Param | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| action | "run" \| "inspect" \| "await" | no | run | Omit for every legacy execution request. "inspect" reads immediately; "await" waits only for terminal lifecycle state. |
| script | string (non-empty) | run only | — | Raw JavaScript workflow script (no Markdown fences). The first statement must be export const meta = { name, description, phases? }. Forbidden for inspect/await. |
| background | boolean | run only | false | Acknowledge after admission and execute in this server process. |
| args | any JSON value | no | — | Optional value exposed to the script as the global args. |
| maxAgents | integer > 0 | no | 1000 | Max agents allowed in this run (engine cap MAX_AGENTS_PER_RUN). Values below 1 are clamped up to 1. |
| concurrency | integer > 0 | no | engine default | Max concurrent agents. Clamped to 16 (the runtime max) by the engine — never rejected. |
| agentRetries | integer ≥ 0 | no | engine default | Retry attempts for recoverable agent failures. Clamped to 3 (the runtime max). |
| agentTimeoutMs | integer > 0 | null | no | none | Per-agent timeout in ms. Omit or pass null for no hard timeout (the engine owns timeouts). |
| tokenBudget | integer > 0 | null | no | none | Hard total-token budget for the whole run. Omit or pass null for no limit. |
| resumeFromRunId | string | no | — | Start a new run from this prior run's persisted journal. Resume rule: args changes don't invalidate the journal; prompt changes cache-miss from the first changed call. Re-send the script and desired args; the longest unchanged prefix replays for zero tokens and the first changed/new call plus its suffix runs live. |
| checkpointReplies | object | no | — | With resumeFromRunId, map checkpointContext.callIndex to the durable checkpoint decision. JSON string keys are coerced to numeric indexes. |
| runId | engine run ID | inspect/await only | — | Required for inspect/await; ^[a-z0-9]+-[a-z0-9]+$, at most 128 characters. |
| waitMs | integer 0–25,000 | await only | 20,000 | Zero is a non-blocking status read. Values are rejected, never clamped. |
| lastN | integer 1–50 | inspect/await only | 20 | Latest matching journal calls. Filtering happens before this selection. |
| labelGlob | string | inspect/await only | all calls | Non-empty, at most 128 Unicode code points. Case-sensitive whole-label */? glob with backslash escaping; trailing backslash is literal. Only known agent labels match. |
| logLines | integer 0–50 | inspect/await only | 20 | Latest run-log lines. |
Example call arguments:
{
"script": "export const meta = { name: 'review', description: 'review a diff' };\nconst r = await agent('Review this diff and summarize risks:\\n' + args.diff);\nreturn r;",
"args": { "diff": "diff --git a/x b/x\n+console.log(1)" },
"concurrency": 4,
"tokenBudget": 200000
}Inspection example:
{
"action": "inspect",
"runId": "mabc1234-k9x2pq",
"lastN": 10,
"labelGlob": "plan-review-*",
"logLines": 20
}Background start and bounded collection:
{
"script": "export const meta = { name: 'review', description: 'review a change' };\nconst report = await agent('Review ' + args.target, { label: 'review' });\nreturn report;",
"args": { "target": "src/auth.ts" },
"background": true,
"concurrency": 4,
"tokenBudget": 200000
}{ "runId": "mabc1234-k9x2pq", "status": "running" }{ "action": "await", "runId": "mabc1234-k9x2pq", "waitMs": 20000 }Output
The tool returns both machine-readable structuredContent and a human-readable text block. The structured shape pins the durable core of the engine's run result:
interface WorkflowExecutionToolResult {
runId: string;
status: "pending" | "running" | "paused" | "completed" | "failed" | "aborted";
result?: unknown; // present only on a completed run — the script's resolved value
tokenUsage?: {
input: number;
output: number;
total: number;
cost: number;
cacheRead?: number;
cacheWrite?: number;
};
logs?: string[];
logTail?: WorkflowLogTail; // paused/failed/aborted only
authContext?: AuthErrorContext; // auth_required pauses only
checkpointContext?: CheckpointContext; // checkpoint_required pauses only
fallbacks?: WorkflowRunFallback[]; // live model/modifier degrades; absent when empty
checkpointsTaken?: WorkflowCheckpointTaken[]; // resolved checkpoints; absent when empty
}
interface WorkflowBackgroundAccepted {
runId: string;
status: "running";
}
interface WorkflowAwaitMetadata {
requestedMs: number;
elapsedMs: number;
returnedBecause: "terminal" | "timeout" | "immediate";
}
interface WorkflowRunAwaitResult<T = unknown> extends WorkflowRunStatus {
wait: WorkflowAwaitMetadata;
tokenUsage?: TokenUsage;
outcome?: WorkflowExecutionToolResult<T>; // exactly when lifecycle status is terminal
}
type WorkflowToolResult =
| WorkflowExecutionToolResult
| WorkflowBackgroundAccepted
| WorkflowRunStatus
| WorkflowRunAwaitResult;| Execution output field | Shape | Notes |
| --- | --- | --- |
| fallbacks | { callIndex, label, phase?, requestedSpec, resolvedModel?, backendId?, kind, message }[] | kind is "model" or "modifier"; live calls only; absent when empty. |
| checkpointsTaken | { callIndex, kind, decision, source }[] | source is "live", "headless-default", "journal-replay", or "injected"; paused checkpoints are omitted; absent when empty. |
These fields appear on foreground execution results and terminal await outcome objects. They are
persisted for cold await, but never copied onto the bounded top-level WorkflowRunStatus returned by
inspect/await.
status lets a host distinguish a completed run from a paused one (resumable via resumeFromRunId) without parsing logs. The tool result is flagged isError when status is failed or aborted. A result field is only present when status === "completed".
An inspect response is exactly the shared WorkflowRunStatus:
interface WorkflowRunStatus {
runId: string;
status: "pending" | "running" | "paused" | "completed" | "failed" | "aborted";
workflowName: string;
phases: string[];
currentPhase?: string;
reason?: string;
errorCode?: WorkflowErrorCode;
logTail: WorkflowLogTail;
calls: WorkflowRunCallStatus[];
filter: { lastN: number; logLines: number; labelGlob?: string };
truncation: WorkflowRunStatusTruncation;
}Each call has its deterministic index, known agent/checkpoint attribution, a compact JSON
resultPreview, and redaction/truncation flags. Inspection never returns script, args, prompts,
histories, hashes, session IDs, cwd, checkpoint/auth details, or raw journal results. Sensitive
keys and credential-shaped strings are redacted before results are structurally compacted; every
text scalar and preview is at most 512 UTF-8 bytes. The entire structured status is at most 24,576
bytes, retaining newest diagnostics by dropping oldest calls, logs, then phases. The accompanying
text is formatted from that bounded status and capped at 8,192 bytes.
An unknown, corrupt, or unreadable run returns isError: true, no structuredContent, and:
No workflow run found for runId "<runId>" in this server's project-scoped run store.Inspecting an existing failed/aborted run is still a successful read (isError: false); branch on
the payload status.
Await inherits that exact safe status projection. Its status fields retain the 24,576-byte budget,
redaction, compaction, filtering, and truncation counters, and its text is capped at 8,192 bytes.
Before terminal state, optional tokenUsage is the cumulative live work observed in this execution;
replayed calls add zero. At terminal state, outcome is the foreground-equivalent execution result:
the authored result and full logs remain raw and unbounded, and are not duplicated into text.
Top-level and outcome token usage are identical. Paused outcomes carry the existing non-secret
authContext or checkpointContext used for CLI-login/resume or checkpoint-reply handling. Result
observability (fallbacks and checkpointsTaken) stays inside the terminal outcome.
Run model
- Foreground by default. Omitted/false
backgroundpreserves the synchronous behavior, request cancellation, progress notifications, live checkpoint elicitation, terminalisError, and result shape. - Detached admission.
background:truereturns the exact two-field running acknowledgement after parsing, backend approval, one of four process-local slot reservations, lease acquisition, and fail-fast initial persistence. It never awaits agent or script-body completion. A fifth active-or-starting request returnsBackground workflow limit reached (4 active or starting runs). Await an existing run and retry.There is no queue. Foreground, inspect, and await consume no slot. - Bounded await.
action:"await"waits only for terminal status.waitMs:0returnsimmediatewhile pending/running; a positive deadline returnstimeoutif still live; an already/newly terminal run returnsterminal. Same-process awaits wake on the background promise; cold awaits poll persistence every 250 ms. Cancelling await clears its timer/poller and returnsWorkflow await for runId "<runId>" was cancelled; the workflow was not cancelled.without stopping, pausing, resuming, or leasing the run. - Read-only inspection.
action: "inspect"reads the manager's freshest in-memory snapshot, then its project-scoped persisted store. It never parses/runs a script, invokes an agent, asks for backend approval, sends progress, elicits, or acquires a run lease. Run ID possession is the capability; UIsessionIdlisting filters do not apply. - Progress notifications. When the host includes a
progressTokenwith the call, the server streamsnotifications/progressas agents settle (it reportssettled / totalagents plus the current phase). With noprogressToken, progress is a no-op. Background runs deliberately have no initiating progress channel; inspect/await are their progress surface. - Terminal status, not exceptions. An ordinary pause/fail/abort does not throw — the run resolves to a
WorkflowRunResultwithstatusalready stamped (completed | paused | failed | aborted) plus an optionalreason/resetHint. Only a malformed script (which fails before a run exists) surfaces as an MCP tool error. - Immediate terminal diagnostics. Paused, failed, and aborted execution results contain a
redacted final-20
logTaileven when empty. The text response rendersrecent run log (last X of Y):before resume guidance. The terminal text is capped at 12,288 UTF-8 bytes; completed results omit this extra tail and preserve the existing fulllogsfield. - Explicit resume. A run can pause for a provider usage limit, missing authentication, or an opted-in durable checkpoint, and failed runs retain their completed journal too. Call
workflowagain with the script, the desiredargs, andresumeFromRunIdset to the priorrunId.argsis not directly hashed: an orchestration-only cap change can reveal new calls while the unchanged prefix replays for zero tokens. If new args change a prompt or another hashed identity field, that call and the complete suffix run live. The resumed MCP request creates a new run ID; an empty or unknown prior ID loads no journal and runs fresh. - Checkpoints. Foreground uses MCP elicitation when advertised. Background never retains that
request-scoped callback: omitted/
"default"returnsdefault ?? true,"abort"becomes failed withWORKFLOW_ABORTED, and"pause"becomes paused withcheckpoint_requiredplusoutcome.checkpointContext. Resume by starting a new run withresumeFromRunIdandcheckpointReplies. - Auth pauses. Await reports
auth_required/AUTH_REQUIREDand the non-secretoutcome.authContext. Log the named backend CLI in out-of-band, then start a new run withresumeFromRunId; no MCP credential channel is added. - Retention and process lifetime. Terminal results are reconstructed from project-scoped
persistence and have no MCP TTL; repeated/cold await works until deletion, corruption, or store
loss. Background means detached from one request, not from this stdio child. Disconnect does not
itself abort work while Node stays alive, but host process exit, SIGTERM/SIGKILL, crash, shutdown,
or machine loss can stop it. The next manager recovers orphaned durable
runningstate topaused; completed journal entries remain resumable, while an in-flight unjournaled call can run again. Later persistence after admission is best effort.
The author-workflow prompt
The server also exposes one MCP prompt: author-workflow. Prompts are a user-controlled primitive — prompt-capable hosts surface them for explicit invocation (Claude Code renders it as the /mcp__<server>__author-workflow slash command) — so this adds nothing to the model-facing tool list, which stays exactly workflow.
Invoking it injects the complete, self-contained workflow-authoring guide (the same content as the published agentprism-workflow-authoring skill: the authoring guide, the exhaustive DSL reference tables, and a complete validated example script), always version-matched to the engine this server runs. Pass the optional task argument to have the guide close with "author a workflow that accomplishes: …, then run it with the workflow tool".
Hosts without prompt support (Codex CLI, at the time of writing) simply never see it — install the authoring skill there instead.
Backends & auth
Each agent() call is dispatched to an ACP agent server chosen by the call's model/tier, falling back to AGENTPRISM_DEFAULT_BACKEND (default claude). The three built-in backends are:
- Claude →
@agentclientprotocol/claude-agent-acp(the Claude Agent SDK over ACP). By default the server resolves that package's bin and runs it under the current Node; if it can't be resolved, it falls back tonpx -y @agentclientprotocol/claude-agent-acp. - Codex →
@automatalabs/codex-acp(a published fork that bakes in the structured-output patch). By default the server resolves that package and runs it under the current Node. - OpenCode →
opencode acp.opencode-aiis intentionally not bundled; install it in the host environment or putopencodeonPATH.
Beyond the built-ins, any ACP agent can be registered as a named backend via AGENTPRISM_BACKENDS (see the table below) and routed to with agent(p, { model: "<name>" }) — or "<name>/<inner-model>" to also select a model from the agent's catalog. Scripts can pass arbitrary session/turn _meta to such agents with agent(p, { meta, promptMeta }).
A workflow script can also declare its own backends in its meta block (meta.backends: { <name>: { command, args?, env?, sessionMeta? } }). Because these spawn commands on this machine, they require approval before the run starts: if the connected client supports MCP elicitation, the user is asked to approve each unique spawn config (approvals stick for the session); otherwise the call fails with an informative error naming the AGENTPRISM_ALLOW_SCRIPT_BACKENDS=1 env opt-in. Host-registered names (AGENTPRISM_BACKENDS) always win over script declarations of the same name.
Authentication belongs to the agents, not this server. Each backend authenticates from its own CLI credential store — log in once with claude /login, codex login, or opencode auth login on the machine that runs this server, and workflows just run; there is no separate auth step here, and no auth state for an MCP host to inspect or manage. If a run genuinely hits an expired/missing login, the backend returns ACP AUTH_REQUIRED and the managed run pauses with reason: "auth_required" plus a non-secret authContext naming the backend: complete that backend's CLI login out-of-band, then call workflow again with the original script and resumeFromRunId — the run continues from its journal. Programmatic auth flows (env-var/gateway credential injection, LLM provider routing) live in the @automatalabs/workflows SDK runner APIs for hosts that embed the engine directly.
Configuration (environment variables)
All settings are read from the environment of the agentprism-workflow process (and inherited by the spawned agent servers).
| Variable | Default | Purpose |
| --- | --- | --- |
| AGENTPRISM_DEFAULT_BACKEND | claude | Backend used when an agent() call's model/tier does not pin a provider: claude, codex, opencode, or a registered custom backend name. Unknown values fall back to Claude. |
| AGENTPRISM_BACKENDS | — | Custom ACP backends as a JSON object: {"<name>": {"command": "…", "args": […], "env": {…}, "sessionMeta": {…}}}. Registered names route model/tier specs before built-in heuristics; claude/codex/opencode are reserved. |
| AGENTPRISM_ALLOW_SCRIPT_BACKENDS | — | 1/true approves script-declared meta.backends headlessly. Only needed for clients without elicitation support — eliciting clients are prompted per spawn config instead. Understand the risk: this lets any workflow script spawn arbitrary commands. |
| AGENTPRISM_ACP_INIT_TIMEOUT_MS | 60000 | Deadline for a backend's one-time ACP initialize handshake; a command that is not an ACP server fails fast with a clear error instead of hanging. |
| AGENTPRISM_ACP_POOL_SIZE | 1 | Long-lived ACP server processes to keep per backend. Each pooled process multiplexes many concurrent sessions; raise it to spread concurrent load across processes. Clamped to ≥ 1. |
| AGENTPRISM_CLAUDE_ACP_CMD | — | Override the command used to launch the Claude ACP server. When set, the default resolution/npx fallback is bypassed. |
| AGENTPRISM_CLAUDE_ACP_ARGS | — | Whitespace-separated argv passed to AGENTPRISM_CLAUDE_ACP_CMD. |
| AGENTPRISM_CODEX_ACP_CMD | — | Override the command used to launch the Codex ACP server. When set, the default bin resolution is bypassed. |
| AGENTPRISM_CODEX_ACP_ARGS | — | Whitespace-separated argv passed to AGENTPRISM_CODEX_ACP_CMD. |
| AGENTPRISM_CODEX_ACP_BIN | resolved @automatalabs/codex-acp main | Override the resolved Codex ACP bin path (used only when AGENTPRISM_CODEX_ACP_CMD is not set). |
| AGENTPRISM_OPENCODE_ACP_CMD | resolved opencode-ai bin or opencode | Override the command used to launch OpenCode ACP. |
| AGENTPRISM_OPENCODE_ACP_ARGS | — | Whitespace-separated argv passed to AGENTPRISM_OPENCODE_ACP_CMD. The automatic launcher uses opencode acp; a command override receives only the args supplied here. |
| AGENTPRISM_PERSISTENCE_ROOT | ~/.agentprism/workflows | Absolute root for persisted run journals and logs used by resume. |
Programmatic use
For embedding the orchestrator in your own program, use @automatalabs/workflows — it is the canonical programmatic SDK:
import { runDynamicWorkflow } from "@automatalabs/workflows";
const run = await runDynamicWorkflow(
`export const meta = { name: "demo", description: "one agent" };
const r = await agent("Say hello in one word.");
return r;`,
{ exec: { concurrency: 4, tokenBudget: 100_000 } },
);
console.log(run.status, run.result);This MCP-server package does export its own building blocks, for hosts that want to mount the same workflow tool on a transport they control rather than the default stdio one:
import { createWorkflowServer } from "@automatalabs/mcp-server";
import { createAcpRunner } from "@automatalabs/workflows";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = createWorkflowServer(createAcpRunner());
await server.connect(new StdioServerTransport());Other exports include workflowToolInputShape / parseWorkflowToolInput /
clampWorkflowInput (primitive schema, action discriminator, execution clamp),
WorkflowExecuteToolInput, WorkflowInspectToolInput, WorkflowAwaitToolInput,
WorkflowExecutionToolResult, WorkflowBackgroundAccepted, WorkflowAwaitMetadata,
WorkflowRunAwaitResult, WorkflowToolResult, MAX_BACKGROUND_RUNS,
workflowToolOutputShape / toWorkflowToolResult,
createProgressReporter, and a main() that runs the default stdio server. For anything beyond
hosting this tool, prefer @automatalabs/workflows.
License
Apache-2.0
