UNPKG

@storybook/addon-mcp

Version:

Help agents automatically write and test stories for your UI components

1,635 lines • 64 kB
import CJS_COMPAT_NODE_URL_lqqi79b8sm from 'node:url';
import CJS_COMPAT_NODE_PATH_lqqi79b8sm from 'node:path';
import CJS_COMPAT_NODE_MODULE_lqqi79b8sm from "node:module";

var __filename = CJS_COMPAT_NODE_URL_lqqi79b8sm.fileURLToPath(import.meta.url);
var __dirname = CJS_COMPAT_NODE_PATH_lqqi79b8sm.dirname(__filename);
var require = CJS_COMPAT_NODE_MODULE_lqqi79b8sm.createRequire(import.meta.url);

// ------------------------------------------------------------
// end of CJS compatibility banner, injected by Storybook's esbuild configuration
// ------------------------------------------------------------

// src/mcp-handler.ts
import { McpServer } from "tmcp";
import { ValibotJsonSchemaAdapter } from "@tmcp/adapter-valibot";
import { HttpTransport } from "@tmcp/transport-http";

// package.json
var package_default = {
  name: "@storybook/addon-mcp",
  version: "10.6.5",
  description: "Help agents automatically write and test stories for your UI components",
  keywords: [
    "ai",
    "mcp",
    "storybook-addon"
  ],
  homepage: "https://github.com/storybookjs/storybook/tree/next/code/addons/mcp",
  bugs: {
    url: "https://github.com/storybookjs/storybook/issues"
  },
  repository: {
    type: "git",
    url: "https://github.com/storybookjs/storybook.git",
    directory: "code/addons/mcp"
  },
  funding: {
    type: "opencollective",
    url: "https://opencollective.com/storybook"
  },
  license: "MIT",
  author: "Storybook Core Team",
  type: "module",
  exports: {
    "./internal/preview-stories-app-script": "./dist/tools/preview-stories/preview-stories-app-script.js",
    "./package.json": "./package.json",
    "./preset": "./dist/preset.js"
  },
  files: [
    "dist/**/*",
    "README.md",
    "*.js",
    "*.d.ts",
    "!src/**/*"
  ],
  dependencies: {
    "@tmcp/adapter-valibot": "^0.1.5",
    "@tmcp/transport-http": "^0.8.5",
    tmcp: "^1.19.4",
    valibot: "^1.4.0"
  },
  devDependencies: {
    "@storybook/addon-a11y": "workspace:*",
    "@storybook/addon-vitest": "workspace:*",
    storybook: "workspace:*",
    typescript: "^6.0.3"
  },
  peerDependencies: {
    "@storybook/addon-vitest": "workspace:^",
    storybook: "workspace:^"
  },
  peerDependenciesMeta: {
    "@storybook/addon-vitest": {
      optional: !0
    }
  },
  publishConfig: {
    access: "public"
  },
  mcpName: "io.github.storybookjs/addon-mcp",
  storybook: {
    displayName: "Addon MCP",
    supportedFrameworks: [
      "react",
      "vue3",
      "angular",
      "web-components",
      "html",
      "svelte",
      "preact",
      "react-native"
    ],
    icon: "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png"
  }
};

// src/mcp-handler.ts
import { buffer } from "node:stream/consumers";

// src/telemetry.ts
import { logger } from "storybook/internal/node-logger";
import { telemetry } from "storybook/internal/telemetry";
async function collectTelemetry({
  event,
  server,
  ...payload
}) {
  try {
    return await telemetry("addon-mcp", {
      event,
      mcpSessionId: server.ctx.sessionId,
      clientInfo: server.ctx.sessionInfo?.clientInfo,
      clientCapabilities: server.ctx.sessionInfo?.clientCapabilities,
      // Which channel drove the call: the `storybook ai` CLI marks its requests with the proxy
      // header; everything else is a direct MCP client.
      transport: server.ctx.custom?.cliClient ? "cli" : "mcp",
      ...payload
    });
  } catch (error) {
    logger.debug(`Error collecting telemetry: ${String(error)}`);
  }
}

// src/mcp-handler.ts
import { logger as logger4 } from "storybook/internal/node-logger";
import { getEffectiveToolAvailability, getToolAvailability } from "storybook/internal/core-server";
import { buildServerInstructions } from "storybook/internal/skills";

// src/constants.ts
var STORYBOOK_MCP_PROXY_HEADER = "X-Storybook-MCP-Proxy", DEFAULT_MCP_ENDPOINT = "/mcp";

// src/tools/tool-registry.ts
import { logger as logger3 } from "storybook/internal/node-logger";
import {
  createCompositionDocsSources,
  createDocsToolset
} from "storybook/internal/toolsets-docs";

// src/utils/format-validation-issues.ts
function formatPath(path2) {
  if (!path2 || path2.length === 0) return "";
  let out = "";
  for (let segment of path2) {
    let key = typeof segment == "object" && segment !== null && "key" in segment ? segment.key : segment;
    typeof key == "number" ? out += `[${key}]` : key != null && (out += out === "" ? String(key) : `.${String(key)}`);
  }
  return out;
}
function summarizeIssue(issue) {
  let path2 = formatPath(issue.path);
  if (issue.kind === "schema" && (issue.type === "object" || issue.type === "strict_object" || issue.type === "loose_object") && typeof issue.expected == "string" && issue.expected.length >= 2 && issue.expected.startsWith('"') && issue.expected.endsWith('"') && issue.received === "undefined") {
    let field = issue.expected.slice(1, -1), parentPathStr = formatPath((issue.path ?? []).slice(0, -1)), where = parentPathStr ? ` at \`${parentPathStr}\`` : "";
    return { path: path2, message: `Missing required field \`${field}\`${where}.` };
  }
  return { path: path2, message: issue.message };
}
function withFriendlyErrors(schema) {
  let original = schema["~standard"];
  return {
    ...schema,
    "~standard": {
      ...original,
      validate(input) {
        let result = original.validate(input), trim = (r) => (
          // Cast: the spec issue type is the base shape; validators
          // (notably valibot) augment with `kind`, `type`, `expected`,
          // `received` — see {@link StandardSchemaIssue}.
          r.issues ? { issues: r.issues.map(summarizeIssue) } : r
        );
        return result instanceof Promise ? result.then(trim) : trim(result);
      }
    }
  };
}

// src/tools/get-storybook-story-instructions.ts
import { resolveSkillInputs } from "storybook/internal/core-server";
import { buildStoryInstructions } from "storybook/internal/skills";

// src/utils/errors.ts
var errorToMCPContent = (error) => ({
  content: [
    {
      type: "text",
      text: `Error: ${error instanceof Error ? error.message : String(error)}`
    }
  ],
  isError: !0
});

// src/tools/tool-names.ts
import { toMcpToolName } from "storybook/open-service";
var PREVIEW_STORIES_TOOL_NAME = toMcpToolName("stories.preview"), GET_CHANGED_STORIES_TOOL_NAME = toMcpToolName("stories.changed"), GET_STORIES_BY_COMPONENT_TOOL_NAME = toMcpToolName("stories.findByComponent"), RUN_STORY_TESTS_TOOL_NAME = toMcpToolName("test.run"), DISPLAY_REVIEW_TOOL_NAME = toMcpToolName("review.create"), GET_UI_BUILDING_INSTRUCTIONS_TOOL_NAME = "get-storybook-story-instructions";

