stageflow 0.3.0 → 0.9.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.
Files changed (164) hide show
  1. package/README.md +44 -14
  2. package/dist/agent/activity.d.ts +2 -0
  3. package/dist/agent/cursorProvider.d.ts +1 -10
  4. package/dist/agent/cursorProvider.js +18 -10
  5. package/dist/agent/fakeAgent.js +11 -7
  6. package/dist/agent/piAdapter.d.ts +2 -1
  7. package/dist/agent/piAdapter.js +28 -25
  8. package/dist/agent/port.d.ts +6 -1
  9. package/dist/agent/port.js +3 -0
  10. package/dist/cli/artifactCommand.d.ts +10 -0
  11. package/dist/cli/artifactCommand.js +120 -0
  12. package/dist/cli/envelopeCommand.d.ts +32 -0
  13. package/dist/cli/envelopeCommand.js +285 -0
  14. package/dist/cli/exportRunCommand.d.ts +10 -0
  15. package/dist/cli/exportRunCommand.js +150 -0
  16. package/dist/cli/handoffFormat.d.ts +22 -0
  17. package/dist/cli/handoffFormat.js +61 -0
  18. package/dist/cli/runCommand.d.ts +5 -1
  19. package/dist/cli/runCommand.js +36 -5
  20. package/dist/cli/runOutput.d.ts +4 -1
  21. package/dist/cli/runOutput.js +36 -8
  22. package/dist/cli/runsCommand.d.ts +22 -0
  23. package/dist/cli/runsCommand.js +591 -0
  24. package/dist/cli/skillsCommand.d.ts +16 -0
  25. package/dist/cli/skillsCommand.js +459 -0
  26. package/dist/cli/validateOutput.d.ts +2 -2
  27. package/dist/cli/validateOutput.js +2 -2
  28. package/dist/cli.js +110 -5
  29. package/dist/config/loadPipeline.js +5 -1
  30. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  31. package/dist/config/normalizePipelineStageEntry.js +25 -1
  32. package/dist/config/pipelineStageKeys.js +1 -1
  33. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  34. package/dist/config/resolveForkEmitContext.js +23 -1
  35. package/dist/config/resolvePipelineDag.js +36 -4
  36. package/dist/envelope/check.d.ts +2 -0
  37. package/dist/envelope/check.js +51 -0
  38. package/dist/envelope/cloneForks.d.ts +2 -0
  39. package/dist/envelope/cloneForks.js +62 -0
  40. package/dist/envelope/forkChoice.js +1 -1
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.js +3 -1
  43. package/dist/mcp/catalogTools.d.ts +3 -0
  44. package/dist/mcp/catalogTools.js +187 -0
  45. package/dist/mcp/controlTools.d.ts +3 -0
  46. package/dist/mcp/controlTools.js +147 -0
  47. package/dist/mcp/deps.d.ts +11 -0
  48. package/dist/mcp/deps.js +1 -0
  49. package/dist/mcp/projectRun.d.ts +1 -40
  50. package/dist/mcp/projectRun.js +1 -64
  51. package/dist/mcp/resources.d.ts +5 -0
  52. package/dist/mcp/resources.js +45 -0
  53. package/dist/mcp/server.d.ts +16 -3
  54. package/dist/mcp/server.js +159 -3
  55. package/dist/mcp/toolResults.d.ts +7 -0
  56. package/dist/mcp/toolResults.js +6 -0
  57. package/dist/mcp/tools.d.ts +2 -7
  58. package/dist/mcp/tools.js +52 -103
  59. package/dist/mcp/waitRun.d.ts +46 -0
  60. package/dist/mcp/waitRun.js +146 -0
  61. package/dist/mcp/waitingGates.d.ts +12 -0
  62. package/dist/mcp/waitingGates.js +83 -0
  63. package/dist/package-meta.d.ts +1 -0
  64. package/dist/package-meta.js +1 -0
  65. package/dist/projection/projectRun.d.ts +44 -0
  66. package/dist/projection/projectRun.js +71 -0
  67. package/dist/prompt/priorEnvelope.d.ts +1 -1
  68. package/dist/prompt/priorEnvelope.js +4 -1
  69. package/dist/runstore/paths.js +2 -0
  70. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  71. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  72. package/dist/runstore/port.d.ts +13 -2
  73. package/dist/runstore/port.js +4 -1
  74. package/dist/runstore/runProjection.d.ts +1 -1
  75. package/dist/runstore/runProjection.js +14 -3
  76. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  77. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  78. package/dist/runstore/stageInstanceId.d.ts +4 -0
  79. package/dist/runstore/stageInstanceId.js +23 -0
  80. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  81. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  82. package/dist/runstore/trackProjection.js +28 -12
  83. package/dist/runstore/workspaceLayout.d.ts +1 -0
  84. package/dist/runstore/workspaceLayout.js +1 -1
  85. package/dist/runtime/cloneSchedule.d.ts +18 -0
  86. package/dist/runtime/cloneSchedule.js +233 -0
  87. package/dist/runtime/envelopeRouting.d.ts +2 -1
  88. package/dist/runtime/envelopeRouting.js +80 -13
  89. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  90. package/dist/runtime/pipelineScheduler.js +166 -30
  91. package/dist/runtime/resumeReconstruct.js +8 -3
  92. package/dist/runtime/runChangeBus.d.ts +18 -0
  93. package/dist/runtime/runChangeBus.js +83 -0
  94. package/dist/runtime/runManager.d.ts +7 -0
  95. package/dist/runtime/runManager.js +31 -2
  96. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  97. package/dist/runtime/runRetryCoordinator.js +4 -1
  98. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  99. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  100. package/dist/runtime/stageRunner.d.ts +1 -0
  101. package/dist/runtime/stageRunner.js +17 -15
  102. package/dist/runtime/stageWorker.js +11 -4
  103. package/dist/server/bootstrap.d.ts +32 -0
  104. package/dist/server/bootstrap.js +56 -0
  105. package/dist/server/createHttpHost.d.ts +34 -0
  106. package/dist/server/createHttpHost.js +70 -0
  107. package/dist/server/http.d.ts +6 -13
  108. package/dist/server/http.js +295 -374
  109. package/dist/server/mcpHost.d.ts +22 -0
  110. package/dist/server/mcpHost.js +21 -0
  111. package/dist/server/operatorResults.d.ts +13 -0
  112. package/dist/server/operatorResults.js +22 -0
  113. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  114. package/dist/tools/emitStageEnvelope.js +22 -6
  115. package/dist/types/envelope.d.ts +2 -0
  116. package/dist/types/forkChoice.d.ts +22 -0
  117. package/dist/types/pipeline.d.ts +8 -0
  118. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  119. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  120. package/dist/ui/index.html +2 -2
  121. package/package.json +2 -1
  122. package/skills/install-suite.sh +107 -0
  123. package/skills/stageflow/SKILL.md +26 -0
  124. package/skills/stageflow/references/control-surface.md +28 -0
  125. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  126. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  127. package/skills/stageflow-author/SKILL.md +58 -0
  128. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  130. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  131. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  132. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  133. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  134. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  135. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  136. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  137. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  138. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  139. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  140. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  141. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  142. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  143. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  144. package/skills/stageflow-delegate/SKILL.md +18 -0
  145. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  146. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  147. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  148. package/skills/stageflow-run/SKILL.md +182 -0
  149. package/skills/stageflow-run/references/mcp-call.md +17 -0
  150. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  151. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  152. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  153. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  154. package/skills/stageflow-session-capture/SKILL.md +94 -0
  155. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  156. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  157. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  158. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  159. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  160. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  161. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  162. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  163. package/skills/stageflow-setup/SKILL.md +110 -0
  164. package/dist/ui/assets/index-CFSzDZje.js +0 -118
