@automatalabs/workflows 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,14 +11,15 @@ or a registered custom ACP agent — driving the actual subprocess to completion
11
11
 
12
12
  This package is the **canonical SDK** that the stdio MCP server
13
13
  [`@automatalabs/mcp-server`](https://www.npmjs.com/package/@automatalabs/mcp-server) is built on.
14
- If you want to expose a `workflow` tool to an MCP host (Claude Code, Zed, …), use that package; if
15
- you want to embed the runner in your own program, use this one.
14
+ Its CLI can also delegate to a build-time embedded copy of that server with the `mcp` subcommand,
15
+ so an MCP host can expose the `workflow` tool without a separate package install. The standalone
16
+ MCP server package remains independently published, while programs embedding the runner continue
17
+ to use this package's workflow/runner APIs.
16
18
 
17
- It is a **programmatic library**, not an MCP stdio server. It is a thin facade over the engine +
18
- ACP packages and adds ACP-defaulted helpers for ordinary runs (`runDynamicWorkflow`) and
19
- substitution tests (`runIsolation`). The ACP layer does use `@modelcontextprotocol/sdk` internally
20
- when it hosts the optional StructuredOutput tool for eligible agents; consumers still interact
21
- through this SDK's workflow/runner APIs rather than MCP server schemas.
19
+ The SDK itself remains a thin programmatic facade over the engine + ACP packages, with
20
+ ACP-defaulted helpers for ordinary runs (`runDynamicWorkflow`) and substitution tests
21
+ (`runIsolation`). The ACP layer also uses `@modelcontextprotocol/sdk` internally when it hosts the
22
+ optional StructuredOutput tool for eligible agents.
22
23
 
23
24
  ---
24
25
 
@@ -778,6 +779,36 @@ formatHarnessConfigReport(report); // the CLI's human table
778
779
 
779
780
  ---
780
781
 
782
+ ## Launching the MCP server — `agentprism-workflows mcp`
783
+
784
+ Register the bundled stdio server directly from the workflows package; no separate
785
+ `@automatalabs/mcp-server` installation is required:
786
+
787
+ ```json
788
+ {
789
+ "mcpServers": {
790
+ "agentprism-workflow": {
791
+ "command": "npx",
792
+ "args": ["-y", "@automatalabs/workflows", "mcp"]
793
+ }
794
+ }
795
+ }
796
+ ```
797
+
798
+ For the source inner loop, build workflows before launching its compiled CLI:
799
+
800
+ ```bash
801
+ pnpm --filter @automatalabs/workflows build
802
+ node packages/workflows/dist/cli.js mcp
803
+ ```
804
+
805
+ If the embedded bundle is absent in a monorepo checkout, the command falls back to the built
806
+ `packages/mcp-server/dist/cli.js`. A root `pnpm build` therefore also supports development before
807
+ running `node packages/workflows/dist/cli.js mcp`. The independently published
808
+ `@automatalabs/mcp-server` package and `agentprism-workflow` bin remain available.
809
+
810
+ ---
811
+
781
812
  ## Structured output
782
813
 
783
814
  Pass a JSON Schema to `agent({ schema })` (in a script) or `runner.run(prompt, { schema })` (direct)
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // The @automatalabs/workflows bin (`agentprism-workflows`). Two subcommands:
2
+ // The @automatalabs/workflows bin (`agentprism-workflows`). Three subcommands:
3
3
  //
4
4
  // agentprism-workflows validate <workflow-file> [options]
5
5
  // agentprism-workflows config [harness ...] [options]
6
+ // agentprism-workflows mcp
6
7
  //
7
8
  // validate checks a workflow script without spending tokens: static parse (meta literal,
8
9
  // syntax, direct nondeterministic call expressions), then a dry run over an in-process
@@ -13,7 +14,12 @@
13
14
  // config runs that same no-prompt probe standalone — no script needed — and prints each
14
15
  // requested harness's advertised config-option catalog (model ids, effort levels, modes,
15
16
  // …). See ./config.ts for the programmatic API (`probeHarnessConfig`).
17
+ //
18
+ // mcp delegates stdio unchanged to the MCP server embedded at build time. In a source
19
+ // checkout without that bundle, it falls back to the separately built mcp-server entry.
20
+ import { spawn } from "node:child_process";
16
21
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
22
+ import { constants as osConstants } from "node:os";
17
23
  import { resolve } from "node:path";
18
24
  import { openWorkflowDir } from "@automatalabs/workflow-engine";
19
25
  import { validateWorkflowScript, formatValidateReport } from "./validate.js";
@@ -27,6 +33,7 @@ Commands:
27
33
  catalog (model ids, effort levels, modes, …) so
28
34
  model/configOptions values come from the live
29
35
  catalog, not guesswork
36
+ mcp launch the embedded AgentPrism stdio MCP server
30
37
 
31
38
  Run \`agentprism-workflows <command> --help\` for that command's options.`;
32
39
  const USAGE = `Usage: agentprism-workflows validate <workflow-file-or-name> [options]
@@ -90,6 +97,13 @@ Options:
90
97
  -h, --help show this help
91
98
 
92
99
  Exit codes: 0 all probed · 1 at least one probe failed · 3 usage error`;
100
+ const MCP_USAGE = `Usage: agentprism-workflows mcp
101
+
102
+ Launches the embedded AgentPrism MCP server over stdio. stdin and stdout are reserved
103
+ for JSON-RPC framing and are inherited unchanged by the server process.
104
+
105
+ Options:
106
+ -h, --help show this help`;
93
107
  let activeCommand = "";
94
108
  function fail(message) {
95
109
  const hint = activeCommand === "" ? "agentprism-workflows --help" : `agentprism-workflows ${activeCommand} --help`;
@@ -143,6 +157,86 @@ async function mainConfig(rest) {
143
157
  writeFileSync(process.stdout.fd, json ? `${JSON.stringify(report, null, 2)}\n` : `${formatHarnessConfigReport(report)}\n`);
144
158
  process.exitCode = report.exitCode;
145
159
  }
160
+ async function mainMcp(rest) {
161
+ if (rest.length === 1 && (rest[0] === "-h" || rest[0] === "--help")) {
162
+ process.stdout.write(`${MCP_USAGE}\n`);
163
+ process.exit(0);
164
+ }
165
+ if (rest.length > 0)
166
+ fail(`mcp does not accept arguments (received: ${rest.join(" ")})`);
167
+ const bundlePath = resolve(import.meta.dirname, "mcp-server.js");
168
+ const monorepoFallbackPath = resolve(import.meta.dirname, "../../mcp-server/dist/cli.js");
169
+ const serverPath = existsSync(bundlePath)
170
+ ? bundlePath
171
+ : existsSync(monorepoFallbackPath)
172
+ ? monorepoFallbackPath
173
+ : undefined;
174
+ if (serverPath === undefined) {
175
+ fail(`MCP server bundle not found at ${bundlePath}, and the monorepo fallback is not built. ` +
176
+ "Run `pnpm --filter @automatalabs/workflows build` to create the bundle, or run `pnpm build` at the repository root.");
177
+ }
178
+ await new Promise((resolvePromise) => {
179
+ const child = spawn(process.execPath, [serverPath], { stdio: "inherit" });
180
+ const forwardedSignals = ["SIGINT", "SIGTERM"];
181
+ let settled = false;
182
+ let cleaned = false;
183
+ const forwarders = new Map();
184
+ const cleanup = () => {
185
+ if (cleaned)
186
+ return;
187
+ cleaned = true;
188
+ for (const [signal, forward] of forwarders)
189
+ process.off(signal, forward);
190
+ child.off("error", onError);
191
+ child.off("exit", onExit);
192
+ };
193
+ const finish = (action) => {
194
+ if (settled)
195
+ return;
196
+ settled = true;
197
+ cleanup();
198
+ action();
199
+ resolvePromise();
200
+ };
201
+ const onError = (error) => {
202
+ finish(() => {
203
+ process.stderr.write(`mcp server failed to start: ${error.message}\n`);
204
+ process.exitCode = 1;
205
+ });
206
+ };
207
+ const onExit = (code, signal) => {
208
+ finish(() => {
209
+ if (signal === null) {
210
+ process.exitCode = code ?? 1;
211
+ return;
212
+ }
213
+ // Preserve signal termination for callers (shells, npx, and MCP hosts). Set a
214
+ // conventional nonzero fallback first in case re-raising is unsupported here.
215
+ process.exitCode = 128 + (osConstants.signals[signal] ?? 1);
216
+ try {
217
+ process.kill(process.pid, signal);
218
+ }
219
+ catch (error) {
220
+ process.stderr.write(`mcp server exited on ${signal}, but the parent could not re-raise it: ${error instanceof Error ? error.message : String(error)}\n`);
221
+ }
222
+ });
223
+ };
224
+ for (const signal of forwardedSignals) {
225
+ const forward = () => {
226
+ try {
227
+ child.kill(signal);
228
+ }
229
+ catch (error) {
230
+ process.stderr.write(`could not forward ${signal} to the mcp server: ${error instanceof Error ? error.message : String(error)}\n`);
231
+ }
232
+ };
233
+ forwarders.set(signal, forward);
234
+ process.on(signal, forward);
235
+ }
236
+ child.once("error", onError);
237
+ child.once("exit", onExit);
238
+ });
239
+ }
146
240
  async function main(argv) {
147
241
  const [command, ...rest] = argv;
148
242
  if (command === undefined || command === "-h" || command === "--help") {
@@ -153,8 +247,12 @@ async function main(argv) {
153
247
  activeCommand = "config";
154
248
  return mainConfig(rest);
155
249
  }
250
+ if (command === "mcp") {
251
+ activeCommand = "mcp";
252
+ return mainMcp(rest);
253
+ }
156
254
  if (command !== "validate")
157
- fail(`unknown command "${command}" — the commands are: validate, config`);
255
+ fail(`unknown command "${command}" — the commands are: validate, config, mcp`);
158
256
  activeCommand = "validate";
159
257
  let file;
160
258
  let json = false;