// src/tools/get-storybook-story-instructions.ts
async function addGetUIBuildingInstructionsTool(server, enabled = () => server.ctx.custom?.toolsets?.dev ?? !0, {
  docsEnabled = !1,
  addonVitestAvailable = !1
} = {}) {
  server.tool(
    {
      name: GET_UI_BUILDING_INSTRUCTIONS_TOOL_NAME,
      title: "Storybook Story Development Instructions",
      get description() {
        let testSupported = (server.ctx.custom?.toolsets?.test ?? !0) && addonVitestAvailable, a11yAvailable = testSupported && (server.ctx.custom?.a11yEnabled ?? !1);
        return getStorybookStoryInstructionsDescription({
          testSupported,
          a11yAvailable
        });
      },
      enabled
    },
    async () => {
      try {
        let { options, disableTelemetry: disableTelemetry2 } = server.ctx.custom ?? {};
        if (!options)
          throw new Error("Options are required in addon context");
        return disableTelemetry2 || await collectTelemetry({
          event: "tool:getUIBuildingInstructions",
          server,
          toolset: "dev"
        }), {
          content: [{ type: "text", text: await buildStorybookStoryInstructions(options, {
            toolsets: server.ctx.custom?.toolsets,
            a11yEnabled: server.ctx.custom?.a11yEnabled,
            addonVitestAvailable,
            docsEnabled,
            reviewEnabled: server.ctx.custom?.reviewEnabled
          }) }]
        };
      } catch (error) {
        return errorToMCPContent(error);
      }
    }
  );
}
function getStorybookStoryInstructionsDescription({
  testSupported,
  a11yAvailable
}) {
  return `Get comprehensive instructions for writing, testing, and fixing Storybook stories (.stories.tsx, .stories.ts, .stories.jsx, .stories.js, .stories.svelte, .stories.vue files).

CRITICAL: You MUST call this tool before:
- Creating new Storybook stories or story files
- Updating or modifying existing Storybook stories
- Adding new story variants or exports to story files
- Editing any file matching *.stories.* patterns
- Writing components that will need stories
- Editing anything that changes how the UI looks \u2014 components, styles, CSS, themes, colors, or design tokens; a shared file with no stories of its own still changes its consumers' stories${testSupported ? `
- Running story tests or fixing test failures` : ""}${a11yAvailable ? `
- Handling accessibility (a11y) violations in stories (fix semantic issues directly; ask before visual/design changes)` : ""}

This tool provides essential Storybook-specific guidance including:
- How to structure stories correctly for Storybook 9
- Required imports (Meta, StoryObj from framework package)
- Test utility imports (from 'storybook/test')
- Story naming conventions and best practices
- Play function patterns for interactive testing
- Mocking strategies for external dependencies
- Story variants and coverage requirements${testSupported ? `
- How to handle test failures${a11yAvailable ? " and accessibility violations" : ""}` : ""}

Even if you're familiar with Storybook, call this tool to ensure you're following the correct patterns, import paths, and conventions for this specific Storybook setup.`;
}
function getStorybookStoryInstructionsToolMetadata(options) {
  return {
    name: GET_UI_BUILDING_INSTRUCTIONS_TOOL_NAME,
    title: "Storybook Story Development Instructions",
    description: getStorybookStoryInstructionsDescription(options)
  };
}
async function buildStorybookStoryInstructions(options, {
  toolsets,
  a11yEnabled: a11yEnabled2,
  addonVitestAvailable,
  docsEnabled,
  reviewEnabled: reviewEnabledOverride
} = {}) {
  let inputs = await resolveSkillInputs(options);
  return buildStoryInstructions({
    transport: "mcp",
    framework: inputs.framework,
    renderer: inputs.renderer,
    changeDetectionEnabled: inputs.changeDetectionEnabled,
    reviewEnabled: reviewEnabledOverride ?? inputs.reviewEnabled,
    testSupported: (toolsets?.test ?? !0) && (addonVitestAvailable ?? inputs.testSupported),
    a11yEnabled: a11yEnabled2 ?? inputs.a11yEnabled,
    docsEnabled: (toolsets?.docs ?? !0) && (docsEnabled ?? inputs.docsEnabled)
  });
}

// src/tools/preview-stories.ts
import url from "node:url";
import fs from "node:fs/promises";

// src/tools/preview-stories/preview-stories-app-template.html
var preview_stories_app_template_default = `<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html[data-theme='light'] {
        color-scheme: light;
      }
      html[data-theme='dark'] {
        color-scheme: dark;
      }
      html,
      body {
        width: 100%;
        margin: 0;
        padding: 0;
        background-color: var(--color-background-secondary);
      }
      body {
        display: flex;
        flex-direction: column;
        gap: 1rem;
      }
      :root {
        /*
				These are fallback values, if the MCP client doesn't set them.
				See https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#theming
				*/
        --color-text-primary: light-dark(black, white);
        --color-border-primary: light-dark(#ccc, #444);
        --color-background-secondary: light-dark(#f9f9f9, #1e1e1e);
        --font-sans:
          -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Oxygen-Sans', Ubuntu, Cantarell,
          'Helvetica Neue', sans-serif;
        --font-heading-xs-size: 1rem;
        --font-heading-xs-line-height: 1.25;
        --border-width-regular: 1px;
      }
      .story-heading {
        font-family: var(--font-sans);
        font-size: var(--font-heading-xs-size);
        line-height: var(--font-heading-xs-line-height);
        color: var(--color-text-primary);
        padding: 0.5rem 0;
      }
      .story-iframe {
        background-color: white;
        border: var(--border-width-regular) solid var(--color-border-primary);
      }
      body:has(> article:only-child) h1 {
        /* if there is only one story rendered, hide its heading */
        display: none;
      }
    </style>
    <template id="preview-template">
      <article>
        <h1 class="story-heading"></h1>
        <iframe class="story-iframe"></iframe>
      </article>
    </template>
    <script type="module">
      // APP_SCRIPT_PLACEHOLDER
    </script>
  </head>
  <body></body>
</html>
`;

// src/tools/preview-stories.ts
var PREVIEW_STORIES_RESOURCE_URI = `ui://${PREVIEW_STORIES_TOOL_NAME}/preview.html`;
async function addPreviewStoriesResource(server) {
  let previewStoryAppScript = await fs.readFile(
    url.fileURLToPath(
      import.meta.resolve("@storybook/addon-mcp/internal/preview-stories-app-script")
    ),
    "utf-8"
  ), appHtml = preview_stories_app_template_default.replace("// APP_SCRIPT_PLACEHOLDER", previewStoryAppScript);
  server.resource(
    {
      name: PREVIEW_STORIES_RESOURCE_URI,
      description: "App resource for the Preview Stories tool",
      uri: PREVIEW_STORIES_RESOURCE_URI,
      mimeType: "text/html;profile=mcp-app"
    },
    () => {
      let origin2 = server.ctx.custom.origin;
      return {
        contents: [
          {
            uri: PREVIEW_STORIES_RESOURCE_URI,
            mimeType: "text/html;profile=mcp-app",
            text: appHtml,
            _meta: {
              ui: {
                prefersBorder: !1,
                domain: origin2,
                csp: {
                  connectDomains: [origin2],
                  resourceDomains: [origin2],
                  frameDomains: [origin2],
                  baseUriDomains: [origin2]
                }
              }
            }
          }
        ]
      };
    }
  );
}

// src/tools/tool-registry.ts
import { OpenServiceMissingToolsetError, toMcpToolName as toMcpToolName3 } from "storybook/open-service";

// src/tools/toolset-tools.ts
import { getService } from "storybook/internal/core-server";
import { logger as logger2 } from "storybook/internal/node-logger";
import { OpenServiceToolsetOutputMismatchError } from "storybook/internal/server-errors";
import {
  getToolset,
  invokeToolsetMethod,
  parseToolsetMethodId,
  resolveToolsetDescription,
  toMcpToolName as toMcpToolName2
} from "storybook/open-service";

// src/tools/toolset-origin.ts
function storybookRootFromRequest(request, trustedOrigin, endpoint) {
  if (request?.url)
    try {
      let url2 = new URL(request.url), normalizedEndpoint = endpoint.replace(/\/$/, ""), normalizedPathname = url2.pathname.replace(/\/$/, ""), rootPath = normalizedPathname.endsWith(normalizedEndpoint) ? normalizedPathname.slice(0, -normalizedEndpoint.length) : normalizedPathname.replace(/\/[^/]+$/, "");
      return `${trustedOrigin.replace(/\/$/, "")}${rootPath}`;
    } catch {
      return;
    }
}
function resolveToolsetOrigin(context) {
  if (context.origin)
    return context.request ? storybookRootFromRequest(
      context.request,
      context.origin,
      context.endpoint ?? DEFAULT_MCP_ENDPOINT
    ) ?? context.origin : context.origin;
}

