Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a610073
initial mcp docs
JReinhold Feb 17, 2026
5674e20
Merge branch 'main' of https://github.com/storybookjs/mcp into docs
JReinhold Feb 23, 2026
06c620e
improve self host docs
JReinhold Feb 23, 2026
0d1db52
add netlify to self hosting doc
JReinhold Feb 23, 2026
07ebfce
include manifests in netlify functions
JReinhold Feb 23, 2026
e33b204
try netlify again
JReinhold Feb 23, 2026
f7d79b4
cleanup self-host readme
JReinhold Feb 24, 2026
81e580a
improve self-hosting docs
JReinhold Feb 24, 2026
d8e9ae0
simplify readmes
JReinhold Feb 24, 2026
e2f7700
format
JReinhold Feb 24, 2026
4bc38d3
Move self-hosting docs into README
kylegach Mar 9, 2026
6e499a6
Merge branch 'main' into docs
kylegach Mar 10, 2026
64186ef
Replace TKs
kylegach Mar 10, 2026
97c6947
Remove redundant docs
kylegach Mar 10, 2026
512a696
Fix formatting
kylegach Mar 10, 2026
7b7f573
Update packages/mcp/README.md
kylegach Mar 10, 2026
d7ce79c
cleanup
JReinhold Mar 11, 2026
42469dc
add correct minimal example
JReinhold Mar 11, 2026
0d487bb
Merge branch 'main' into docs
JReinhold Mar 12, 2026
27bbbad
allow lock file changes to netlify example
JReinhold Mar 12, 2026
f119704
Merge branch 'docs' of https://github.com/storybookjs/mcp into docs
JReinhold Mar 12, 2026
4e77aea
Update packages/mcp/README.md
JReinhold Mar 13, 2026
816abcb
bring back self-hosting lockfile
JReinhold Mar 13, 2026
3308464
update lock file
JReinhold Mar 13, 2026
dee42ed
try building packages first
JReinhold Mar 13, 2026
91d0388
try bundling with esbuild
JReinhold Mar 13, 2026
c22aee2
try externalising @storybook/mcp
JReinhold Mar 13, 2026
f1e7e68
stop netlify from bundling source
JReinhold Mar 13, 2026
180e8bc
cleanup
JReinhold Mar 13, 2026
445f3a0
cleanup
JReinhold Mar 13, 2026
5af6b6d
cleanup
JReinhold Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bright-masks-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@storybook/addon-mcp': patch
'@storybook/mcp': patch
---

Simplify package READMEs for docs-site-first documentation
50 changes: 50 additions & 0 deletions apps/self-host-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Self-hosting example (`@storybook/mcp`)

This app shows the smallest practical way to run `@storybook/mcp` as an HTTP endpoint in Node.js or Netlify Functions.

It is available to experiment with at https://storybook-mcp-self-host-example.netlify.app/mcp

## Run

From the repository root:

```bash
pnpm install
cd apps/self-host-mcp
pnpm start
```

MCP endpoint: `http://localhost:13316/mcp`

## Options

```bash
cd apps/self-host-mcp
pnpm start -- --port 13316 --manifestsPath ./manifests
```

- `--port`: HTTP port to serve
- `--manifestsPath`: local directory or remote base URL containing `components.json` and optionally `docs.json`

## Use your own components

To try this server with your own component library, first build your Storybook so it generates manifests, then copy the content of the `manifests` directory from the build-output (usually `./storybook-static/manifests`) into this example's `manifests/` directory.

In practice, you want `components.json` (and `docs.json` if available) in `apps/self-host-mcp/manifests/` before running `pnpm start`.

## Run on Netlify Functions

This example also includes a Netlify function at `netlify/functions/mcp.ts` and routing in `netlify.toml`.

1. Build your Storybook and copy generated manifests into `apps/self-host-mcp/manifests/` (or set `MANIFESTS_PATH` to a remote URL).
2. Deploy `apps/self-host-mcp` as a Netlify project.
3. Your MCP endpoint is available at `/mcp` (rewritten to `/.netlify/functions/mcp`).

## What this demonstrates

- Instantiate a handler with `createStorybookMcpHandler`
- Route only `/mcp` requests to MCP transport
- Provide a custom `manifestProvider` for local or remote manifest sources
- Use the same handler implementation for both Node and Netlify Functions

