Skip to content

Implement get-stories-by-component tool#249

Merged
yannbf merged 9 commits into
mainfrom
yann/story-discovery
Jun 3, 2026
Merged

Implement get-stories-by-component tool#249
yannbf merged 9 commits into
mainfrom
yann/story-discovery

Conversation

@yannbf
Copy link
Copy Markdown
Member

@yannbf yannbf commented May 29, 2026

This PR adds a new MCP tool, get-stories-by-component, that answers the question: "Which stories render this component?"

You give it one or more component source file paths, and it returns the real Storybook story IDs that depend on those files. Under the hood it uses Storybook's live dependency graph, so the results are always accurate and up to date (including hot-reloaded changes during dev).

Results are ranked by distance — how directly a story uses the component:

0 = the path you passed is itself a story file
1 = a story for a component that imports it directly
2+ = a story that pulls it in transitively (e.g. through a page or wrapper)
This lets an AI agent take anything — a file it just edited, a feature the user named, a shared token/util — turn it into a list of component paths, and get back grounded story IDs it can hand to preview-stories (for preview URLs) or display-review (for a review page). It never has to guess IDs from filenames.

@yannbf yannbf self-assigned this May 29, 2026
Copilot AI review requested due to automatic review settings May 29, 2026 12:05
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: d746e95

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@storybook/addon-mcp Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link
Copy Markdown

netlify Bot commented May 29, 2026

Deploy Preview for storybook-mcp-self-host-example canceled.

Name Link
🔨 Latest commit d746e95
🔍 Latest deploy log https://app.netlify.com/projects/storybook-mcp-self-host-example/deploys/6a1feb15a7005900086d8bc4

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 29, 2026

npx https://pkg.pr.new/storybookjs/mcp/@storybook/addon-mcp@249
npx https://pkg.pr.new/storybookjs/mcp/@storybook/mcp@249
npx https://pkg.pr.new/storybookjs/mcp/@storybook/mcp-proxy@249

commit: d746e95

@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 79.84190% with 51 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.61%. Comparing base (4e7061c) to head (d746e95).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...es/addon-mcp/src/tools/get-stories-by-component.ts 77.77% 7 Missing and 11 partials ⚠️
...s/addon-mcp/src/utils/resolve-component-stories.ts 72.58% 7 Missing and 10 partials ⚠️
...es/addon-mcp/src/utils/format-validation-issues.ts 80.95% 0 Missing and 4 partials ⚠️
packages/addon-mcp/src/tools/display-review.ts 83.33% 1 Missing and 2 partials ⚠️
...ackages/addon-mcp/src/tools/get-changed-stories.ts 70.00% 1 Missing and 2 partials ⚠️
packages/addon-mcp/src/tools/preview-stories.ts 25.00% 1 Missing and 2 partials ⚠️
packages/addon-mcp/src/mcp-handler.ts 50.00% 0 Missing and 2 partials ⚠️
packages/addon-mcp/src/utils/change-detection.ts 90.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #249      +/-   ##
==========================================
+ Coverage   78.40%   78.61%   +0.20%     
==========================================
  Files          57       62       +5     
  Lines        1565     1800     +235     
  Branches      434      500      +66     
==========================================
+ Hits         1227     1415     +188     
- Misses        202      218      +16     
- Partials      136      167      +31     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new get-stories-by-component MCP tool that maps component file paths to the stories which render them, backed by Storybook's experimental change-detection reverse dependency index. Also threads an "unreachable working-tree changes" hint into get-changed-stories (so the agent doesn't conclude "no impact" when edits sit outside the story graph), hardens display-review by rejecting fabricated story IDs against the live index, and adds a friendlier validation-error wrapper plus updated agent instructions.

Changes:

  • New tool get-stories-by-component + supporting endpoint (/storybook-mcp/component-stories) and backwards-compat shim for experimental_getActiveChangeDetectionService.
  • get-changed-stories now front-loads a coverage-gap banner and appends a sanity-check hint when modified files aren't reachable from any story.
  • display-review validates every storyId against the live index and rejects unknown IDs; withFriendlyErrors trims Valibot issue dumps; instructions describe the new "input → component paths → stories" chain.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/addon-mcp/src/tools/get-stories-by-component.ts (+test) New tool and serialization helpers, with maxDistance clipping and inline-snapshot tests.