// src/tools/toolset-tools.ts
function resolveToolset(options, server) {
  return options.resolveToolset ? options.resolveToolset(server) : getToolset(parseToolsetMethodId(options.method).toolsetId);
}
function resolveMethod(toolset, options) {
  return toolset.methods[parseToolsetMethodId(options.method).methodName];
}
function isAgentFacingError(error) {
  return error instanceof Error && error.agentFacing === !0;
}
async function toStructuredContent(outputSchema, data) {
  if (!outputSchema || data === void 0)
    return;
  let result = await outputSchema["~standard"].validate(data);
  if (result.issues)
    throw new OpenServiceToolsetOutputMismatchError({ issues: result.issues });
  return result.value;
}
function buildContext(server) {
  return {
    transport: "mcp",
    // The toolset origin is the complete UI base URL, including any deployment subpath.
    origin: resolveToolsetOrigin(server.ctx.custom ?? {}),
    getService: (serviceId, serviceOptions) => getService(serviceId, serviceOptions)
  };
}
async function reportToolsetTelemetry(server, event, report) {
  if (!report || server.ctx.custom?.disableTelemetry)
    return;
  let { payload, toolset, tool } = report;
  await collectTelemetry({ event, server, ...payload, toolset, tool });
}
async function callToolsetMethod(server, options, input) {
  let toolset = resolveToolset(options, server), { methodName } = parseToolsetMethodId(options.method), method = toolset.methods[methodName];
  try {
    let outcome = await invokeToolsetMethod(toolset, methodName, input, buildContext(server));
    await reportToolsetTelemetry(server, options.mcpEventName, outcome.telemetry);
    let structuredContent = await toStructuredContent(method.output, outcome.data);
    return {
      content: (Array.isArray(outcome.markdown) ? outcome.markdown : [outcome.markdown]).map((text) => ({ type: "text", text })),
      ...structuredContent !== void 0 ? { structuredContent } : {},
      ...outcome.ok ? {} : { isError: !0 }
    };
  } catch (error) {
    return isAgentFacingError(error) ? { content: [{ type: "text", text: error.message }], isError: !0 } : (logger2.error(`MCP tool "${toMcpToolName2(options.method)}" failed: ${error}`), errorToMCPContent(error));
  }
}
function getToolsetToolMetadata(options) {
  let method = resolveMethod(resolveToolset(options), options), descriptionCtx = {
    transport: "mcp",
    getService: (serviceId, serviceOptions) => getService(serviceId, serviceOptions)
  }, entries = method.input.entries, hasInput = !entries || Object.keys(entries).length > 0;
  return {
    name: toMcpToolName2(options.method),
    title: method.title,
    description: resolveToolsetDescription(method.description, descriptionCtx),
    ...hasInput ? { schema: options.wrapSchema ? options.wrapSchema(method.input) : method.input } : {},
    ...method.output ? { outputSchema: method.output } : {},
    ...options.extras
  };
}
function registerToolsetTool(server, options, enabled) {
  server.tool(
    { ...getToolsetToolMetadata(options), enabled },
    ((input) => callToolsetMethod(server, options, input))
  );
}

// src/tools/tool-registry.ts
var isToolsetEnabled = (toolset, toolsets) => toolsets?.[toolset] ?? !0, isToolAvailable = (definition, context) => definition.available?.(context) ?? !0, isMetadataToolEnabled = (definition, context) => isToolsetEnabled(definition.toolset, context.toolsets) && isToolAvailable(definition, context), createToolsetEnabled = (server, toolset) => () => server.ctx.custom?.toolsets?.[toolset] ?? !0;
function fromToolset(definition) {
  let { options, available, wrapEnabled, ...rest } = definition;
  return {
    ...rest,
    // Read from the constant, not the registry: this array is built at import time, while toolsets
    // register later from their preset hooks. Each availability gate is written to match the
    // condition under which its toolset registers; if they still disagree, resolution fails loudly
    // (getToolset throws) and the registry drops that one tool with an error log rather than taking
    // down the whole server (see resolveDefinitionOrDrop).
    name: toMcpToolName3(options.method),
    available: (context) => available?.(context) ?? !0,
    getMetadata: () => getToolsetToolMetadata(options),
    register: async (server, context, enabled) => {
      registerToolsetTool(server, options, wrapEnabled?.(server, context, enabled) ?? enabled);
    }
  };
}
function compositionDocsToolset(server) {
  let custom = server?.ctx.custom;
  return createDocsToolset({
    sources: createCompositionDocsSources({
      sources: custom?.sources ?? [{ id: "local", title: "Local" }],
      manifestProvider: custom?.manifestProvider,
      getRequest: () => custom?.request,
      localAccess: custom?.localAccess
    })
  });
}
var DOCS_EVENT_NAMES = {
  "docs.list": "tool:listAllDocumentation",
  "docs.show": "tool:getDocumentation",
  "docs.showStory": "tool:getDocumentationForStory"
};
function docsToolDefinition(method) {
  let forContext = (context) => ({
    method,
    mcpEventName: DOCS_EVENT_NAMES[method],
    ...context.multiSource ? { resolveToolset: (server) => compositionDocsToolset(server) } : {}
  });
  return {
    name: toMcpToolName3(method),
    toolset: "docs",
    available: ({ availability }) => availability.docsEnabled,
    getMetadata: (context) => getToolsetToolMetadata(forContext(context)),
    register: async (server, context, enabled) => {
      registerToolsetTool(server, forContext(context), enabled);
    }
  };
}
var docsToolDefinitions = [
  docsToolDefinition("docs.list"),
  docsToolDefinition("docs.show"),
  docsToolDefinition("docs.showStory")
], addonToolDefinitions = [
  fromToolset({
    toolset: "dev",
    options: {
      method: "stories.preview",
      mcpEventName: "tool:previewStories",
      extras: { _meta: { ui: { resourceUri: PREVIEW_STORIES_RESOURCE_URI } } }
    }
  }),
  {
    name: GET_UI_BUILDING_INSTRUCTIONS_TOOL_NAME,
    toolset: "dev",
    getMetadata: ({ availability, toolsets }) => {
      let testSupported = isToolsetEnabled("test", toolsets) && availability.testSupported;
      return getStorybookStoryInstructionsToolMetadata({
        testSupported,
        a11yAvailable: testSupported && availability.a11yEnabled
      });
    },
    register: (server, { availability, toolsets }, enabled) => addGetUIBuildingInstructionsTool(server, enabled, {
      docsEnabled: isToolsetEnabled("docs", toolsets) && availability.docsEnabled,
      addonVitestAvailable: availability.testSupported
    }),
    getLocalTool: ({ availability, toolsets, options }) => ({
      call: async () => ({ content: [{ type: "text", text: await buildStorybookStoryInstructions(options, {
        toolsets,
        a11yEnabled: availability.a11yEnabled,
        addonVitestAvailable: availability.testSupported,
        docsEnabled: isToolsetEnabled("docs", toolsets) && availability.docsEnabled,
        reviewEnabled: availability.reviewEnabled
      }) }] })
    })
  },
  fromToolset({
    toolset: "dev",
    available: ({ availability }) => availability.changeDetectionEnabled,
    options: { method: "stories.changed", mcpEventName: "tool:getChangedStories" }
  }),
  fromToolset({
    toolset: "dev",
    available: ({ availability }) => availability.moduleGraphSupported,
    options: { method: "stories.findByComponent", mcpEventName: "tool:getStoriesByComponent" }
  }),
  fromToolset({
    toolset: "dev",
    // Registered whenever the CLI default could turn review on; the per-request `reviewEnabled`
    // context (explicit flag, or the trusted local-client header) decides whether a given MCP
    // client actually sees the tool.
    available: ({ availability }) => availability.reviewEnabledForCli,
    wrapEnabled: (server, { availability }, enabled) => async () => (await enabled?.() ?? !0) && (server.ctx.custom?.reviewEnabled ?? availability.reviewEnabled),
    options: {
      method: "review.create",
      mcpEventName: "tool:displayReview",
      wrapSchema: withFriendlyErrors
    }
  }),
  fromToolset({
    toolset: "test",
    available: ({ availability }) => availability.testSupported,
    options: { method: "test.run", mcpEventName: "tool:runStoryTests" }
  }),
  // Docs run on the core docs toolset in both modes. A composition builds its toolset per request,
  // because the sources it reads and the provider that fetches them belong to the request.
  ...docsToolDefinitions
];
function dropToolIfToolsetMissing(name, error) {
  if (!(error instanceof OpenServiceMissingToolsetError))
    throw error;
  logger3.error(`Skipping MCP tool "${name}", its backing toolset is not registered: ${error}`);
}
function resolveDefinitionOrDrop(name, resolve) {
  try {
    return resolve();
  } catch (error) {
    return dropToolIfToolsetMissing(name, error);
  }
}
function getAddonToolMetadata(context) {
  return addonToolDefinitions.filter((definition) => isMetadataToolEnabled(definition, context)).flatMap((definition) => {
    let metadata = resolveDefinitionOrDrop(
      definition.name,
      () => definition.getMetadata(context)
    );
    return metadata ? [metadata] : [];
  });
}
function getAddonLocalTools(context) {
  return Object.fromEntries(
    addonToolDefinitions.filter((definition) => isMetadataToolEnabled(definition, context)).flatMap((definition) => {
      let localTool = resolveDefinitionOrDrop(
        definition.name,
        () => definition.getLocalTool?.(context)
      );
      return localTool ? [[definition.name, localTool]] : [];
    })
  );
}
async function registerAddonMcpTools(server, context) {
  isToolsetEnabled("dev", context.toolsets) && await addPreviewStoriesResource(server);
  for (let definition of addonToolDefinitions)
    if (isToolsetEnabled(definition.toolset, context.toolsets) && isToolAvailable(definition, context))
      try {
        await definition.register(
          server,
          context,
          createToolsetEnabled(server, definition.toolset)
        );
      } catch (error) {
        dropToolIfToolsetMissing(definition.name, error);
      }
}

