#
tokens: 2724/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── dist
│   └── index.js
├── getStories.ts
├── index.ts
├── package-lock.json
├── package.json
├── README.md
├── test.ts
├── tsconfig.json
└── tsdown.config.ts
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | cache/
2 | node_modules/
3 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Storybook MCP Server
 2 | 
 3 | A Model Context Protocol server for interacting with Storybook.
 4 | 
 5 | ## Usage
 6 | 
 7 | ```json
 8 | {
 9 |   "mcpServers": {
10 |     "storybook": {
11 |       "command": "npx",
12 |       "args": ["-y", "mcp-storybook@latest"]
13 |     }
14 |   }
15 | }
16 | ```
17 | 
18 | ## Tools
19 | 
20 | ### get-stories
21 | 
22 | Retrieves a list of stories from a Storybook configuration.
23 | 
24 | **Parameters:**
25 | 
26 | - `configDir` (string): Absolute path to directory containing the .storybook config folder
27 | 
28 | **Returns:**
29 | 
30 | - List of story ids
31 | 
32 | ## Technical Details
33 | 
34 | - Built using `@modelcontextprotocol/sdk`
35 | - Uses stdio transport for communication
36 | - Caches data in `./cache` relative to script location
37 | 
38 | Unfortunately we need to install any framework that might be encountered to this package so that the index building doesn't fail.
39 | 
```

--------------------------------------------------------------------------------
/tsdown.config.ts:
--------------------------------------------------------------------------------

```typescript
1 | import { defineConfig } from "tsdown";
2 | 
3 | export default defineConfig({
4 |   entry: ["./index.ts"],
5 |   outDir: "./dist",
6 |   format: ["cjs"],
7 |   target: "node22",
8 | });
9 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "es6",
 4 |     "module": "commonjs",
 5 |     "moduleResolution": "node",
 6 |     "lib": ["ESNext"],
 7 |     "noEmit": true,
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["index.ts"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getStories } from "./getStories";
 2 | 
 3 | const run = async () => {
 4 |   const examples = [
 5 |     // Absolute path
 6 |     "/Users/danielwilliams/Workspace/storybook/repro/sb-react-ex/.storybook",
 7 |     // Relative path
 8 |     "../repro/sb-react-ex/.storybook",
 9 |     // this shit
10 |     "./../repro/sb-react-ex/.storybook",
11 |   ];
12 | 
13 |   for (const configDir of examples) {
14 |     console.log(`\nTesting configDir: ${configDir}`);
15 |     const stories = await getStories({ configDir });
16 |     console.log(stories);
17 |   }
18 | };
19 | 
20 | run();
21 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "main": "dist/index.js",
 3 |   "name": "mcp-storybook",
 4 |   "type": "commonjs",
 5 |   "version": "0.0.12",
 6 |   "private": false,
 7 |   "bin": "dist/index.js",
 8 |   "scripts": {
 9 |     "start": "node dist/index.js",
10 |     "dev": "tsdown --watch",
11 |     "build": "tsdown",
12 |     "inspector": "npx @modelcontextprotocol/inspector",
13 |     "test": "bun test.ts"
14 |   },
15 |   "dependencies": {
16 |     "@modelcontextprotocol/sdk": "^1.13.2",
17 |     "@storybook/csf": "^0.1.13",
18 |     "storybook": "^9.0.13",
19 |     "zod": "^3.25.67",
20 |     "@storybook/react-vite": "^9.0.13",
21 |     "@storybook/react-native": "^9.0.9",
22 |     "@storybook/react-native-web-vite": "^9.0.13",
23 |     "@storybook/nextjs": "^9.0.13"
24 |   },
25 |   "devDependencies": {
26 |     "tsdown": "^0.12.9",
27 |     "@types/node": "^22.13.5",
28 |     "typescript": "^5.8.3",
29 |     "concurrently": "^9.2.0"
30 |   }
31 | }
32 | 
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 4 | import { z } from "zod";
 5 | import { getStories } from "./getStories";
 6 | 
 7 | const server = new McpServer({
 8 |   name: "storybook",
 9 |   version: "1.0.0",
10 | });
11 | 
12 | server.tool(
13 |   "get-stories",
14 |   "Get a list of story ids for your storybook project, use this to list stories.",
15 |   {
16 |     configDir: z
17 |       .string()
18 |       .min(1)
19 |       .describe(
20 |         "The absolute path to directory containing the .storybook config folder (/the-full-path/to/your/project/.storybook)."
21 |       )
22 |       .default(`${process.cwd()}/.storybook`),
23 |   },
24 |   async ({ configDir }) => {
25 |     return {
26 |       content: [
27 |         {
28 |           type: "text",
29 |           text: await getStories({ configDir }),
30 |         },
31 |       ],
32 |     };
33 |   }
34 | );
35 | 
36 | server.tool(
37 |   "get-story-url",
38 |   "Get the URL for a story by its story id.",
39 |   {
40 |     storyId: z.string().min(1).describe("The story id to get the URL for."),
41 |     baseUrl: z
42 |       .string()
43 |       .min(1)
44 |       .describe("Base URL of the Storybook instance.")
45 |       .default("http://localhost:6006"),
46 |   },
47 |   async ({ storyId, baseUrl }) => {
48 |     return {
49 |       content: [
50 |         {
51 |           type: "text",
52 |           text: `${baseUrl}/?path=/story/${storyId}`,
53 |         },
54 |       ],
55 |     };
56 |   }
57 | );
58 | 
59 | // server.tool(
60 | //   'go-to-story',
61 | //   'Go to a story',
62 | //   {
63 | //     storyKind: z.string(),
64 | //     storyName: z.string(),
65 | //   },
66 | //   async ({ storyKind, storyName }) => {
67 | //     const storyId = toId(storyKind, storyName);
68 | //     return {
69 | //       content: [],
70 | //     };
71 | //   }
72 | // );
73 | 
74 | async function main() {
75 |   const transport = new StdioServerTransport();
76 |   console.error("Server starting...");
77 |   await server.connect(transport);
78 |   console.error("Server started successfully");
79 | }
80 | 
81 | main().catch((error) => {
82 |   console.error("Fatal error in main():", error);
83 |   process.exit(1);
84 | });
85 | 
```

--------------------------------------------------------------------------------
/getStories.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { StoryIndex } from "storybook/internal/types";
 2 | import * as path from "path";
 3 | import * as fs from "fs";
 4 | 
 5 | async function getIndex({ configDir }: { configDir: string }) {
 6 |   process.env.CACHE_DIR = __dirname + "/cache";
 7 |   const { buildIndex } = require("storybook/internal/core-server");
 8 |   const index: StoryIndex = await buildIndex({
 9 |     configDir,
10 |   });
11 | 
12 |   return Object.entries(index.entries)
13 |     .filter(([_, entry]) => entry.type === "story")
14 |     .map(([storyId, _entry]) => storyId)
15 |     .join("\n");
16 | }
17 | 
18 | async function tryToResolveConfigDir({ configDir }: { configDir: string }) {
19 |   const mainFiles = ["main.js", "main.ts", "main.mjs", "main.cjs"];
20 |   const cwd = process.cwd();
21 |   const parentDir = path.dirname(cwd);
22 |   const configDirNoDot = configDir.startsWith("./")
23 |     ? configDir.slice(2)
24 |     : configDir;
25 |   const baseDirs =
26 |     configDir !== configDirNoDot ? [configDir, configDirNoDot] : [configDir];
27 | 
28 |   let possibleDirs: string[] = [];
29 |   for (const base of baseDirs) {
30 |     possibleDirs.push(
31 |       path.isAbsolute(base) ? base : path.resolve(base),
32 |       path.resolve(cwd, base),
33 |       path.join(cwd, ".storybook"),
34 |       path.join(__dirname, base),
35 |       path.resolve(parentDir, base),
36 |       path.join(parentDir, ".storybook")
37 |     );
38 |   }
39 | 
40 |   possibleDirs = Array.from(new Set(possibleDirs));
41 | 
42 |   let resolvedConfigDir: string | null = null;
43 | 
44 |   for (const dir of possibleDirs) {
45 |     for (const mainFile of mainFiles) {
46 |       if (fs.existsSync(path.join(dir, mainFile))) {
47 |         resolvedConfigDir = dir;
48 |         break;
49 |       }
50 |     }
51 |     if (resolvedConfigDir) break;
52 |   }
53 | 
54 |   if (resolvedConfigDir) {
55 |     try {
56 |       return getIndex({ configDir: resolvedConfigDir });
57 |     } catch (error) {
58 |       return [
59 |         `Error building index with configDir ${resolvedConfigDir}`,
60 |         `${error}`,
61 |         `try again with an absolute path like /the-full-path/to/your/project/.storybook`,
62 |       ].join("\n\n");
63 |     }
64 |   } else {
65 |     return [
66 |       `Error building index with configDir ${resolvedConfigDir}`,
67 |       `make sure you are passing the correct configDir`,
68 |       `Checked: ${possibleDirs.join(", ")}`,
69 |       `try again with an absolute path like /the-full-path/to/your/project/.storybook`,
70 |     ].join("\n\n");
71 |   }
72 | }
73 | 
74 | export async function getStories({ configDir }: { configDir: string }) {
75 |   try {
76 |     return getIndex({ configDir });
77 |   } catch (error) {
78 |     // Resolve configDir to an absolute path and check for main.js, main.ts, main.mjs, main.cjs
79 |     return tryToResolveConfigDir({ configDir });
80 |   }
81 | }
82 | 
```