Skip to content

Commit 0196ae1

Browse files
authored
Merge pull request #32 from storybookjs/copilot/add-manifest-provider-option
Add manifestProvider option to allow custom manifest retrieval
2 parents 8265596 + 531a2d4 commit 0196ae1

16 files changed

+455
-352
lines changed

.changeset/beige-eagles-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@storybook/mcp': patch
3+
---
4+
5+
Support passing in a custom manifestProvider option to the MCP server, falling back to fetch() as the default

.github/instructions/mcp.instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ Or with coverage enabled:
8888
pnpm test run --coverage
8989
```
9090

91+
**Important**: Vitest automatically clears all mocks between tests, so you should never need to call `vi.clearAllMocks()` in a `beforeEach` hook.
92+
9193
### Inspector Tool
9294

9395
```bash

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ storybook-static/
44
build-storybook.log
55
.DS_Store
66
.env
7+
coverage/
78

89
# Turborepo
910
.turbo

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dist
22
pnpm-lock.yaml
3+
coverage

packages/mcp/serve.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { serve } from 'srvx';
33
import fs from 'node:fs/promises';
44

55
const storybookMcpHandler = await createStorybookMcpHandler({
6-
// fetch the manifest from this local server, itself
7-
source: 'http://localhost:13316/fixtures/full-manifest.fixture.json',
6+
// Use the local path directly via manifestProvider
7+
source: './fixtures/full-manifest.fixture.json',
8+
manifestProvider: async (source) => {
9+
// Read the manifest from the local file system
10+
return await fs.readFile(source, 'utf-8');
11+
},
812
});
913