// src/mcp-handler.ts
var transport, origin, initialize, disableTelemetry, a11yEnabled, reviewGates, initializeMCPServer = async (options, multiSource) => {
  let core = await options.presets.apply("core", {}), features2 = await options.presets.apply("features", {});
  disableTelemetry = core?.disableTelemetry ?? !1;
  let rawAvailability = await getToolAvailability(options, { features: features2 }), availability = getEffectiveToolAvailability(rawAvailability, { multiSource });
  a11yEnabled = availability.a11yEnabled, reviewGates = {
    reviewEnabled: availability.reviewEnabled,
    reviewEnabledForCli: availability.reviewEnabledForCli
  };
  let server, serverOptions = {
    adapter: new ValibotJsonSchemaAdapter(),
    get instructions() {
      return buildServerInstructions({
        transport: "mcp",
        devEnabled: server?.ctx.custom?.toolsets?.dev ?? !0,
        testSupported: (server?.ctx.custom?.toolsets?.test ?? !0) && availability.testSupported,
        docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? !0) && availability.docsEnabled,
        changeDetectionEnabled: availability.changeDetectionEnabled,
        moduleGraphSupported: availability.moduleGraphSupported,
        reviewEnabled: server?.ctx.custom?.reviewEnabled ?? availability.reviewEnabled
      });
    },
    capabilities: {
      tools: { listChanged: !0 },
      resources: { listChanged: !0 }
    }
  };
  return server = new McpServer(
    {
      name: package_default.name,
      version: package_default.version,
      description: package_default.description
    },
    serverOptions
  ).withContext(), disableTelemetry || server.on("initialize", async () => {
    await collectTelemetry({ event: "session:initialized", server });
  }), await registerAddonMcpTools(server, { availability, multiSource }), transport = new HttpTransport(server, { path: null }), origin = `http://localhost:${options.port}`, logger4.debug(`MCP server origin: ${origin}`), server;
}, mcpServerHandler = async ({
  req,
  res,
  options,
  addonOptions,
  endpoint = DEFAULT_MCP_ENDPOINT,
  sources,
  manifestProvider,
  localAccess,
  compositionAuth
}) => {
  initialize || (initialize = initializeMCPServer(
    options,
    sources?.some((s) => s.url)
  )), await initialize;
  let webRequest = await incomingMessageToWebRequest(req), addonContext = {
    options,
    endpoint,
    toolsets: getToolsets(webRequest, addonOptions),
    reviewEnabled: isReviewEnabledForRequest(webRequest, reviewGates),
    cliClient: webRequest.headers.get(STORYBOOK_MCP_PROXY_HEADER) === "true",
    origin,
    disableTelemetry,
    a11yEnabled,
    request: webRequest,
    sources,
    manifestProvider,
    localAccess
  }, response = await transport.respond(webRequest, addonContext);
  if (response) {
    let body = await response.arrayBuffer(), finalResponse = compositionAuth.hadAuthError(webRequest) ? new Response("401 - Unauthorized", {
      status: 401,
      headers: {
        "Content-Type": "text/plain",
        "WWW-Authenticate": compositionAuth.buildWwwAuthenticate(origin)
      }
    }) : new Response(body, { status: response.status, headers: response.headers });
    await webResponseToServerResponse(finalResponse, res);
  }
};
async function incomingMessageToWebRequest(req) {
  let host = req.headers.host || "localhost", protocol = "encrypted" in req.socket && req.socket.encrypted ? "https" : "http", url2 = new URL(req.url || "/", `${protocol}://${host}`), bodyBuffer = await buffer(req);
  return new Request(url2, {
    method: req.method,
    headers: req.headers,
    body: bodyBuffer.length > 0 ? new Uint8Array(bodyBuffer) : void 0
  });
}
async function webResponseToServerResponse(webResponse, nodeResponse) {
  if (nodeResponse.statusCode = webResponse.status, webResponse.headers.forEach((value, key) => {
    nodeResponse.setHeader(key, value);
  }), webResponse.body) {
    let reader = webResponse.body.getReader();
    try {
      for (; ; ) {
        let { done, value } = await reader.read();
        if (done) break;
        nodeResponse.write(value);
      }
    } finally {
      reader.releaseLock();
    }
  }
  nodeResponse.end();
}
function isReviewEnabledForRequest(request, gates) {
  return gates.reviewEnabled || gates.reviewEnabledForCli && request.headers.get(STORYBOOK_MCP_PROXY_HEADER) === "true";
}
function getToolsets(request, addonOptions) {
  let toolsetHeader = request.headers.get("X-MCP-Toolsets");
  if (!toolsetHeader || toolsetHeader.trim() === "")
    return addonOptions.toolsets;
  let toolsets = {
    dev: !1,
    docs: !1,
    test: !1
  }, enabledToolsets = toolsetHeader.split(",");
  for (let enabledToolset of enabledToolsets) {
    let trimmedToolset = enabledToolset.trim();
    trimmedToolset in toolsets && (toolsets[trimmedToolset] = !0);
  }
  return toolsets;
}

// src/types.ts
import * as v from "valibot";
var isLiteralEndpointPathname = (endpoint) => {
  try {
    let { pathname } = new URL(endpoint, "http://storybook.local");
    return pathname === endpoint && pathname !== "/";
  } catch {
    return !1;
  }
}, AddonOptions = v.object({
  endpoint: v.optional(
    v.pipe(
      v.string(),
      v.check(isLiteralEndpointPathname, "Endpoint must be a literal URL pathname")
    )
  ),
  toolsets: v.optional(
    v.object({
      dev: v.exactOptional(v.boolean(), !0),
      docs: v.exactOptional(v.boolean(), !0),
      test: v.exactOptional(v.boolean(), !0)
    }),
    {
      // Default values for toolsets
      dev: !0,
      docs: !0,
      test: !0
    }
  )
});

// src/preset.ts
import * as v5 from "valibot";
import {
  createLocalDocsAccess,
  getEffectiveToolAvailability as getEffectiveToolAvailability3,
  getToolAvailability as getToolAvailability3,
  isValidHost,
  loadManifests
} from "storybook/internal/core-server";
import { isWebContainer } from "storybook/internal/common";

