@storybook/react-native 10.3.0-next.0 → 10.3.0-next.1
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/dist/metro/withStorybook.d.ts +7 -0
- package/dist/metro/withStorybook.js +423 -71
- package/dist/node.d.ts +15 -2
- package/dist/node.js +397 -55
- package/dist/repack/withStorybook.d.ts +7 -0
- package/dist/repack/withStorybook.js +412 -64
- package/metro-env.d.ts +69 -0
- package/package.json +16 -9
- package/scripts/generate.js +1 -3
|
@@ -42,6 +42,13 @@ interface WithStorybookOptions {
|
|
|
42
42
|
* This will mock out the default storybook ui so you don't need to install all its dependencies like reanimated etc.
|
|
43
43
|
*/
|
|
44
44
|
liteMode?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to enable MCP (Model Context Protocol) server support. Defaults to false.
|
|
47
|
+
* When enabled, adds an /mcp endpoint to the channel server,
|
|
48
|
+
* allowing AI agents (Claude Code, Cursor, etc.) to query component documentation.
|
|
49
|
+
* If websockets are disabled, MCP documentation tools still work but story selection is unavailable.
|
|
50
|
+
*/
|
|
51
|
+
experimental_mcp?: boolean;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Configures Metro bundler to work with Storybook in React Native.
|
|
@@ -4,6 +4,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
7
10
|
var __commonJS = (cb, mod) => function __require() {
|
|
8
11
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
12
|
};
|
|
@@ -220,7 +223,6 @@ var require_generate = __commonJS({
|
|
|
220
223
|
directory: "${specifier.directory}",
|
|
221
224
|
files: "${specifier.files}",
|
|
222
225
|
importPathMatcher: /${reg.source}/,
|
|
223
|
-
${useJs ? "" : "// @ts-ignore"}
|
|
224
226
|
req: require.context(
|
|
225
227
|
'${pathToStory}',
|
|
226
228
|
${r},
|
|
@@ -291,7 +293,7 @@ declare global {
|
|
|
291
293
|
}
|
|
292
294
|
`;
|
|
293
295
|
const fileContent = `/* do not change this file, it is auto generated by storybook. */
|
|
294
|
-
import { start, updateView${useJs ? "" : ", View, type Features"} } from '@storybook/react-native';
|
|
296
|
+
${useJs ? "" : '/// <reference types="@storybook/react-native/metro-env" />\n'}import { start, updateView${useJs ? "" : ", View, type Features"} } from '@storybook/react-native';
|
|
295
297
|
|
|
296
298
|
${registeredAddons.join("\n")}
|
|
297
299
|
|
|
@@ -306,7 +308,6 @@ const annotations = ${annotations};
|
|
|
306
308
|
globalThis.STORIES = normalizedStories;
|
|
307
309
|
${channelHost ? `globalThis.STORYBOOK_WEBSOCKET = { host: '${channelHost}', port: ${port ?? 7007} };` : ""}
|
|
308
310
|
|
|
309
|
-
${useJs ? "" : "// @ts-ignore"}
|
|
310
311
|
module?.hot?.accept?.();
|
|
311
312
|
${featuresAssignment ? `
|
|
312
313
|
${featuresAssignment}
|
|
@@ -336,42 +337,11 @@ export const view${useJs ? "" : ": View"} = globalThis.view;
|
|
|
336
337
|
}
|
|
337
338
|
});
|
|
338
339
|
|
|
339
|
-
// src/metro/withStorybook.ts
|
|
340
|
-
var withStorybook_exports = {};
|
|
341
|
-
__export(withStorybook_exports, {
|
|
342
|
-
withStorybook: () => withStorybook
|
|
343
|
-
});
|
|
344
|
-
module.exports = __toCommonJS(withStorybook_exports);
|
|
345
|
-
var path2 = __toESM(require("path"));
|
|
346
|
-
var import_generate = __toESM(require_generate());
|
|
347
|
-
var import_common3 = require("storybook/internal/common");
|
|
348
|
-
var import_telemetry = require("storybook/internal/telemetry");
|
|
349
|
-
|
|
350
|
-
// src/metro/channelServer.ts
|
|
351
|
-
var import_ws = require("ws");
|
|
352
|
-
var import_node_http = require("http");
|
|
353
|
-
|
|
354
340
|
// src/metro/buildIndex.ts
|
|
355
|
-
var
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var import_csf_tools = require("storybook/internal/csf-tools");
|
|
360
|
-
var import_csf = require("storybook/internal/csf");
|
|
361
|
-
var import_preview_api = require("storybook/internal/preview-api");
|
|
362
|
-
var import_common2 = __toESM(require_common());
|
|
363
|
-
var cwd = process.cwd();
|
|
364
|
-
var makeTitle = (fileName, specifier, userTitle) => {
|
|
365
|
-
const title = (0, import_preview_api.userOrAutoTitleFromSpecifier)(fileName, specifier, userTitle);
|
|
366
|
-
if (title) {
|
|
367
|
-
return title.replace("./", "");
|
|
368
|
-
} else if (userTitle) {
|
|
369
|
-
return userTitle.replace("./", "");
|
|
370
|
-
} else {
|
|
371
|
-
console.error("Could not generate title!!");
|
|
372
|
-
process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
};
|
|
341
|
+
var buildIndex_exports = {};
|
|
342
|
+
__export(buildIndex_exports, {
|
|
343
|
+
buildIndex: () => buildIndex
|
|
344
|
+
});
|
|
375
345
|
function ensureRelativePathHasDot(relativePath) {
|
|
376
346
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
377
347
|
}
|
|
@@ -459,14 +429,369 @@ async function buildIndex({ configPath }) {
|
|
|
459
429
|
return index;
|
|
460
430
|
}
|
|
461
431
|
}
|
|
432
|
+
var import_common, import_node_fs, import_glob, import_path, import_csf_tools, import_csf, import_preview_api, import_common2, cwd, makeTitle;
|
|
433
|
+
var init_buildIndex = __esm({
|
|
434
|
+
"src/metro/buildIndex.ts"() {
|
|
435
|
+
import_common = require("storybook/internal/common");
|
|
436
|
+
import_node_fs = require("fs");
|
|
437
|
+
import_glob = require("glob");
|
|
438
|
+
import_path = __toESM(require("path"));
|
|
439
|
+
import_csf_tools = require("storybook/internal/csf-tools");
|
|
440
|
+
import_csf = require("storybook/internal/csf");
|
|
441
|
+
import_preview_api = require("storybook/internal/preview-api");
|
|
442
|
+
import_common2 = __toESM(require_common());
|
|
443
|
+
cwd = process.cwd();
|
|
444
|
+
makeTitle = (fileName, specifier, userTitle) => {
|
|
445
|
+
const title = (0, import_preview_api.userOrAutoTitleFromSpecifier)(fileName, specifier, userTitle);
|
|
446
|
+
if (title) {
|
|
447
|
+
return title.replace("./", "");
|
|
448
|
+
} else if (userTitle) {
|
|
449
|
+
return userTitle.replace("./", "");
|
|
450
|
+
} else {
|
|
451
|
+
console.error("Could not generate title!!");
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// src/metro/manifest/storyInstructions.ts
|
|
459
|
+
var storyInstructions_exports = {};
|
|
460
|
+
__export(storyInstructions_exports, {
|
|
461
|
+
storyInstructions: () => storyInstructions
|
|
462
|
+
});
|
|
463
|
+
var storyInstructions;
|
|
464
|
+
var init_storyInstructions = __esm({
|
|
465
|
+
"src/metro/manifest/storyInstructions.ts"() {
|
|
466
|
+
storyInstructions = `# Writing React Native UI Components
|
|
467
|
+
|
|
468
|
+
When writing UI, prefer breaking larger components up into smaller parts.
|
|
469
|
+
|
|
470
|
+
ALWAYS write a Storybook story for any component written. If editing a component, ensure appropriate changes have been made to stories for that component.
|
|
471
|
+
|
|
472
|
+
## How to write good stories
|
|
473
|
+
|
|
474
|
+
Goal: Cover every distinct piece of business logic and state the component can reach (happy paths, error/edge states, loading, permissions/roles, empty states, variations from props/context). Avoid redundant stories that show the same logic.
|
|
475
|
+
|
|
476
|
+
Interactivity: For interactive components, create separate stories that demonstrate each interaction state. Use \`fn()\` from \`storybook/test\` to mock callback props so you can verify they are wired up correctly.
|
|
477
|
+
|
|
478
|
+
Data/setup: Provide realistic props, state, and mocked data. Include meaningful labels/text to make behaviors observable. Stub network/services with deterministic fixtures; keep stories reliable.
|
|
479
|
+
|
|
480
|
+
Variants to consider (pick only those that change behavior): default vs. alternate themes; loading vs. loaded vs. empty vs. error; validated vs. invalid input; permissions/roles/capabilities; feature flags; size/density/layout variants that alter logic.
|
|
481
|
+
|
|
482
|
+
Accessibility: Use semantic roles/labels where applicable.
|
|
483
|
+
|
|
484
|
+
Naming/structure: Use clear story names that describe the scenario ("Error state after failed submit"). Group related variants logically; don't duplicate.
|
|
485
|
+
|
|
486
|
+
Imports/format: Import Meta/StoryObj from the framework package. Keep stories minimal\u2014only what's needed to demonstrate behavior.
|
|
487
|
+
|
|
488
|
+
## React Native Storybook Essentials
|
|
489
|
+
|
|
490
|
+
### Framework and Renderer
|
|
491
|
+
|
|
492
|
+
React Native Storybook uses \`@storybook/react-native\` as the framework. Stories use the same CSF (Component Story Format) as web Storybook.
|
|
493
|
+
|
|
494
|
+
### Meta and StoryObj imports
|
|
495
|
+
|
|
496
|
+
\`\`\`ts
|
|
497
|
+
import type { Meta, StoryObj } from '@storybook/react-native';
|
|
498
|
+
\`\`\`
|
|
499
|
+
|
|
500
|
+
### Story file structure
|
|
501
|
+
|
|
502
|
+
\`\`\`tsx
|
|
503
|
+
import type { Meta, StoryObj } from '@storybook/react-native';
|
|
504
|
+
import { MyComponent } from './MyComponent';
|
|
505
|
+
|
|
506
|
+
const meta: Meta<typeof MyComponent> = {
|
|
507
|
+
title: 'Components/MyComponent',
|
|
508
|
+
component: MyComponent,
|
|
509
|
+
args: {
|
|
510
|
+
// default args
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
export default meta;
|
|
515
|
+
|
|
516
|
+
type Story = StoryObj<typeof meta>;
|
|
517
|
+
|
|
518
|
+
export const Default: Story = {};
|
|
519
|
+
|
|
520
|
+
export const WithCustomProps: Story = {
|
|
521
|
+
args: {
|
|
522
|
+
label: 'Custom Label',
|
|
523
|
+
variant: 'secondary',
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
\`\`\`
|
|
527
|
+
|
|
528
|
+
### Global State Changes
|
|
529
|
+
|
|
530
|
+
The \`globals\` annotation has been renamed to \`initialGlobals\`:
|
|
531
|
+
|
|
532
|
+
\`\`\`diff
|
|
533
|
+
// .rnstorybook/preview.js
|
|
534
|
+
export default {
|
|
535
|
+
- globals: { theme: 'light' }
|
|
536
|
+
+ initialGlobals: { theme: 'light' }
|
|
537
|
+
};
|
|
538
|
+
\`\`\`
|
|
539
|
+
|
|
540
|
+
### React Native Specific Considerations
|
|
541
|
+
|
|
542
|
+
- The config directory is \`.rnstorybook\` (not \`.storybook\`)
|
|
543
|
+
- Stories run on-device (iOS/Android), not in a browser
|
|
544
|
+
- Use React Native components (\`View\`, \`Text\`, \`Pressable\`, etc.), not HTML elements
|
|
545
|
+
- \`StyleSheet\` or inline styles instead of CSS
|
|
546
|
+
- No DOM APIs \u2014 use React Native's layout system (Flexbox)
|
|
547
|
+
- For navigation-dependent components, mock the navigation context
|
|
548
|
+
- For platform-specific stories, use \`Platform.OS\` checks or separate story files
|
|
549
|
+
- Test on both iOS and Android when possible
|
|
550
|
+
|
|
551
|
+
### Key Requirements
|
|
552
|
+
|
|
553
|
+
- **Node.js 20+**, **TypeScript 4.9+**
|
|
554
|
+
- React Native 0.72+
|
|
555
|
+
- Storybook 10+
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// src/metro/withStorybook.ts
|
|
561
|
+
var withStorybook_exports = {};
|
|
562
|
+
__export(withStorybook_exports, {
|
|
563
|
+
withStorybook: () => withStorybook
|
|
564
|
+
});
|
|
565
|
+
module.exports = __toCommonJS(withStorybook_exports);
|
|
566
|
+
var path2 = __toESM(require("path"));
|
|
567
|
+
var import_generate = __toESM(require_generate());
|
|
568
|
+
var import_common3 = require("storybook/internal/common");
|
|
569
|
+
var import_telemetry = require("storybook/internal/telemetry");
|
|
570
|
+
|
|
571
|
+
// src/metro/channelServer.ts
|
|
572
|
+
var import_ws = require("ws");
|
|
573
|
+
var import_node_http = require("http");
|
|
574
|
+
init_buildIndex();
|
|
575
|
+
|
|
576
|
+
// src/metro/mcpServer.ts
|
|
577
|
+
var import_consumers = require("stream/consumers");
|
|
578
|
+
function toHeaderEntries(nodeHeaders) {
|
|
579
|
+
const entries = [];
|
|
580
|
+
for (const [key, value] of Object.entries(nodeHeaders)) {
|
|
581
|
+
if (value === void 0) continue;
|
|
582
|
+
entries.push([key, Array.isArray(value) ? value.join(", ") : value]);
|
|
583
|
+
}
|
|
584
|
+
return entries;
|
|
585
|
+
}
|
|
586
|
+
async function incomingMessageToWebRequest(req) {
|
|
587
|
+
const host = req.headers.host || "localhost";
|
|
588
|
+
const isTLS = "encrypted" in req.socket && req.socket.encrypted;
|
|
589
|
+
const protocol = isTLS ? "https" : "http";
|
|
590
|
+
const url = new URL(req.url || "/", `${protocol}://${host}`);
|
|
591
|
+
const bodyBuffer = await (0, import_consumers.buffer)(req);
|
|
592
|
+
return new Request(url, {
|
|
593
|
+
method: req.method,
|
|
594
|
+
headers: toHeaderEntries(req.headers),
|
|
595
|
+
body: bodyBuffer.length > 0 ? new Uint8Array(bodyBuffer) : void 0
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
async function webResponseToServerResponse(webResponse, nodeResponse) {
|
|
599
|
+
nodeResponse.statusCode = webResponse.status;
|
|
600
|
+
webResponse.headers.forEach((value, key) => {
|
|
601
|
+
nodeResponse.setHeader(key, value);
|
|
602
|
+
});
|
|
603
|
+
if (webResponse.body) {
|
|
604
|
+
const reader = webResponse.body.getReader();
|
|
605
|
+
try {
|
|
606
|
+
while (true) {
|
|
607
|
+
const { done, value } = await reader.read();
|
|
608
|
+
if (done) break;
|
|
609
|
+
nodeResponse.write(value);
|
|
610
|
+
}
|
|
611
|
+
} finally {
|
|
612
|
+
reader.releaseLock();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
nodeResponse.end();
|
|
616
|
+
}
|
|
617
|
+
function createMcpHandler(configPath, wss) {
|
|
618
|
+
let handler = null;
|
|
619
|
+
let initPromise = null;
|
|
620
|
+
async function init() {
|
|
621
|
+
if (handler) return;
|
|
622
|
+
if (initPromise) {
|
|
623
|
+
await initPromise;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
initPromise = (async () => {
|
|
627
|
+
try {
|
|
628
|
+
const [
|
|
629
|
+
{ McpServer },
|
|
630
|
+
{ ValibotJsonSchemaAdapter },
|
|
631
|
+
{ HttpTransport },
|
|
632
|
+
{
|
|
633
|
+
addListAllDocumentationTool,
|
|
634
|
+
addGetDocumentationTool,
|
|
635
|
+
addGetComponentStoryDocumentationTool
|
|
636
|
+
},
|
|
637
|
+
{ storyInstructions: storyInstructions2 },
|
|
638
|
+
{ buildIndex: buildIndex2 },
|
|
639
|
+
valibot,
|
|
640
|
+
{ experimental_manifests }
|
|
641
|
+
] = await Promise.all([
|
|
642
|
+
import("tmcp"),
|
|
643
|
+
import("@tmcp/adapter-valibot"),
|
|
644
|
+
import("@tmcp/transport-http"),
|
|
645
|
+
import("@storybook/mcp"),
|
|
646
|
+
Promise.resolve().then(() => (init_storyInstructions(), storyInstructions_exports)),
|
|
647
|
+
Promise.resolve().then(() => (init_buildIndex(), buildIndex_exports)),
|
|
648
|
+
import("valibot"),
|
|
649
|
+
import("@storybook/react/preset")
|
|
650
|
+
]);
|
|
651
|
+
const manifestProvider = async (_request, manifestPath) => {
|
|
652
|
+
if (manifestPath.includes("docs.json")) {
|
|
653
|
+
throw new Error("Docs manifest not available in React Native Storybook");
|
|
654
|
+
}
|
|
655
|
+
const index = await buildIndex2({ configPath });
|
|
656
|
+
const entries = Object.values(index.entries);
|
|
657
|
+
const manifest = await experimental_manifests({}, { manifestEntries: entries });
|
|
658
|
+
return JSON.stringify(manifest.components);
|
|
659
|
+
};
|
|
660
|
+
const server = new McpServer(
|
|
661
|
+
{
|
|
662
|
+
name: "@storybook/react-native",
|
|
663
|
+
version: "1.0.0",
|
|
664
|
+
description: "Storybook React Native MCP server"
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
adapter: new ValibotJsonSchemaAdapter(),
|
|
668
|
+
capabilities: {
|
|
669
|
+
tools: { listChanged: true }
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
).withContext();
|
|
673
|
+
addListAllDocumentationTool(server);
|
|
674
|
+
addGetDocumentationTool(server);
|
|
675
|
+
addGetComponentStoryDocumentationTool(server);
|
|
676
|
+
server.tool(
|
|
677
|
+
{
|
|
678
|
+
name: "get-storybook-story-instructions",
|
|
679
|
+
title: "React Native Storybook Story Instructions",
|
|
680
|
+
description: "Get instructions for writing React Native Storybook stories. Call this before creating or modifying story files (.stories.tsx, .stories.ts)."
|
|
681
|
+
},
|
|
682
|
+
async () => ({
|
|
683
|
+
content: [{ type: "text", text: storyInstructions2 }]
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
if (wss) {
|
|
687
|
+
const broadcastEvent = (event) => {
|
|
688
|
+
const message = JSON.stringify(event);
|
|
689
|
+
wss.clients.forEach((client) => {
|
|
690
|
+
if (client.readyState === 1) {
|
|
691
|
+
client.send(message);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
server.tool(
|
|
696
|
+
{
|
|
697
|
+
name: "select-story",
|
|
698
|
+
title: "Select Story",
|
|
699
|
+
description: 'Select and display a story on the connected device. Use the story ID in the format "title--name" (e.g. "button--primary"). Use the list-all-documentation tool to discover available components and stories.',
|
|
700
|
+
schema: valibot.object({ storyId: valibot.string() })
|
|
701
|
+
},
|
|
702
|
+
async ({ storyId }) => {
|
|
703
|
+
try {
|
|
704
|
+
const index = await buildIndex2({ configPath });
|
|
705
|
+
if (!index.entries[storyId]) {
|
|
706
|
+
const availableIds = Object.keys(index.entries).slice(0, 10);
|
|
707
|
+
return {
|
|
708
|
+
content: [
|
|
709
|
+
{
|
|
710
|
+
type: "text",
|
|
711
|
+
text: `Story "${storyId}" not found. Available stories include: ${availableIds.join(", ")}` + (Object.keys(index.entries).length > 10 ? ", ..." : "")
|
|
712
|
+
}
|
|
713
|
+
],
|
|
714
|
+
isError: true
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
broadcastEvent({
|
|
718
|
+
type: "setCurrentStory",
|
|
719
|
+
args: [{ storyId, viewMode: "story" }]
|
|
720
|
+
});
|
|
721
|
+
const entry = index.entries[storyId];
|
|
722
|
+
return {
|
|
723
|
+
content: [
|
|
724
|
+
{
|
|
725
|
+
type: "text",
|
|
726
|
+
text: `Selected story "${entry.name}" (${entry.title}) on connected devices.`
|
|
727
|
+
}
|
|
728
|
+
]
|
|
729
|
+
};
|
|
730
|
+
} catch (error) {
|
|
731
|
+
return {
|
|
732
|
+
content: [
|
|
733
|
+
{
|
|
734
|
+
type: "text",
|
|
735
|
+
text: `Failed to select story: ${error instanceof Error ? error.message : String(error)}`
|
|
736
|
+
}
|
|
737
|
+
],
|
|
738
|
+
isError: true
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
const transport = new HttpTransport(server, { path: null });
|
|
745
|
+
handler = (req) => transport.respond(req, {
|
|
746
|
+
request: req,
|
|
747
|
+
manifestProvider
|
|
748
|
+
});
|
|
749
|
+
console.log("[Storybook] MCP server initialized");
|
|
750
|
+
} catch (error) {
|
|
751
|
+
initPromise = null;
|
|
752
|
+
console.error("[Storybook] Failed to initialize MCP server:", error);
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
})();
|
|
756
|
+
await initPromise;
|
|
757
|
+
}
|
|
758
|
+
async function handleMcpRequest(req, res) {
|
|
759
|
+
try {
|
|
760
|
+
await init();
|
|
761
|
+
if (!handler) {
|
|
762
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
763
|
+
res.end(JSON.stringify({ error: "MCP handler not initialized" }));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const webRequest = await incomingMessageToWebRequest(req);
|
|
767
|
+
const webResponse = await handler(webRequest);
|
|
768
|
+
await webResponseToServerResponse(webResponse, res);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
console.error("[Storybook] MCP request failed:", error);
|
|
771
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
772
|
+
res.end(JSON.stringify({ error: "MCP request failed" }));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function preInit() {
|
|
776
|
+
init().catch(
|
|
777
|
+
(e) => console.warn("[Storybook] MCP pre-initialization failed (will retry on first request):", e)
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return { handleMcpRequest, preInit };
|
|
781
|
+
}
|
|
462
782
|
|
|
463
783
|
// src/metro/channelServer.ts
|
|
464
784
|
function createChannelServer({
|
|
465
785
|
port = 7007,
|
|
466
786
|
host = void 0,
|
|
467
|
-
configPath
|
|
787
|
+
configPath,
|
|
788
|
+
experimental_mcp = false,
|
|
789
|
+
websockets = true
|
|
468
790
|
}) {
|
|
469
|
-
const httpServer = (0, import_node_http.createServer)(
|
|
791
|
+
const httpServer = (0, import_node_http.createServer)();
|
|
792
|
+
const wss = websockets ? new import_ws.WebSocketServer({ server: httpServer }) : null;
|
|
793
|
+
const mcpServer = experimental_mcp ? createMcpHandler(configPath, wss ?? void 0) : null;
|
|
794
|
+
httpServer.on("request", async (req, res) => {
|
|
470
795
|
if (req.method === "OPTIONS") {
|
|
471
796
|
res.writeHead(204);
|
|
472
797
|
res.end();
|
|
@@ -485,6 +810,11 @@ function createChannelServer({
|
|
|
485
810
|
return;
|
|
486
811
|
}
|
|
487
812
|
if (req.method === "POST" && req.url === "/send-event") {
|
|
813
|
+
if (!wss) {
|
|
814
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
815
|
+
res.end(JSON.stringify({ success: false, error: "WebSockets are disabled" }));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
488
818
|
let body = "";
|
|
489
819
|
req.on("data", (chunk) => {
|
|
490
820
|
body += chunk.toString();
|
|
@@ -503,31 +833,36 @@ function createChannelServer({
|
|
|
503
833
|
});
|
|
504
834
|
return;
|
|
505
835
|
}
|
|
836
|
+
if (mcpServer && req.url === "/mcp" && (req.method === "POST" || req.method === "GET")) {
|
|
837
|
+
await mcpServer.handleMcpRequest(req, res);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
506
840
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
507
841
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
508
842
|
});
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
});
|
|
512
|
-
setInterval(function ping() {
|
|
513
|
-
wss.clients.forEach(function each(client) {
|
|
514
|
-
if (client.readyState === import_ws.WebSocket.OPEN) {
|
|
515
|
-
client.send(JSON.stringify({ type: "ping", args: [] }));
|
|
516
|
-
}
|
|
843
|
+
if (wss) {
|
|
844
|
+
wss.on("error", () => {
|
|
517
845
|
});
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
846
|
+
setInterval(function ping() {
|
|
847
|
+
wss.clients.forEach(function each(client) {
|
|
848
|
+
if (client.readyState === import_ws.WebSocket.OPEN) {
|
|
849
|
+
client.send(JSON.stringify({ type: "ping", args: [] }));
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
}, 1e4);
|
|
853
|
+
wss.on("connection", function connection(ws) {
|
|
854
|
+
console.log("WebSocket connection established");
|
|
855
|
+
ws.on("error", console.error);
|
|
856
|
+
ws.on("message", function message(data) {
|
|
857
|
+
try {
|
|
858
|
+
const json = JSON.parse(data.toString());
|
|
859
|
+
wss.clients.forEach((wsClient) => wsClient.send(JSON.stringify(json)));
|
|
860
|
+
} catch (error) {
|
|
861
|
+
console.error(error);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
529
864
|
});
|
|
530
|
-
}
|
|
865
|
+
}
|
|
531
866
|
httpServer.on("error", (error) => {
|
|
532
867
|
if (error.code === "EADDRINUSE") {
|
|
533
868
|
console.warn(
|
|
@@ -538,8 +873,10 @@ function createChannelServer({
|
|
|
538
873
|
}
|
|
539
874
|
});
|
|
540
875
|
httpServer.listen(port, host, () => {
|
|
541
|
-
|
|
876
|
+
const protocol = wss ? "WebSocket" : "HTTP";
|
|
877
|
+
console.log(`${protocol} server listening on ${host ?? "localhost"}:${port}`);
|
|
542
878
|
});
|
|
879
|
+
mcpServer?.preInit();
|
|
543
880
|
return wss;
|
|
544
881
|
}
|
|
545
882
|
|
|
@@ -557,7 +894,8 @@ function withStorybook(config, options = {
|
|
|
557
894
|
useJs = false,
|
|
558
895
|
enabled = true,
|
|
559
896
|
docTools = true,
|
|
560
|
-
liteMode = false
|
|
897
|
+
liteMode = false,
|
|
898
|
+
experimental_mcp = false
|
|
561
899
|
} = options;
|
|
562
900
|
const disableTelemetry = (0, import_common3.optionalEnvToBoolean)(process.env.STORYBOOK_DISABLE_TELEMETRY);
|
|
563
901
|
if (!disableTelemetry && enabled) {
|
|
@@ -598,17 +936,31 @@ function withStorybook(config, options = {
|
|
|
598
936
|
}
|
|
599
937
|
};
|
|
600
938
|
}
|
|
601
|
-
if (websockets) {
|
|
602
|
-
const port = websockets === "auto" ? 7007 : websockets
|
|
603
|
-
const host = websockets === "auto" ? "auto" : websockets
|
|
604
|
-
createChannelServer({
|
|
605
|
-
|
|
939
|
+
if (websockets || experimental_mcp) {
|
|
940
|
+
const port = websockets === "auto" ? 7007 : websockets?.port ?? 7007;
|
|
941
|
+
const host = websockets === "auto" ? "auto" : websockets?.host;
|
|
942
|
+
createChannelServer({
|
|
943
|
+
port,
|
|
944
|
+
host: host === "auto" ? void 0 : host,
|
|
606
945
|
configPath,
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
host,
|
|
610
|
-
port
|
|
946
|
+
experimental_mcp,
|
|
947
|
+
websockets: Boolean(websockets)
|
|
611
948
|
});
|
|
949
|
+
if (websockets) {
|
|
950
|
+
(0, import_generate.generate)({
|
|
951
|
+
configPath,
|
|
952
|
+
useJs,
|
|
953
|
+
docTools,
|
|
954
|
+
host,
|
|
955
|
+
port
|
|
956
|
+
});
|
|
957
|
+
} else {
|
|
958
|
+
(0, import_generate.generate)({
|
|
959
|
+
configPath,
|
|
960
|
+
useJs,
|
|
961
|
+
docTools
|
|
962
|
+
});
|
|
963
|
+
}
|
|
612
964
|
} else {
|
|
613
965
|
(0, import_generate.generate)({
|
|
614
966
|
configPath,
|
package/dist/node.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ interface ChannelServerOptions {
|
|
|
17
17
|
* The path to the Storybook config folder.
|
|
18
18
|
*/
|
|
19
19
|
configPath: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to enable MCP (Model Context Protocol) server support.
|
|
22
|
+
* When enabled, adds an /mcp endpoint.
|
|
23
|
+
*/
|
|
24
|
+
experimental_mcp?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether to enable WebSocket support.
|
|
27
|
+
* When false, starts only the HTTP server endpoints.
|
|
28
|
+
*/
|
|
29
|
+
websockets?: boolean;
|
|
20
30
|
}
|
|
21
31
|
/**
|
|
22
32
|
* Creates a channel server for syncing storybook instances and sending events.
|
|
@@ -24,14 +34,17 @@ interface ChannelServerOptions {
|
|
|
24
34
|
* - WebSocket: broadcasts all received messages to all connected clients
|
|
25
35
|
* - POST /send-event: sends an event to all WebSocket clients
|
|
26
36
|
* - GET /index.json: returns the story index built from story files
|
|
37
|
+
* - POST /mcp: MCP endpoint for AI agent integration (when experimental_mcp option is enabled)
|
|
27
38
|
*
|
|
28
39
|
* @param options - Configuration options for the channel server.
|
|
29
40
|
* @param options.port - The port to listen on.
|
|
30
41
|
* @param options.host - The host to bind to.
|
|
31
42
|
* @param options.configPath - The path to the Storybook config folder.
|
|
32
|
-
* @
|
|
43
|
+
* @param options.experimental_mcp - Whether to enable MCP server support.
|
|
44
|
+
* @param options.websockets - Whether to enable WebSocket server support.
|
|
45
|
+
* @returns The created WebSocketServer instance, or null when websockets are disabled.
|
|
33
46
|
*/
|
|
34
|
-
declare function createChannelServer({ port, host, configPath, }: ChannelServerOptions): WebSocketServer;
|
|
47
|
+
declare function createChannelServer({ port, host, configPath, experimental_mcp, websockets, }: ChannelServerOptions): WebSocketServer | null;
|
|
35
48
|
|
|
36
49
|
declare function buildIndex({ configPath }: {
|
|
37
50
|
configPath: string;
|