# 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:
--------------------------------------------------------------------------------
```
cache/
node_modules/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Storybook MCP Server
A Model Context Protocol server for interacting with Storybook.
## Usage
```json
{
"mcpServers": {
"storybook": {
"command": "npx",
"args": ["-y", "mcp-storybook@latest"]
}
}
}
```
## Tools
### get-stories
Retrieves a list of stories from a Storybook configuration.
**Parameters:**
- `configDir` (string): Absolute path to directory containing the .storybook config folder
**Returns:**
- List of story ids
## Technical Details
- Built using `@modelcontextprotocol/sdk`
- Uses stdio transport for communication
- Caches data in `./cache` relative to script location
Unfortunately we need to install any framework that might be encountered to this package so that the index building doesn't fail.
```
--------------------------------------------------------------------------------
/tsdown.config.ts:
--------------------------------------------------------------------------------
```typescript
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["./index.ts"],
outDir: "./dist",
format: ["cjs"],
target: "node22",
});
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ESNext"],
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["index.ts"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/test.ts:
--------------------------------------------------------------------------------
```typescript
import { getStories } from "./getStories";
const run = async () => {
const examples = [
// Absolute path
"/Users/danielwilliams/Workspace/storybook/repro/sb-react-ex/.storybook",
// Relative path
"../repro/sb-react-ex/.storybook",
// this shit
"./../repro/sb-react-ex/.storybook",
];
for (const configDir of examples) {
console.log(`\nTesting configDir: ${configDir}`);
const stories = await getStories({ configDir });
console.log(stories);
}
};
run();
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"main": "dist/index.js",
"name": "mcp-storybook",
"type": "commonjs",
"version": "0.0.12",
"private": false,
"bin": "dist/index.js",
"scripts": {
"start": "node dist/index.js",
"dev": "tsdown --watch",
"build": "tsdown",
"inspector": "npx @modelcontextprotocol/inspector",
"test": "bun test.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.2",
"@storybook/csf": "^0.1.13",
"storybook": "^9.0.13",
"zod": "^3.25.67",
"@storybook/react-vite": "^9.0.13",
"@storybook/react-native": "^9.0.9",
"@storybook/react-native-web-vite": "^9.0.13",
"@storybook/nextjs": "^9.0.13"
},
"devDependencies": {
"tsdown": "^0.12.9",
"@types/node": "^22.13.5",
"typescript": "^5.8.3",
"concurrently": "^9.2.0"
}
}
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { getStories } from "./getStories";
const server = new McpServer({
name: "storybook",
version: "1.0.0",
});
server.tool(
"get-stories",
"Get a list of story ids for your storybook project, use this to list stories.",
{
configDir: z
.string()
.min(1)
.describe(
"The absolute path to directory containing the .storybook config folder (/the-full-path/to/your/project/.storybook)."
)
.default(`${process.cwd()}/.storybook`),
},
async ({ configDir }) => {
return {
content: [
{
type: "text",
text: await getStories({ configDir }),
},
],
};
}
);
server.tool(
"get-story-url",
"Get the URL for a story by its story id.",
{
storyId: z.string().min(1).describe("The story id to get the URL for."),
baseUrl: z
.string()
.min(1)
.describe("Base URL of the Storybook instance.")
.default("http://localhost:6006"),
},
async ({ storyId, baseUrl }) => {
return {
content: [
{
type: "text",
text: `${baseUrl}/?path=/story/${storyId}`,
},
],
};
}
);
// server.tool(
// 'go-to-story',
// 'Go to a story',
// {
// storyKind: z.string(),
// storyName: z.string(),
// },
// async ({ storyKind, storyName }) => {
// const storyId = toId(storyKind, storyName);
// return {
// content: [],
// };
// }
// );
async function main() {
const transport = new StdioServerTransport();
console.error("Server starting...");
await server.connect(transport);
console.error("Server started successfully");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/getStories.ts:
--------------------------------------------------------------------------------
```typescript
import { StoryIndex } from "storybook/internal/types";
import * as path from "path";
import * as fs from "fs";
async function getIndex({ configDir }: { configDir: string }) {
process.env.CACHE_DIR = __dirname + "/cache";
const { buildIndex } = require("storybook/internal/core-server");
const index: StoryIndex = await buildIndex({
configDir,
});
return Object.entries(index.entries)
.filter(([_, entry]) => entry.type === "story")
.map(([storyId, _entry]) => storyId)
.join("\n");
}
async function tryToResolveConfigDir({ configDir }: { configDir: string }) {
const mainFiles = ["main.js", "main.ts", "main.mjs", "main.cjs"];
const cwd = process.cwd();
const parentDir = path.dirname(cwd);
const configDirNoDot = configDir.startsWith("./")
? configDir.slice(2)
: configDir;
const baseDirs =
configDir !== configDirNoDot ? [configDir, configDirNoDot] : [configDir];
let possibleDirs: string[] = [];
for (const base of baseDirs) {
possibleDirs.push(
path.isAbsolute(base) ? base : path.resolve(base),
path.resolve(cwd, base),
path.join(cwd, ".storybook"),
path.join(__dirname, base),
path.resolve(parentDir, base),
path.join(parentDir, ".storybook")
);
}
possibleDirs = Array.from(new Set(possibleDirs));
let resolvedConfigDir: string | null = null;
for (const dir of possibleDirs) {
for (const mainFile of mainFiles) {
if (fs.existsSync(path.join(dir, mainFile))) {
resolvedConfigDir = dir;
break;
}
}
if (resolvedConfigDir) break;
}
if (resolvedConfigDir) {
try {
return getIndex({ configDir: resolvedConfigDir });
} catch (error) {
return [
`Error building index with configDir ${resolvedConfigDir}`,
`${error}`,
`try again with an absolute path like /the-full-path/to/your/project/.storybook`,
].join("\n\n");
}
} else {
return [
`Error building index with configDir ${resolvedConfigDir}`,
`make sure you are passing the correct configDir`,
`Checked: ${possibleDirs.join(", ")}`,
`try again with an absolute path like /the-full-path/to/your/project/.storybook`,
].join("\n\n");
}
}
export async function getStories({ configDir }: { configDir: string }) {
try {
return getIndex({ configDir });
} catch (error) {
// Resolve configDir to an absolute path and check for main.js, main.ts, main.mjs, main.cjs
return tryToResolveConfigDir({ configDir });
}
}
```