// src/template.html
var template_default = `<!doctype html>
<html>
  <head>
    <style>
      @font-face {
        font-family: 'Nunito Sans';
        font-style: normal;
        font-weight: 400;
        font-display: swap;
        src: url('./sb-common-assets/nunito-sans-regular.woff2') format('woff2');
      }

      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      html,
      body {
        height: 100%;
        font-family:
          'Nunito Sans',
          -apple-system,
          BlinkMacSystemFont,
          'Segoe UI',
          Roboto,
          Oxygen,
          Ubuntu,
          Cantarell,
          sans-serif;
      }

      body {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        text-align: center;
        padding: 2rem;
        background-color: #ffffff;
        color: rgb(46, 52, 56);
        line-height: 1.6;
      }

      p {
        margin-bottom: 1rem;
      }

      code {
        font-family: 'Monaco', 'Courier New', monospace;
        background: #f5f5f5;
        padding: 0.2em 0.4em;
        border-radius: 3px;
      }

      a {
        color: #1ea7fd;
      }

      .container {
        display: flex;
        flex-direction: column;
        align-items: center;
      }

      .toolsets {
        margin: 1.5rem 0;
        text-align: left;
        max-width: 500px;
      }

      .toolsets h3 {
        font-size: 1rem;
        margin-bottom: 0.75rem;
        text-align: center;
      }

      .toolset {
        margin-bottom: 1rem;
        padding: 0.75rem 1rem;
        border-radius: 6px;
        background: #f8f9fa;
        border: 1px solid #e9ecef;
      }

      .toolset-header {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        font-weight: 600;
        margin-bottom: 0.5rem;
      }

      .toolset-status {
        display: inline-block;
        padding: 0.15em 0.5em;
        border-radius: 3px;
        font-size: 0.75rem;
        font-weight: 500;
        text-transform: uppercase;
      }

      .toolset-status.enabled {
        background: #d4edda;
        color: #155724;
      }

      .toolset-status.disabled {
        background: #f8d7da;
        color: #721c24;
      }

      .toolset-tools {
        font-size: 0.875rem;
        color: #6c757d;
        padding-left: 1.5rem;
        margin: 0;
      }

      .toolset-tools li {
        margin-bottom: 0.25rem;
      }

      .toolset-tools code {
        font-size: 0.8rem;
      }

      .toolset-notice {
        font-size: 0.8rem;
        color: #856404;
        background: #fff3cd;
        padding: 0.5rem;
        border-radius: 4px;
        margin-top: 0.5rem;
      }

      .toolset-notice a {
        color: #533f03;
      }

      @media (prefers-color-scheme: dark) {
        body {
          background-color: rgb(34, 36, 37);
          color: rgb(201, 205, 207);
        }

        code {
          background: rgba(255, 255, 255, 0.1);
        }

        .toolset {
          background: rgba(255, 255, 255, 0.05);
          border-color: rgba(255, 255, 255, 0.1);
        }

        .toolset-tools {
          color: #adb5bd;
        }

        .toolset-status.enabled {
          background: rgba(40, 167, 69, 0.2);
          color: #75d67e;
        }

        .toolset-status.disabled {
          background: rgba(220, 53, 69, 0.2);
          color: #f5a6ad;
        }

        .toolset-notice {
          background: rgba(255, 193, 7, 0.15);
          color: #ffc107;
        }

        .toolset-notice a {
          color: #ffe066;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <p>
        Storybook MCP server successfully running via
        <code>@storybook/addon-mcp</code>.
      </p>
      <p>
        See how to connect to it from your coding agent in
        <a target="_blank" href="https://github.com/storybookjs/storybook/tree/next/code/addons/mcp"
          >the addon's README</a
        >.
      </p>

      <div class="toolsets">
        <h3>Available Toolsets</h3>

        <div class="toolset">
          <div class="toolset-header">
            <span>dev</span>
            <span class="toolset-status {{DEV_STATUS}}">{{DEV_STATUS}}</span>
          </div>
          <ul class="toolset-tools">
            <li><code>stories-preview</code></li>
            <li><code>get-storybook-story-instructions</code></li>
            <li>
              <code>stories-changed</code>
              <span class="toolset-status {{CHANGE_DETECTION_STATUS}}"
                >{{CHANGE_DETECTION_STATUS}}</span
              >
            </li>
            <li>
              <code>stories-find-by-component</code>
              <span class="toolset-status {{STORIES_BY_COMPONENT_STATUS}}"
                >{{STORIES_BY_COMPONENT_STATUS}}</span
              >
            </li>
            <li>
              <code>review-create</code>
              <span class="toolset-status {{REVIEW_STATUS}}">{{REVIEW_STATUS}}</span>
            </li>
          </ul>
          {{DEV_NOTICE}}
        </div>

        <div class="toolset">
          <div class="toolset-header">
            <span>docs</span>
            <span class="toolset-status {{DOCS_STATUS}}">{{DOCS_STATUS}}</span>
          </div>
          <ul class="toolset-tools">
            <li><code>docs-list</code></li>
            <li><code>docs-show</code></li>
            <li><code>docs-show-story</code></li>
          </ul>
          {{DOCS_NOTICE}}
        </div>

        <div class="toolset">
          <div class="toolset-header">
            <span>test</span>
            <span class="toolset-status {{TEST_STATUS}}">{{TEST_STATUS}}</span>
          </div>
          <ul class="toolset-tools">
            <li><code>test-run</code>{{A11Y_BADGE}}</li>
          </ul>
          {{TEST_NOTICE}}
        </div>
      </div>

      {{MANIFEST_DEBUGGER_LINK}}
    </div>
  </body>
</html>
`;

// src/preset.ts
import path from "node:path";