@@ -0,0 +1,147 @@
1
+ import { z } from "zod";
2
+ import { mapRetryStageFailure, mapStartFailure, } from "../server/operatorResults.js";
3
+ import { parseAskOperatorAnswer } from "../tools/askOperator.js";
4
+ import { textResult } from "./toolResults.js";
5
+ import { projectWaitingGates } from "./waitingGates.js";
6
+ export function registerControlTools(server, deps) {
7
+ const { manager, store } = deps;
8
+ server.registerTool("list_waiting", {
9
+ description: "List stages waiting for operator input across runs (optional runId filter). Includes pending_prompt and waiting_* fields per gate.",
10
+ inputSchema: z.object({
11
+ runId: z.string().optional(),
12
+ }),
13
+ }, async ({ runId }) => {
14
+ const waiting = await projectWaitingGates(store, { runId });
15
+ return textResult({ waiting });
16
+ });
17
+ server.registerTool("answer_gate", {
18
+ description: "Deliver an AskOperatorAnswer for a stage in waiting_for_input (same semantics as POST /api/runs/:id/stages/:stageId/answer). Pass answer as { promptId, kind, ... } matching the pending prompt.",
19
+ inputSchema: z.object({
20
+ runId: z.string(),
21
+ stageId: z.string(),
22
+ answer: z.record(z.string(), z.unknown()),
23
+ }),
24
+ }, async ({ runId, stageId, answer }) => {
25
+ let parsed;
26
+ try {
27
+ parsed = parseAskOperatorAnswer(answer);
28
+ }
29
+ catch (err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ return textResult({ error: message, status: 400 }, true);
32
+ }
33
+ const result = await manager.deliverAnswer(runId, stageId, parsed);
34
+ if (!result.ok) {
35
+ return textResult({ error: result.reason, status: result.status }, true);
36
+ }
37
+ return textResult({ ok: true });
38
+ });
39
+ server.registerTool("list_stage_events", {
40
+ description: "List persisted stage log events for a run stage (optional attempt filter). Prefer over bloating get_run.",
41
+ inputSchema: z.object({
42
+ runId: z.string(),
43
+ stageId: z.string(),
44
+ attempt: z.number().int().positive().optional(),
45
+ }),
46
+ }, async ({ runId, stageId, attempt }) => {
47
+ try {
48
+ await store.readRunMeta(runId);
49
+ }
50
+ catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ return textResult({ error: message, status: 404 }, true);
53
+ }
54
+ try {
55
+ const detail = await store.readRun(runId);
56
+ const stage = detail.stages.find((s) => s.stage_id === stageId);
57
+ if (!stage) {
58
+ return textResult({ error: `Stage not found: ${stageId}`, status: 404 }, true);
59
+ }
60
+ const events = await store.listStageEvents(runId, stageId, attempt);
61
+ return textResult({ runId, stageId, attempt, events });
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ const notFound = /not found|no such/i.test(message);
66
+ return textResult({ error: message, status: notFound ? 404 : 500 }, true);
67
+ }
68
+ });
69
+ server.registerTool("get_envelope", {
70
+ description: "Read the full StageEnvelope for a run stage (latest attempt). Returns 404 when absent.",
71
+ inputSchema: z.object({
72
+ runId: z.string(),
73
+ stageId: z.string(),
74
+ }),
75
+ }, async ({ runId, stageId }) => {
76
+ try {
77
+ await store.readRunMeta(runId);
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ return textResult({ error: message, status: 404 }, true);
82
+ }
83
+ try {
84
+ const detail = await store.readRun(runId);
85
+ if (!detail.stages.some((s) => s.stage_id === stageId)) {
86
+ return textResult({ error: `Stage not found: ${stageId}`, status: 404 }, true);
87
+ }
88
+ const envelope = await store.readEnvelope(runId, stageId);
89
+ return textResult({ runId, stageId, envelope });
90
+ }
91
+ catch (err) {
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ const notFound = /not found|no such|envelope/i.test(message);
94
+ return textResult({ error: message, status: notFound ? 404 : 500 }, true);
95
+ }
96
+ });
97
+ server.registerTool("retry_stage", {
98
+ description: "Retry a failed stage (same as POST .../retry). Waiting stages cannot be retried — answer them with answer_gate instead.",
99
+ inputSchema: z.object({
100
+ runId: z.string(),
101
+ stageId: z.string(),
102
+ }),
103
+ }, async ({ runId, stageId }) => {
104
+ const result = await manager.retryStage(runId, stageId);
105
+ if (!result.ok) {
106
+ return textResult({ ...mapRetryStageFailure(result), status: result.status }, true);
107
+ }
108
+ return textResult({
109
+ runId: result.runId,
110
+ stageId: result.stageId,
111
+ attemptIndex: result.attemptIndex,
112
+ });
113
+ });
114
+ server.registerTool("abandon_stage", {
115
+ description: "Abandon a running stage (marks it failed/interrupted). Does not dismiss HITL — waiting stages return 409; answer them with answer_gate. There is no run-level cancel tool.",
116
+ inputSchema: z.object({
117
+ runId: z.string(),
118
+ stageId: z.string(),
119
+ }),
120
+ }, async ({ runId, stageId }) => {
121
+ const result = await manager.abandonStage(runId, stageId);
122
+ if (!result.ok) {
123
+ const fail = result;
124
+ return textResult({ error: fail.reason, status: fail.status }, true);
125
+ }
126
+ return textResult({
127
+ ok: true,
128
+ runId: result.runId,
129
+ stageId: result.stageId,
130
+ });
131
+ });
132
+ server.registerTool("rerun", {
133
+ description: "Start a new run from a completed or failed run's pipeline/task locators. Returns { runId } for the new run.",
134
+ inputSchema: z.object({
135
+ runId: z.string(),
136
+ }),
137
+ }, async ({ runId }) => {
138
+ const result = await manager.rerun(runId);
139
+ if (!result.ok) {
140
+ return textResult({
141
+ ...mapStartFailure(result),
142
+ ...(result.status !== undefined ? { status: result.status } : {}),
143
+ }, true);
144
+ }
145
+ return textResult({ runId: result.runId });
146
+ });
147
+ }
@@ -0,0 +1,11 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { RunChangeBus } from "../runtime/runChangeBus.js";
3
+ import type { RunManager } from "../runtime/runManager.js";
4
+ export type McpToolDeps = {
5
+ manager: RunManager;
6
+ store: RunStore;
7
+ cwd: string;
8
+ };
9
+ export type McpHttpDeps = McpToolDeps & {
10
+ runChangeBus: RunChangeBus;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,40 +1 @@
1
- import type { RunDetail, StageSnapshot } from "../runstore/port.js";
2
- import type { PipelineTrackProjection, RunSummary } from "../runstore/port.js";
3
- import type { AskOperatorPrompt } from "../tools/askOperator.js";
4
- import type { StageEnvelope } from "../types/envelope.js";
5
- export type McpStageProjection = {
6
- stage_id: string;
7
- status: StageSnapshot["status"];
8
- envelope: {
9
- status: StageEnvelope["status"];
10
- summary: string;
11
- artifacts: string[];
12
- payload?: Record<string, unknown>;
13
- } | null;
14
- artifacts: string[];
15
- last_at?: string;
16
- pending_prompt?: AskOperatorPrompt;
17
- };
18
- export type McpRunProjection = {
19
- run_id: string;
20
- pipeline_id: string;
21
- task_id?: string;
22
- pipeline_path?: string;
23
- task_path?: string;
24
- project_root?: string;
25
- status: RunDetail["status"];
26
- created_at: string;
27
- updated_at?: string;
28
- stages: McpStageProjection[];
29
- pipeline_track: PipelineTrackProjection;
30
- waiting_stage_id?: RunSummary["waiting_stage_id"];
31
- waiting_stage_ids?: RunSummary["waiting_stage_ids"];
32
- waiting_summary?: RunSummary["waiting_summary"];
33
- waiting_kind?: RunSummary["waiting_kind"];
34
- waiting_prompt_id?: RunSummary["waiting_prompt_id"];
35
- waiting_artifacts?: RunSummary["waiting_artifacts"];
36
- waiting_questions?: RunSummary["waiting_questions"];
37
- failed_stage_id?: RunSummary["failed_stage_id"];
38
- failed_reason?: RunSummary["failed_reason"];
39
- };
40
- export declare function projectRunForMcp(detail: RunDetail): McpRunProjection;
1
+ export { projectRun as projectRunForMcp, type RunProjection as McpRunProjection, type StageProjection as McpStageProjection, } from "../projection/projectRun.js";
@@ -1,64 +1 @@
1
- export function projectRunForMcp(detail) {
2
- return {
3
- run_id: detail.run_id,
4
- pipeline_id: detail.pipeline_id,
5
- task_id: detail.task_id,
6
- ...(detail.pipeline_path !== undefined
7
- ? { pipeline_path: detail.pipeline_path }
8
- : {}),
9
- ...(detail.task_path !== undefined ? { task_path: detail.task_path } : {}),
10
- ...(detail.project_root !== undefined
11
- ? { project_root: detail.project_root }
12
- : {}),
13
- status: detail.status,
14
- created_at: detail.created_at,
15
- updated_at: detail.updated_at,
16
- pipeline_track: detail.pipeline_track,
17
- ...(detail.waiting_stage_id !== undefined
18
- ? { waiting_stage_id: detail.waiting_stage_id }
19
- : {}),
20
- ...(detail.waiting_stage_ids !== undefined
21
- ? { waiting_stage_ids: detail.waiting_stage_ids }
22
- : {}),
23
- ...(detail.waiting_summary !== undefined
24
- ? { waiting_summary: detail.waiting_summary }
25
- : {}),
26
- ...(detail.waiting_kind !== undefined
27
- ? { waiting_kind: detail.waiting_kind }
28
- : {}),
29
- ...(detail.waiting_prompt_id !== undefined
30
- ? { waiting_prompt_id: detail.waiting_prompt_id }
31
- : {}),
32
- ...(detail.waiting_artifacts !== undefined
33
- ? { waiting_artifacts: detail.waiting_artifacts }
34
- : {}),
35
- ...(detail.waiting_questions !== undefined
36
- ? { waiting_questions: detail.waiting_questions }
37
- : {}),
38
- ...(detail.failed_stage_id !== undefined
39
- ? { failed_stage_id: detail.failed_stage_id }
40
- : {}),
41
- ...(detail.failed_reason !== undefined
42
- ? { failed_reason: detail.failed_reason }
43
- : {}),
44
- stages: detail.stages.map((stage) => ({
45
- stage_id: stage.stage_id,
46
- status: stage.status,
47
- envelope: stage.envelope
48
- ? {
49
- status: stage.envelope.status,
50
- summary: stage.envelope.summary,
51
- artifacts: stage.envelope.artifacts,
52
- ...(stage.envelope.payload !== undefined
53
- ? { payload: stage.envelope.payload }
54
- : {}),
55
- }
56
- : null,
57
- artifacts: stage.artifacts,
58
- last_at: stage.last_at,
59
- ...(stage.pending_prompt
60
- ? { pending_prompt: stage.pending_prompt }
61
- : {}),
62
- })),
63
- };
64
- }
1
+ export { projectRun as projectRunForMcp, } from "../projection/projectRun.js";
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export declare const RUN_RESOURCE_URI_TEMPLATE = "stageflow://runs/{runId}";
4
+ export declare function runResourceUri(runId: string): string;
5
+ export declare function registerRunResources(server: McpServer, deps: McpToolDeps): void;
@@ -0,0 +1,45 @@
1
+ import { ResourceNotFoundError, ResourceTemplate, } from "@modelcontextprotocol/server";
2
+ import { projectRunForMcp } from "./projectRun.js";
3
+ export const RUN_RESOURCE_URI_TEMPLATE = "stageflow://runs/{runId}";
4
+ export function runResourceUri(runId) {
5
+ return `stageflow://runs/${runId}`;
6
+ }
7
+ export function registerRunResources(server, deps) {
8
+ const { store } = deps;
9
+ server.registerResource("run", new ResourceTemplate(RUN_RESOURCE_URI_TEMPLATE, {
10
+ list: async () => {
11
+ const runs = await store.listRuns();
12
+ return {
13
+ resources: runs.map((run) => ({
14
+ uri: runResourceUri(run.run_id),
15
+ name: run.run_id,
16
+ mimeType: "application/json",
17
+ })),
18
+ };
19
+ },
20
+ }), {
21
+ description: "Lean Stageflow run projection (same shape as get_run; no stage events)",
22
+ mimeType: "application/json",
23
+ }, async (uri, variables) => {
24
+ const runId = variables.runId;
25
+ if (typeof runId !== "string" || runId.length === 0) {
26
+ throw new ResourceNotFoundError(uri.href);
27
+ }
28
+ try {
29
+ const detail = await store.readRun(runId);
30
+ const projected = projectRunForMcp(detail);
31
+ return {
32
+ contents: [
33
+ {
34
+ uri: uri.href,
35
+ mimeType: "application/json",
36
+ text: JSON.stringify(projected, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ catch {
42
+ throw new ResourceNotFoundError(uri.href);
43
+ }
44
+ });
45
+ }
@@ -1,7 +1,20 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
- import { type McpToolDeps } from "./tools.js";
4
- export type { McpToolDeps };
3
+ import type { McpHttpDeps, McpToolDeps } from "./deps.js";
4
+ export type { McpHttpDeps, McpToolDeps };
5
+ export type McpHttpOptions = {
6
+ /** Test/debug escape hatch — per-request create/teardown (no sessions). */
7
+ mcpStateless?: boolean;
8
+ };
5
9
  export declare function createStageflowMcpServer(deps: McpToolDeps): McpServer;
6
- /** Stateless Streamable HTTP: new server + transport per request. */
10
+ export type McpHttpHandler = {
11
+ handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
12
+ close: () => Promise<void>;
13
+ };
14
+ /** Stateful Streamable HTTP session host (product default) or stateless escape hatch. */
15
+ export declare function createMcpHttpHandler(deps: McpHttpDeps, options?: McpHttpOptions): McpHttpHandler;
7
16
  export declare function handleMcpHttpRequest(req: IncomingMessage, res: ServerResponse, deps: McpToolDeps): Promise<void>;
17
+ export declare function resolveMcpStateless(options?: {
18
+ mcpStateless?: boolean;
19
+ env?: NodeJS.ProcessEnv;
20
+ }): boolean;
@@ -1,16 +1,67 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
2
- import { McpServer } from "@modelcontextprotocol/server";
3
+ import { isInitializeRequest, McpServer, } from "@modelcontextprotocol/server";
4
+ import { registerRunResources, runResourceUri } from "./resources.js";
3
5
  import { registerMcpTools } from "./tools.js";
6
+ function jsonRpcError(res, status, code, message) {
7
+ const payload = JSON.stringify({
8
+ jsonrpc: "2.0",
9
+ error: { code, message },
10
+ id: null,
11
+ });
12
+ res.writeHead(status, {
13
+ "Content-Type": "application/json; charset=utf-8",
14
+ "Content-Length": Buffer.byteLength(payload),
15
+ });
16
+ res.end(payload);
17
+ }
18
+ async function readJsonBody(req) {
19
+ const chunks = [];
20
+ for await (const chunk of req) {
21
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
22
+ }
23
+ if (chunks.length === 0)
24
+ return undefined;
25
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
26
+ }
27
+ function sessionIdFromRequest(req) {
28
+ const raw = req.headers["mcp-session-id"];
29
+ if (typeof raw === "string" && raw.length > 0)
30
+ return raw;
31
+ if (Array.isArray(raw) && typeof raw[0] === "string" && raw[0].length > 0) {
32
+ return raw[0];
33
+ }
34
+ return undefined;
35
+ }
36
+ function toolDepsFrom(deps) {
37
+ return { manager: deps.manager, store: deps.store, cwd: deps.cwd };
38
+ }
39
+ function attachBusNotifications(server, bus) {
40
+ return bus.on((event) => {
41
+ const uri = runResourceUri(event.runId);
42
+ void server.server.sendResourceUpdated({ uri }).catch(() => undefined);
43
+ if (event.kind === "created") {
44
+ void server.server.sendResourceListChanged().catch(() => undefined);
45
+ }
46
+ });
47
+ }
4
48
  export function createStageflowMcpServer(deps) {
5
49
  const server = new McpServer({
6
50
  name: "stageflow",
7
51
  version: "0.2.0",
52
+ }, {
53
+ capabilities: {
54
+ resources: {
55
+ subscribe: true,
56
+ listChanged: true,
57
+ },
58
+ },
8
59
  });
9
60
  registerMcpTools(server, deps);
61
+ registerRunResources(server, deps);
10
62
  return server;
11
63
  }
12
- /** Stateless Streamable HTTP: new server + transport per request. */
13
- export async function handleMcpHttpRequest(req, res, deps) {
64
+ async function handleStatelessMcpRequest(req, res, deps) {
14
65
  const server = createStageflowMcpServer(deps);
15
66
  const transport = new NodeStreamableHTTPServerTransport({
16
67
  sessionIdGenerator: undefined,
@@ -24,3 +75,108 @@ export async function handleMcpHttpRequest(req, res, deps) {
24
75
  await server.close().catch(() => undefined);
25
76
  }
26
77
  }
78
+ /** Stateful Streamable HTTP session host (product default) or stateless escape hatch. */
79
+ export function createMcpHttpHandler(deps, options = {}) {
80
+ const stateless = options.mcpStateless === true;
81
+ const sessions = new Map();
82
+ const tools = toolDepsFrom(deps);
83
+ async function disposeSession(sessionId, opts) {
84
+ const entry = sessions.get(sessionId);
85
+ if (!entry)
86
+ return;
87
+ sessions.delete(sessionId);
88
+ const unsub = entry.unsubscribeBus;
89
+ entry.unsubscribeBus = undefined;
90
+ unsub?.();
91
+ if (opts.closeTransport) {
92
+ await entry.transport.close().catch(() => undefined);
93
+ }
94
+ await entry.server.close().catch(() => undefined);
95
+ }
96
+ async function closeSession(sessionId) {
97
+ await disposeSession(sessionId, { closeTransport: true });
98
+ }
99
+ async function handleStateful(req, res) {
100
+ const sessionId = sessionIdFromRequest(req);
101
+ const method = req.method ?? "GET";
102
+ if (sessionId && sessions.has(sessionId)) {
103
+ const entry = sessions.get(sessionId);
104
+ await entry.transport.handleRequest(req, res);
105
+ return;
106
+ }
107
+ if (sessionId) {
108
+ jsonRpcError(res, 404, -32001, "Session not found");
109
+ return;
110
+ }
111
+ if (method === "POST") {
112
+ let body;
113
+ try {
114
+ body = await readJsonBody(req);
115
+ }
116
+ catch {
117
+ jsonRpcError(res, 400, -32700, "Parse error");
118
+ return;
119
+ }
120
+ if (!isInitializeRequest(body)) {
121
+ jsonRpcError(res, 400, -32000, "Bad Request: Session ID required");
122
+ return;
123
+ }
124
+ const server = createStageflowMcpServer(tools);
125
+ let sessionRegistered = false;
126
+ const transport = new NodeStreamableHTTPServerTransport({
127
+ sessionIdGenerator: () => randomUUID(),
128
+ onsessioninitialized: (id) => {
129
+ sessions.set(id, {
130
+ transport,
131
+ server,
132
+ unsubscribeBus: attachBusNotifications(server, deps.runChangeBus),
133
+ });
134
+ sessionRegistered = true;
135
+ },
136
+ onsessionclosed: (id) => {
137
+ void disposeSession(id, { closeTransport: false });
138
+ },
139
+ });
140
+ transport.onclose = () => {
141
+ const id = transport.sessionId;
142
+ if (!id)
143
+ return;
144
+ void disposeSession(id, { closeTransport: false });
145
+ };
146
+ try {
147
+ await server.connect(transport);
148
+ await transport.handleRequest(req, res, body);
149
+ }
150
+ finally {
151
+ if (!sessionRegistered) {
152
+ await transport.close().catch(() => undefined);
153
+ await server.close().catch(() => undefined);
154
+ }
155
+ }
156
+ return;
157
+ }
158
+ jsonRpcError(res, 400, -32000, "Bad Request: Session ID required");
159
+ }
160
+ return {
161
+ handle: async (req, res) => {
162
+ if (stateless) {
163
+ await handleStatelessMcpRequest(req, res, tools);
164
+ return;
165
+ }
166
+ await handleStateful(req, res);
167
+ },
168
+ close: async () => {
169
+ const ids = [...sessions.keys()];
170
+ await Promise.all(ids.map((id) => closeSession(id)));
171
+ },
172
+ };
173
+ }
174
+ export async function handleMcpHttpRequest(req, res, deps) {
175
+ await handleStatelessMcpRequest(req, res, deps);
176
+ }
177
+ export function resolveMcpStateless(options) {
178
+ if (options?.mcpStateless === true)
179
+ return true;
180
+ const env = options?.env ?? process.env;
181
+ return env.STAGEFLOW_MCP_STATELESS === "1";
182
+ }
@@ -0,0 +1,7 @@
1
+ export declare function textResult(data: unknown, isError?: boolean): {
2
+ content: {
3
+ type: "text";
4
+ text: string;
5
+ }[];
6
+ isError?: boolean | undefined;
7
+ };
@@ -0,0 +1,6 @@
1
+ export function textResult(data, isError = false) {
2
+ return {
3
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
4
+ ...(isError ? { isError: true } : {}),
5
+ };
6
+ }
@@ -1,9 +1,4 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/server";
2
- import type { RunStore } from "../runstore/port.js";
3
- import type { RunManager } from "../runtime/runManager.js";
4
- export type McpToolDeps = {
5
- manager: RunManager;
6
- store: RunStore;
7
- cwd: string;
8
- };
2
+ import type { McpToolDeps } from "./deps.js";
3
+ export type { McpToolDeps };
9
4
  export declare function registerMcpTools(server: McpServer, deps: McpToolDeps): void;