Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 14 additions & 8 deletions code/addons/mcp/scripts/generate-tools-api-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
import { buildServerInstructions } from '../src/instructions/build-server-instructions.ts';
import { registerCoreToolsetsForTest } from '../src/test-support/register-core-toolsets.ts';
import { getAddonToolMetadata, type ToolMetadata } from '../src/tools/tool-registry.ts';
import type { ToolAvailability } from '../src/utils/get-tool-availability.ts';

Expand All @@ -41,6 +42,15 @@ const availability = (reviewEnabled: boolean): ToolAvailability => ({
docgenServer: false,
});

/**
* Metadata is resolved from the live toolset registry, so each rendered mode registers the real
* toolsets first — review-off and review-on descriptions genuinely differ per registration.
*/
function toolMetadataFor(reviewEnabled: boolean): ToolMetadata[] {
registerCoreToolsetsForTest({ reviewEnabled });
return getAddonToolMetadata({ availability: availability(reviewEnabled) });
}

const instructions = (reviewEnabled: boolean) =>
buildServerInstructions({
devEnabled: true,
Expand Down Expand Up @@ -218,7 +228,7 @@ async function renderTool(tool: ToolMetadata): Promise<string> {
}

async function renderServerSection(title: string, reviewEnabled: boolean): Promise<string> {
const tools = getAddonToolMetadata({ availability: availability(reviewEnabled) });
const tools = toolMetadataFor(reviewEnabled);
const toolSections = await Promise.all(tools.map(renderTool));
return [
`## ${title}`,
Expand All @@ -236,12 +246,8 @@ async function renderServerSection(title: string, reviewEnabled: boolean): Promi
}

function diffSummary(): string {
const off = new Map(
getAddonToolMetadata({ availability: availability(false) }).map((t) => [t.name, t])
);
const on = new Map(
getAddonToolMetadata({ availability: availability(true) }).map((t) => [t.name, t])
);
const off = new Map(toolMetadataFor(false).map((t) => [t.name, t]));
const on = new Map(toolMetadataFor(true).map((t) => [t.name, t]));
const added = [...on.keys()].filter((name) => !off.has(name));
const removed = [...off.keys()].filter((name) => !on.has(name));
const changed = [...on.keys()].filter(
Expand Down Expand Up @@ -306,7 +312,7 @@ async function runCliHelp(args: string[]): Promise<string> {
}

async function renderCliSection(): Promise<string> {
const toolNames = getAddonToolMetadata({ availability: availability(true) }).map((t) => t.name);
const toolNames = toolMetadataFor(true).map((t) => t.name);
const commands = ['setup', ...toolNames];
const [topLevel, ...commandHelps] = await Promise.all([
runCliHelp([]),
Expand Down
3 changes: 3 additions & 0 deletions code/addons/mcp/src/mcp-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { registerCoreToolsetsForTest } from './test-support/register-core-toolsets.ts';
import {
incomingMessageToWebRequest,
webResponseToServerResponse,
Expand Down Expand Up @@ -205,6 +206,8 @@ describe('mcpServerHandler', () => {
beforeEach(async () => {
// Reset modules and get fresh handler for each test to avoid state pollution
vi.resetModules();
// The `services` preset hook does this in a real Storybook, before the MCP server boots.
registerCoreToolsetsForTest();
const handler = await import('./mcp-handler.ts');
mcpServerHandler = handler.mcpServerHandler;
});
Expand Down
3 changes: 3 additions & 0 deletions code/addons/mcp/src/preset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { experimental_devServer } from './preset.ts';
import * as mcpHandlerModule from './mcp-handler.ts';
import * as runStoryTests from './tools/run-story-tests.ts';
import * as moduleGraph from './utils/module-graph.ts';
import { registerCoreToolsetsForTest } from './test-support/register-core-toolsets.ts';

describe('experimental_devServer', () => {
let mockApp: any;
let mockOptions: Options;
let mcpHandler: any;

beforeEach(() => {
// The `services` preset hook does this in a real Storybook before the dev server mounts /mcp.
registerCoreToolsetsForTest();
vi.restoreAllMocks();

mockApp = {
Expand Down
3 changes: 3 additions & 0 deletions code/addons/mcp/src/storybook-ai-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { registerCoreToolsetsForTest } from './test-support/register-core-toolsets.ts';
import { McpServer } from 'tmcp';
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
import { GET_TOOL_NAME } from '@storybook/mcp';
Expand Down Expand Up @@ -47,6 +48,8 @@ vi.mock('./tools/run-story-tests.ts', async (importActual) => ({

describe('buildStorybookAiMetadata', () => {
beforeEach(() => {
// The `services` preset hook does this in a real Storybook before metadata is built.
registerCoreToolsetsForTest();
vi.clearAllMocks();
vi.stubGlobal('fetch', vi.fn(mockManifestFetch(true)));
vi.mocked(isModuleGraphSupported).mockResolvedValue(true);
Expand Down
35 changes: 35 additions & 0 deletions code/addons/mcp/src/test-support/register-core-toolsets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Registers the core toolsets the MCP tools are adapters over.
*
* Test support: in a real Storybook the `services` preset hook registers these before the MCP
* server boots, so a test that exercises tool registration has to stand that step up too. The real
* toolsets are used (with stub runtime dependencies) rather than fakes, so the adapter is tested
* against the definitions it ships with.
*/

import type { StoryIndex } from 'storybook/internal/types';
import { clearToolsetRegistry } from 'storybook/open-service';
import { createStoriesToolset, registerToolset } from 'storybook/internal/core-server';

const EMPTY_INDEX: StoryIndex = { v: 5, entries: {} };

export function registerCoreToolsetsForTest({
index = EMPTY_INDEX,
reviewEnabled = true,
}: { index?: StoryIndex; reviewEnabled?: boolean } = {}) {
clearToolsetRegistry();

const storyIndex = { getIndex: async () => index };

registerToolset(
createStoriesToolset({
storyIndex,
git: {
getRepoRoot: async () => process.cwd(),
getChangedFiles: async () => ({ changed: new Set<string>(), new: new Set<string>() }),
},
changeStatuses: { getAll: () => ({}) },
reviewEnabled,
})
);
}
Loading
Loading