// src/auth/composition-auth.ts
import {
  ComponentManifestMap,
  DocsManifestMap
} from "storybook/internal/toolsets-docs";
import { logger as logger5 } from "storybook/internal/node-logger";
import * as v2 from "valibot";
var OAuthResourceMetadata = v2.object({
  resource: v2.optional(v2.string()),
  authorization_servers: v2.pipe(v2.array(v2.string()), v2.minLength(1)),
  scopes_supported: v2.optional(v2.array(v2.string()))
}), OAuthServerMetadata = v2.object({
  issuer: v2.string(),
  authorization_endpoint: v2.string(),
  token_endpoint: v2.string(),
  scopes_supported: v2.optional(v2.array(v2.string()))
}), MANIFEST_CACHE_TTL = 3600 * 1e3, REVALIDATION_TTL = 60 * 1e3, AuthenticationError = class extends Error {
  constructor(url2) {
    super(`Authentication failed for ${url2}. Your token may be invalid or expired.`), this.name = "AuthenticationError";
  }
}, CompositionAuth = class {
  #authRequirement = null;
  #authRequiredUrls = [];
  #refsWithManifests = [];
  #manifestCache = /* @__PURE__ */ new Map();
  #lastToken = null;
  #authErrors = /* @__PURE__ */ new WeakMap();
  /** Initialize by checking which refs require authentication and have manifests. */
  async initialize(refs) {
    for (let ref of refs)
      try {
        let result = await this.#checkRef(ref.url);
        if (result === "manifest-not-found") {
          logger5.warn(
            `[addon-mcp] Skipping composed ref "${ref.title}" (${ref.url}): components manifest not found at ${ref.url}/manifests/components.json. Enable the \`componentsManifest\` feature on that Storybook to compose it into MCP.`
          );
          continue;
        }
        if (result === "manifest-invalid") {
          logger5.warn(
            `[addon-mcp] Skipping composed ref "${ref.title}" (${ref.url}): its components manifest failed validation. This usually means a version mismatch between the composed Storybook and this one \u2014 upgrade the older side.`
          );
          continue;
        }
        if (this.#refsWithManifests.push(ref), result === "public") continue;
        if (this.#authRequiredUrls.push(ref.url), !this.#authRequirement)
          this.#authRequirement = result;
        else {
          let existingServer = this.#authRequirement.resourceMetadata.authorization_servers[0], newServer = result.resourceMetadata.authorization_servers[0];
          existingServer !== newServer && logger5.warn(
            `[addon-mcp] Composed ref "${ref.title}" uses a different OAuth server (${newServer}) than the first authenticated ref (${existingServer}). Only the first OAuth server will be used for authentication.`
          );
        }
      } catch (error) {
        logger5.warn(
          `[addon-mcp] Skipping composed ref "${ref.title}" (${ref.url}): the ref could not be reached (${error instanceof Error ? error.message : String(error)}). Refs are only checked at dev-server boot \u2014 restart Storybook once the ref is reachable.`
        );
      }
  }
  get requiresAuth() {
    return this.#authRequiredUrls.length > 0;
  }
  get authUrls() {
    return this.#authRequiredUrls;
  }
  /** Check if a request encountered an auth error during manifest fetching. */
  hadAuthError(request) {
    return this.#authErrors.has(request);
  }
  /** Check if a URL requires authentication based on discovered auth requirements. */
  #isAuthRequiredUrl(url2) {
    return this.#authRequiredUrls.some((authUrl) => url2.startsWith(authUrl));
  }
  /** Build .well-known/oauth-protected-resource response. */
  buildWellKnown(origin2) {
    return this.#authRequirement ? {
      resource: `${origin2}/mcp`,
      authorization_servers: this.#authRequirement.resourceMetadata.authorization_servers,
      scopes_supported: this.#authRequirement.resourceMetadata.scopes_supported
    } : null;
  }
  /** Build WWW-Authenticate header for 401 responses */
  buildWwwAuthenticate(origin2) {
    return `Bearer error="unauthorized", error_description="Authorization needed for composed Storybooks", resource_metadata="${origin2}/.well-known/oauth-protected-resource"`;
  }
  /** Build sources configuration: local first, then refs that have manifests. */
  buildSources() {
    return [
      { id: "local", title: "Local" },
      ...this.#refsWithManifests.map((ref) => ({
        id: ref.id,
        title: ref.title,
        url: ref.url
      }))
    ];
  }
  /**
   * Create a manifest provider for multi-source mode.
   *
   * Remote sources are fetched over HTTP with auth and caching; the local source fetches from
   * `localOrigin`. In `experimentalDocgenServer` mode core 404s the local `/manifests/*.json`, and
   * the composition reads that source through its own access instead of this provider.
   */
  createManifestProvider(localOrigin) {
    return async (request, path2, source) => {
      let token = extractBearerToken(request?.headers.get("Authorization")), remoteSource = source?.url ? { ...source, url: source.url } : void 0, baseUrl = remoteSource?.url ?? localOrigin, manifestUrl = `${baseUrl}${path2.replace(/^\.\//, "/")}`, isRemote = !!remoteSource, tokenForRequest = isRemote && this.#isAuthRequiredUrl(baseUrl) ? token : null;
      if (token && token !== this.#lastToken && (this.#manifestCache.clear(), this.#lastToken = token), isRemote) {
        let cached = this.#manifestCache.get(manifestUrl);
        if (cached)
          if (Date.now() - cached.timestamp > MANIFEST_CACHE_TTL)
            this.#manifestCache.delete(manifestUrl);
          else
            return (!cached.lastRevalidatedAt || Date.now() - cached.lastRevalidatedAt > REVALIDATION_TTL) && (cached.lastRevalidatedAt = Date.now(), this.#fetchManifest(manifestUrl, tokenForRequest).then(
              (text) => this.#manifestCache.set(manifestUrl, {
                text,
                timestamp: Date.now(),
                lastRevalidatedAt: Date.now()
              })
            ).catch(() => {
            })), cached.text;
      }
      try {
        let text = await this.#fetchManifest(manifestUrl, tokenForRequest);
        return isRemote && this.#manifestCache.set(manifestUrl, { text, timestamp: Date.now() }), text;
      } catch (error) {
        throw error instanceof AuthenticationError && request && this.#authErrors.set(request, error), error;
      }
    };
  }
  /**
   * Fetch a manifest with optional auth token.
   * If the response is 200 but not a valid manifest, checks /mcp for auth issues.
   */
  async #fetchManifest(url2, token) {
    let headers = { Accept: "application/json" };
    token && (headers.Authorization = `Bearer ${token}`);
    let response = await fetch(url2, { headers });
    if (response.status === 401)
      throw new AuthenticationError(url2);
    if (!response.ok)
      throw new Error(`Failed to fetch ${url2}: ${response.status}`);
    let text = await response.text(), { pathname } = new URL(url2), isComponentsManifest = pathname.endsWith("/manifests/components.json"), isDocsManifest = pathname.endsWith("/manifests/docs.json"), schema = isComponentsManifest ? ComponentManifestMap : isDocsManifest ? DocsManifestMap : v2.unknown();
    if (v2.safeParse(v2.pipe(v2.string(), v2.parseJson(), schema), text).success)
      return text;
    throw await this.#isMcpUnauthorized(new URL(url2).origin) ? new AuthenticationError(url2) : new Error(
      `Invalid manifest response from ${url2}: expected valid JSON manifest but got unexpected content.`
    );
  }
  /**
   * Check a ref to determine if it has a manifest and whether it requires auth.
   * Returns 'public' if the ref has a valid manifest without auth,
   * an AuthRequirement if auth is needed, or the reason the ref has no usable manifest:
   * 'manifest-not-found' for a non-OK or non-JSON response (SPA fallback pages land here),
   * 'manifest-invalid' for real JSON that fails the manifest schema.
   */
  async #checkRef(refUrl) {
    let response = await fetch(`${refUrl}/manifests/components.json`, {
      headers: { Accept: "application/json" }
    }), authReq = await this.#parseAuthFromResponse(response);
    if (authReq) return authReq;
    let noManifestReason = "manifest-not-found";
    if (response.ok) {
      let text = await response.text(), json = v2.safeParse(v2.pipe(v2.string(), v2.parseJson()), text);
      if (json.success) {
        if (v2.safeParse(ComponentManifestMap, json.output).success)
          return "public";
        noManifestReason = "manifest-invalid";
      }
    }
    let mcpAuth = await this.#checkMcpAuth(refUrl);
    return mcpAuth || noManifestReason;
  }
  /** Check /mcp endpoint for 401 auth requirement. */
  async #checkMcpAuth(refUrl) {
    let response = await fetch(`${refUrl}/mcp`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
    });
    return this.#parseAuthFromResponse(response);
  }
  /** Quick check: does the remote /mcp return 401? */
  async #isMcpUnauthorized(origin2) {
    try {
      return (await fetch(`${origin2}/mcp`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
      })).status === 401;
    } catch {
      return !1;
    }
  }
  /** Extract auth requirement from a 401 response's WWW-Authenticate header. */
  async #parseAuthFromResponse(response) {
    if (response.status !== 401) return null;
    let wwwAuth = response.headers.get("WWW-Authenticate");
    if (!wwwAuth) return null;
    let match = wwwAuth.match(/resource_metadata="([^"]+)"/);
    if (!match?.[1]) return null;
    let resourceMetadataUrl = match[1], resourceResponse = await fetch(resourceMetadataUrl);
    if (!resourceResponse.ok)
      return logger5.warn(
        `[addon-mcp] Failed to fetch OAuth resource metadata from ${resourceMetadataUrl}: ${resourceResponse.status}`
      ), null;
    let resourceResult = v2.safeParse(OAuthResourceMetadata, await resourceResponse.json());
    if (!resourceResult.success)
      return logger5.warn(
        `[addon-mcp] Invalid OAuth resource metadata from ${resourceMetadataUrl}: ${resourceResult.issues.map((i) => i.message).join(", ")}`
      ), null;
    let serverMetadataUrl = `${resourceResult.output.authorization_servers[0]}/.well-known/oauth-authorization-server`, serverResponse = await fetch(serverMetadataUrl);
    if (!serverResponse.ok)
      return logger5.warn(
        `[addon-mcp] Failed to fetch OAuth server metadata from ${serverMetadataUrl}: ${serverResponse.status}`
      ), null;
    let serverResult = v2.safeParse(OAuthServerMetadata, await serverResponse.json());
    return serverResult.success ? {
      resourceMetadataUrl,
      resourceMetadata: resourceResult.output,
      serverMetadata: serverResult.output
    } : (logger5.warn(
      `[addon-mcp] Invalid OAuth server metadata from ${serverMetadataUrl}: ${serverResult.issues.map((i) => i.message).join(", ")}`
    ), null);
  }
};
function extractBearerToken(authHeader) {
  let bearer = (Array.isArray(authHeader) ? authHeader : [authHeader]).find((value) => typeof value == "string" && value.startsWith("Bearer "));
  return bearer ? bearer.slice(7) : null;
}

