@storybook/addon-mcp 0.0.2 → 0.0.3

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 (2) hide show
  1. package/dist/preset.js +100 -57
  2. package/package.json +2 -2
package/dist/preset.js CHANGED
@@ -5,6 +5,7 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
5
5
  import path from 'path';
6
6
  import { storyNameFromExport } from 'storybook/internal/csf';
7
7
  import { logger } from 'storybook/internal/node-logger';
8
+ import { telemetry } from 'storybook/internal/telemetry';
8
9
 
9
10
  var __defProp = Object.defineProperty;
10
11
  var __export = (target, all) => {
@@ -15,7 +16,7 @@ var __export = (target, all) => {
15
16
  // package.json
16
17
  var package_default = {
17
18
  name: "@storybook/addon-mcp",
18
- version: "0.0.2"};
19
+ version: "0.0.3"};
19
20
 
20
21
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
21
22
  var external_exports = {};
@@ -4059,6 +4060,30 @@ var NEVER = INVALID;
4059
4060
 
4060
4061
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js
4061
4062
  var zod_default = external_exports;
4063
+ async function collectTelemetry({
4064
+ event,
4065
+ mcpSessionId,
4066
+ ...payload
4067
+ }) {
4068
+ if (disableTelemetry) {
4069
+ return;
4070
+ }
4071
+ try {
4072
+ return await telemetry("addon-mcp", {
4073
+ event,
4074
+ mcpSessionId,
4075
+ client: mcpSessionIdToClientMap[mcpSessionId],
4076
+ ...payload
4077
+ });
4078
+ } catch (error) {
4079
+ logger.debug("Error collecting telemetry:", error);
4080
+ }
4081
+ }
4082
+ var mcpSessionIdToClientMap = {};
4083
+ var disableTelemetry = false;
4084
+ var setDisableTelemetry = (value = false) => {
4085
+ disableTelemetry = value;
4086
+ };
4062
4087
 
4063
4088
  // src/tools/get-story-urls.ts
4064
4089
  var inputStoriesSchema = zod_default.array(
@@ -4088,11 +4113,12 @@ function registerStoryUrlsTool({
4088
4113
  urls: outputUrlsSchema
4089
4114
  }
4090
4115
  },
4091
- async ({ stories }) => {
4116
+ async ({ stories }, { sessionId }) => {
4092
4117
  const index = await (await fetch(`${origin}/index.json`)).json();
4093
4118
  const entriesList = Object.values(index.entries);
4094
4119
  logger.debug("index entries found:", entriesList.length);
4095
4120
  const result = [];
4121
+ let foundStoryCount = 0;
4096
4122
  for (const {
4097
4123
  exportName,
4098
4124
  explicitStoryName,
@@ -4114,6 +4140,7 @@ function registerStoryUrlsTool({
4114
4140
  if (foundStoryId) {
4115
4141
  logger.debug("Found story ID:", foundStoryId);
4116
4142
  result.push(`${origin}/?path=/story/${foundStoryId}`);
4143
+ foundStoryCount++;
4117
4144
  } else {
4118
4145
  logger.debug("No story found");
4119
4146
  let errorMessage = `No story found for export name "${exportName}" with absolute file path "${absoluteStoryPath}"`;
@@ -4123,13 +4150,17 @@ function registerStoryUrlsTool({
4123
4150
  result.push(errorMessage);
4124
4151
  }
4125
4152
  }
4153
+ await collectTelemetry({
4154
+ event: "tool:getStoryUrls",
4155
+ mcpSessionId: sessionId,
4156
+ inputStoryCount: stories.length,
4157
+ outputStoryCount: foundStoryCount
4158
+ });
4126
4159
  return {
4127
- content: [
4128
- {
4129
- type: "text",
4130
- text: result.length > 1 ? `- ${result.join("\n- ")}` : result[0]
4131
- }
4132
- ],
4160
+ content: result.map((text) => ({
4161
+ type: "text",
4162
+ text
4163
+ })),
4133
4164
  // Note: Claude Code seems to ignore structuredContent at the moment https://github.com/anthropics/claude-code/issues/4427
4134
4165
  structuredContent: { urls: result }
4135
4166
  };
@@ -4137,46 +4168,12 @@ function registerStoryUrlsTool({
4137
4168
  );
4138
4169
  }
4139
4170
 
4140
- // src/tools/get-ui-building-instructions.ts
4141
- var INSTRUCTIONS = `
4142
- # Writing User Interfaces Components
4143
-
4144
- When writing UI components, prefer breaking larger components up into smaller parts.
4145
-
4146
- ALWAYS write a Storybook story for any component written. If editing a component, ensure appropriate changes have been made to stories for that component.
4147
-
4148
- When writing stories, use CSF3 importing types from '@storybook/nextjs-vite'. Here is a simple example:
4149
-
4150
- \`\`\`ts
4151
- import { Meta, StoryObj } from '@storybook/nextjs-vite';
4152
-
4153
- import { Break } from './Break';
4154
-
4155
- type Story = StoryObj<typeof Break>;
4156
-
4157
- const meta: Meta<typeof Break> = {
4158
- component: Break,
4159
- args: {},
4160
- };
4161
-
4162
- export default meta;
4163
-
4164
- export const Horizontal: Story = {
4165
- args: {
4166
- orientation: 'horizontal',
4167
- },
4168
- };
4169
- \`\`\`
4170
-
4171
- ALWAYS provide story links after any changes to stories files, including changes to existing stories.
4172
- After changing any UI components, ALWAYS search for related stories that might cover the changes you've made.
4173
- If you find any, provide the story links to the user. THIS IS VERY IMPORTANT, as it allows the user
4174
- to visually inspect the modifications you've made.
4175
- Even later in a session when changing UI components or stories that have already been linked to previously, YOU MUST PROVIDE THE LINKS AGAIN.
4176
- Use the ${GET_STORY_URLS_TOOL_NAME} tool /and provide links in the format \`[Story Name](<story_url_from_tool>)\`
4177
- whenever you create, modify, or update any story file.
4178
- `;
4179
- function registerUIBuildingTool(server) {
4171
+ // src/ui-building-instructions.md
4172
+ var ui_building_instructions_default = "# Storybook 9 Essential Changes for Story Writing\n\n## Package Consolidation\n\n### `Meta` and `StoryObj` imports\n\nUpdate story imports to use the framework package:\n\n```diff\n- import { Meta, StoryObj } from '{{RENDERER}}';\n+ import { Meta, StoryObj } from '{{FRAMEWORK}}';\n```\n\n### Test utility imports\n\nUpdate test imports to use `storybook/test` instead of `@storybook/test`\n\n```diff\n- import { fn } from '@storybook/test';\n+ import { fn } from 'storybook/test';\n```\n\n## Global State Changes\n\nThe `globals` annotation has be renamed to `initialGlobals`:\n\n```diff\n// .storybook/preview.js\nexport default {\n- globals: { theme: 'light' }\n+ initialGlobals: { theme: 'light' }\n};\n```\n\n## Autodocs Configuration\n\nInstead of `parameters.docs.autodocs` in main.js, use tags:\n\n```js\n// .storybook/preview.js or in individual stories\nexport default {\n tags: [\"autodocs\"], // generates autodocs for all stories\n};\n```\n\n## Mocking imports in Storybook\n\nTo mock imports in Storybook, use Storybook's mocking features. ALWAYS mock external dependencies to ensure stories render consistently.\n\n1. **Register in the mock in Storybook's preview file**:\n To mock dependendencies, you MUST register a module mock in `.storybook/preview.ts` (or equivalent):\n\n```js\nimport { sb } from \"storybook/test\";\n\n// Full mock (replaces with empty fn functions)\nsb.mock(import(\"some-library\"));\n```\n\n2. **Specify mock values in stories**:\n You can override the behaviour of the mocks per-story using `beforeEach` and the `mocked()` type function:\n\n```js\nimport { expect, mocked, fn } from 'storybook/test';\nimport { library } from 'some-library';\n\nconst meta = {\n component: AuthButton,\n beforeEach: async () => {\n mocked(library).mockResolvedValue({ user: 'data' });\n },\n};\n\nexport const LoggedIn: Story = {\n play: async ({ canvas }) => {\n await expect(library).toHaveBeenCalled();\n },\n};\n```\n\nBefore doing this ensure you have mocked the import in the preview file.\n\n## Key Requirements\n\n- **Node.js 20+**, **TypeScript 4.9+**, **Vite 5+**\n- React Native uses `.rnstorybook` directory\n- `afterEach` hook is now stable (was `experimental_afterEach`)\n\n# Story Linking Agent Behavior\n\n- ALWAYS provide story links after any changes to stories files, including changes to existing stories.\n- After changing any UI components, ALWAYS search for related stories that might cover the changes you've made. If you find any, provide the story links to the user. THIS IS VERY IMPORTANT, as it allows the user to visually inspect the changes you've made. Even later in a session when changing UI components or stories that have already been linked to previously, YOU MUST PROVIDE THE LINKS AGAIN.\n- Use the {{GET_STORY_URLS_TOOL_NAME}} tool to get the correct URLs for links to stories.\n";
4173
+ function registerUIBuildingTool({
4174
+ server,
4175
+ options
4176
+ }) {
4180
4177
  server.registerTool(
4181
4178
  "get_ui_building_instructions",
4182
4179
  {
@@ -4187,23 +4184,69 @@ function registerUIBuildingTool(server) {
4187
4184
  limited to adding or updating new components, pages, screens or layouts.`,
4188
4185
  inputSchema: {}
4189
4186
  },
4190
- async () => ({
4191
- content: [{ type: "text", text: INSTRUCTIONS }]
4192
- })
4187
+ async (_, { sessionId }) => {
4188
+ await collectTelemetry({
4189
+ event: "tool:getUIBuildingInstructions",
4190
+ mcpSessionId: sessionId
4191
+ });
4192
+ const frameworkPreset = await options.presets.apply("framework");
4193
+ const framework = typeof frameworkPreset === "string" ? frameworkPreset : frameworkPreset?.name;
4194
+ const renderer = frameworkToRendererMap[framework];
4195
+ if (!framework || !renderer) {
4196
+ logger.debug("Unable to determine framework or renderer", {
4197
+ frameworkPreset,
4198
+ framework,
4199
+ renderer
4200
+ });
4201
+ }
4202
+ const uiInstructions = ui_building_instructions_default.replace("{{FRAMEWORK}}", framework).replace("{{RENDERER}}", renderer ?? framework).replace("{{GET_STORY_URLS_TOOL_NAME}}", GET_STORY_URLS_TOOL_NAME);
4203
+ return {
4204
+ content: [{ type: "text", text: uiInstructions }]
4205
+ };
4206
+ }
4193
4207
  );
4194
4208
  }
4209
+ var frameworkToRendererMap = {
4210
+ "@storybook/react-vite": "@storybook/react",
4211
+ "@storybook/react-webpack5": "@storybook/react",
4212
+ "@storybook/nextjs": "@storybook/react",
4213
+ "@storybook/nextjs-vite": "@storybook/react",
4214
+ "@storybook/react-native-web-vite": "@storybook/react",
4215
+ "@storybook/vue3-vite": "@storybook/vue3",
4216
+ "@nuxtjs/storybook": "@storybook/vue3",
4217
+ "@storybook/angular": "@storybook/angular",
4218
+ "@storybook/svelte-vite": "@storybook/svelte",
4219
+ "@storybook/sveltekit": "@storybook/svelte",
4220
+ "@storybook/preact-vite": "@storybook/preact",
4221
+ "@storybook/web-components-vite": "@storybook/web-components",
4222
+ "@storybook/html-vite": "@storybook/html"
4223
+ };
4195
4224
 
4196
4225
  // src/mcp-handler.ts
4197
- function createMcpServer(options) {
4226
+ async function createMcpServer(options, client) {
4198
4227
  const transport = new StreamableHTTPServerTransport({
4199
4228
  sessionIdGenerator: () => randomUUID(),
4200
- onsessioninitialized: (sessionId) => {
4229
+ onsessioninitialized: async (sessionId) => {
4201
4230
  transports[sessionId] = transport;
4231
+ const { disableTelemetry: disableTelemetry2 } = await options.presets.apply(
4232
+ "core",
4233
+ {}
4234
+ );
4235
+ setDisableTelemetry(disableTelemetry2);
4236
+ mcpSessionIdToClientMap[sessionId] = client;
4237
+ await collectTelemetry({
4238
+ event: "session:initialized",
4239
+ mcpSessionId: sessionId
4240
+ });
4202
4241
  }
4203
4242
  });
4204
4243
  transport.onclose = () => {
4205
- if (transport.sessionId) {
4206
- delete transports[transport.sessionId];
4244
+ if (!transport.sessionId) {
4245
+ return;
4246
+ }
4247
+ delete transports[transport.sessionId];
4248
+ if (mcpSessionIdToClientMap[transport.sessionId]) {
4249
+ delete mcpSessionIdToClientMap[transport.sessionId];
4207
4250
  }
4208
4251
  };
4209
4252
  const server = new McpServer({
@@ -4211,7 +4254,7 @@ function createMcpServer(options) {
4211
4254
  version: package_default.version
4212
4255
  });
4213
4256
  registerStoryUrlsTool({ server, options });
4214
- registerUIBuildingTool(server);
4257
+ registerUIBuildingTool({ server, options });
4215
4258
  server.connect(transport);
4216
4259
  return transport;
4217
4260
  }
@@ -4228,7 +4271,7 @@ var handlePostRequest = async (req, res, options) => {
4228
4271
  if (sessionId && transports[sessionId]) {
4229
4272
  transport = transports[sessionId];
4230
4273
  } else if (!sessionId && isInitializeRequest(body)) {
4231
- transport = await createMcpServer(options);
4274
+ transport = await createMcpServer(options, body.params.clientInfo.name);
4232
4275
  } else {
4233
4276
  res.statusCode = 400;
4234
4277
  res.end(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/addon-mcp",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "everything you need to build a Storybook addon",
5
5
  "keywords": [
6
6
  "storybook-addons",
@@ -86,7 +86,7 @@
86
86
  "build-storybook": "storybook build",
87
87
  "build:watch": "pnpm run build -- --watch",
88
88
  "changeset": "changeset",
89
- "inspect": "mcp-inspector --config .mcp.json",
89
+ "inspect": "mcp-inspector --transport streamable-http --server-url http://localhost:6006/mcp",
90
90
  "release": "pnpm run build && pnpm changeset publish",
91
91
  "start": "run-p build:watch \"storybook --quiet\"",
92
92
  "storybook": "storybook dev --port 6006 --loglevel debug",