# 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 |
```