Skip to content

Commit e53012e

Browse files
authored
Merge pull request #194 from storybookjs/fix-sources-create-api
Fix `createStorybookMcpHandler` not forwarding the `sources` option
2 parents 762c0e8 + eb0ea73 commit e53012e

File tree

3 files changed

+95
-7
lines changed

3 files changed

+95
-7
lines changed

.changeset/fair-pianos-smile.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+
Forward `sources` through `createStorybookMcpHandler()` into the per-request transport context.

packages/mcp/src/index.test.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest';
22
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
33
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
44
import { createStorybookMcpHandler } from './index.ts';
5+
import type { StorybookContext } from './types.ts';
56
import smallManifestFixture from '../fixtures/small-manifest.fixture.json' with { type: 'json' };
67
import smallDocsManifestFixture from '../fixtures/small-docs-manifest.fixture.json' with { type: 'json' };
78

@@ -42,12 +43,15 @@ describe('createStorybookMcpHandler', () => {
4243
/**
4344
* Helper to setup client with a mock fetch that routes to our handler
4445
*/
45-
async function setupClient(handler: Awaited<ReturnType<typeof createStorybookMcpHandler>>) {
46+
async function setupClient(
47+
handler: Awaited<ReturnType<typeof createStorybookMcpHandler>>,
48+
context?: StorybookContext,
49+
) {
4650
// Mock global fetch to route to our handler
4751
fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
4852
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
4953
const request = new Request(url, init);
50-
return await handler(request);
54+
return await handler(request, context);
5155
});
5256
(global as any).fetch = fetchMock;
5357

@@ -168,6 +172,85 @@ describe('createStorybookMcpHandler', () => {
168172
});
169173
});
170174

175+
it('should forward handler-level sources into the transport context', async () => {
176+
const manifestProvider = createManifestProviderMockWithDocs();
177+
const sources = [
178+
{ id: 'local', title: 'Local' },
179+
{ id: 'remote', title: 'Remote', url: 'https://example.com/storybook' },
180+
];
181+
182+
const handler = await createStorybookMcpHandler({
183+
manifestProvider,
184+
sources,
185+
});
186+
await setupClient(handler);
187+
188+
await client.callTool({
189+
name: 'list-all-documentation',
190+
arguments: {},
191+
});
192+
193+
expect(manifestProvider).toHaveBeenCalledWith(
194+
expect.any(Request),
195+
'./manifests/components.json',
196+
sources[0],
197+
);
198+
expect(manifestProvider).toHaveBeenCalledWith(
199+
expect.any(Request),
200+
'./manifests/docs.json',
201+
sources[0],
202+
);
203+
expect(manifestProvider).toHaveBeenCalledWith(
204+
expect.any(Request),
205+
'./manifests/components.json',
206+
sources[1],
207+
);
208+
expect(manifestProvider).toHaveBeenCalledWith(
209+
expect.any(Request),
210+
'./manifests/docs.json',
211+
sources[1],
212+
);
213+
});
214+
215+
it('should allow per-request sources to override handler-level sources', async () => {
216+
const manifestProvider = createManifestProviderMockWithDocs();
217+
const handlerSources = [
218+
{ id: 'handler', title: 'Handler', url: 'https://handler.example.com' },
219+
];
220+
const requestSources = [
221+
{ id: 'request', title: 'Request', url: 'https://request.example.com' },
222+
];
223+
224+
const handler = await createStorybookMcpHandler({
225+
manifestProvider,
226+
sources: handlerSources,
227+
});
228+
await setupClient(handler, {
229+
sources: requestSources,
230+
});
231+
232+
await client.callTool({
233+
name: 'list-all-documentation',
234+
arguments: {},
235+
});
236+
237+
expect(manifestProvider).toHaveBeenCalledWith(
238+
expect.any(Request),
239+
'./manifests/components.json',
240+
requestSources[0],
241+
);
242+
expect(manifestProvider).toHaveBeenCalledWith(
243+
expect.any(Request),
244+
'./manifests/docs.json',
245+
requestSources[0],
246+
);
247+
expect(manifestProvider).not.toHaveBeenCalledWith(
248+
expect.any(Request),
249+
expect.any(String),
250+
handlerSources[0],
251+
);
252+
});
253+
171254
it('should call onListAllDocumentation handler when tool is invoked', async () => {
172255
const onListAllDocumentation = vi.fn();
173256
const manifestProvider = createManifestProviderMock();

packages/mcp/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type Handler = (req: Request, context?: StorybookContext) => Promise<Response>;
7676
export const createStorybookMcpHandler = async (
7777
options: StorybookMcpHandlerOptions = {},
7878
): Promise<Handler> => {
79+
const { onSessionInitialize, ...defaultContext } = options;
7980
const adapter = new ValibotJsonSchemaAdapter();
8081
const server = new McpServer(
8182
{
@@ -92,8 +93,8 @@ export const createStorybookMcpHandler = async (
9293
},
9394
).withContext<StorybookContext>();
9495

95-
if (options.onSessionInitialize) {
96-
server.on('initialize', options.onSessionInitialize);
96+
if (onSessionInitialize) {
97+
server.on('initialize', onSessionInitialize);
9798
}
9899

99100
await addListAllDocumentationTool(server);
@@ -104,10 +105,9 @@ export const createStorybookMcpHandler = async (
104105

105106
return (async (req, context) => {
106107
return await transport.respond(req, {
108+
...defaultContext,
109+
...context,
107110
request: req,
108-
manifestProvider: context?.manifestProvider ?? options.manifestProvider,
109-
onListAllDocumentation: context?.onListAllDocumentation ?? options.onListAllDocumentation,
110-
onGetDocumentation: context?.onGetDocumentation ?? options.onGetDocumentation,
111111
});
112112
}) as Handler;
113113
};

0 commit comments

Comments
 (0)