// src/auth/resolve-composition-sources.ts
import { ComponentManifestMap as ComponentManifestMap2 } from "storybook/internal/toolsets-docs";
import * as v3 from "valibot";

// src/auth/get-refs-from-config.ts
async function getRefsFromConfig(options) {
  try {
    let refs = await options.presets.apply("refs", {});
    return !refs || typeof refs != "object" ? [] : Object.entries(refs).flatMap(([key, value]) => {
      if (!value || typeof value != "object")
        return [];
      let { title, url: url2, disable } = value;
      return disable || typeof url2 != "string" || url2.length === 0 ? [] : [
        {
          id: key,
          title: typeof title == "string" && title.length > 0 ? title : key,
          url: url2.replace(/\/$/, "")
        }
      ];
    });
  } catch {
    return [];
  }
}

// src/auth/resolve-composition-sources.ts
var MANIFEST_PROBE_TIMEOUT_MS = 3e3;
async function resolveCompositionSources(options) {
  let refs = await getRefsFromConfig(options), compositionAuth = new CompositionAuth();
  if (refs.length === 0)
    return { refs, compositionAuth, sources: void 0, multiSource: !1 };
  await compositionAuth.initialize(refs);
  let sources = compositionAuth.buildSources();
  return {
    refs,
    compositionAuth,
    sources,
    multiSource: sources.some((source) => !!source.url)
  };
}
async function resolveServerlessCompositionSources(options) {
  let refs = await getRefsFromConfig(options), refsWithManifests = await Promise.all(
    refs.map(
      async (ref) => await hasServerlessComponentManifestSource(ref.url) ? ref : void 0
    )
  ), sources = [
    { id: "local", title: "Local" },
    ...refsWithManifests.filter((ref) => !!ref).map((ref) => ({
      id: ref.id,
      title: ref.title,
      url: ref.url
    }))
  ];
  return {
    refs,
    sources,
    multiSource: sources.some((source) => !!source.url)
  };
}
async function hasServerlessComponentManifestSource(refUrl) {
  let controller = new AbortController(), timeout = setTimeout(() => controller.abort(), MANIFEST_PROBE_TIMEOUT_MS);
  try {
    let response = await fetch(
      new URL("manifests/components.json", `${refUrl.replace(/\/$/, "")}/`),
      {
        headers: { Accept: "application/json" },
        signal: controller.signal
      }
    );
    if (response.status === 401 && hasOAuthResourceMetadataChallenge(response))
      return !0;
    if (!response.ok)
      return !1;
    let text = await response.text();
    return v3.safeParse(v3.pipe(v3.string(), v3.parseJson(), ComponentManifestMap2), text).success;
  } catch {
    return !1;
  } finally {
    clearTimeout(timeout);
  }
}
function hasOAuthResourceMetadataChallenge(response) {
  return /resource_metadata="[^"]+"/.test(response.headers.get("WWW-Authenticate") ?? "");
}

// src/preset.ts
import { logger as logger7 } from "storybook/internal/node-logger";

// src/storybook-ai-metadata.ts
import { ValibotJsonSchemaAdapter as ValibotJsonSchemaAdapter2 } from "@tmcp/adapter-valibot";
import * as v4 from "valibot";
import {
  getEffectiveToolAvailability as getEffectiveToolAvailability2,
  getToolAvailability as getToolAvailability2,
  isModuleGraphSupportedByBuilder
} from "storybook/internal/core-server";
import { buildServerInstructions as buildServerInstructions2 } from "storybook/internal/skills";
var createEmptyInputSchema = () => ({ type: "object", properties: {} });
async function buildStorybookAiMetadata(options, existingMetadata) {
  let toolsets = v4.parse(AddonOptions, {
    endpoint: options.endpoint,
    toolsets: options.toolsets ?? {}
  }).toolsets, features2 = await options.presets.apply("features", {}), devEnabled = toolsets?.dev ?? !0, moduleGraphSupported = await isModuleGraphSupportedByBuilder(options), rawAvailability = await getToolAvailability2(options, {
    features: features2,
    moduleGraphSupported
  }), availability = {
    ...rawAvailability,
    reviewEnabled: rawAvailability.reviewEnabledForCli
  }, testSupported = (toolsets?.test ?? !0) && availability.testSupported, docsToolsetEnabled = toolsets?.docs ?? !0, multiSource = docsToolsetEnabled ? (await resolveServerlessCompositionSources(options)).multiSource : !1, registryAvailability = getEffectiveToolAvailability2(availability, { multiSource }), docsEnabled = docsToolsetEnabled && registryAvailability.docsEnabled, registryContext = { availability: registryAvailability, multiSource, toolsets }, toolMetadata = getAddonToolMetadata(registryContext), localTools = {
    ...existingMetadata?.localTools,
    ...getAddonLocalTools({ ...registryContext, options })
  };
  return {
    instructions: joinInstructions(
      existingMetadata?.instructions,
      buildServerInstructions2({
        transport: "mcp",
        devEnabled,
        testSupported,
        docsEnabled,
        changeDetectionEnabled: availability.changeDetectionEnabled,
        moduleGraphSupported: availability.moduleGraphSupported,
        reviewEnabled: availability.reviewEnabled
      })
    ),
    tools: mergeToolDescriptors(
      existingMetadata?.tools ?? [],
      await toToolDescriptors(toolMetadata)
    ),
    localTools
  };
}
async function toToolDescriptors(toolMetadata) {
  let adapter = new ValibotJsonSchemaAdapter2();
  return Promise.all(
    toolMetadata.map(async ({ schema, outputSchema, ...metadata }) => ({
      ...metadata,
      inputSchema: schema ? await toJsonSchema(adapter, schema) : createEmptyInputSchema(),
      ...outputSchema ? { outputSchema: await toJsonSchema(adapter, outputSchema) } : {}
    }))
  );
}
async function toJsonSchema(adapter, schema) {
  return await adapter.toJsonSchema(
    schema
  );
}
function joinInstructions(...sections) {
  let uniqueSections = [], seen = /* @__PURE__ */ new Set();
  for (let section of sections) {
    let trimmed = section?.trim();
    trimmed && !seen.has(trimmed) && (seen.add(trimmed), uniqueSections.push(trimmed));
  }
  return uniqueSections.join(`

`);
}
function mergeToolDescriptors(...groups) {
  let toolsByName = /* @__PURE__ */ new Map();
  for (let tool of groups.flat())
    toolsByName.has(tool.name) && toolsByName.delete(tool.name), toolsByName.set(tool.name, tool);
  return [...toolsByName.values()];
}

// src/utils/get-story-index.ts
import { logger as logger6 } from "storybook/internal/node-logger";
async function getStoryIndex(options) {
  let generator = await options.presets.apply(
    "storyIndexGenerator"
  );
  if (!generator)
    throw new Error(
      "Storybook story index generator is unavailable. These MCP tools require a running Storybook dev server with a builder that exposes the story index."
    );
  let index = await generator.getIndex();
  return logger6.debug(`Story index entries found: ${Object.keys(index.entries).length}`), index;
}

