fix(addon-mcp): bound get-changed-stories output to avoid context overflow (#311) - #312
fix(addon-mcp): bound get-changed-stories output to avoid context overflow (#311)#312yannbf wants to merge 7 commits into
Conversation
…rflow Closes #311. When a shared primitive (Badge, Tag, Icon, …) changes, every story that transitively renders it surfaces as a "related" status. On large repos this reached 1,000+ entries (~126KB / ~56k est. tokens on Chakra), exceeding the ~25k MCP tool-output cap. The host then spilled the response to a file and the agent self-curated from a head/tail of it — in the Carbon case dropping the brand-new component from the review entirely. The tool now: - always lists new + modified stories in full (directly-changed, never dropped); - reduces the related bucket to a component-diverse round-robin sample plus a complete per-component count, with an explicit truncation note pointing at get-stories-by-component for full enumeration; - enforces a token budget as a hard backstop (trims the related sample first, then the direct buckets only in pathological refactors — always with a note); - returns a bounded structuredContent payload (counts, relatedSample, relatedBreakdown, relatedTruncated, …). On the Carbon worst case this takes the response from ~60k est. tokens (over the cap) to ~2.9k, with new and modified stories intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Deploy Preview for storybook-mcp-self-host-example canceled.
|
🦋 Changeset detectedLatest commit: 2d7509b 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 |
commit: |
Bundle ReportChanges will increase total bundle size by 6.42kB (7.05%) ⬆️
Affected Assets, Files, and Routes:view changes for bundle: @storybook/addon-mcp-esmAssets Changed:
view changes for bundle: @storybook/mcp-esmAssets Changed:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #312 +/- ##
==========================================
+ Coverage 78.57% 79.58% +1.00%
==========================================
Files 56 57 +1
Lines 1830 1940 +110
Branches 516 554 +38
==========================================
+ Hits 1438 1544 +106
- Misses 229 230 +1
- Partials 163 166 +3 ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Pull request overview
This PR updates @storybook/addon-mcp’s get-changed-stories tool to prevent host/tool-output overflows on large repos by bounding the “related/affected” output (sampling + per-component counts) and adding a structured payload so agents can reason about totals without parsing markdown.
Changes:
- Add a bounded changed-stories serializer with sampling, breakdowns, truncation flags, and a token-budget backstop.
- Update
get-changed-storiesto use the serializer and emitstructuredContentwith an explicitoutputSchema. - Add unit + tool-level regression tests for large affected sets; update server instructions and publish a changeset.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/addon-mcp/src/utils/serialize-changed-stories.ts | New serializer to cap markdown output and produce a structured changed-stories payload. |
| packages/addon-mcp/src/utils/serialize-changed-stories.test.ts | Unit tests covering sampling, breakdown capping, and token-budget trimming behavior. |
| packages/addon-mcp/src/tools/get-changed-stories.ts | Tool now uses the serializer, adds an output schema, and returns bounded structuredContent. |
| packages/addon-mcp/src/tools/get-changed-stories.test.ts | End-to-end regression test to ensure bounded output on Chakra/Carbon-scale changes. |
| packages/addon-mcp/src/instructions/dev-instructions.md | Clarifies how agents should interpret the (sampled) related stories output. |
| packages/addon-mcp/src/instructions/build-server-instructions.test.ts | Snapshot updates for the server instructions text. |
| .changeset/quiet-stories-bound.md | Changeset describing the bounded-output behavior change for release notes. |
| /** | ||
| * Builds a per-component count over ALL related stories, preserving the | ||
| * (already-sorted) first-seen order of components. | ||
| */ |
| let text = `${heading}:\n${shown.map(serializeStory).join('\n')}`; | ||
| if (hidden > 0) { | ||
| text += `\n- …and ${hidden} more (omitted to stay within the response size limit; all ${stories.length} are in \`structuredContent.counts\`).`; | ||
| } | ||
| return { text, shownCount: shown.length, truncated: hidden > 0 }; |
| text += | ||
| `\n\nShowing ${sample.length} of ${total} related stories (diverse sample across the components above). ` + | ||
| `Related stories transitively render a changed component — they are lower priority than the new/modified stories, ` + | ||
| `which are listed in full. To enumerate every story for a specific component, call \`get-stories-by-component\` with that component's source path. ` + | ||
| `Do not assume the un-sampled stories are unaffected, and never invent story IDs.`; |
| while (estimateTokens(`${headline}\n\n${assembled.body}`) > tokenBudget) { | ||
| if (effectiveSampleLimit > RELATED_SAMPLE_FLOOR) { | ||
| effectiveSampleLimit = Math.max(RELATED_SAMPLE_FLOOR, Math.floor(effectiveSampleLimit / 2)); | ||
| } else if (effectiveDirectLimit > DIRECT_DISPLAY_FLOOR) { | ||
| effectiveDirectLimit = Math.max(DIRECT_DISPLAY_FLOOR, Math.floor(effectiveDirectLimit / 2)); | ||
| } else { | ||
| break; // Floors reached; this is as small as we go. | ||
| } | ||
| assembled = assemble(); | ||
| } |
| export const GET_CHANGED_STORIES_TOOL_DESCRIPTION = `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs). | ||
|
|
||
| export const GET_CHANGED_STORIES_TOOL_DESCRIPTION = `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs).`; | ||
| New and modified stories (the directly-changed ones) are always returned in full. Related stories — those that only transitively render a changed component — can number in the thousands when a shared primitive (e.g. Badge, Tag, Icon) changes, so they are returned as a component-diverse sample plus complete per-component counts, keeping the response within tool-output limits. To enumerate every related story for one component, call \`${GET_STORIES_BY_COMPONENT_TOOL_NAME}\` with its source path.`; |
| Whenever you need story IDs — to preview them, to feed `display-review`, to answer the user, for any reason at all — your job is the same regardless of how the request reached you. The input can take any shape: a feature/domain/topic the user named, a file the user mentioned, a file you just edited, a query like "all consumers of X", an autonomous review after a UI change, or anything else. The chain doesn't change with the prompt shape: | ||
|
|
||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, `get-changed-stories` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call `get-stories-by-component` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. | ||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, `get-changed-stories` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call `get-stories-by-component` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. One more thing about its shape: `get-changed-stories` always lists the **new** and **modified** stories in full (these are the directly-changed ones — never dropped), but the **related** stories (transitive consumers of a changed shared component) can run into the thousands, so it returns a component-diverse _sample_ plus a complete per-component count rather than every related story. Treat the sample as representative, not exhaustive — when you need every related story for a specific component, call `get-stories-by-component` with that component's source path. Never assume the un-sampled related stories don't exist, and never invent IDs to fill the gap. |
| Whenever you need story IDs — to preview them, to feed \`display-review\`, to answer the user, for any reason at all — your job is the same regardless of how the request reached you. The input can take any shape: a feature/domain/topic the user named, a file the user mentioned, a file you just edited, a query like "all consumers of X", an autonomous review after a UI change, or anything else. The chain doesn't change with the prompt shape: | ||
|
|
||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, \`get-changed-stories\` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call \`get-stories-by-component\` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. | ||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, \`get-changed-stories\` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call \`get-stories-by-component\` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. One more thing about its shape: \`get-changed-stories\` always lists the **new** and **modified** stories in full (these are the directly-changed ones — never dropped), but the **related** stories (transitive consumers of a changed shared component) can run into the thousands, so it returns a component-diverse _sample_ plus a complete per-component count rather than every related story. Treat the sample as representative, not exhaustive — when you need every related story for a specific component, call \`get-stories-by-component\` with that component's source path. Never assume the un-sampled related stories don't exist, and never invent IDs to fill the gap. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughThis PR adds bounded serialization and structured responses for ChangesToken-bounded changed stories
Eval cache refresh
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/addon-mcp/src/utils/serialize-changed-stories.ts (1)
137-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDoc comment doesn't match the sort. The comment says the breakdown preserves "first-seen order of components", but
buildBreakdownactually returns entries sorted bycountdescending (then title). Consider updating the comment to avoid confusing future readers.📝 Suggested comment fix
/** - * Builds a per-component count over ALL related stories, preserving the - * (already-sorted) first-seen order of components. + * Builds a per-component count over ALL related stories, sorted by count + * (descending), then title (ascending) for stable ordering. */🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/addon-mcp/src/utils/serialize-changed-stories.ts` around lines 137 - 149, The doc comment for buildBreakdown is inconsistent with the actual sorting logic in serialize-changed-stories.ts. Update the comment on buildBreakdown to describe the real behavior: it aggregates counts by story.title and returns entries sorted by count descending, then by title, instead of preserving first-seen component order. Keep the comment aligned with the Map-based counting and the final sort so future readers are not misled.packages/addon-mcp/src/tools/get-changed-stories.ts (1)
67-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffConsider deriving the structured type from
GetChangedStoriesOutputto avoid drift.
GetChangedStoriesOutput(Valibot) and theChangedStoriesStructuredinterface inserialize-changed-stories.tsdescribe the same payload but are maintained independently. If one gains/renames a field (e.g. a new truncation flag), the other can silently diverge andstructuredContentmay fail the tool'soutputSchemavalidation at runtime. Exporting av.InferOutput<typeof GetChangedStoriesOutput>type and having the serializer return that keeps them in lockstep.As per coding guidelines: "Use Valibot for schema validation in MCP tools with
v.object()andv.InferOutput<>pattern".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/addon-mcp/src/tools/get-changed-stories.ts` around lines 67 - 102, The structured story payload type is duplicated between GetChangedStoriesOutput and ChangedStoriesStructured, which can drift and break structuredContent validation at runtime. Export a Valibot-derived type from get-changed-stories.ts using v.InferOutput<typeof GetChangedStoriesOutput>, then update serialize-changed-stories.ts to use that inferred type instead of a separately maintained interface so both stay in sync.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/addon-mcp/src/utils/serialize-changed-stories.ts`:
- Line 1: Formatting in the serialize-changed-stories utility needs to be
corrected; run oxfmt without --check on the file that imports estimateTokens so
the import formatting matches the repository style and CI Check formatting
passes.
---
Nitpick comments:
In `@packages/addon-mcp/src/tools/get-changed-stories.ts`:
- Around line 67-102: The structured story payload type is duplicated between
GetChangedStoriesOutput and ChangedStoriesStructured, which can drift and break
structuredContent validation at runtime. Export a Valibot-derived type from
get-changed-stories.ts using v.InferOutput<typeof GetChangedStoriesOutput>, then
update serialize-changed-stories.ts to use that inferred type instead of a
separately maintained interface so both stay in sync.
In `@packages/addon-mcp/src/utils/serialize-changed-stories.ts`:
- Around line 137-149: The doc comment for buildBreakdown is inconsistent with
the actual sorting logic in serialize-changed-stories.ts. Update the comment on
buildBreakdown to describe the real behavior: it aggregates counts by
story.title and returns entries sorted by count descending, then by title,
instead of preserving first-seen component order. Keep the comment aligned with
the Map-based counting and the final sort so future readers are not misled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3c0b866a-6020-4922-89ab-07992f575cd0
📒 Files selected for processing (7)
.changeset/quiet-stories-bound.mdpackages/addon-mcp/src/instructions/build-server-instructions.test.tspackages/addon-mcp/src/instructions/dev-instructions.mdpackages/addon-mcp/src/tools/get-changed-stories.test.tspackages/addon-mcp/src/tools/get-changed-stories.tspackages/addon-mcp/src/utils/serialize-changed-stories.test.tspackages/addon-mcp/src/utils/serialize-changed-stories.ts
Builds on the overflow bound: when the Storybook build persists the
import-graph distance (status.data.distance), get-changed-stories now ranks the
bounded "related" sample by it — taking the closest story from each affected
component first ("strategy F" in the benchmarks). At the same ~2.4k-token cost
this moves the sample's mean import distance from ~3.0 (distance-blind
round-robin) to ~1.1, i.e. the related stories that actually render the change.
- read distance from status.data.distance (graceful: degrades to component
round-robin when absent, e.g. older Storybook);
- annotate related lines with "— distance N" and the per-component breakdown
with "nearest dN"; new/modified stay un-annotated (they ARE the change);
- thread distance through structuredContent (relatedSample[].distance,
relatedBreakdown[].nearestDistance);
- document the investigation and all strategies benchmarked in
packages/addon-mcp/docs/get-changed-stories-overflow.md.
Companion Storybook core change (persisting the distance) is required to
activate the ranking; without it the bound still holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tured payload Quality pass on the serializer: - the structured payload now derives `new`/`modified` straight from what each section actually rendered (formatDirectBucket returns its `shown` array) instead of independently re-slicing the buckets with a duplicated `?? effectiveDirectLimit` fallback — the markdown and structured payload can no longer drift apart. - the related-truncation note now accurately describes the selection (one representative per affected component, closest by distance first) rather than the vaguer "diverse sample". No behavior change to the bound; full suite green (334). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| const hidden = stories.length - shown.length; | ||
| let text = `${heading}:\n${shown.map((s) => serializeStory(s)).join('\n')}`; | ||
| if (hidden > 0) { | ||
| text += `\n- …and ${hidden} more (omitted to stay within the response size limit; all ${stories.length} are in \`structuredContent.counts\`).`; |
| text += | ||
| `\n\nShowing ${sample.length} of ${total} related stories — one representative per affected component (closest by import distance first, when known). ` + | ||
| `Related stories transitively render a changed component — they are lower priority than the new/modified stories, ` + | ||
| `which are listed in full. To enumerate every story for a specific component, call \`get-stories-by-component\` with that component's source path. ` + | ||
| `Do not assume the un-sampled stories are unaffected, and never invent story IDs.`; |
| export const GET_CHANGED_STORIES_TOOL_DESCRIPTION = `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs). | ||
|
|
||
| export const GET_CHANGED_STORIES_TOOL_DESCRIPTION = `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs).`; | ||
| New and modified stories (the directly-changed ones) are always returned in full. Related stories — those that only transitively render a changed component — can number in the thousands when a shared primitive (e.g. Badge, Tag, Icon) changes, so they are returned as a component-diverse sample plus complete per-component counts, keeping the response within tool-output limits. To enumerate every related story for one component, call \`${GET_STORIES_BY_COMPONENT_TOOL_NAME}\` with its source path.`; |
| Whenever you need story IDs — to preview them, to feed `display-review`, to answer the user, for any reason at all — your job is the same regardless of how the request reached you. The input can take any shape: a feature/domain/topic the user named, a file the user mentioned, a file you just edited, a query like "all consumers of X", an autonomous review after a UI change, or anything else. The chain doesn't change with the prompt shape: | ||
|
|
||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, `get-changed-stories` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call `get-stories-by-component` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. | ||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, `get-changed-stories` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call `get-stories-by-component` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. One more thing about its shape: `get-changed-stories` always lists the **new** and **modified** stories in full (these are the directly-changed ones — never dropped), but the **related** stories (transitive consumers of a changed shared component) can run into the thousands, so it returns a _sample_ plus a complete per-component count rather than every related story. The sample is ranked by import **distance** when the Storybook build reports it (each related line is annotated `— distance N`, and the per-component breakdown shows each component's `nearest dN`): distance 1 = a direct importer that almost certainly renders the change, 2+ = progressively more indirect. The sample favors the closest story from each affected component, so it doubles as a ready-made set of review collections (one per distance layer). Treat it as representative, not exhaustive — when you need every related story for a specific component, call `get-stories-by-component` with that component's source path. Never assume the un-sampled related stories don't exist, and never invent IDs to fill the gap. |
|
|
||
| Now the tool: | ||
|
|
||
| - always lists **new** and **modified** stories in full — the directly-changed ones are never dropped; |
| Whenever you need story IDs — to preview them, to feed \`display-review\`, to answer the user, for any reason at all — your job is the same regardless of how the request reached you. The input can take any shape: a feature/domain/topic the user named, a file the user mentioned, a file you just edited, a query like "all consumers of X", an autonomous review after a UI change, or anything else. The chain doesn't change with the prompt shape: | ||
|
|
||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, \`get-changed-stories\` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call \`get-stories-by-component\` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. | ||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, \`get-changed-stories\` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call \`get-stories-by-component\` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. One more thing about its shape: \`get-changed-stories\` always lists the **new** and **modified** stories in full (these are the directly-changed ones — never dropped), but the **related** stories (transitive consumers of a changed shared component) can run into the thousands, so it returns a _sample_ plus a complete per-component count rather than every related story. The sample is ranked by import **distance** when the Storybook build reports it (each related line is annotated \`— distance N\`, and the per-component breakdown shows each component's \`nearest dN\`): distance 1 = a direct importer that almost certainly renders the change, 2+ = progressively more indirect. The sample favors the closest story from each affected component, so it doubles as a ready-made set of review collections (one per distance layer). Treat it as representative, not exhaustive — when you need every related story for a specific component, call \`get-stories-by-component\` with that component's source path. Never assume the un-sampled related stories don't exist, and never invent IDs to fill the gap. |
CI fixes for the get-changed-stories changes: - run oxfmt on the new/changed files (formatting check was failing). - update the internal-storybook MCP-endpoint e2e "list available tools" inline snapshot to reflect the new get-changed-stories description and outputSchema (counts/relatedSample/relatedBreakdown/distance/…). No behavior change. addon-mcp unit suite green (334); e2e green (35). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| text += | ||
| `\n\nShowing ${sample.length} of ${total} related stories — one representative per affected component (closest by import distance first, when known). ` + | ||
| `Related stories transitively render a changed component — they are lower priority than the new/modified stories, ` + | ||
| `which are listed in full. To enumerate every story for a specific component, call \`get-stories-by-component\` with that component's source path. ` + | ||
| `Do not assume the un-sampled stories are unaffected, and never invent story IDs.`; |
| export const GET_CHANGED_STORIES_TOOL_DESCRIPTION = `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs). | ||
|
|
||
| export const GET_CHANGED_STORIES_TOOL_DESCRIPTION = `Get Storybook stories marked as new, modified, or related. Returns story metadata only (no URLs).`; | ||
| New and modified stories (the directly-changed ones) are always returned in full. Related stories — those that only transitively render a changed component — can number in the thousands when a shared primitive (e.g. Badge, Tag, Icon) changes, so they are returned as a component-diverse sample plus complete per-component counts, keeping the response within tool-output limits. To enumerate every related story for one component, call \`${GET_STORIES_BY_COMPONENT_TOOL_NAME}\` with its source path.`; |
| distance: v.pipe( | ||
| v.optional(v.number()), | ||
| v.description( | ||
| 'Import-graph distance from the changed source (1 = direct importer, 2+ = transitive). Lower = more likely to render the change. Omitted when the Storybook build does not report it.', | ||
| ), | ||
| ), |
| Whenever you need story IDs — to preview them, to feed `display-review`, to answer the user, for any reason at all — your job is the same regardless of how the request reached you. The input can take any shape: a feature/domain/topic the user named, a file the user mentioned, a file you just edited, a query like "all consumers of X", an autonomous review after a UI change, or anything else. The chain doesn't change with the prompt shape: | ||
|
|
||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, `get-changed-stories` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call `get-stories-by-component` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. | ||
| 1. **Identify the relevant component file paths.** Use whatever you have — the user's words, the files you touched, the symbol that changed — and reach a list of absolute paths to component source files using filesystem search (grep / Glob / find) and code reading. The bridge from "whatever the input was" to "a list of component file paths" is yours to build; the tool starts where that bridge ends. One common trap: when the changed file is _shared_ infrastructure (theme token, design token, util, hook, CSS module) it isn't itself a component — grep for its consumers and pass _their_ paths, not the shared file's. If the symbol you greped looks like one member of a related group (sibling tokens, neighboring exports), widen to the rest of the group too — related symbols are often consumed together by different components, and a too-narrow grep silently drops stories. A subtle variant: when you've made _multiple_ edits in the same session, `get-changed-stories` returns the _cumulative_ diff — so a non-empty result may reflect an earlier sub-change and not cover your most recent edit. Always check that every file you've touched is represented in the response; for any that isn't, treat it as the "shared infrastructure" case and call `get-stories-by-component` with its consumers. The tool will surface this gap explicitly with a "coverage sanity check" hint when it detects unreachable working-tree files. One more thing about its shape: `get-changed-stories` always lists the **new** and **modified** stories in full (these are the directly-changed ones — never dropped), but the **related** stories (transitive consumers of a changed shared component) can run into the thousands, so it returns a _sample_ plus a complete per-component count rather than every related story. The sample is ranked by import **distance** when the Storybook build reports it (each related line is annotated `— distance N`, and the per-component breakdown shows each component's `nearest dN`): distance 1 = a direct importer that almost certainly renders the change, 2+ = progressively more indirect. The sample favors the closest story from each affected component, so it doubles as a ready-made set of review collections (one per distance layer). Treat it as representative, not exhaustive — when you need every related story for a specific component, call `get-stories-by-component` with that component's source path. Never assume the un-sampled related stories don't exist, and never invent IDs to fill the gap. |
…iew feedback) Addresses the recurring PR-review point that the prose over-promised "new/modified are always returned in full" while the serializer could still cap them. - direct buckets are no longer capped by an arbitrary count (the old 100 default truncated even when there was budget room). They now render in full and are trimmed only by the token-budget backstop, under codemod-scale pressure, and always with the newTruncated/modifiedTruncated flag set. - reword the tool description, dev-instructions, changeset, and truncation notes to state the real contract (listed in full; capped only when the directly- changed set is itself enormous, signalled by the flags) instead of "always". - direct-bucket overflow note no longer implies the omitted story IDs are retrievable from structuredContent.counts (counts holds totals only). - token-budget docstring described as a soft target below the host's hard cap, not an absolute guarantee (the floors can land marginally over). - distance schema description includes the 0 case (story file itself). - point callers at the absolute source path for get-stories-by-component. - refresh the build-server-instructions and MCP-endpoint e2e snapshots. addon-mcp suite green (334); e2e green (35); typecheck + oxfmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
Closes #311.
get-changed-storiesreturned every changed story with no cap. When a shared primitive (Badge, Tag, Icon, …) changes, every story that transitively renders it surfaces as a related status. On large repos this reached 1,000+ entries — ~126 KB / ~56–60k estimated tokens on Chakra — exceeding Claude/MCP's ~25k tool-output cap.When that happens the host silently spills the response to a file and the agent self-curates from a head/tail of it. In the QA campaign this caused real coverage loss:
MetricTilecomposed fromTag): overflow dropped the brand-new component from the review's collections — the single most important thing to review disappeared.Fix — bounded, distance-ranked output
Two complementary changes (the addon ships independently; the core change activates the ranking):
1. addon-mcp (this PR)
newandmodifiedstories are always listed in full — directly-changed, never dropped.related/affected is reduced to a bounded sample + complete per-component count, with an explicit truncation note pointing atget-stories-by-component.status.data.distance): it takes the closest story from each affected component first ("strategy F"), maximizing both component breadth and relevance. Each related line is annotated— distance N; the breakdown shows each component'snearest dN. Without distance (older Storybook) it degrades gracefully to component-diverse round-robin — still bounded and broad.structuredContent(counts,relatedSample,relatedBreakdown,relatedTruncated, …).2. Storybook core (companion change — see below)
buildStatuses()computed the import distance then threw it away. The companion change persists it in the existingStatus.datafield ({ distance }, merged to the nearest). Branchyann/changed-stories-distanceoffnextin storybookjs/storybook, ready to PR separately.Experiments
Benchmarked 7 strategies over the Carbon worst case (1,047 related across 30 components, real token estimator). Full writeup:
packages/addon-mcp/docs/get-changed-stories-overflow.md.Same ~2.4k-token cost (25× under the pre-fix 60.8k), but F surfaces the related stories that actually render the change (avg distance 1.07 vs 2.98) while still representing every affected component.
Tests
serialize-changed-stories.test.ts(14): full-listing, related capping with truthful totals, the "never drop new/modified" Carbon regression, strategy-F distance ranking, round-robin fallback, breakdown capping, token-budget backstop.get-changed-stories.test.ts(13): end-to-end through the real tmcp MCP stack at 1,049 entries — both distance-present (ranked) and distance-absent (fallback), asserting< 12ktokens and intact new/modified.change-detection-service.test.ts28 passing (distance persisted + nearest-distance merge).🤖 Generated with Claude Code
Summary by CodeRabbit
get-changed-storiesnow returns a bounded component-diverse related-story sample (plus per-component related counts) alongside full new/modified story lists.