1014
serve({
@@ -15,15 +19,6 @@ serve({
1519
return await storybookMcpHandler(req);
1620
}
1721

18-
// Serve local manifests for the tools to use
19-
if (pathname.startsWith('/fixtures')) {
20-
try {
21-
const fixture = await fs.readFile(`.${pathname}`, 'utf-8');
22-
return new Response(fixture, {
23-
headers: { 'Content-Type': 'application/json' },
24-
});
25-
} catch {}
26-
}
2722
return new Response('Not found', { status: 404 });
2823
},
2924
port: 13316,

packages/mcp/src/index.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,8 @@ export type { StorybookContext } from './types.ts';
2121

2222
type Handler = (req: Request, context?: StorybookContext) => Promise<Response>;
2323

24-
type StorybookMcpHandlerOptions = {
25-
/**
26-
* The default source URL for fetching component manifests.
27-
* Can be overridden per-request via context parameter.
28-
*/
29-
source?: string;
30-
};
31-
3224
export const createStorybookMcpHandler = async (
33-
options: StorybookMcpHandlerOptions = {},
25+
options: StorybookContext = {},
3426
): Promise<Handler> => {
3527
const adapter = new ValibotJsonSchemaAdapter();
3628
const server = new McpServer(
@@ -56,7 +48,9 @@ export const createStorybookMcpHandler = async (
5648

5749
return (async (req, context) => {
5850
const source = context?.source ?? options.source;
51+
const manifestProvider =
52+
context?.manifestProvider ?? options.manifestProvider;
5953

60-
return await transport.respond(req, { source });
54+
return await transport.respond(req, { source, manifestProvider });
6155
}) as Handler;
6256
};

packages/mcp/src/tools/get-component-documentation.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import {
77
} from './get-component-documentation.ts';
88
import type { StorybookContext } from '../types.ts';
99
import smallManifestFixture from '../../fixtures/small-manifest.fixture.json' with { type: 'json' };
10-
import * as fetchManifest from '../utils/fetch-manifest.ts';
10+
import * as getManifest from '../utils/get-manifest.ts';
1111

1212
describe('getComponentDocumentationTool', () => {
1313
let server: McpServer<any, StorybookContext>;
14-
let fetchManifestSpy: any;
14+
let getManifestSpy: any;
1515

1616
beforeEach(async () => {
1717
const adapter = new ValibotJsonSchemaAdapter();
@@ -45,9 +45,9 @@ describe('getComponentDocumentationTool', () => {
4545
);
4646
await addGetComponentDocumentationTool(server);
4747

48-
// Mock fetchManifest to return the fixture
49-
fetchManifestSpy = vi.spyOn(fetchManifest, 'fetchManifest');
50-
fetchManifestSpy.mockResolvedValue(smallManifestFixture);
48+
// Mock getManifest to return the fixture
49+
getManifestSpy = vi.spyOn(getManifest, 'getManifest');
50+
getManifestSpy.mockResolvedValue(smallManifestFixture);
5151
});
5252

5353
it('should return formatted documentation for a single component', async () => {
@@ -266,8 +266,8 @@ describe('getComponentDocumentationTool', () => {
266266
});
267267

268268
it('should handle fetch errors gracefully', async () => {
269-
fetchManifestSpy.mockRejectedValue(
270-
new fetchManifest.ManifestFetchError(
269+
getManifestSpy.mockRejectedValue(
270+
new getManifest.ManifestGetError(
271271
'Failed to fetch manifest: 404 Not Found',
272272
'https://example.com/manifest.json',
273273
),
@@ -291,7 +291,7 @@ describe('getComponentDocumentationTool', () => {
291291
{
292292
"content": [
293293
{
294-
"text": "Error fetching manifest: Failed to fetch manifest: 404 Not Found",
294+
"text": "Error getting manifest: Failed to fetch manifest: 404 Not Found",
295295
"type": "text",
296296
},
297297
],

packages/mcp/src/tools/get-component-documentation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as v from 'valibot';
22
import type { McpServer } from 'tmcp';
33
import type { StorybookContext } from '../types.ts';
4-
import { fetchManifest, errorToMCPContent } from '../utils/fetch-manifest.ts';
4+
import { getManifest, errorToMCPContent } from '../utils/get-manifest.ts';
55
import { formatComponentManifest } from '../utils/format-manifest.ts';
66

77
export const GET_TOOL_NAME = 'get-component-documentation';
@@ -29,7 +29,10 @@ export async function addGetComponentDocumentationTool(
2929
},
3030
async (input: GetComponentDocumentationInput) => {
3131
try {
32-
const manifest = await fetchManifest(server.ctx.custom?.source);
32+
const manifest = await getManifest(
33+
server.ctx.custom?.source,
34+
server.ctx.custom?.manifestProvider,
35+
);
3336

3437
const content = [];
3538
const notFoundIds: string[] = [];

packages/mcp/src/tools/list-all-components.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import {
77
} from './list-all-components.ts';
88
import type { StorybookContext } from '../types.ts';
99
import smallManifestFixture from '../../fixtures/small-manifest.fixture.json' with { type: 'json' };
10-
import * as fetchManifest from '../utils/fetch-manifest.ts';
10+
import * as getManifest from '../utils/get-manifest.ts';
1111

1212
describe('listAllComponentsTool', () => {
1313
let server: McpServer<any, StorybookContext>;
14-
let fetchManifestSpy: any;
14+
let getManifestSpy: any;
1515

1616
beforeEach(async () => {
1717
const adapter = new ValibotJsonSchemaAdapter();
@@ -45,9 +45,9 @@ describe('listAllComponentsTool', () => {
4545
);
4646
await addListAllComponentsTool(server);
4747

48-
// Mock fetchManifest to return the fixture
49-
fetchManifestSpy = vi.spyOn(fetchManifest, 'fetchManifest');
50-
fetchManifestSpy.mockResolvedValue(smallManifestFixture);
48+
// Mock getManifest to return the fixture
49+
getManifestSpy = vi.spyOn(getManifest, 'getManifest');
50+
getManifestSpy.mockResolvedValue(smallManifestFixture);
5151
});
5252

5353
it('should return a list of all components', async () => {
@@ -98,8 +98,8 @@ describe('listAllComponentsTool', () => {
9898
});
9999

100100
it('should handle fetch errors gracefully', async () => {
101-
fetchManifestSpy.mockRejectedValue(
102-
new fetchManifest.ManifestFetchError(
101+
getManifestSpy.mockRejectedValue(
102+
new getManifest.ManifestGetError(
103103
'Failed to fetch manifest: 404 Not Found',
104104
'https://example.com/manifest.json',
105105
),
@@ -121,7 +121,7 @@ describe('listAllComponentsTool', () => {
121121
{
122122
"content": [
123123
{
124-
"text": "Error fetching manifest: Failed to fetch manifest: 404 Not Found",
124+
"text": "Error getting manifest: Failed to fetch manifest: 404 Not Found",
125125
"type": "text",
126126
},
127127
],
@@ -131,7 +131,7 @@ describe('listAllComponentsTool', () => {
131131
});
132132

133133
it('should handle unexpected errors gracefully', async () => {
134-
fetchManifestSpy.mockRejectedValue(new Error('Network timeout'));
134+
getManifestSpy.mockRejectedValue(new Error('Network timeout'));
135135

136136
const request = {
137137
jsonrpc: '2.0' as const,

packages/mcp/src/tools/list-all-components.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { McpServer } from 'tmcp';
22
import type { StorybookContext } from '../types.ts';
3-
import { fetchManifest, errorToMCPContent } from '../utils/fetch-manifest.ts';
3+
import { getManifest, errorToMCPContent } from '../utils/get-manifest.ts';
44
import { formatComponentManifestMapToList } from '../utils/format-manifest.ts';
55

66
export const LIST_TOOL_NAME = 'list-all-components';
@@ -17,7 +17,10 @@ export async function addListAllComponentsTool(
1717
},
1818
async () => {
1919
try {
20-
const manifest = await fetchManifest(server.ctx.custom?.source);
20+
const manifest = await getManifest(
21+
server.ctx.custom?.source,
22+
server.ctx.custom?.manifestProvider,
23+
);
2124

2225
const componentList = formatComponentManifestMapToList(manifest);
2326
return {

0 commit comments

Comments
 (0)