For full guidance and Netlify Functions adaptation notes, see [packages/mcp/docs/self-hosting.md](../../packages/mcp/docs/self-hosting.md).
Comment thread
JReinhold marked this conversation as resolved.
Outdated
26 changes: 26 additions & 0 deletions apps/self-host-mcp/manifests/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"v": 1,
"components": {
"button": {
"id": "button",
"name": "Button",
"path": "./src/components/Button.tsx",
"import": "./src/components/Button.tsx",
"summary": "A basic action trigger used throughout the interface.",
"stories": [
{
"id": "button--primary",
"name": "Primary",
"summary": "Primary button style for main actions.",
"snippet": "<Button variant=\"primary\">Save</Button>"
},
{
"id": "button--secondary",
"name": "Secondary",
"summary": "Secondary button style for supporting actions.",
"snippet": "<Button variant=\"secondary\">Cancel</Button>"
}
]
}
}
}
13 changes: 13 additions & 0 deletions apps/self-host-mcp/manifests/docs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"v": 1,
"docs": {
"getting-started": {
"id": "getting-started",
"name": "Getting Started",
"title": "Docs/Getting Started",
"path": "./docs/getting-started.mdx",
"summary": "Introduces the design system and component usage conventions.",
"content": "Use Button for interactive actions. Prefer primary for primary actions and secondary for alternate actions."
}
}
}
12 changes: 12 additions & 0 deletions apps/self-host-mcp/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[build]
base = "apps/self-host-mcp"
command = "pnpm install --frozen-lockfile"
publish = "."

[[redirects]]
from = "/mcp"
to = "/.netlify/functions/mcp"
status = 200

[functions]
included_files = ["manifests/**"]
19 changes: 19 additions & 0 deletions apps/self-host-mcp/netlify/functions/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createMcpHandler } from '../../server.ts';

const manifestsPath = process.env.MANIFESTS_PATH ?? './manifests';

let cachedHandlerPromise: ReturnType<typeof createMcpHandler> | undefined;

export default async function handler(request: Request): Promise<Response> {
const pathname = new URL(request.url).pathname;
if (pathname !== '/mcp' && pathname !== '/.netlify/functions/mcp') {
return new Response('Not found', { status: 404 });
}

if (!cachedHandlerPromise) {
cachedHandlerPromise = createMcpHandler(manifestsPath);
Comment thread
JReinhold marked this conversation as resolved.
Comment thread
JReinhold marked this conversation as resolved.
}

const mcpHandler = await cachedHandlerPromise;
return mcpHandler(request);
}
18 changes: 18 additions & 0 deletions apps/self-host-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@storybook/mcp-self-host",
"version": "0.0.0",
"private": true,
"description": "Self-hosting example for @storybook/mcp",
"type": "module",
"scripts": {
"start": "node ./server.ts"
Comment thread
JReinhold marked this conversation as resolved.
},
"dependencies": {
"@storybook/mcp": "latest",
Comment thread
JReinhold marked this conversation as resolved.
"srvx": "^0.8.16"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "~5.9.0"
}
}
162 changes: 162 additions & 0 deletions apps/self-host-mcp/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions apps/self-host-mcp/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createStorybookMcpHandler } from '@storybook/mcp';
import fs from 'node:fs/promises';
import { basename, resolve } from 'node:path';

export function createMcpHandler(manifestsPath: string) {
return createStorybookMcpHandler({
manifestProvider: async (_request: Request | undefined, path: string) => {
const fileName = basename(path);

if (manifestsPath.startsWith('http://') || manifestsPath.startsWith('https://')) {
const response = await fetch(`${manifestsPath}/${fileName}`);
if (!response.ok) {
throw new Error(
`Failed to fetch manifest from ${manifestsPath}/${fileName}: ${response.status} ${response.statusText}`,
);
}
return await response.text();
}

return await fs.readFile(resolve(manifestsPath, fileName), 'utf-8');
},
});
}

// when running node ./server.ts
if (import.meta.main) {
const [{ serve }, { parseArgs }] = await Promise.all([import('srvx'), import('node:util')]);

const args = parseArgs({
options: {
port: {
type: 'string',
default: '13316',
},
manifestsPath: {
type: 'string',
default: './manifests',
},
},
});

const port = Number(args.values.port);
const manifestsPath = args.values.manifestsPath ?? './manifests';
Comment thread
JReinhold marked this conversation as resolved.
Outdated
const storybookMcpHandler = await createMcpHandler(manifestsPath);

serve({
port,
async fetch(request: Request) {
if (new URL(request.url).pathname !== '/mcp') {
return new Response('Not found', { status: 404 });
}

return await storybookMcpHandler(request);
},
});

console.log(`@storybook/mcp example server listening on http://localhost:${port}/mcp`);
}
8 changes: 8 additions & 0 deletions apps/self-host-mcp/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Loading
Loading