@storybook/addon-mcp
Version:
Help agents automatically write and test stories for your UI components
175 lines (172 loc) • 6.07 kB
JavaScript
// src/constants.ts
var MCP_APP_PARAM = "mcp-app", MCP_APP_SIZE_CHANGED_EVENT = "storybook-mcp:size-changed";
// package.json
var package_default = {
name: "@storybook/addon-mcp",
version: "10.6.5",
description: "Help agents automatically write and test stories for your UI components",
keywords: [
"ai",
"mcp",
"storybook-addon"
],
homepage: "https://github.com/storybookjs/storybook/tree/next/code/addons/mcp",
bugs: {
url: "https://github.com/storybookjs/storybook/issues"
},
repository: {
type: "git",
url: "https://github.com/storybookjs/storybook.git",
directory: "code/addons/mcp"
},
funding: {
type: "opencollective",
url: "https://opencollective.com/storybook"
},
license: "MIT",
author: "Storybook Core Team",
type: "module",
exports: {
"./internal/preview-stories-app-script": "./dist/tools/preview-stories/preview-stories-app-script.js",
"./package.json": "./package.json",
"./preset": "./dist/preset.js"
},
files: [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts",
"!src/**/*"
],
dependencies: {
"@tmcp/adapter-valibot": "^0.1.5",
"@tmcp/transport-http": "^0.8.5",
tmcp: "^1.19.4",
valibot: "^1.4.0"
},
devDependencies: {
"@storybook/addon-a11y": "workspace:*",
"@storybook/addon-vitest": "workspace:*",
storybook: "workspace:*",
typescript: "^6.0.3"
},
peerDependencies: {
"@storybook/addon-vitest": "workspace:^",
storybook: "workspace:^"
},
peerDependenciesMeta: {
"@storybook/addon-vitest": {
optional: !0
}
},
publishConfig: {
access: "public"
},
mcpName: "io.github.storybookjs/addon-mcp",
storybook: {
displayName: "Addon MCP",
supportedFrameworks: [
"react",
"vue3",
"angular",
"web-components",
"html",
"svelte",
"preact",
"react-native"
],
icon: "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png"
}
};
// src/tools/preview-stories/preview-stories-app-script.ts
var LATEST_PROTOCOL_VERSION = "2025-11-21", METHODS = {
// Lifecycle
INITIALIZE: "ui/initialize",
INITIALIZED: "ui/notifications/initialized",
// Tool data (Host -> Guest)
TOOL_INPUT: "ui/notifications/tool-input",
TOOL_INPUT_PARTIAL: "ui/notifications/tool-input-partial",
TOOL_RESULT: "ui/notifications/tool-result",
TOOL_CANCELLED: "ui/notifications/tool-cancelled",
// Context & UI
HOST_CONTEXT_CHANGED: "ui/notifications/host-context-changed",
SIZE_CHANGED: "ui/notifications/size-changed",
RESOURCE_TEARDOWN: "ui/resource-teardown",
// Standard MCP methods
TOOLS_CALL: "tools/call",
NOTIFICATIONS_MESSAGE: "notifications/message",
OPEN_LINK: "ui/open-link",
MESSAGE: "ui/message"
}, nextId = 1;
function sendHostRequest(method, params) {
let id = nextId++, { promise, resolve, reject } = Promise.withResolvers();
return window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*"), window.addEventListener("message", function listener(event) {
event.data?.id === id && (window.removeEventListener("message", listener), event.data?.result ? resolve(event.data.result) : event.data?.error && reject(new Error(String(event.data.error))));
}), promise;
}
function sendHostNotification(method, params) {
window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
}
function onHostNotification(method, handler) {
window.addEventListener("message", function(event) {
event.data?.method === method && handler(event.data.params);
});
}
var initializeResult = await sendHostRequest(METHODS.INITIALIZE, {
appInfo: {
name: "storybook-story-preview",
version: package_default.version
},
appCapabilities: {},
protocolVersion: LATEST_PROTOCOL_VERSION
});
applyHostStyles(initializeResult?.hostContext);
onHostNotification(METHODS.TOOL_RESULT, loadStoryIframes);
onHostNotification(METHODS.HOST_CONTEXT_CHANGED, applyHostStyles);
sendHostNotification(METHODS.INITIALIZED, {});
window.addEventListener("message", function(event) {
if (event.data?.type !== MCP_APP_SIZE_CHANGED_EVENT)
return;
let iframes = document.querySelectorAll(".story-iframe"), hasResizedIframes = !1;
for (let iframe of iframes)
if (iframe.contentWindow === event.source) {
iframe.style.height = (event.data.height ?? 0) + "px", hasResizedIframes = !0;
break;
}
hasResizedIframes && resizeApp();
});
function applyHostStyles(hostContext) {
if (hostContext?.theme && document.documentElement.setAttribute("data-theme", hostContext.theme), !!hostContext?.styles?.variables) {
for (let [key, value] of Object.entries(hostContext.styles.variables))
value && document.documentElement.style.setProperty(key, value);
resizeApp();
}
}
function resizeApp() {
console.log("Resizing app to fit content", {
width: document.body.scrollWidth,
height: document.body.scrollHeight
}), sendHostNotification(METHODS.SIZE_CHANGED, {
width: document.body.scrollWidth,
height: document.body.scrollHeight
});
}
function loadStoryIframes(params) {
let stories = params.structuredContent?.stories;
if (!stories || stories.length === 0) {
console.warn("No preview URLs found in tool result.");
return;
}
let template = document.getElementById("preview-template");
for (let storyResult of stories) {
if ("error" in storyResult) {
console.warn("Skipping story with error:", storyResult.error);
continue;
}
let clone = template.content.cloneNode(!0), article = clone.querySelector("article"), heading = clone.querySelector("h1"), iframe = clone.querySelector("iframe");
heading.textContent = `${storyResult.title} - ${storyResult.name}`, iframe.style.width = "100%", iframe.style.height = "0";
let iframeSrc = storyResult.previewUrl.replace("/?path=/story/", "/iframe.html?id="), url = new URL(iframeSrc);
url.searchParams.set(MCP_APP_PARAM, "true"), iframe.src = url.toString(), document.body.appendChild(article);
}
resizeApp();
}