From 7c96f50e707617a461b4f502300d2a0a663422ff Mon Sep 17 00:00:00 2001 From: yannbf Date: Tue, 2 Jun 2026 16:02:59 +0200 Subject: [PATCH 1/9] add dynamic tools to Available Toolsets --- .changeset/spotty-toolsets-surface.md | 5 ++ packages/addon-mcp/src/mcp-handler.ts | 16 +++--- packages/addon-mcp/src/preset.test.ts | 24 +++++++++ packages/addon-mcp/src/preset.ts | 27 ++++++++++ packages/addon-mcp/src/template.html | 5 ++ .../src/utils/get-tool-availability.ts | 51 +++++++++++++++++++ 6 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 .changeset/spotty-toolsets-surface.md create mode 100644 packages/addon-mcp/src/utils/get-tool-availability.ts diff --git a/.changeset/spotty-toolsets-surface.md b/.changeset/spotty-toolsets-surface.md new file mode 100644 index 00000000..27283e44 --- /dev/null +++ b/.changeset/spotty-toolsets-surface.md @@ -0,0 +1,5 @@ +--- +"@storybook/addon-mcp": patch +--- + +Surface the change-detection and review tools on the `/mcp` landing page. The "Available Toolsets" list now includes `get-stories-by-component`, `get-changed-stories`, and `display-review` under **dev** (each with an enabled/disabled badge reflecting its real runtime gate), and `get-documentation-for-story` under **docs**. The page and the MCP server now derive tool availability from a single shared helper (`getToolAvailability`) so the rendered badges can't drift from the tools that are actually registered. diff --git a/packages/addon-mcp/src/mcp-handler.ts b/packages/addon-mcp/src/mcp-handler.ts index 40cfa7fa..44f61ea3 100644 --- a/packages/addon-mcp/src/mcp-handler.ts +++ b/packages/addon-mcp/src/mcp-handler.ts @@ -20,14 +20,13 @@ import { collectTelemetry } from './telemetry.ts'; import type { AddonContext, AddonOptionsOutput } from './types.ts'; import { logger } from 'storybook/internal/node-logger'; import { getManifestStatus } from './tools/is-manifest-available.ts'; -import { getReviewStatus } from './utils/is-review-available.ts'; +import { getToolAvailability } from './utils/get-tool-availability.ts'; import { addRunStoryTestsTool, getAddonVitestConstants } from './tools/run-story-tests.ts'; import { estimateTokens } from './utils/estimate-tokens.ts'; import { isAddonA11yEnabled } from './utils/is-addon-a11y-enabled.ts'; import type { CompositionAuth } from './auth/index.ts'; import { buildServerInstructions } from './instructions/build-server-instructions.ts'; import { DEFAULT_MCP_ENDPOINT } from './constants.ts'; -import { isDependencyGraphSupported } from './utils/change-detection.ts'; let transport: HttpTransport | undefined; let origin: string | undefined; @@ -39,19 +38,16 @@ let a11yEnabled: boolean | undefined; const initializeMCPServer = async (options: Options, multiSource?: boolean) => { const core = await options.presets.apply('core', {}); const features = await options.presets.apply('features', {}); - // The dependency graph and the change-detection status pipeline are independent in Storybook: - // the graph runs whenever the dev-server has a supporting builder; `features.changeDetection` - // only gates the status pipeline that powers `get-changed-stories`. - const dependencyGraphSupported = await isDependencyGraphSupported(); - const changeDetectionEnabled = (features?.changeDetection ?? false) && dependencyGraphSupported; disableTelemetry = core?.disableTelemetry ?? false; // Determine tool availability before creating server so instructions can be tailored. - // Reuse the already-resolved `features` so getReviewStatus doesn't re-call - // `presets.apply('features', …)` and risk a different snapshot. + // Shares one source of truth with the browser landing page (see get-tool-availability.ts) + // so the registered tools and the page's enabled/disabled badges can't drift. Reuse the + // already-resolved `features` so it doesn't re-apply the preset and risk a different snapshot. + const { dependencyGraphSupported, changeDetectionEnabled, reviewStatus } = + await getToolAvailability(options, { features }); const addonVitestConstants = await getAddonVitestConstants(); const manifestStatus = await getManifestStatus(options); - const reviewStatus = await getReviewStatus(options, { features }); a11yEnabled = await isAddonA11yEnabled(options); let server: McpServer; diff --git a/packages/addon-mcp/src/preset.test.ts b/packages/addon-mcp/src/preset.test.ts index c7e4d5d5..d877169c 100644 --- a/packages/addon-mcp/src/preset.test.ts +++ b/packages/addon-mcp/src/preset.test.ts @@ -354,6 +354,30 @@ describe('experimental_devServer', () => { expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining(' { + let getHandler: any; + mockApp.get = vi.fn((_path, handler) => { + getHandler = handler; + }); + + await (experimental_devServer as any)(mockApp, mockOptions); + + const mockRes = { writeHead: vi.fn(), end: vi.fn() } as any; + await getHandler({ headers: { accept: 'text/html' } } as any, mockRes); + + const html = mockRes.end.mock.calls[0][0] as string; + for (const tool of [ + 'get-stories-by-component', + 'get-changed-stories', + 'display-review', + 'get-documentation-for-story', + ]) { + expect(html).toContain(`${tool}`); + } + // Every placeholder must be substituted — no `{{...}}` may leak to the page. + expect(html).not.toMatch(/\{\{[A-Z_]+\}\}/); + }); + it('should show Storybook version requirement for addon-vitest and a manual manifest link', async () => { vi.spyOn(runStoryTests, 'getAddonVitestConstants').mockResolvedValue(undefined); const manifestEnabledOptions = { diff --git a/packages/addon-mcp/src/preset.ts b/packages/addon-mcp/src/preset.ts index b6cbc5dd..f98300b4 100644 --- a/packages/addon-mcp/src/preset.ts +++ b/packages/addon-mcp/src/preset.ts @@ -5,6 +5,7 @@ import * as v from 'valibot'; import { getManifestStatus } from './tools/is-manifest-available.ts'; import { getAddonVitestConstants } from './tools/run-story-tests.ts'; import { isAddonA11yEnabled } from './utils/is-addon-a11y-enabled.ts'; +import { getToolAvailability } from './utils/get-tool-availability.ts'; import htmlTemplate from './template.html'; import path from 'node:path'; import { @@ -113,6 +114,10 @@ export const experimental_devServer: PresetPropertyFn< const manifestStatus = await getManifestStatus(options); const addonVitestConstants = await getAddonVitestConstants(); const a11yEnabled = await isAddonA11yEnabled(options); + // Same gates the MCP server uses to register these tools, so the page can't + // claim a tool is available when it isn't (and vice versa). + const { dependencyGraphSupported, changeDetectionEnabled, reviewStatus } = + await getToolAvailability(options); const isDevEnabled = addonOptions.toolsets?.dev ?? true; const isDocsEnabled = manifestStatus.available && (addonOptions.toolsets?.docs ?? true); @@ -163,8 +168,30 @@ export const experimental_devServer: PresetPropertyFn< ? ' + accessibility' : ''; + // `get-stories-by-component`, `get-changed-stories`, and `display-review` are gated + // independently of the `dev` toolset (they need the dependency graph, the change-detection + // feature flag, and `@storybook/addon-review` respectively), so each shows its own badge. + const devNoticeLines = [ + !dependencyGraphSupported && + `get-stories-by-component and get-changed-stories require a dev server with a builder that supports the dependency graph (e.g. Vite).`, + dependencyGraphSupported && + !changeDetectionEnabled && + `get-changed-stories additionally requires enabling the changeDetection feature flag.`, + !reviewStatus.available && + `display-review requires the changeDetection feature flag and @storybook/addon-review.`, + ].filter(Boolean); + const devNotice = devNoticeLines.length + ? `
${devNoticeLines.join('
')}
` + : ''; + + const statusWord = (enabled: boolean) => (enabled ? 'enabled' : 'disabled'); + const html = htmlTemplate .replaceAll('{{DEV_STATUS}}', isDevEnabled ? 'enabled' : 'disabled') + .replaceAll('{{STORIES_BY_COMPONENT_STATUS}}', statusWord(dependencyGraphSupported)) + .replaceAll('{{CHANGE_DETECTION_STATUS}}', statusWord(changeDetectionEnabled)) + .replaceAll('{{REVIEW_STATUS}}', statusWord(reviewStatus.available)) + .replace('{{DEV_NOTICE}}', devNotice) .replaceAll('{{DOCS_STATUS}}', isDocsEnabled ? 'enabled' : 'disabled') .replace('{{DOCS_NOTICE}}', docsNotice) .replaceAll('{{TEST_STATUS}}', isTestEnabled ? 'enabled' : 'disabled') diff --git a/packages/addon-mcp/src/template.html b/packages/addon-mcp/src/template.html index 4a9ab260..c4da1409 100644 --- a/packages/addon-mcp/src/template.html +++ b/packages/addon-mcp/src/template.html @@ -205,7 +205,11 @@

Available Toolsets

  • preview-stories
  • get-storybook-story-instructions
  • +
  • get-changed-stories {{CHANGE_DETECTION_STATUS}}
  • +
  • get-stories-by-component {{STORIES_BY_COMPONENT_STATUS}}
  • +
  • display-review {{REVIEW_STATUS}}
+ {{DEV_NOTICE}}
@@ -216,6 +220,7 @@

Available Toolsets

  • list-all-documentation
  • get-documentation
  • +
  • get-documentation-for-story
{{DOCS_NOTICE}}
diff --git a/packages/addon-mcp/src/utils/get-tool-availability.ts b/packages/addon-mcp/src/utils/get-tool-availability.ts new file mode 100644 index 00000000..148d83b8 --- /dev/null +++ b/packages/addon-mcp/src/utils/get-tool-availability.ts @@ -0,0 +1,51 @@ +import type { Options } from 'storybook/internal/types'; +import { isDependencyGraphSupported } from './change-detection.ts'; +import { getReviewStatus, type ReviewStatus } from './is-review-available.ts'; + +export interface ToolAvailability { + /** + * Storybook ships the dependency-graph API (the dev-server builder supports it). + * Gates `get-stories-by-component`. + */ + dependencyGraphSupported: boolean; + /** + * The change-detection status pipeline AND the dependency graph are both available. + * Gates `get-changed-stories`. + */ + changeDetectionEnabled: boolean; + /** Full review status; `.available` gates `display-review`. */ + reviewStatus: ReviewStatus; +} + +export interface GetToolAvailabilityOptions { + /** + * Pre-resolved `features` preset. Pass it to avoid re-applying the preset and + * risking a different snapshot than the caller already resolved. + */ + features?: { changeDetection?: boolean } | undefined; +} + +/** + * Single source of truth for the runtime gates that decide whether the + * change-detection and review tools are registered. + * + * Used both by the MCP server (to actually register the tools) and by the + * browser landing page (to show accurate enabled/disabled badges), so the two + * can never drift apart. + */ +export async function getToolAvailability( + options: Options, + { features }: GetToolAvailabilityOptions = {}, +): Promise { + const resolvedFeatures = + features ?? + ((await options.presets.apply('features', {})) as { changeDetection?: boolean } | undefined); + // The dependency graph and the change-detection status pipeline are independent in Storybook: + // the graph runs whenever the dev-server has a supporting builder; `features.changeDetection` + // only gates the status pipeline that powers `get-changed-stories`. + const dependencyGraphSupported = await isDependencyGraphSupported(); + const changeDetectionEnabled = (resolvedFeatures?.changeDetection ?? false) && dependencyGraphSupported; + const reviewStatus = await getReviewStatus(options, { features: resolvedFeatures }); + + return { dependencyGraphSupported, changeDetectionEnabled, reviewStatus }; +} From ca1596f14065d5dd2bddb4a97f2dcc930a501cc5 Mon Sep 17 00:00:00 2001 From: yannbf Date: Tue, 2 Jun 2026 16:39:42 +0200 Subject: [PATCH 2/9] fix: apply oxfmt formatting to dynamic-tools files Co-Authored-By: Claude Opus 4.8 --- packages/addon-mcp/src/template.html | 19 ++++++++++++++++--- .../src/utils/get-tool-availability.ts | 3 ++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/addon-mcp/src/template.html b/packages/addon-mcp/src/template.html index c4da1409..e3af1066 100644 --- a/packages/addon-mcp/src/template.html +++ b/packages/addon-mcp/src/template.html @@ -205,9 +205,22 @@

Available Toolsets

  • preview-stories
  • get-storybook-story-instructions
  • -
  • get-changed-stories {{CHANGE_DETECTION_STATUS}}
  • -
  • get-stories-by-component {{STORIES_BY_COMPONENT_STATUS}}
  • -
  • display-review {{REVIEW_STATUS}}
  • +
  • + get-changed-stories + {{CHANGE_DETECTION_STATUS}} +
  • +
  • + get-stories-by-component + {{STORIES_BY_COMPONENT_STATUS}} +
  • +
  • + display-review + {{REVIEW_STATUS}} +
{{DEV_NOTICE}} diff --git a/packages/addon-mcp/src/utils/get-tool-availability.ts b/packages/addon-mcp/src/utils/get-tool-availability.ts index 148d83b8..d4d61a1c 100644 --- a/packages/addon-mcp/src/utils/get-tool-availability.ts +++ b/packages/addon-mcp/src/utils/get-tool-availability.ts @@ -44,7 +44,8 @@ export async function getToolAvailability( // the graph runs whenever the dev-server has a supporting builder; `features.changeDetection` // only gates the status pipeline that powers `get-changed-stories`. const dependencyGraphSupported = await isDependencyGraphSupported(); - const changeDetectionEnabled = (resolvedFeatures?.changeDetection ?? false) && dependencyGraphSupported; + const changeDetectionEnabled = + (resolvedFeatures?.changeDetection ?? false) && dependencyGraphSupported; const reviewStatus = await getReviewStatus(options, { features: resolvedFeatures }); return { dependencyGraphSupported, changeDetectionEnabled, reviewStatus }; From 3e0e4ded028098ac251006619729c61b4937e59b Mon Sep 17 00:00:00 2001 From: yannbf Date: Wed, 3 Jun 2026 11:18:28 +0200 Subject: [PATCH 3/9] temp fix --- packages/addon-mcp/src/tools/get-stories-by-component.ts | 2 +- packages/mcp-proxy/src/types/record/v1.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/addon-mcp/src/tools/get-stories-by-component.ts b/packages/addon-mcp/src/tools/get-stories-by-component.ts index ed707e22..39302b48 100644 --- a/packages/addon-mcp/src/tools/get-stories-by-component.ts +++ b/packages/addon-mcp/src/tools/get-stories-by-component.ts @@ -29,7 +29,7 @@ Story files (\`*.stories.*\`) are accepted too: they appear at distance 0 as sel ), ), maxDistance: v.pipe( - v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), + v.optional(v.pipe(v.number(), v.minValue(1), v.integer())), v.description( `Ceiling on the import depth to include in results. Must be a positive integer. - 1: only stories that directly import the component. diff --git a/packages/mcp-proxy/src/types/record/v1.ts b/packages/mcp-proxy/src/types/record/v1.ts index 225f7720..7801d814 100644 --- a/packages/mcp-proxy/src/types/record/v1.ts +++ b/packages/mcp-proxy/src/types/record/v1.ts @@ -11,10 +11,10 @@ export type McpStatusV1 = v.InferOutput; export const StorybookInstanceRecordV1Schema = v.object({ schemaVersion: v.literal(1), instanceId: v.string(), - pid: v.pipe(v.number(), v.integer(), v.minValue(1)), + pid: v.pipe(v.number(), v.minValue(1), v.integer()), cwd: v.string(), url: v.string(), - port: v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(65535)), + port: v.pipe(v.number(), v.minValue(1), v.maxValue(65535), v.integer()), storybookVersion: v.optional(v.string()), startedAt: v.optional(v.string()), updatedAt: v.optional(v.string()), From d6f172900484863ede8e91d590209dd0023a7ceb Mon Sep 17 00:00:00 2001 From: yannbf Date: Wed, 3 Jun 2026 12:49:19 +0200 Subject: [PATCH 4/9] Revert "temp fix" This reverts commit 3e0e4ded028098ac251006619729c61b4937e59b. --- packages/addon-mcp/src/tools/get-stories-by-component.ts | 2 +- packages/mcp-proxy/src/types/record/v1.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/addon-mcp/src/tools/get-stories-by-component.ts b/packages/addon-mcp/src/tools/get-stories-by-component.ts index 39302b48..ed707e22 100644 --- a/packages/addon-mcp/src/tools/get-stories-by-component.ts +++ b/packages/addon-mcp/src/tools/get-stories-by-component.ts @@ -29,7 +29,7 @@ Story files (\`*.stories.*\`) are accepted too: they appear at distance 0 as sel ), ), maxDistance: v.pipe( - v.optional(v.pipe(v.number(), v.minValue(1), v.integer())), + v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), v.description( `Ceiling on the import depth to include in results. Must be a positive integer. - 1: only stories that directly import the component. diff --git a/packages/mcp-proxy/src/types/record/v1.ts b/packages/mcp-proxy/src/types/record/v1.ts index 7801d814..225f7720 100644 --- a/packages/mcp-proxy/src/types/record/v1.ts +++ b/packages/mcp-proxy/src/types/record/v1.ts @@ -11,10 +11,10 @@ export type McpStatusV1 = v.InferOutput; export const StorybookInstanceRecordV1Schema = v.object({ schemaVersion: v.literal(1), instanceId: v.string(), - pid: v.pipe(v.number(), v.minValue(1), v.integer()), + pid: v.pipe(v.number(), v.integer(), v.minValue(1)), cwd: v.string(), url: v.string(), - port: v.pipe(v.number(), v.minValue(1), v.maxValue(65535), v.integer()), + port: v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(65535)), storybookVersion: v.optional(v.string()), startedAt: v.optional(v.string()), updatedAt: v.optional(v.string()), From abaf32bf178efbb6d9fdf0519d27ffe4d5c5e0c8 Mon Sep 17 00:00:00 2001 From: yannbf Date: Wed, 3 Jun 2026 12:50:17 +0200 Subject: [PATCH 5/9] address review feedback --- packages/addon-mcp/src/mcp-handler.ts | 28 +++---- packages/addon-mcp/src/preset.test.ts | 35 +++++++++ packages/addon-mcp/src/preset.ts | 60 ++++++++------- .../src/utils/get-tool-availability.ts | 73 +++++++++++++------ 4 files changed, 133 insertions(+), 63 deletions(-) diff --git a/packages/addon-mcp/src/mcp-handler.ts b/packages/addon-mcp/src/mcp-handler.ts index 44f61ea3..0911d60d 100644 --- a/packages/addon-mcp/src/mcp-handler.ts +++ b/packages/addon-mcp/src/mcp-handler.ts @@ -19,11 +19,9 @@ import { buffer } from 'node:stream/consumers'; import { collectTelemetry } from './telemetry.ts'; import type { AddonContext, AddonOptionsOutput } from './types.ts'; import { logger } from 'storybook/internal/node-logger'; -import { getManifestStatus } from './tools/is-manifest-available.ts'; import { getToolAvailability } from './utils/get-tool-availability.ts'; -import { addRunStoryTestsTool, getAddonVitestConstants } from './tools/run-story-tests.ts'; +import { addRunStoryTestsTool } from './tools/run-story-tests.ts'; import { estimateTokens } from './utils/estimate-tokens.ts'; -import { isAddonA11yEnabled } from './utils/is-addon-a11y-enabled.ts'; import type { CompositionAuth } from './auth/index.ts'; import { buildServerInstructions } from './instructions/build-server-instructions.ts'; import { DEFAULT_MCP_ENDPOINT } from './constants.ts'; @@ -44,11 +42,15 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { // Shares one source of truth with the browser landing page (see get-tool-availability.ts) // so the registered tools and the page's enabled/disabled badges can't drift. Reuse the // already-resolved `features` so it doesn't re-apply the preset and risk a different snapshot. - const { dependencyGraphSupported, changeDetectionEnabled, reviewStatus } = - await getToolAvailability(options, { features }); - const addonVitestConstants = await getAddonVitestConstants(); - const manifestStatus = await getManifestStatus(options); - a11yEnabled = await isAddonA11yEnabled(options); + const { + dependencyGraphSupported, + changeDetectionEnabled, + reviewAvailable, + docsAvailable, + testSupported, + a11yEnabled: a11yAvailable, + } = await getToolAvailability(options, { features }); + a11yEnabled = a11yAvailable; let server: McpServer; @@ -57,11 +59,11 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { get instructions() { return buildServerInstructions({ devEnabled: server?.ctx.custom?.toolsets?.dev ?? true, - testEnabled: (server?.ctx.custom?.toolsets?.test ?? true) && !!addonVitestConstants, - docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? true) && manifestStatus.available, + testEnabled: (server?.ctx.custom?.toolsets?.test ?? true) && testSupported, + docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? true) && docsAvailable, changeDetectionEnabled, dependencyGraphAvailable: dependencyGraphSupported, - reviewEnabled: reviewStatus.available, + reviewEnabled: reviewAvailable, }); }, capabilities: { @@ -98,7 +100,7 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { await addGetStoriesByComponentTool(server); } - if (reviewStatus.available) { + if (reviewAvailable) { await addDisplayReviewTool(server); } @@ -106,7 +108,7 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { await addRunStoryTestsTool(server, { a11yEnabled }); // Only register the additional tools if the component manifest feature is enabled - if (manifestStatus.available) { + if (docsAvailable) { logger.info('Experimental components manifest feature detected - registering component tools'); const contextAwareEnabled = () => server.ctx.custom?.toolsets?.docs ?? true; await addListAllDocumentationTool(server, contextAwareEnabled); diff --git a/packages/addon-mcp/src/preset.test.ts b/packages/addon-mcp/src/preset.test.ts index d877169c..1df99add 100644 --- a/packages/addon-mcp/src/preset.test.ts +++ b/packages/addon-mcp/src/preset.test.ts @@ -5,6 +5,7 @@ import { experimental_devServer } from './preset.ts'; import { STORYBOOK_MCP_PROXY_HEADER } from './auth/index.ts'; import * as mcpHandlerModule from './mcp-handler.ts'; import * as runStoryTests from './tools/run-story-tests.ts'; +import * as changeDetection from './utils/change-detection.ts'; describe('experimental_devServer', () => { let mockApp: any; @@ -378,6 +379,40 @@ describe('experimental_devServer', () => { expect(html).not.toMatch(/\{\{[A-Z_]+\}\}/); }); + it('marks dev tools disabled on the landing page when the dev toolset is turned off', async () => { + // Dependency graph IS supported, so `get-stories-by-component` would otherwise badge + // as enabled — proving the badge now also honors the `dev` toolset being disabled. + vi.spyOn(changeDetection, 'isDependencyGraphSupported').mockResolvedValue(true); + + let getHandler: any; + mockApp.get = vi.fn((_path, handler) => { + getHandler = handler; + }); + + const devOffOptions = { + ...mockOptions, + toolsets: { dev: false }, + } as unknown as Options; + + await (experimental_devServer as any)(mockApp, devOffOptions); + + const mockRes = { writeHead: vi.fn(), end: vi.fn() } as any; + await getHandler({ headers: { accept: 'text/html' } } as any, mockRes); + + const html = mockRes.end.mock.calls[0][0] as string; + + const badgeFor = (tool: string) => + html.match( + new RegExp(`${tool}\\s*dev toolset is disabled via addon options.'); + expect(html).not.toMatch(/\{\{[A-Z_]+\}\}/); + }); + it('should show Storybook version requirement for addon-vitest and a manual manifest link', async () => { vi.spyOn(runStoryTests, 'getAddonVitestConstants').mockResolvedValue(undefined); const manifestEnabledOptions = { diff --git a/packages/addon-mcp/src/preset.ts b/packages/addon-mcp/src/preset.ts index f98300b4..3bbf179c 100644 --- a/packages/addon-mcp/src/preset.ts +++ b/packages/addon-mcp/src/preset.ts @@ -2,9 +2,6 @@ import { mcpServerHandler } from './mcp-handler.ts'; import type { PresetPropertyFn, StorybookConfigRaw } from 'storybook/internal/types'; import { AddonOptions, type AddonOptionsInput } from './types.ts'; import * as v from 'valibot'; -import { getManifestStatus } from './tools/is-manifest-available.ts'; -import { getAddonVitestConstants } from './tools/run-story-tests.ts'; -import { isAddonA11yEnabled } from './utils/is-addon-a11y-enabled.ts'; import { getToolAvailability } from './utils/get-tool-availability.ts'; import htmlTemplate from './template.html'; import path from 'node:path'; @@ -111,17 +108,22 @@ export const experimental_devServer: PresetPropertyFn< }); }); - const manifestStatus = await getManifestStatus(options); - const addonVitestConstants = await getAddonVitestConstants(); - const a11yEnabled = await isAddonA11yEnabled(options); // Same gates the MCP server uses to register these tools, so the page can't // claim a tool is available when it isn't (and vice versa). - const { dependencyGraphSupported, changeDetectionEnabled, reviewStatus } = - await getToolAvailability(options); + const { + dependencyGraphSupported, + changeDetectionEnabled, + reviewAvailable, + docsAvailable, + docsHasManifests, + docsFeatureEnabled, + testSupported, + a11yEnabled, + } = await getToolAvailability(options); const isDevEnabled = addonOptions.toolsets?.dev ?? true; - const isDocsEnabled = manifestStatus.available && (addonOptions.toolsets?.docs ?? true); - const isTestEnabled = !!addonVitestConstants && (addonOptions.toolsets?.test ?? true); + const isDocsEnabled = docsAvailable && (addonOptions.toolsets?.docs ?? true); + const isTestEnabled = testSupported && (addonOptions.toolsets?.test ?? true); app!.get(endpoint, (req, res) => { if (!req.headers['accept']?.includes('text/html')) { @@ -143,11 +145,11 @@ export const experimental_devServer: PresetPropertyFn< res.writeHead(200, { 'Content-Type': 'text/html' }); let docsNotice = ''; - if (!manifestStatus.hasManifests) { + if (!docsHasManifests) { docsNotice = `
This toolset is only supported in React-based setups.
`; - } else if (!manifestStatus.hasFeatureFlag) { + } else if (!docsFeatureEnabled) { docsNotice = `
This toolset requires enabling the component manifest feature. Learn how to enable it @@ -155,7 +157,7 @@ export const experimental_devServer: PresetPropertyFn< } const testNoticeLines = [ - !addonVitestConstants && + !testSupported && `This toolset requires Storybook 10.3.0+ with @storybook/addon-vitest. Learn how to set it up`, !a11yEnabled && `Add @storybook/addon-a11y for accessibility testing. Learn more`, @@ -171,15 +173,18 @@ export const experimental_devServer: PresetPropertyFn< // `get-stories-by-component`, `get-changed-stories`, and `display-review` are gated // independently of the `dev` toolset (they need the dependency graph, the change-detection // feature flag, and `@storybook/addon-review` respectively), so each shows its own badge. - const devNoticeLines = [ - !dependencyGraphSupported && - `get-stories-by-component and get-changed-stories require a dev server with a builder that supports the dependency graph (e.g. Vite).`, - dependencyGraphSupported && - !changeDetectionEnabled && - `get-changed-stories additionally requires enabling the changeDetection feature flag.`, - !reviewStatus.available && - `display-review requires the changeDetection feature flag and @storybook/addon-review.`, - ].filter(Boolean); + // When the whole `dev` toolset is turned off via addon options every dev tool is + // disabled regardless of its own gate, so explain that instead of the per-tool reasons. + const devNoticeLines = !isDevEnabled + ? [`The dev toolset is disabled via addon options.`] + : [ + !dependencyGraphSupported && + `get-stories-by-component requires a dev server with a builder that supports the dependency graph (e.g. Vite).`, + !changeDetectionEnabled && + `get-changed-stories requires enabling the changeDetection feature flag.`, + !reviewAvailable && + `display-review requires the changeDetection feature flag and @storybook/addon-review.`, + ].filter(Boolean); const devNotice = devNoticeLines.length ? `
${devNoticeLines.join('
')}
` : ''; @@ -188,9 +193,12 @@ export const experimental_devServer: PresetPropertyFn< const html = htmlTemplate .replaceAll('{{DEV_STATUS}}', isDevEnabled ? 'enabled' : 'disabled') - .replaceAll('{{STORIES_BY_COMPONENT_STATUS}}', statusWord(dependencyGraphSupported)) - .replaceAll('{{CHANGE_DETECTION_STATUS}}', statusWord(changeDetectionEnabled)) - .replaceAll('{{REVIEW_STATUS}}', statusWord(reviewStatus.available)) + .replaceAll( + '{{STORIES_BY_COMPONENT_STATUS}}', + statusWord(isDevEnabled && dependencyGraphSupported), + ) + .replaceAll('{{CHANGE_DETECTION_STATUS}}', statusWord(isDevEnabled && changeDetectionEnabled)) + .replaceAll('{{REVIEW_STATUS}}', statusWord(isDevEnabled && reviewAvailable)) .replace('{{DEV_NOTICE}}', devNotice) .replaceAll('{{DOCS_STATUS}}', isDocsEnabled ? 'enabled' : 'disabled') .replace('{{DOCS_NOTICE}}', docsNotice) @@ -198,7 +206,7 @@ export const experimental_devServer: PresetPropertyFn< .replace('{{TEST_NOTICE}}', testNotice) .replace( '{{MANIFEST_DEBUGGER_LINK}}', - manifestStatus.available + docsAvailable ? '

View the component manifest debugger.

' : '', ) diff --git a/packages/addon-mcp/src/utils/get-tool-availability.ts b/packages/addon-mcp/src/utils/get-tool-availability.ts index d4d61a1c..b911c838 100644 --- a/packages/addon-mcp/src/utils/get-tool-availability.ts +++ b/packages/addon-mcp/src/utils/get-tool-availability.ts @@ -1,20 +1,27 @@ import type { Options } from 'storybook/internal/types'; import { isDependencyGraphSupported } from './change-detection.ts'; -import { getReviewStatus, type ReviewStatus } from './is-review-available.ts'; +import { getReviewStatus } from './is-review-available.ts'; +import { getManifestStatus } from '../tools/is-manifest-available.ts'; +import { getAddonVitestConstants } from '../tools/run-story-tests.ts'; +import { isAddonA11yEnabled } from './is-addon-a11y-enabled.ts'; export interface ToolAvailability { - /** - * Storybook ships the dependency-graph API (the dev-server builder supports it). - * Gates `get-stories-by-component`. - */ + /** Dev-server builder supports the dependency-graph API. Gates `get-stories-by-component`. */ dependencyGraphSupported: boolean; - /** - * The change-detection status pipeline AND the dependency graph are both available. - * Gates `get-changed-stories`. - */ + /** The `changeDetection` feature flag is enabled. Gates `get-changed-stories`. */ changeDetectionEnabled: boolean; - /** Full review status; `.available` gates `display-review`. */ - reviewStatus: ReviewStatus; + /** `changeDetection` flag + `@storybook/addon-review` are both present. Gates `display-review`. */ + reviewAvailable: boolean; + /** Component-manifest feature is on AND manifests were found. Gates the `docs` toolset. */ + docsAvailable: boolean; + /** Any component manifests were found (drives the docs "why disabled" copy). */ + docsHasManifests: boolean; + /** The component-manifest feature flag is enabled (drives the docs "why disabled" copy). */ + docsFeatureEnabled: boolean; + /** `@storybook/addon-vitest` is installed. Gates the `test` toolset (`run-story-tests`). */ + testSupported: boolean; + /** `@storybook/addon-a11y` is enabled. Gates the accessibility sub-feature of `run-story-tests`. */ + a11yEnabled: boolean; } export interface GetToolAvailabilityOptions { @@ -26,12 +33,14 @@ export interface GetToolAvailabilityOptions { } /** - * Single source of truth for the runtime gates that decide whether the - * change-detection and review tools are registered. + * Single source of truth for the runtime gates that decide whether each tool is + * registered (and how the landing page badges it). * - * Used both by the MCP server (to actually register the tools) and by the - * browser landing page (to show accurate enabled/disabled badges), so the two - * can never drift apart. + * Every dynamic gate lives here — the dependency graph, the change-detection + * pipeline, review, the component manifest (docs), addon-vitest (test) and the + * accessibility sub-feature — so the MCP server (which registers the tools) and + * the browser landing page (which shows enabled/disabled badges) can never drift + * apart. Add new gates here rather than computing them ad-hoc at a call site. */ export async function getToolAvailability( options: Options, @@ -40,13 +49,29 @@ export async function getToolAvailability( const resolvedFeatures = features ?? ((await options.presets.apply('features', {})) as { changeDetection?: boolean } | undefined); - // The dependency graph and the change-detection status pipeline are independent in Storybook: - // the graph runs whenever the dev-server has a supporting builder; `features.changeDetection` - // only gates the status pipeline that powers `get-changed-stories`. - const dependencyGraphSupported = await isDependencyGraphSupported(); - const changeDetectionEnabled = - (resolvedFeatures?.changeDetection ?? false) && dependencyGraphSupported; - const reviewStatus = await getReviewStatus(options, { features: resolvedFeatures }); - return { dependencyGraphSupported, changeDetectionEnabled, reviewStatus }; + const [ + dependencyGraphSupported, + reviewStatus, + manifestStatus, + addonVitestConstants, + a11yEnabled, + ] = await Promise.all([ + isDependencyGraphSupported(), + getReviewStatus(options, { features: resolvedFeatures }), + getManifestStatus(options), + getAddonVitestConstants(), + isAddonA11yEnabled(options), + ]); + + return { + dependencyGraphSupported, + changeDetectionEnabled: resolvedFeatures?.changeDetection ?? false, + reviewAvailable: reviewStatus.available, + docsAvailable: manifestStatus.available, + docsHasManifests: manifestStatus.hasManifests, + docsFeatureEnabled: manifestStatus.hasFeatureFlag, + testSupported: !!addonVitestConstants, + a11yEnabled, + }; } From 2cb2c99239e44fc84a179719f0e9b7011d3c44eb Mon Sep 17 00:00:00 2001 From: yannbf Date: Thu, 4 Jun 2026 08:28:27 +0200 Subject: [PATCH 6/9] improve naming consistency --- .../build-server-instructions.test.ts | 2 +- .../instructions/build-server-instructions.ts | 6 ++-- packages/addon-mcp/src/mcp-handler.ts | 29 +++++++------------ packages/addon-mcp/src/preset.ts | 12 ++++---- .../src/utils/get-tool-availability.ts | 8 ++--- 5 files changed, 25 insertions(+), 32 deletions(-) diff --git a/packages/addon-mcp/src/instructions/build-server-instructions.test.ts b/packages/addon-mcp/src/instructions/build-server-instructions.test.ts index df92dcdc..49861b23 100644 --- a/packages/addon-mcp/src/instructions/build-server-instructions.test.ts +++ b/packages/addon-mcp/src/instructions/build-server-instructions.test.ts @@ -133,7 +133,7 @@ describe('buildServerInstructions', () => { testEnabled: false, docsEnabled: false, changeDetectionEnabled: false, - dependencyGraphAvailable: true, + dependencyGraphSupported: true, }); // The status-store-driven get-changed-stories isn't registered, but the reverse diff --git a/packages/addon-mcp/src/instructions/build-server-instructions.ts b/packages/addon-mcp/src/instructions/build-server-instructions.ts index b87cbe7b..3a859551 100644 --- a/packages/addon-mcp/src/instructions/build-server-instructions.ts +++ b/packages/addon-mcp/src/instructions/build-server-instructions.ts @@ -13,7 +13,7 @@ export type BuildServerInstructionsOptions = { * When true and `changeDetectionEnabled` is false, the workflow falls back to manual lookup * via `get-stories-by-component` instead of the status-store-driven `get-changed-stories`. */ - dependencyGraphAvailable?: boolean; + dependencyGraphSupported?: boolean; reviewEnabled?: boolean; }; @@ -22,11 +22,11 @@ export function buildServerInstructions(options: BuildServerInstructionsOptions) if (options.devEnabled) { const changeDetection = options.changeDetectionEnabled ?? false; - const graphAvailable = options.dependencyGraphAvailable ?? false; + const graphSupported = options.dependencyGraphSupported ?? false; const reviewEnabled = options.reviewEnabled ?? false; const previewStoriesStep = changeDetection ? 'After changing any component or story, call **get-changed-stories** to discover new/modified/related stories, then call **preview-stories** to retrieve preview URLs.' - : graphAvailable + : graphSupported ? 'After changing any component or story, call **get-stories-by-component** with the absolute paths of the files you touched to find the stories that render them, then call **preview-stories** to retrieve preview URLs.' : 'After changing any component or story, call **preview-stories** to retrieve preview URLs.'; sections.push( diff --git a/packages/addon-mcp/src/mcp-handler.ts b/packages/addon-mcp/src/mcp-handler.ts index 0911d60d..2e5088b0 100644 --- a/packages/addon-mcp/src/mcp-handler.ts +++ b/packages/addon-mcp/src/mcp-handler.ts @@ -42,15 +42,8 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { // Shares one source of truth with the browser landing page (see get-tool-availability.ts) // so the registered tools and the page's enabled/disabled badges can't drift. Reuse the // already-resolved `features` so it doesn't re-apply the preset and risk a different snapshot. - const { - dependencyGraphSupported, - changeDetectionEnabled, - reviewAvailable, - docsAvailable, - testSupported, - a11yEnabled: a11yAvailable, - } = await getToolAvailability(options, { features }); - a11yEnabled = a11yAvailable; + const availability = await getToolAvailability(options, { features }); + a11yEnabled = availability.a11yEnabled; let server: McpServer; @@ -59,11 +52,11 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { get instructions() { return buildServerInstructions({ devEnabled: server?.ctx.custom?.toolsets?.dev ?? true, - testEnabled: (server?.ctx.custom?.toolsets?.test ?? true) && testSupported, - docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? true) && docsAvailable, - changeDetectionEnabled, - dependencyGraphAvailable: dependencyGraphSupported, - reviewEnabled: reviewAvailable, + testEnabled: (server?.ctx.custom?.toolsets?.test ?? true) && availability.testSupported, + docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? true) && availability.docsEnabled, + changeDetectionEnabled: availability.changeDetectionEnabled, + dependencyGraphSupported: availability.dependencyGraphSupported, + reviewEnabled: availability.reviewEnabled, }); }, capabilities: { @@ -91,16 +84,16 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { await addPreviewStoriesTool(server); await addGetUIBuildingInstructionsTool(server); - if (changeDetectionEnabled) { + if (availability.changeDetectionEnabled) { await addGetChangedStoriesTool(server); } // get-stories-by-component only needs the dependency graph, not the status pipeline. - if (dependencyGraphSupported) { + if (availability.dependencyGraphSupported) { await addGetStoriesByComponentTool(server); } - if (reviewAvailable) { + if (availability.reviewEnabled) { await addDisplayReviewTool(server); } @@ -108,7 +101,7 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { await addRunStoryTestsTool(server, { a11yEnabled }); // Only register the additional tools if the component manifest feature is enabled - if (docsAvailable) { + if (availability.docsEnabled) { logger.info('Experimental components manifest feature detected - registering component tools'); const contextAwareEnabled = () => server.ctx.custom?.toolsets?.docs ?? true; await addListAllDocumentationTool(server, contextAwareEnabled); diff --git a/packages/addon-mcp/src/preset.ts b/packages/addon-mcp/src/preset.ts index 3bbf179c..150fb513 100644 --- a/packages/addon-mcp/src/preset.ts +++ b/packages/addon-mcp/src/preset.ts @@ -113,8 +113,8 @@ export const experimental_devServer: PresetPropertyFn< const { dependencyGraphSupported, changeDetectionEnabled, - reviewAvailable, - docsAvailable, + reviewEnabled, + docsEnabled, docsHasManifests, docsFeatureEnabled, testSupported, @@ -122,7 +122,7 @@ export const experimental_devServer: PresetPropertyFn< } = await getToolAvailability(options); const isDevEnabled = addonOptions.toolsets?.dev ?? true; - const isDocsEnabled = docsAvailable && (addonOptions.toolsets?.docs ?? true); + const isDocsEnabled = docsEnabled && (addonOptions.toolsets?.docs ?? true); const isTestEnabled = testSupported && (addonOptions.toolsets?.test ?? true); app!.get(endpoint, (req, res) => { @@ -182,7 +182,7 @@ export const experimental_devServer: PresetPropertyFn< `get-stories-by-component requires a dev server with a builder that supports the dependency graph (e.g. Vite).`, !changeDetectionEnabled && `get-changed-stories requires enabling the changeDetection feature flag.`, - !reviewAvailable && + !reviewEnabled && `display-review requires the changeDetection feature flag and @storybook/addon-review.`, ].filter(Boolean); const devNotice = devNoticeLines.length @@ -198,7 +198,7 @@ export const experimental_devServer: PresetPropertyFn< statusWord(isDevEnabled && dependencyGraphSupported), ) .replaceAll('{{CHANGE_DETECTION_STATUS}}', statusWord(isDevEnabled && changeDetectionEnabled)) - .replaceAll('{{REVIEW_STATUS}}', statusWord(isDevEnabled && reviewAvailable)) + .replaceAll('{{REVIEW_STATUS}}', statusWord(isDevEnabled && reviewEnabled)) .replace('{{DEV_NOTICE}}', devNotice) .replaceAll('{{DOCS_STATUS}}', isDocsEnabled ? 'enabled' : 'disabled') .replace('{{DOCS_NOTICE}}', docsNotice) @@ -206,7 +206,7 @@ export const experimental_devServer: PresetPropertyFn< .replace('{{TEST_NOTICE}}', testNotice) .replace( '{{MANIFEST_DEBUGGER_LINK}}', - docsAvailable + docsEnabled ? '

View the component manifest debugger.

' : '', ) diff --git a/packages/addon-mcp/src/utils/get-tool-availability.ts b/packages/addon-mcp/src/utils/get-tool-availability.ts index b911c838..8abd2b9c 100644 --- a/packages/addon-mcp/src/utils/get-tool-availability.ts +++ b/packages/addon-mcp/src/utils/get-tool-availability.ts @@ -11,9 +11,9 @@ export interface ToolAvailability { /** The `changeDetection` feature flag is enabled. Gates `get-changed-stories`. */ changeDetectionEnabled: boolean; /** `changeDetection` flag + `@storybook/addon-review` are both present. Gates `display-review`. */ - reviewAvailable: boolean; + reviewEnabled: boolean; /** Component-manifest feature is on AND manifests were found. Gates the `docs` toolset. */ - docsAvailable: boolean; + docsEnabled: boolean; /** Any component manifests were found (drives the docs "why disabled" copy). */ docsHasManifests: boolean; /** The component-manifest feature flag is enabled (drives the docs "why disabled" copy). */ @@ -67,8 +67,8 @@ export async function getToolAvailability( return { dependencyGraphSupported, changeDetectionEnabled: resolvedFeatures?.changeDetection ?? false, - reviewAvailable: reviewStatus.available, - docsAvailable: manifestStatus.available, + reviewEnabled: reviewStatus.available, + docsEnabled: manifestStatus.available, docsHasManifests: manifestStatus.hasManifests, docsFeatureEnabled: manifestStatus.hasFeatureFlag, testSupported: !!addonVitestConstants, From f0b200040f008081ba9bff90da23867445ac7010 Mon Sep 17 00:00:00 2001 From: yannbf Date: Thu, 4 Jun 2026 08:34:56 +0200 Subject: [PATCH 7/9] add tests --- packages/addon-mcp/src/mcp-handler.test.ts | 81 ++++++++++++++++++++++ packages/addon-mcp/src/preset.test.ts | 35 ++++++++++ 2 files changed, 116 insertions(+) diff --git a/packages/addon-mcp/src/mcp-handler.test.ts b/packages/addon-mcp/src/mcp-handler.test.ts index 0f266f25..287da081 100644 --- a/packages/addon-mcp/src/mcp-handler.test.ts +++ b/packages/addon-mcp/src/mcp-handler.test.ts @@ -9,6 +9,15 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import { PassThrough } from 'node:stream'; import { CompositionAuth } from './auth/index.ts'; +// Lets us flip `@storybook/addon-review` presence so the review tool's gate can be exercised. +// Keep the real exports (e.g. `normalizeStoryPath`) so unrelated tools still register. +const { mockGetAddonNames } = vi.hoisted(() => ({ mockGetAddonNames: vi.fn(() => [] as string[]) })); +vi.mock('storybook/internal/common', async (importActual) => ({ + ...(await importActual()), + loadMainConfig: vi.fn().mockResolvedValue({}), + getAddonNames: () => mockGetAddonNames(), +})); + // Test helpers to reduce boilerplate function createMockIncomingMessage(options: { method?: string; @@ -233,6 +242,45 @@ describe('mcpServerHandler', () => { }; } + // Initializes the server then asks for `tools/list`, returning the registered tool names. + async function getRegisteredToolNames(mockOptions: any, port: number): Promise { + const host = `localhost:${port}`; + const addonOptions = { toolsets: { dev: true, docs: true } }; + + const initReq = createMockIncomingMessage({ + method: 'POST', + headers: { 'content-type': 'application/json', host }, + body: createMCPInitializeRequest(), + }); + const { response: initResponse } = createMockServerResponse(); + await mcpServerHandler({ + req: initReq, + res: initResponse, + options: mockOptions, + addonOptions, + compositionAuth: new CompositionAuth(), + }); + + const listReq = createMockIncomingMessage({ + method: 'POST', + headers: { 'content-type': 'application/json', host }, + body: { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }, + }); + const { response: listResponse, getResponseData } = createMockServerResponse(); + await mcpServerHandler({ + req: listReq, + res: listResponse, + options: mockOptions, + addonOptions, + compositionAuth: new CompositionAuth(), + }); + + const { body } = getResponseData(); + const dataLine = body.split('\n').find((line) => line.startsWith('data: ')); + const parsed = JSON.parse(dataLine!.replace(/^data: /, '').trim()); + return parsed.result.tools.map((t: any) => t.name); + } + it('should initialize MCP server and handle requests', async () => { const mockOptions = createMockOptions(); const mockReq = createMockIncomingMessage({ @@ -464,6 +512,39 @@ describe('mcpServerHandler', () => { expect(toolNames).toContain('get-documentation'); expect(toolNames).toContain('get-documentation-for-story'); }); + + it('registers get-changed-stories when the changeDetection feature flag is on', async () => { + const mockOptions = createMockOptions({ + port: 6009, + presets: { + apply: vi.fn(async (key: string, defaultValue?: any) => { + if (key === 'core') return { disableTelemetry: false }; + if (key === 'features') return { changeDetection: true }; + return defaultValue; + }), + }, + }); + + const toolNames = await getRegisteredToolNames(mockOptions, 6009); + expect(toolNames).toContain('get-changed-stories'); + }); + + it('registers display-review when changeDetection and @storybook/addon-review are present', async () => { + mockGetAddonNames.mockReturnValue(['@storybook/addon-review']); + const mockOptions = createMockOptions({ + port: 6010, + presets: { + apply: vi.fn(async (key: string, defaultValue?: any) => { + if (key === 'core') return { disableTelemetry: false }; + if (key === 'features') return { changeDetection: true }; + return defaultValue; + }), + }, + }); + + const toolNames = await getRegisteredToolNames(mockOptions, 6010); + expect(toolNames).toContain('display-review'); + }); }); describe('getToolsets', () => { diff --git a/packages/addon-mcp/src/preset.test.ts b/packages/addon-mcp/src/preset.test.ts index 1df99add..b5de48b5 100644 --- a/packages/addon-mcp/src/preset.test.ts +++ b/packages/addon-mcp/src/preset.test.ts @@ -413,6 +413,41 @@ describe('experimental_devServer', () => { expect(html).not.toMatch(/\{\{[A-Z_]+\}\}/); }); + it('prompts to enable the manifest feature when manifests exist but the flag is off', async () => { + // Manifests are present on disk, but `componentsManifest` is not enabled — the docs + // toolset is unavailable for a different reason than "no manifests", so the page shows + // the "enable the feature" notice rather than the React-only one. + const manifestsNoFlagOptions = { + ...mockOptions, + presets: { + apply: vi.fn(async (key: string) => { + if (key === 'features') { + return { componentsManifest: false }; + } + if (key === 'experimental_manifests') { + return { components: { v: 1, components: {} } }; + } + return undefined; + }), + }, + } as unknown as Options; + + let getHandler: any; + mockApp.get = vi.fn((_path, handler) => { + getHandler = handler; + }); + + await (experimental_devServer as any)(mockApp, manifestsNoFlagOptions); + + const mockRes = { writeHead: vi.fn(), end: vi.fn() } as any; + await getHandler({ headers: { accept: 'text/html' } } as any, mockRes); + + const html = mockRes.end.mock.calls[0][0] as string; + expect(html).toContain('This toolset requires enabling the component manifest feature.'); + expect(html).not.toContain('only supported in React-based setups'); + expect(html).not.toMatch(/\{\{[A-Z_]+\}\}/); + }); + it('should show Storybook version requirement for addon-vitest and a manual manifest link', async () => { vi.spyOn(runStoryTests, 'getAddonVitestConstants').mockResolvedValue(undefined); const manifestEnabledOptions = { From 1744cad29d31fb1d84fdbcaa87a519de5ed08d74 Mon Sep 17 00:00:00 2001 From: yannbf Date: Thu, 4 Jun 2026 08:36:14 +0200 Subject: [PATCH 8/9] format --- packages/addon-mcp/src/mcp-handler.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/addon-mcp/src/mcp-handler.test.ts b/packages/addon-mcp/src/mcp-handler.test.ts index 287da081..a832f5ff 100644 --- a/packages/addon-mcp/src/mcp-handler.test.ts +++ b/packages/addon-mcp/src/mcp-handler.test.ts @@ -11,7 +11,9 @@ import { CompositionAuth } from './auth/index.ts'; // Lets us flip `@storybook/addon-review` presence so the review tool's gate can be exercised. // Keep the real exports (e.g. `normalizeStoryPath`) so unrelated tools still register. -const { mockGetAddonNames } = vi.hoisted(() => ({ mockGetAddonNames: vi.fn(() => [] as string[]) })); +const { mockGetAddonNames } = vi.hoisted(() => ({ + mockGetAddonNames: vi.fn(() => [] as string[]), +})); vi.mock('storybook/internal/common', async (importActual) => ({ ...(await importActual()), loadMainConfig: vi.fn().mockResolvedValue({}), From 902debf8d5f69340d4505bca27a927b3c41a3f90 Mon Sep 17 00:00:00 2001 From: yannbf Date: Thu, 4 Jun 2026 08:38:51 +0200 Subject: [PATCH 9/9] update release note --- .changeset/spotty-toolsets-surface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/spotty-toolsets-surface.md b/.changeset/spotty-toolsets-surface.md index 27283e44..aab42dde 100644 --- a/.changeset/spotty-toolsets-surface.md +++ b/.changeset/spotty-toolsets-surface.md @@ -2,4 +2,4 @@ "@storybook/addon-mcp": patch --- -Surface the change-detection and review tools on the `/mcp` landing page. The "Available Toolsets" list now includes `get-stories-by-component`, `get-changed-stories`, and `display-review` under **dev** (each with an enabled/disabled badge reflecting its real runtime gate), and `get-documentation-for-story` under **docs**. The page and the MCP server now derive tool availability from a single shared helper (`getToolAvailability`) so the rendered badges can't drift from the tools that are actually registered. +Surface the change-detection and review tools on the `/mcp` landing page. The "Available Toolsets" list now includes `get-stories-by-component`, `get-changed-stories`, and `display-review` under **dev** (each with an enabled/disabled badge reflecting its real runtime gate), and `get-documentation-for-story` under **docs**.