#
tokens: 2058/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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 });
  }
}

```