Implement get-stories-by-component tool#249
Conversation
🦋 Changeset detectedLatest commit: d746e95 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
✅ Deploy Preview for storybook-mcp-self-host-example canceled.
|
commit: |
There was a problem hiding this comment.
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 forexperimental_getActiveChangeDetectionService. get-changed-storiesnow front-loads a coverage-gap banner and appends a sanity-check hint when modified files aren't reachable from any story.display-reviewvalidates everystoryIdagainst the live index and rejects unknown IDs;withFriendlyErrorstrims 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. |
| 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.', | ||
| ), | ||
| ), |
| 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.`, | ||
| ), |
e346262 to
b60853d
Compare
…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>
b60853d to
0e69c1a
Compare
Bundle ReportChanges will increase total bundle size by 28.92kB (17.79%) ⬆️
ℹ️ *Bundle size includes cached data from a previous commit Affected Assets, Files, and Routes:view changes for bundle: @storybook/addon-mcp-esmAssets Changed:
Files in
|
ghengeveld
left a comment
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This seems like a good followup for another PR (to do in all tools) 👍
| const dependencyGraphSupported = await isDependencyGraphSupported(); | ||
| const changeDetectionEnabled = (features?.changeDetection ?? false) && dependencyGraphSupported; |
| // 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.`, |
| 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, |
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>
There was a problem hiding this comment.
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
| if (!disableTelemetry) { | ||
| await collectTelemetry({ | ||
| event: 'tool:getStoriesByComponent', | ||
| server, | ||
| toolset: 'dev', | ||
| componentCount: input.componentPaths.length, | ||
| matchedComponentCount: input.componentPaths.length - unmatchedCount, | ||
| totalMatchCount: totalMatches, | ||
| maxDistance: effectiveMaxDistance, | ||
| }); |
| interface FriendlyIssue { | ||
| path: string; | ||
| message: string; | ||
| } |
| 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})`; |
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.