packages/addon-mcp/src/utils/component-stories-endpoint.ts (+test) Server-side resolver and HTTP handler over Storybook's reverse index, with barrel expansion.
packages/addon-mcp/src/utils/fetch-component-stories.ts Client-side fetcher used by the new tool.
packages/addon-mcp/src/utils/change-detection.ts Backwards-compat probe shim around experimental_getActiveChangeDetectionService.
packages/addon-mcp/src/utils/detect-unreachable-changes.ts (+test) Detects modified files unreachable from the story graph and formats banner/hint strings.
packages/addon-mcp/src/utils/format-validation-issues.ts (+test) withFriendlyErrors wrapper that trims Valibot issue payloads.
packages/addon-mcp/src/tools/get-changed-stories.ts (+test) Wires unreachable banner + tail hint into both empty and non-empty responses.
packages/addon-mcp/src/tools/display-review.ts (+test) Validates story IDs against live index; uses friendly-error schema wrapper.
packages/addon-mcp/src/tools/tool-names.ts Adds GET_STORIES_BY_COMPONENT_TOOL_NAME constant.
packages/addon-mcp/src/constants.ts Adds COMPONENT_STORIES_ENDPOINT constant.
packages/addon-mcp/src/preset.ts Mounts the new endpoint, gated on change-detection support.
packages/addon-mcp/src/mcp-handler.ts Registers the new tool when change detection is enabled and supported.
packages/addon-mcp/src/instructions/{dev-instructions.md, build-server-instructions.ts, .test.ts} Documents the new "input → stories" chain and reworks the display-review guidance.

Comment thread packages/addon-mcp/src/preset.ts Outdated
Comment on lines +25 to +46
maxDistance: v.pipe(
v.optional(v.number()),
v.description(
`Optional ceiling on the import depth to include in results.
- 1: only stories that directly import the component.
- 2+: also include stories that reach the component through N hops.
Omit to include everything. Lower values trade recall for precision; useful when one shared component (Button, Icon, …) would otherwise sweep in dozens of consumer stories.`,
),
),
});