// src/preset.ts
var previewAnnotations = async (existingAnnotations = []) => [...existingAnnotations, path.join(import.meta.dirname, "preview.js")], experimental_devServer = async (app, options) => {
  let addonOptions = v5.parse(AddonOptions, {
    endpoint: options.endpoint,
    toolsets: options.toolsets ?? {}
  }), origin2 = `http://localhost:${options.port}`, endpoint = addonOptions.endpoint ?? DEFAULT_MCP_ENDPOINT, core = await options.presets.apply("core", {}), hostValidation = {
    host: options.host,
    allowedHosts: core?.allowedHosts,
    localAddress: options.localAddress,
    networkAddress: options.networkAddress
  }, skipOriginValidation = isWebContainer(), rejectDisallowedOrigin = (req, res) => {
    if (skipOriginValidation)
      return !1;
    let { origin: requestOrigin } = req.headers;
    if (!requestOrigin)
      return !1;
    try {
      if (isValidHost(new URL(requestOrigin).host, hostValidation))
        return !1;
    } catch {
    }
    return res.writeHead(403, { "Content-Type": "text/plain" }), res.end("Invalid origin"), !0;
  }, rejectNonJsonBody = (req, res) => req.headers["content-type"]?.split(";")[0].trim().toLowerCase() === "application/json" ? !1 : (res.writeHead(415, { "Content-Type": "text/plain" }), res.end("Unsupported Media Type: Content-Type must be application/json"), !0), { refs, compositionAuth, sources, multiSource } = await resolveCompositionSources(options), rawAvailability = await getToolAvailability3(options), localAccess = rawAvailability.docgenServer && refs.length > 0 ? createLocalDocsAccess({
    storyIndex: { getIndex: () => getStoryIndex(options) },
    getManifests: () => loadManifests(options.presets)
  }) : void 0, createManifestProvider;
  refs.length > 0 && (logger7.info(`Initialized composition with ${refs.length} remote Storybook(s)`), compositionAuth.requiresAuth && logger7.info(`Auth required for: ${compositionAuth.authUrls.join(", ")}`), logger7.info(`Sources: ${(sources ?? []).map((s) => s.id).join(", ")}`), createManifestProvider = () => compositionAuth.createManifestProvider(origin2));
  let manifestAccessFor = (req) => ({
    manifestProvider: createManifestProvider?.(req),
    localAccess
  });
  app.get("/.well-known/oauth-protected-resource", (_req, res) => {
    let wellKnown = compositionAuth.buildWellKnown(origin2);
    if (!wellKnown) {
      res.writeHead(404), res.end("Not found");
      return;
    }
    res.writeHead(200, { "Content-Type": "application/json" }), res.end(JSON.stringify(wellKnown));
  });
  let requireAuth = (req, res) => {
    let token = extractBearerToken(req.headers.authorization);
    return compositionAuth.requiresAuth && !token ? (res.writeHead(401, {
      "Content-Type": "text/plain",
      "WWW-Authenticate": compositionAuth.buildWwwAuthenticate(origin2)
    }), res.end("401 - Unauthorized"), !0) : !1;
  };
  app.post(endpoint, (req, res) => {
    if (!rejectDisallowedOrigin(req, res) && !requireAuth(req, res) && !rejectNonJsonBody(req, res))
      return mcpServerHandler({
        req,
        res,
        options,
        addonOptions,
        endpoint,
        sources,
        ...manifestAccessFor(req),
        compositionAuth
      });
  });
  let {
    moduleGraphSupported,
    changeDetectionEnabled,
    reviewEnabled,
    reviewEnabledForCli,
    docsEnabled,
    docsHasManifests,
    docsFeatureEnabled,
    testSupported,
    a11yEnabled: a11yEnabled2
  } = getEffectiveToolAvailability3(rawAvailability, { multiSource }), isDevEnabled = addonOptions.toolsets?.dev ?? !0, isDocsEnabled = docsEnabled && (addonOptions.toolsets?.docs ?? !0), isTestEnabled = testSupported && (addonOptions.toolsets?.test ?? !0);
  return app.get(endpoint, (req, res) => {
    if (rejectDisallowedOrigin(req, res)) return;
    if (!req.headers.accept?.includes("text/html"))
      return requireAuth(req, res) ? void 0 : mcpServerHandler({
        req,
        res,
        options,
        addonOptions,
        endpoint,
        sources,
        ...manifestAccessFor(req),
        compositionAuth
      });
    res.writeHead(200, { "Content-Type": "text/html" });
    let docsNotice = "";
    docsHasManifests ? docsFeatureEnabled || (docsNotice = `<div class="toolset-notice">
				This toolset requires enabling the component manifest feature.
				<a target="_blank" href="https://github.com/storybookjs/storybook/tree/next/code/addons/mcp">Learn how to enable it</a>
			</div>`) : docsNotice = docsFeatureEnabled ? `<div class="toolset-notice">
				This toolset requires a components manifest, which your framework does not generate. On <code>@storybook/vue3-vite</code>, also enable <code>experimentalDocgenServer</code>.
				<a target="_blank" href="https://storybook.js.org/docs/ai/mcp/overview#framework-support">See which frameworks are supported</a>
			</div>` : `<div class="toolset-notice">
				This toolset requires a components manifest, which is generated by the React frameworks, <code>@storybook/angular-vite</code> and <code>@storybook/vue3-vite</code>. Enable the <code>componentsManifest</code> feature, plus <code>experimentalDocgenServer</code> on <code>@storybook/vue3-vite</code>.
				<a target="_blank" href="https://storybook.js.org/docs/ai/mcp/overview#framework-support">Learn how to enable it</a>
			</div>`;
    let testNoticeLines = [
      !testSupported && 'This toolset requires Storybook 10.3.0+ with <code>@storybook/addon-vitest</code>. <a target="_blank" href="https://storybook.js.org/docs/writing-tests/integrations/vitest-addon#install-and-set-up">Learn how to set it up</a>',
      !a11yEnabled2 && 'Add <code>@storybook/addon-a11y</code> for accessibility testing. <a target="_blank" href="https://storybook.js.org/docs/writing-tests/accessibility-testing">Learn more</a>'
    ].filter(Boolean), testNotice = testNoticeLines.length ? `<div class="toolset-notice">${testNoticeLines.join("<br>")}</div>` : "", a11yBadge = a11yEnabled2 ? ' <span class="toolset-status enabled">+ accessibility</span>' : "", devNoticeLines = isDevEnabled ? [
      !moduleGraphSupported && "<code>stories-find-by-component</code> requires a dev server with a builder that supports the module graph (Vite or Webpack 5).",
      !changeDetectionEnabled && "<code>stories-changed</code> requires enabling the <code>changeDetection</code> feature flag.",
      !reviewEnabled && (reviewEnabledForCli ? "<code>review-create</code> is enabled for <code>storybook ai</code> CLI clients (the Claude/Codex plugins); direct MCP clients need the <code>experimentalReview</code> feature flag." : "<code>review-create</code> requires the <code>changeDetection</code> feature flag and is off when <code>experimentalReview</code> is set to <code>false</code>.")
    ].filter(Boolean) : ["The <code>dev</code> toolset is disabled via addon options."], devNotice = devNoticeLines.length ? `<div class="toolset-notice">${devNoticeLines.join("<br>")}</div>` : "", statusWord = (enabled) => enabled ? "enabled" : "disabled", html = template_default.replaceAll("{{DEV_STATUS}}", isDevEnabled ? "enabled" : "disabled").replaceAll(
      "{{STORIES_BY_COMPONENT_STATUS}}",
      statusWord(isDevEnabled && moduleGraphSupported)
    ).replaceAll("{{CHANGE_DETECTION_STATUS}}", statusWord(isDevEnabled && changeDetectionEnabled)).replaceAll("{{REVIEW_STATUS}}", statusWord(isDevEnabled && reviewEnabledForCli)).replace("{{DEV_NOTICE}}", devNotice).replaceAll("{{DOCS_STATUS}}", isDocsEnabled ? "enabled" : "disabled").replace("{{DOCS_NOTICE}}", docsNotice).replaceAll("{{TEST_STATUS}}", isTestEnabled ? "enabled" : "disabled").replace("{{TEST_NOTICE}}", testNotice).replace(
      "{{MANIFEST_DEBUGGER_LINK}}",
      rawAvailability.docsEnabled ? '<p>View the <a href="/manifests/components.html">component manifest debugger</a>.</p>' : ""
    ).replace("{{A11Y_BADGE}}", a11yBadge);
    res.end(html);
  }), app;
}, experimental_storybookAi = async (existingMetadata, options) => buildStorybookAiMetadata(options, existingMetadata), features = async (existingFeatures) => ({
  ...existingFeatures,
  componentsManifest: !0
});
export {
  experimental_devServer,
  experimental_storybookAi,
  features,
  previewAnnotations
};