Skip to content

fix(addon-mcp): bound get-changed-stories output to avoid context overflow (#311) - #312

Open
yannbf wants to merge 7 commits into
mainfrom
yann/tool-context-window
Open

fix(addon-mcp): bound get-changed-stories output to avoid context overflow (#311)#312
yannbf wants to merge 7 commits into
mainfrom
yann/tool-context-window

Conversation

@yannbf

@yannbf yannbf commented Jun 25, 2026

Copy link
Copy Markdown
Member

Problem

Closes #311.

get-changed-stories returned 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:

  • Carbon (MetricTile composed from Tag): overflow dropped the brand-new component from the review's collections — the single most important thing to review disappeared.
  • Chakra (1,047 lines / 126 KB) and Yamada (157 KB): spilled to a file.

Fix — bounded, distance-ranked output

Two complementary changes (the addon ships independently; the core change activates the ranking):

1. addon-mcp (this PR)

  • new and modified stories 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 at get-stories-by-component.
  • The sample is ranked by import distance when available (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's nearest dN. Without distance (older Storybook) it degrades gracefully to component-diverse round-robin — still bounded and broad.
  • Token-budget hard backstop — trims the related sample first, then the direct buckets only in pathological refactors, always with a note rather than a silent drop.
  • Structured 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 existing Status.data field ({ distance }, merged to the nearest). Branch yann/changed-stories-distance off next in 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.

Strategy tokens overflow distinctComps avgDist direct importers
A full-dump (status quo) 60,817 30 3.02 63
B round-robin (distance-blind) 2,415 30 2.98 4
F per-component closest (shipped) 2,417 30 1.07 37

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 < 12k tokens and intact new/modified.
  • Full addon-mcp suite: 334 passing, typecheck clean, lint clean.
  • Companion Storybook core change: change-detection-service.test.ts 28 passing (distance persisted + nearest-distance merge).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • get-changed-stories now returns a bounded component-diverse related-story sample (plus per-component related counts) alongside full new/modified story lists.
    • Related selection is distance-aware when available, and results include structured metadata and truncation flags.
  • Bug Fixes
    • Prevents tool output overflow in large Storybook repos via token-budget enforcement (new/modified remain intact in normal cases).
  • Tests / Documentation
    • Expanded overflow, ranking, and edge-case regression coverage, including structured output and snapshot updates.
    • Updated overflow investigation/resolution documentation.
  • Chores
    • Ignores local eval run cache artifacts.

…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>
Copilot AI review requested due to automatic review settings June 25, 2026 06:43
@netlify

netlify Bot commented Jun 25, 2026

Copy link
Copy Markdown

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

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

@changeset-bot

changeset-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2d7509b

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 Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 25, 2026

Copy link
Copy Markdown
npx https://pkg.pr.new/storybookjs/mcp/@storybook/addon-mcp@312
npx https://pkg.pr.new/storybookjs/mcp/@storybook/mcp@312

commit: 2d7509b

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Bundle Report

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

Bundle name Size Change
@storybook/mcp-esm 56.61kB 2.84kB (5.28%) ⬆️⚠️
@storybook/addon-mcp-esm 5.05kB 3.58kB (242.28%) ⬆️⚠️

Affected Assets, Files, and Routes:

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

Assets Changed:

Asset Name Size Change Total Size Change (%)
preview-*.js (New) 5.05kB 5.05kB 100.0% 🚀
preview.js (Deleted) -1.48kB 0 bytes -100.0% 🗑️
view changes for bundle: @storybook/mcp-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
index.js 772 bytes 37.14kB 2.12%
index.d.ts 2.07kB 18.27kB 12.75% ⚠️

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.80000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.58%. Comparing base (64d6c81) to head (2d7509b).
⚠️ Report is 272 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...s/addon-mcp/src/utils/serialize-changed-stories.ts 97.36% 1 Missing and 2 partials ⚠️
...ackages/addon-mcp/src/tools/get-changed-stories.ts 90.90% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-stories to use the serializer and emit structuredContent with an explicit outputSchema.
  • 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.

Comment on lines +137 to +140
/**
* Builds a per-component count over ALL related stories, preserving the
* (already-sorted) first-seen order of components.
*/
Comment on lines +214 to +218
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 };
Comment on lines +251 to +255
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.`;
Comment on lines +321 to +330
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();
}
Comment on lines +20 to +22
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.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05b2ecf5-e653-408b-9735-2c12c90cf0e1

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0f673 and 2d7509b.

📒 Files selected for processing (6)
  • .changeset/quiet-stories-bound.md
  • apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts
  • packages/addon-mcp/src/instructions/build-server-instructions.test.ts
  • packages/addon-mcp/src/instructions/dev-instructions.md
  • packages/addon-mcp/src/tools/get-changed-stories.ts
  • packages/addon-mcp/src/utils/serialize-changed-stories.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/quiet-stories-bound.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/addon-mcp/src/instructions/build-server-instructions.test.ts
  • apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts
  • packages/addon-mcp/src/tools/get-changed-stories.ts
  • packages/addon-mcp/src/utils/serialize-changed-stories.ts
  • packages/addon-mcp/src/instructions/dev-instructions.md

📝 Walkthrough

Walkthrough

This PR adds bounded serialization and structured responses for get-changed-stories, including full new/modified story lists, sampled related stories with per-component counts, token-budget enforcement, and updated tool instructions, snapshots, tests, and changelog notes.

Changes

Token-bounded changed stories

Layer / File(s) Summary
Serializer core
packages/addon-mcp/src/utils/serialize-changed-stories.ts
ChangedStory shapes, component breakdown sampling, markdown formatting, and token-budget trimming now produce bounded headline, body, and structured output.
Tool wiring
packages/addon-mcp/src/tools/get-changed-stories.ts
get-changed-stories now exposes GetChangedStoriesOutput, returns structured empty/non-empty responses, and serializes output through serializeChangedStories.
Serializer tests
packages/addon-mcp/src/utils/serialize-changed-stories.test.ts
Vitest cases cover full lists, sampled related stories, token limits, direct-bucket trimming, distance-aware ranking, and empty output.
Tool tests
packages/addon-mcp/src/tools/get-changed-stories.test.ts
Snapshot updates and a large mixed-status test check the revised related-story rendering, output size bound, totals, distance ordering, and truncation flags.
Instructions and notes
packages/addon-mcp/src/instructions/*, packages/addon-mcp/docs/get-changed-stories-overflow.md, .changeset/quiet-stories-bound.md
Developer instructions, inline snapshots, the changeset note, and the overflow investigation doc describe sampled related-story output, get-stories-by-component, and the token-limit fix.

Eval cache refresh

Layer / File(s) Summary
Eval cache ignore
.gitignore
Adds an ignore rule for eval/.eval-cache/.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • storybookjs/mcp#298: Shares the same packages/addon-mcp/src/tools/get-changed-stories.ts tool registration and metadata path, which this PR extends with output schema and structured responses.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/addon-mcp/src/utils/serialize-changed-stories.ts (1)

137-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc comment doesn't match the sort. The comment says the breakdown preserves "first-seen order of components", but buildBreakdown actually returns entries sorted by count descending (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 tradeoff

Consider deriving the structured type from GetChangedStoriesOutput to avoid drift.

GetChangedStoriesOutput (Valibot) and the ChangedStoriesStructured interface in serialize-changed-stories.ts describe the same payload but are maintained independently. If one gains/renames a field (e.g. a new truncation flag), the other can silently diverge and structuredContent may fail the tool's outputSchema validation at runtime. Exporting a v.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() and v.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

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6c81 and 500a215.

📒 Files selected for processing (7)
  • .changeset/quiet-stories-bound.md
  • packages/addon-mcp/src/instructions/build-server-instructions.test.ts
  • packages/addon-mcp/src/instructions/dev-instructions.md
  • packages/addon-mcp/src/tools/get-changed-stories.test.ts
  • packages/addon-mcp/src/tools/get-changed-stories.ts
  • packages/addon-mcp/src/utils/serialize-changed-stories.test.ts
  • packages/addon-mcp/src/utils/serialize-changed-stories.ts

Comment thread packages/addon-mcp/src/utils/serialize-changed-stories.ts
yannbf and others added 2 commits June 25, 2026 09:04
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>
Copilot AI review requested due to automatic review settings June 25, 2026 07:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 8 out of 8 changed files in this pull request and generated 7 comments.

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\`).`;
Comment on lines +315 to +319
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.`;
Comment on lines +20 to +22
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.`;
Comment thread packages/addon-mcp/src/tools/get-changed-stories.ts
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.
Comment thread .changeset/quiet-stories-bound.md Outdated

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.
yannbf and others added 2 commits June 25, 2026 14:47
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>
Copilot AI review requested due to automatic review settings June 25, 2026 12:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines +313 to +317
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.`;
Comment on lines +20 to +22
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.`;
Comment on lines +86 to +91
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.
yannbf and others added 2 commits June 25, 2026 14:55
…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>
Copilot AI review requested due to automatic review settings June 25, 2026 12:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.

get-changed-stories exceeds the agent tool-output token limit on large repos → review silently degrades

2 participants