const StoryMatch = v.object({
storyId: v.string(),
title: v.string(),
name: v.string(),
importPath: v.string(),
distance: v.pipe(
v.number(),
v.description(
'Import-graph depth from the story file to the component (lower = stronger). 1: story file directly imports the component. 2+: reached through N hops.',
),
),
Comment on lines +19 to +23
v.description(
`Absolute paths to component source files (e.g. "/repo/src/Button.tsx").
Pass the components you actually want stories for — typically files you just read, edited, or that the user mentioned.
Do not pass story files (\`*.stories.*\`); pass the component the story renders.`,
),
@yannbf yannbf force-pushed the yann/story-discovery branch from e346262 to b60853d Compare May 29, 2026 12:14
@yannbf yannbf marked this pull request as draft June 1, 2026 21:05
yannbf and others added 2 commits June 2, 2026 08:44
…detection

Adds the get-stories-by-component tool backed by Storybook's reverse
dependency graph, mapping component source files to the stories that render
them so agents can hand real story IDs to preview-stories / display-review.

Includes the change-detection audit fixes:
- dedupe component paths and canonicalise via fs.realpathSync.native
- distinguish "path not found" from "no stories yet"
- definitive graph-not-built messaging (no misleading "retry shortly")
- maxDistance schema validation + internal default, telemetry on resolved value
- unreachable-working-tree-change hints for get-changed-stories
- resolve-component-stories extraction + dev-instructions workflow guidance

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…IndexGenerator preset

Replace the loopback `fetch(${origin}/index.json)` with an in-process lookup
through `options.presets.apply('storyIndexGenerator').getIndex()`. The dev
server registers and memoises a single StoryIndexGenerator, so this returns the
live, HMR-fresh index with no network round-trip.

- add utils/get-story-index.ts (getStoryIndex(options)) + tests
- migrate get-changed-stories, preview-stories, display-review, run-story-tests
  and get-stories-by-component from fetchStoryIndex(origin) to getStoryIndex(options)
- drop the now-redundant per-origin TTL cache in get-stories-by-component
  (the generator already memoises lastIndex)
- remove utils/fetch-story-index.ts and update test mocks

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Bundle Report

Changes will increase total bundle size by 28.92kB (17.79%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
@storybook/addon-mcp-esm* 111.72kB 28.92kB (34.93%) ⬆️⚠️

ℹ️ *Bundle size includes cached data from a previous commit

Affected Assets, Files, and Routes:

view changes for bundle: @storybook/addon-mcp-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
preset.js 28.92kB 111.72kB 34.93% ⚠️

Files in preset.js:

  • ./src/tools/run-story-tests.ts → Total Size: 10.96kB

  • ./src/utils/get-story-index.ts → Total Size: 1.25kB

  • ./src/tools/preview-stories.ts → Total Size: 4.12kB

  • ./src/tools/tool-names.ts → Total Size: 490 bytes

  • ./src/utils/detect-unreachable-changes.ts → Total Size: 5.0kB

  • ./src/utils/change-detection.ts → Total Size: 778 bytes

  • ./src/instructions/dev-instructions.md → Total Size: 4.66kB

  • ./src/utils/format-validation-issues.ts → Total Size: 1.88kB

  • ./src/instructions/build-server-instructions.ts → Total Size: 2.11kB

  • ./src/tools/get-changed-stories.ts → Total Size: 3.93kB

  • ./src/tools/get-stories-by-component.ts → Total Size: 8.79kB

  • ./src/mcp-handler.ts → Total Size: 5.93kB

  • ./src/tools/display-review.ts → Total Size: 7.79kB

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 10 comments.

Comment thread packages/addon-mcp/src/tools/get-stories-by-component.test.ts
Comment thread packages/addon-mcp/src/tools/get-stories-by-component.test.ts
Comment thread packages/addon-mcp/src/tools/get-stories-by-component.test.ts
Comment thread apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts Outdated
Comment thread packages/addon-mcp/src/mcp-handler.ts
Comment thread packages/addon-mcp/src/tools/get-stories-by-component.ts
Comment thread packages/addon-mcp/src/utils/resolve-component-stories.ts Outdated
Comment thread packages/addon-mcp/src/utils/detect-unreachable-changes.ts Outdated
Comment thread packages/addon-mcp/src/utils/resolve-component-stories.ts Outdated
Comment thread packages/addon-mcp/src/tools/get-stories-by-component.test.ts
Copy link
Copy Markdown
Member

@ghengeveld ghengeveld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few non-blocking documentation/contract observations on the new get-stories-by-component tool, focused on token efficiency and keeping the tool's self-description consistent with its implementation and the server instructions.

Comment thread packages/addon-mcp/src/tools/get-stories-by-component.ts Outdated
Comment thread packages/addon-mcp/src/tools/get-stories-by-component.ts
Comment thread packages/addon-mcp/src/tools/get-stories-by-component.ts
Results are sorted by \`distance\` (lower = stronger signal). Prefer the lowest-distance results first; widen only when needed. For shared components like Button or Icon, expect many indirect (\`distance\` ≥ 2) matches — pass \`maxDistance\` to cap noise.`,
schema: GetStoriesByComponentInput,
outputSchema: GetStoriesByComponentOutput,
enabled: () => server.ctx.custom?.toolsets?.dev ?? true,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional / soft suggestion: tool annotations. tmcp's tool options accept annotations (ToolAnnotations), and this tool is read-only and idempotent. Adding annotations: { readOnlyHint: true, idempotentHint: true } would let MCP clients reason about safety / auto-approval and is aligned with MCP guidance. None of the addon's tools currently set annotations, so this could be a small follow-up applied across all the read tools rather than just here — not blocking.

Copy link
Copy Markdown
Member Author

@yannbf yannbf Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a good followup for another PR (to do in all tools) 👍

@yannbf yannbf marked this pull request as ready for review June 2, 2026 12:47
Copilot AI review requested due to automatic review settings June 2, 2026 12:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.

Comment on lines +45 to +46
const dependencyGraphSupported = await isDependencyGraphSupported();
const changeDetectionEnabled = (features?.changeDetection ?? false) && dependencyGraphSupported;
Comment on lines +100 to +103
// get-stories-by-component only needs the dependency graph, not the status pipeline.
if (dependencyGraphSupported) {
await addGetStoriesByComponentTool(server);
}

Never invent IDs from file names, feature names, or memory; if a component has no matches here, it has no stories yet (say so, don't fabricate).

Backed by Storybook's live reverse dependency graph, available only when the dev server runs a builder that supports change detection (e.g. Vite) — otherwise returns a typed error. Results are sorted by \`distance\` (lower = stronger); for shared components like Button or Icon, expect many indirect matches and use \`maxDistance\` to cap noise.`,
Comment on lines +263 to +285
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
const unmatchedCount = results.filter(
(r) => !r.pathNotFound && r.matches.length === 0,
).length;

const textSections = results.map(({ componentPath, matches, clipped, pathNotFound }) =>
serializeComponentSection(componentPath, matches, {
maxDistance: effectiveMaxDistance,
clipped,
pathNotFound,
}),
);

const text =
textSections.length > 0 ? textSections.join('\n\n') : 'No component paths provided.';

if (!disableTelemetry) {
await collectTelemetry({
event: 'tool:getStoriesByComponent',
server,
toolset: 'dev',
componentCount: input.componentPaths.length,
matchedComponentCount: input.componentPaths.length - unmatchedCount,
yannbf and others added 2 commits June 2, 2026 16:36
The get-stories-by-component and get-changed-stories tools require
Storybook's experimental_getDependencyGraphService API, which the pinned
10.5.0-alpha.2 doesn't ship but 10.5.0-alpha.4 does. With the old version
those tools never registered in CI, so the e2e suite saw 6 tools instead
of 8.

- Bump all @storybook/* catalog entries to 10.5.0-alpha.4
- Add `subtype: 'story'` to StoryIndexEntry test fixtures (alpha.4's
  StoryIndexEntry requires it; fixes TS2352 in resolve-component-stories.test.ts)
- Refresh the tools/list e2e snapshot for the get-stories-by-component
  pathNotFound output field

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 3, 2026 08:51
@yannbf yannbf merged commit b59718f into main Jun 3, 2026
17 checks passed
@yannbf yannbf deleted the yann/story-discovery branch June 3, 2026 08:53
@storybook-app-bot storybook-app-bot Bot mentioned this pull request Jun 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 32 changed files in this pull request and generated 3 comments.

Files not reviewed (3)
  • apps/internal-storybook/pnpm-lock.yaml: Language not supported
  • eval/pnpm-lock.yaml: Language not supported
  • packages/addon-mcp/pnpm-lock.yaml: Language not supported

Comment on lines +279 to +288
if (!disableTelemetry) {
await collectTelemetry({
event: 'tool:getStoriesByComponent',
server,
toolset: 'dev',
componentCount: input.componentPaths.length,
matchedComponentCount: input.componentPaths.length - unmatchedCount,
totalMatchCount: totalMatches,
maxDistance: effectiveMaxDistance,
});
Comment on lines +28 to +31
interface FriendlyIssue {
path: string;
message: string;
}
Comment on lines +156 to +158
const componentCount = new Set(matches.map((m) => m.title)).size;
const bucketSummary = distances.map((d) => `d${d}=${byDistance.get(d)!.length}`).join(', ');
const summary = `→ ${matches.length} ${pluralize(matches.length, 'story', 'stories')} across ${componentCount} ${pluralize(componentCount, 'component')}, distances ${minDist}..${maxDist} (${bucketSummary})`;
@storybook-app-bot storybook-app-bot Bot mentioned this pull request Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants