executable-stories-mcp 0.2.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 ADDED
@@ -0,0 +1,81 @@
1
+ # executable-stories-mcp
2
+
3
+ MCP server for executable-stories behavior catalogs.
4
+
5
+ It consumes `story-report-json` output from `executable-stories-formatters` and exposes scenario discovery and focused test-run tools for coding agents.
6
+
7
+ ## Create the Report
8
+
9
+ ```bash
10
+ executable-stories format .executable-stories/raw-run.json \
11
+ --format story-report-json,scenario-index-json,behavior-manifest-json \
12
+ --output-dir reports \
13
+ --output-name index
14
+ ```
15
+
16
+ Default StoryReport path used by the MCP server:
17
+
18
+ ```text
19
+ reports/index.story-report.json
20
+ ```
21
+
22
+ ## Run
23
+
24
+ ```bash
25
+ npx executable-stories-mcp
26
+ ```
27
+
28
+ The binary speaks the MCP stdio transport. For an HTTP interface, see [HTTP API](#http-api-optional) below.
29
+
30
+ ## Tools
31
+
32
+ ### Read-only (StoryReport query)
33
+
34
+ - `list_scenarios` — scenario index items with status, source, tags, tickets, covers, steps, doc kinds, errors. Optional `statuses` / `tags` / `sourceFiles` filters
35
+ - `get_scenario` — one scenario by id or exact title
36
+ - `get_failing_scenarios` — failed scenarios only
37
+ - `get_scenarios_for_paths` — code→scenario: scenarios whose declared `covers` globs match given product-code paths
38
+ - `get_feature_summary` — per-feature pass/fail counts
39
+ - `get_scenario_index` — Storybook-like `scenario-index` v1 artifact
40
+ - `get_behavior_manifest` — tags, source files, doc coverage, debugger warnings (incl. `missing-covers`)
41
+ - `get_behavior_diff` — compare two StoryReports by scenario id (regressed / fixed / added / removed)
42
+
43
+ ### Execution
44
+
45
+ - `run_scenario` — run one scenario via the host framework (`vitest`, `jest`, `playwright`, or `cypress`). **Executes real tests.**
46
+
47
+ Each tool accepts optional `reportPath` to point at a specific StoryReport JSON file.
48
+
49
+ ## Contract
50
+
51
+ StoryReport v1 JSON remains the canonical artifact:
52
+
53
+ ```bash
54
+ executable-stories format <raw-run.json> --format story-report-json
55
+ ```
56
+
57
+ MCP is a query and optional execution layer over that artifact.
58
+
59
+ ## HTTP API (optional)
60
+
61
+ For non-MCP clients or local debugging, start the HTTP server programmatically (the `executable-stories-mcp` binary itself only speaks stdio):
62
+
63
+ ```typescript
64
+ import { startHttpServer } from "executable-stories-mcp/http";
65
+ await startHttpServer({ reportPath: "reports/index.story-report.json", port: 7357 });
66
+ ```
67
+
68
+ Endpoints (each maps to the matching MCP tool):
69
+
70
+ - `GET /health`
71
+ - `GET /scenarios` (optional `?status=&tag=&sourceFile=`) → `list_scenarios`
72
+ - `GET /scenarios/failing` → `get_failing_scenarios`
73
+ - `GET /scenarios/covering?path=…` → `get_scenarios_for_paths`
74
+ - `GET /scenarios/:id` → `get_scenario`
75
+ - `GET /scenarios-index` → `get_scenario_index`
76
+ - `GET /features` → `get_feature_summary`
77
+ - `GET /manifest` → `get_behavior_manifest`
78
+ - `GET /diff?baseline=&current=` → `get_behavior_diff`
79
+ - `POST /run-scenarios` → `run_scenario`
80
+
81
+ Every GET accepts a `?reportPath=` query parameter.
package/dist/http.cjs ADDED
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/http.ts
31
+ var http_exports = {};
32
+ __export(http_exports, {
33
+ createHttpServer: () => createHttpServer,
34
+ startHttpServer: () => startHttpServer
35
+ });
36
+ module.exports = __toCommonJS(http_exports);
37
+ var import_node_http = __toESM(require("http"), 1);
38
+
39
+ // src/index.ts
40
+ var fs = __toESM(require("fs"), 1);
41
+ var path = __toESM(require("path"), 1);
42
+ var import_node_child_process = require("child_process");
43
+ var import_executable_stories_formatters = require("executable-stories-formatters");
44
+ function loadStoryReport(reportPath) {
45
+ const absolutePath = path.resolve(reportPath);
46
+ const parsed = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
47
+ assertStoryReport(parsed, absolutePath);
48
+ return parsed;
49
+ }
50
+ function listScenarios(report, filters) {
51
+ return (0, import_executable_stories_formatters.toScenarioIndex)(report, filters).scenarios;
52
+ }
53
+ function getFailingScenarios(report) {
54
+ return listScenarios(report, { statuses: ["failed"] });
55
+ }
56
+ function getScenariosForPaths(report, paths) {
57
+ return (0, import_executable_stories_formatters.scenariosCoveringPaths)((0, import_executable_stories_formatters.toScenarioIndex)(report), paths);
58
+ }
59
+ function getBehaviorDiff(baseline, current) {
60
+ return (0, import_executable_stories_formatters.diffStoryReports)(baseline, current);
61
+ }
62
+ function getScenario(report, idOrTitle) {
63
+ for (const feature of report.features) {
64
+ const scenario = feature.scenarios.find(
65
+ (candidate) => candidate.id === idOrTitle || candidate.title === idOrTitle
66
+ );
67
+ if (scenario) return { feature, scenario };
68
+ }
69
+ return void 0;
70
+ }
71
+ function getFeatureSummary(report) {
72
+ return report.features.map((feature) => ({
73
+ id: feature.id,
74
+ title: feature.title,
75
+ sourceFile: feature.sourceFile,
76
+ total: feature.summary.total,
77
+ passed: feature.summary.passed,
78
+ failed: feature.summary.failed,
79
+ skipped: feature.summary.skipped,
80
+ pending: feature.summary.pending,
81
+ durationMs: feature.summary.durationMs
82
+ }));
83
+ }
84
+ function resolveReportPath(reportPath) {
85
+ return path.resolve(reportPath ?? "reports/index.story-report.json");
86
+ }
87
+ function getScenarioIndex(report) {
88
+ return (0, import_executable_stories_formatters.toScenarioIndex)(report);
89
+ }
90
+ function getBehaviorManifest(report) {
91
+ return (0, import_executable_stories_formatters.toBehaviorManifest)(report);
92
+ }
93
+ var readOnlyTools = [
94
+ {
95
+ name: "get_failing_scenarios",
96
+ title: "Get failing scenarios",
97
+ description: "List failing executable story scenarios from StoryReport JSON.",
98
+ route: "/scenarios/failing",
99
+ run: getFailingScenarios
100
+ },
101
+ {
102
+ name: "get_feature_summary",
103
+ title: "Get feature summary",
104
+ description: "Summarize features and scenario status counts from StoryReport JSON.",
105
+ route: "/features",
106
+ run: getFeatureSummary
107
+ },
108
+ {
109
+ name: "get_scenario_index",
110
+ title: "Get scenario index",
111
+ description: "Return the Storybook-like scenario index artifact (schema v1) derived from StoryReport JSON.",
112
+ route: "/scenarios-index",
113
+ run: getScenarioIndex
114
+ },
115
+ {
116
+ name: "get_behavior_manifest",
117
+ title: "Get behavior manifest",
118
+ description: "Return agent-oriented manifest metadata: source files, tags, doc coverage, debugger warnings.",
119
+ route: "/manifest",
120
+ run: getBehaviorManifest
121
+ }
122
+ ];
123
+ var RUNNERS = {
124
+ vitest: {
125
+ framework: "vitest",
126
+ buildCommand: ({ sourceFile, scenarioTitle }) => ({
127
+ command: "pnpm",
128
+ args: ["exec", "vitest", "run", sourceFile, ...scenarioTitle ? ["-t", scenarioTitle] : []]
129
+ })
130
+ },
131
+ jest: {
132
+ framework: "jest",
133
+ buildCommand: ({ sourceFile, scenarioTitle }) => ({
134
+ command: "pnpm",
135
+ args: ["exec", "jest", sourceFile, ...scenarioTitle ? ["-t", scenarioTitle] : [], "--runInBand"]
136
+ })
137
+ },
138
+ playwright: {
139
+ framework: "playwright",
140
+ detect: (sourceFile) => sourceFile.includes(".story.spec."),
141
+ buildCommand: ({ sourceFile, scenarioTitle }) => ({
142
+ command: "pnpm",
143
+ args: ["exec", "playwright", "test", sourceFile, ...scenarioTitle ? ["-g", scenarioTitle] : []]
144
+ })
145
+ },
146
+ cypress: {
147
+ framework: "cypress",
148
+ detect: (sourceFile) => sourceFile.includes(".story.cy."),
149
+ buildCommand: ({ sourceFile }) => ({
150
+ command: "pnpm",
151
+ args: ["exec", "cypress", "run", "--spec", sourceFile]
152
+ })
153
+ }
154
+ };
155
+ function buildFocusedRunCommand(args) {
156
+ return RUNNERS[args.framework].buildCommand(args);
157
+ }
158
+ async function runFocusedScenario(args) {
159
+ const command = buildFocusedRunCommand(args);
160
+ const spawnFn = args.spawnFn ?? import_node_child_process.spawn;
161
+ return new Promise((resolve2) => {
162
+ const child = spawnFn(command.command, command.args, {
163
+ cwd: args.cwd,
164
+ env: process.env
165
+ });
166
+ let stdout = "";
167
+ let stderr = "";
168
+ child.stdout.on("data", (chunk) => {
169
+ stdout += String(chunk);
170
+ });
171
+ child.stderr.on("data", (chunk) => {
172
+ stderr += String(chunk);
173
+ });
174
+ child.on("error", (error) => {
175
+ resolve2({
176
+ ok: false,
177
+ exitCode: null,
178
+ command: command.command,
179
+ args: command.args,
180
+ stdout,
181
+ stderr: stderr + error.message
182
+ });
183
+ });
184
+ child.on("close", (exitCode) => {
185
+ resolve2({
186
+ ok: exitCode === 0,
187
+ exitCode,
188
+ command: command.command,
189
+ args: command.args,
190
+ stdout,
191
+ stderr
192
+ });
193
+ });
194
+ });
195
+ }
196
+ function assertStoryReport(value, reportPath) {
197
+ if (!value || typeof value !== "object") {
198
+ throw new Error(`Invalid StoryReport JSON in ${reportPath}: expected object`);
199
+ }
200
+ const report = value;
201
+ if (typeof report.schemaVersion !== "string" || !report.schemaVersion.startsWith("1.")) {
202
+ throw new Error(
203
+ `Invalid StoryReport JSON in ${reportPath}: expected schemaVersion 1.x`
204
+ );
205
+ }
206
+ if (!Array.isArray(report.features)) {
207
+ throw new Error(`Invalid StoryReport JSON in ${reportPath}: expected features array`);
208
+ }
209
+ }
210
+
211
+ // src/http.ts
212
+ var readOnlyRoutes = new Map(readOnlyTools.map((tool) => [tool.route, tool.run]));
213
+ function parseFilters(params) {
214
+ const statuses = params.getAll("status");
215
+ const tags = params.getAll("tag");
216
+ const sourceFiles = params.getAll("sourceFile");
217
+ return {
218
+ statuses: statuses.length ? statuses : void 0,
219
+ tags: tags.length ? tags : void 0,
220
+ sourceFiles: sourceFiles.length ? sourceFiles : void 0
221
+ };
222
+ }
223
+ function createHttpServer(options = {}) {
224
+ return import_node_http.default.createServer(async (request, response) => {
225
+ try {
226
+ if (!request.url) {
227
+ sendJson(response, 404, { error: "Missing URL" });
228
+ return;
229
+ }
230
+ const url = new URL(request.url, "http://localhost");
231
+ const reportPath = url.searchParams.get("reportPath") ?? options.reportPath;
232
+ if (request.method === "GET" && url.pathname === "/health") {
233
+ sendJson(response, 200, { ok: true, name: "executable-stories-mcp" });
234
+ return;
235
+ }
236
+ if (request.method === "GET" && url.pathname === "/diff") {
237
+ const baseline = loadStoryReport(
238
+ resolveReportPath(url.searchParams.get("baseline") ?? void 0)
239
+ );
240
+ const current = loadStoryReport(
241
+ resolveReportPath(url.searchParams.get("current") ?? reportPath ?? void 0)
242
+ );
243
+ sendJson(response, 200, getBehaviorDiff(baseline, current));
244
+ return;
245
+ }
246
+ if (request.method === "GET") {
247
+ const report = loadStoryReport(resolveReportPath(reportPath));
248
+ if (url.pathname === "/scenarios") {
249
+ sendJson(response, 200, listScenarios(report, parseFilters(url.searchParams)));
250
+ return;
251
+ }
252
+ if (url.pathname === "/scenarios/covering") {
253
+ sendJson(response, 200, getScenariosForPaths(report, url.searchParams.getAll("path")));
254
+ return;
255
+ }
256
+ const handler = readOnlyRoutes.get(url.pathname);
257
+ if (handler) {
258
+ sendJson(response, 200, handler(report));
259
+ return;
260
+ }
261
+ if (url.pathname.startsWith("/scenarios/")) {
262
+ const id = decodeURIComponent(url.pathname.slice("/scenarios/".length));
263
+ const scenario = getScenario(report, id);
264
+ sendJson(response, scenario ? 200 : 404, scenario ?? { error: `Scenario not found: ${id}` });
265
+ return;
266
+ }
267
+ }
268
+ if (request.method === "POST" && url.pathname === "/run-scenarios") {
269
+ const body = await readJsonBody(request);
270
+ const result = await runFocusedScenario({
271
+ framework: body.framework,
272
+ sourceFile: String(body.sourceFile),
273
+ scenarioTitle: typeof body.scenarioTitle === "string" ? body.scenarioTitle : void 0,
274
+ cwd: typeof body.cwd === "string" ? body.cwd : void 0
275
+ });
276
+ sendJson(response, result.ok ? 200 : 500, result);
277
+ return;
278
+ }
279
+ sendJson(response, 404, { error: "Not found" });
280
+ } catch (error) {
281
+ sendJson(response, 500, { error: error.message });
282
+ }
283
+ });
284
+ }
285
+ async function startHttpServer(options = {}) {
286
+ const server = createHttpServer(options);
287
+ const port = options.port ?? 7357;
288
+ const host = options.host ?? "127.0.0.1";
289
+ await new Promise((resolve2) => {
290
+ server.listen(port, host, resolve2);
291
+ });
292
+ return server;
293
+ }
294
+ function sendJson(response, statusCode, value) {
295
+ response.writeHead(statusCode, {
296
+ "content-type": "application/json; charset=utf-8",
297
+ "access-control-allow-origin": "*"
298
+ });
299
+ response.end(JSON.stringify(value, null, 2));
300
+ }
301
+ async function readJsonBody(request) {
302
+ const chunks = [];
303
+ for await (const chunk of request) {
304
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
305
+ }
306
+ return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
307
+ }
308
+ // Annotate the CommonJS export names for ESM import in node:
309
+ 0 && (module.exports = {
310
+ createHttpServer,
311
+ startHttpServer
312
+ });
313
+ //# sourceMappingURL=http.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http.ts","../src/index.ts"],"sourcesContent":["import http from \"node:http\";\n\nimport {\n getBehaviorDiff,\n getScenario,\n getScenariosForPaths,\n listScenarios,\n loadStoryReport,\n readOnlyTools,\n resolveReportPath,\n runFocusedScenario,\n type FocusedRunFramework,\n type ScenarioIndexFilters,\n} from \"./index.js\";\n\nexport interface HttpServerOptions {\n port?: number;\n host?: string;\n reportPath?: string;\n}\n\n/** Exact GET routes shared with the stdio MCP server (see readOnlyTools). */\nconst readOnlyRoutes = new Map(readOnlyTools.map((tool) => [tool.route, tool.run]));\n\n/** Parse the repeatable filter query params (?status=&tag=&sourceFile=). */\nfunction parseFilters(params: URLSearchParams): ScenarioIndexFilters {\n const statuses = params.getAll(\"status\");\n const tags = params.getAll(\"tag\");\n const sourceFiles = params.getAll(\"sourceFile\");\n return {\n statuses: statuses.length ? (statuses as ScenarioIndexFilters[\"statuses\"]) : undefined,\n tags: tags.length ? tags : undefined,\n sourceFiles: sourceFiles.length ? sourceFiles : undefined,\n };\n}\n\nexport function createHttpServer(options: HttpServerOptions = {}): http.Server {\n return http.createServer(async (request, response) => {\n try {\n if (!request.url) {\n sendJson(response, 404, { error: \"Missing URL\" });\n return;\n }\n\n const url = new URL(request.url, \"http://localhost\");\n const reportPath = url.searchParams.get(\"reportPath\") ?? options.reportPath;\n\n if (request.method === \"GET\" && url.pathname === \"/health\") {\n sendJson(response, 200, { ok: true, name: \"executable-stories-mcp\" });\n return;\n }\n\n // Diff compares two reports, so it resolves before the single-report load.\n if (request.method === \"GET\" && url.pathname === \"/diff\") {\n const baseline = loadStoryReport(\n resolveReportPath(url.searchParams.get(\"baseline\") ?? undefined),\n );\n const current = loadStoryReport(\n resolveReportPath(url.searchParams.get(\"current\") ?? reportPath ?? undefined),\n );\n sendJson(response, 200, getBehaviorDiff(baseline, current));\n return;\n }\n\n if (request.method === \"GET\") {\n const report = loadStoryReport(resolveReportPath(reportPath));\n\n // Arg-taking routes resolve before the no-arg catalog and the dynamic\n // /scenarios/:id route.\n if (url.pathname === \"/scenarios\") {\n sendJson(response, 200, listScenarios(report, parseFilters(url.searchParams)));\n return;\n }\n if (url.pathname === \"/scenarios/covering\") {\n sendJson(response, 200, getScenariosForPaths(report, url.searchParams.getAll(\"path\")));\n return;\n }\n\n // Exact catalog routes resolve before the dynamic /scenarios/:id route,\n // so /scenarios/failing keeps working as a fixed endpoint.\n const handler = readOnlyRoutes.get(url.pathname);\n if (handler) {\n sendJson(response, 200, handler(report));\n return;\n }\n if (url.pathname.startsWith(\"/scenarios/\")) {\n const id = decodeURIComponent(url.pathname.slice(\"/scenarios/\".length));\n const scenario = getScenario(report, id);\n sendJson(response, scenario ? 200 : 404, scenario ?? { error: `Scenario not found: ${id}` });\n return;\n }\n }\n\n if (request.method === \"POST\" && url.pathname === \"/run-scenarios\") {\n const body = await readJsonBody(request);\n const result = await runFocusedScenario({\n framework: body.framework as FocusedRunFramework,\n sourceFile: String(body.sourceFile),\n scenarioTitle: typeof body.scenarioTitle === \"string\" ? body.scenarioTitle : undefined,\n cwd: typeof body.cwd === \"string\" ? body.cwd : undefined,\n });\n sendJson(response, result.ok ? 200 : 500, result);\n return;\n }\n\n sendJson(response, 404, { error: \"Not found\" });\n } catch (error) {\n sendJson(response, 500, { error: (error as Error).message });\n }\n });\n}\n\nexport async function startHttpServer(options: HttpServerOptions = {}): Promise<http.Server> {\n const server = createHttpServer(options);\n const port = options.port ?? 7357;\n const host = options.host ?? \"127.0.0.1\";\n await new Promise<void>((resolve) => {\n server.listen(port, host, resolve);\n });\n return server;\n}\n\nfunction sendJson(response: http.ServerResponse, statusCode: number, value: unknown): void {\n response.writeHead(statusCode, {\n \"content-type\": \"application/json; charset=utf-8\",\n \"access-control-allow-origin\": \"*\",\n });\n response.end(JSON.stringify(value, null, 2));\n}\n\nasync function readJsonBody(request: http.IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n for await (const chunk of request) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return JSON.parse(Buffer.concat(chunks).toString(\"utf8\") || \"{}\") as Record<string, unknown>;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { spawn, type ChildProcessWithoutNullStreams } from \"node:child_process\";\n\nimport type {\n ReportFeature,\n ReportScenario,\n StoryReport,\n BehaviorManifest,\n BehaviorDiff,\n ScenarioIndex,\n ScenarioIndexFilters,\n ScenarioIndexItem,\n} from \"executable-stories-formatters\";\nimport {\n diffStoryReports,\n scenariosCoveringPaths,\n toBehaviorManifest,\n toScenarioIndex,\n} from \"executable-stories-formatters\";\n\n// Scenario serialization is owned by the formatters package; re-export so MCP\n// consumers get the same shape without a parallel definition to maintain.\nexport type { ScenarioIndexItem, ScenarioIndexFilters };\n\nexport interface FeatureSummaryItem {\n id: string;\n title: string;\n sourceFile: string;\n total: number;\n passed: number;\n failed: number;\n skipped: number;\n pending: number;\n durationMs: number;\n}\n\nexport interface ScenarioLookup {\n feature: ReportFeature;\n scenario: ReportScenario;\n}\n\nexport function loadStoryReport(reportPath: string): StoryReport {\n const absolutePath = path.resolve(reportPath);\n const parsed: unknown = JSON.parse(fs.readFileSync(absolutePath, \"utf8\"));\n assertStoryReport(parsed, absolutePath);\n return parsed;\n}\n\nexport function listScenarios(\n report: StoryReport,\n filters?: ScenarioIndexFilters,\n): ScenarioIndexItem[] {\n return toScenarioIndex(report, filters).scenarios;\n}\n\nexport function getFailingScenarios(report: StoryReport): ScenarioIndexItem[] {\n return listScenarios(report, { statuses: [\"failed\"] });\n}\n\nexport function getScenariosForPaths(report: StoryReport, paths: string[]): ScenarioIndexItem[] {\n return scenariosCoveringPaths(toScenarioIndex(report), paths);\n}\n\nexport function getBehaviorDiff(baseline: StoryReport, current: StoryReport): BehaviorDiff {\n return diffStoryReports(baseline, current);\n}\n\nexport function getScenario(report: StoryReport, idOrTitle: string): ScenarioLookup | undefined {\n for (const feature of report.features) {\n const scenario = feature.scenarios.find(\n (candidate) => candidate.id === idOrTitle || candidate.title === idOrTitle,\n );\n if (scenario) return { feature, scenario };\n }\n return undefined;\n}\n\nexport function getFeatureSummary(report: StoryReport): FeatureSummaryItem[] {\n return report.features.map((feature) => ({\n id: feature.id,\n title: feature.title,\n sourceFile: feature.sourceFile,\n total: feature.summary.total,\n passed: feature.summary.passed,\n failed: feature.summary.failed,\n skipped: feature.summary.skipped,\n pending: feature.summary.pending,\n durationMs: feature.summary.durationMs,\n }));\n}\n\nexport function resolveReportPath(reportPath?: string): string {\n return path.resolve(reportPath ?? \"reports/index.story-report.json\");\n}\n\nexport function getScenarioIndex(report: StoryReport): ScenarioIndex {\n return toScenarioIndex(report);\n}\n\nexport function getBehaviorManifest(report: StoryReport): BehaviorManifest {\n return toBehaviorManifest(report);\n}\n\n/**\n * Single source of truth for the read-only tools, consumed by both the stdio\n * MCP server and the HTTP server so the two transports cannot drift apart.\n * Tools needing extra arguments (get_scenario, run_scenario) are wired up\n * directly in each transport.\n */\nexport interface ReadOnlyTool {\n /** MCP tool name. */\n name: string;\n /** Human-readable MCP tool title. */\n title: string;\n /** Shared description used by both transports. */\n description: string;\n /** HTTP route that exposes the same data. */\n route: string;\n /** Pure projection from a loaded report to its JSON payload. */\n run: (report: StoryReport) => unknown;\n}\n\nexport const readOnlyTools: ReadOnlyTool[] = [\n {\n name: \"get_failing_scenarios\",\n title: \"Get failing scenarios\",\n description: \"List failing executable story scenarios from StoryReport JSON.\",\n route: \"/scenarios/failing\",\n run: getFailingScenarios,\n },\n {\n name: \"get_feature_summary\",\n title: \"Get feature summary\",\n description: \"Summarize features and scenario status counts from StoryReport JSON.\",\n route: \"/features\",\n run: getFeatureSummary,\n },\n {\n name: \"get_scenario_index\",\n title: \"Get scenario index\",\n description:\n \"Return the Storybook-like scenario index artifact (schema v1) derived from StoryReport JSON.\",\n route: \"/scenarios-index\",\n run: getScenarioIndex,\n },\n {\n name: \"get_behavior_manifest\",\n title: \"Get behavior manifest\",\n description:\n \"Return agent-oriented manifest metadata: source files, tags, doc coverage, debugger warnings.\",\n route: \"/manifest\",\n run: getBehaviorManifest,\n },\n];\n\nexport type FocusedRunFramework = \"vitest\" | \"jest\" | \"playwright\" | \"cypress\";\n\nexport interface FocusedRunCommandArgs {\n framework: FocusedRunFramework;\n sourceFile: string;\n scenarioTitle?: string;\n}\n\nexport interface FocusedRunCommand {\n command: string;\n args: string[];\n}\n\nexport interface FocusedRunResult {\n ok: boolean;\n exitCode: number | null;\n command: string;\n args: string[];\n stdout: string;\n stderr: string;\n}\n\n/**\n * One runner per host framework. The seam that keeps `run_scenario` extensible:\n * adding a non-JS framework (go, cargo, pytest, dotnet) later is a single new\n * entry here — no changes to inference, command building, or the transports.\n */\nexport interface RunnerDefinition {\n framework: FocusedRunFramework;\n /** Infer this framework from a source-file path, when unambiguous. */\n detect?: (sourceFile: string) => boolean;\n /** Build the focused-run command for this framework. */\n buildCommand: (args: { sourceFile: string; scenarioTitle?: string }) => FocusedRunCommand;\n}\n\nexport const RUNNERS: Record<FocusedRunFramework, RunnerDefinition> = {\n vitest: {\n framework: \"vitest\",\n buildCommand: ({ sourceFile, scenarioTitle }) => ({\n command: \"pnpm\",\n args: [\"exec\", \"vitest\", \"run\", sourceFile, ...(scenarioTitle ? [\"-t\", scenarioTitle] : [])],\n }),\n },\n jest: {\n framework: \"jest\",\n buildCommand: ({ sourceFile, scenarioTitle }) => ({\n command: \"pnpm\",\n args: [\"exec\", \"jest\", sourceFile, ...(scenarioTitle ? [\"-t\", scenarioTitle] : []), \"--runInBand\"],\n }),\n },\n playwright: {\n framework: \"playwright\",\n detect: (sourceFile) => sourceFile.includes(\".story.spec.\"),\n buildCommand: ({ sourceFile, scenarioTitle }) => ({\n command: \"pnpm\",\n args: [\"exec\", \"playwright\", \"test\", sourceFile, ...(scenarioTitle ? [\"-g\", scenarioTitle] : [])],\n }),\n },\n cypress: {\n framework: \"cypress\",\n detect: (sourceFile) => sourceFile.includes(\".story.cy.\"),\n buildCommand: ({ sourceFile }) => ({\n command: \"pnpm\",\n args: [\"exec\", \"cypress\", \"run\", \"--spec\", sourceFile],\n }),\n },\n};\n\nexport function inferFrameworkFromSourceFile(\n sourceFile: string,\n): FocusedRunFramework | undefined {\n for (const runner of Object.values(RUNNERS)) {\n if (runner.detect?.(sourceFile)) return runner.framework;\n }\n return undefined;\n}\n\nexport function resolveFocusedRunFramework(args: {\n sourceFile: string;\n framework?: FocusedRunFramework;\n}): FocusedRunFramework {\n if (args.framework) return args.framework;\n const inferred = inferFrameworkFromSourceFile(args.sourceFile);\n if (inferred) return inferred;\n throw new Error(\n `Could not infer test framework from ${args.sourceFile}. Pass framework: vitest | jest | playwright | cypress.`,\n );\n}\n\nexport function buildFocusedRunCommand(args: FocusedRunCommandArgs): FocusedRunCommand {\n return RUNNERS[args.framework].buildCommand(args);\n}\n\nexport async function runFocusedScenario(args: FocusedRunCommandArgs & {\n cwd?: string;\n spawnFn?: typeof spawn;\n}): Promise<FocusedRunResult> {\n const command = buildFocusedRunCommand(args);\n const spawnFn = args.spawnFn ?? spawn;\n\n return new Promise((resolve) => {\n const child = spawnFn(command.command, command.args, {\n cwd: args.cwd,\n env: process.env,\n }) as ChildProcessWithoutNullStreams;\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk) => {\n stdout += String(chunk);\n });\n child.stderr.on(\"data\", (chunk) => {\n stderr += String(chunk);\n });\n child.on(\"error\", (error) => {\n resolve({\n ok: false,\n exitCode: null,\n command: command.command,\n args: command.args,\n stdout,\n stderr: stderr + error.message,\n });\n });\n child.on(\"close\", (exitCode) => {\n resolve({\n ok: exitCode === 0,\n exitCode,\n command: command.command,\n args: command.args,\n stdout,\n stderr,\n });\n });\n });\n}\n\nfunction assertStoryReport(\n value: unknown,\n reportPath: string,\n): asserts value is StoryReport {\n if (!value || typeof value !== \"object\") {\n throw new Error(`Invalid StoryReport JSON in ${reportPath}: expected object`);\n }\n const report = value as Partial<StoryReport>;\n if (typeof report.schemaVersion !== \"string\" || !report.schemaVersion.startsWith(\"1.\")) {\n throw new Error(\n `Invalid StoryReport JSON in ${reportPath}: expected schemaVersion 1.x`,\n );\n }\n if (!Array.isArray(report.features)) {\n throw new Error(`Invalid StoryReport JSON in ${reportPath}: expected features array`);\n }\n}\n"],"mappings":"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","names":["resolve","http","resolve"]}
@@ -0,0 +1,11 @@
1
+ import http from 'node:http';
2
+
3
+ interface HttpServerOptions {
4
+ port?: number;
5
+ host?: string;
6
+ reportPath?: string;
7
+ }
8
+ declare function createHttpServer(options?: HttpServerOptions): http.Server;
9
+ declare function startHttpServer(options?: HttpServerOptions): Promise<http.Server>;
10
+
11
+ export { type HttpServerOptions, createHttpServer, startHttpServer };
package/dist/http.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import http from 'node:http';
2
+
3
+ interface HttpServerOptions {
4
+ port?: number;
5
+ host?: string;
6
+ reportPath?: string;
7
+ }
8
+ declare function createHttpServer(options?: HttpServerOptions): http.Server;
9
+ declare function startHttpServer(options?: HttpServerOptions): Promise<http.Server>;
10
+
11
+ export { type HttpServerOptions, createHttpServer, startHttpServer };