Skip to content

Handle private composed Storybooks in MCP proxy#235

Closed
kasperpeulen wants to merge 6 commits into
mainfrom
kasper/mcp-proxy-private-composition
Closed

Handle private composed Storybooks in MCP proxy#235
kasperpeulen wants to merge 6 commits into
mainfrom
kasper/mcp-proxy-private-composition

Conversation

@kasperpeulen
Copy link
Copy Markdown
Member

@kasperpeulen kasperpeulen commented May 21, 2026

TL;DR

The local Storybook MCP proxy cannot authenticate into private composed Storybooks. This PR keeps those sources visible and returns normal MCP guidance telling the agent to use the composed Storybook's own /mcp endpoint, while direct non-proxy requests keep the existing OAuth challenge behavior.

Targets main directly.

Behavior Contract

  • Regular direct requests to /mcp still get the OAuth 401 challenge when composed refs require auth.
  • Requests from @storybook/mcp-proxy are marked with X-Storybook-MCP-Proxy: true, so addon-mcp lets verified local proxy traffic reach the docs tools instead of challenging at the route boundary.
  • The direct OAuth-client path and local-proxy path are represented once as request access mode, then baked into the per-request manifest provider.
  • Composed sources are tracked as per-source state (public, requires-auth, or unknown) rather than parallel auth/source collections.
  • Private composed refs remain listed in multi-source docs output. They are not filtered out.
  • Private-source guidance is modeled as data via ManifestProviderResult / SourceManifestFailure, not a normal throw/catch path.
  • SourceManifestFailure stays pure data (kind, endpoint, authProvider, or fetch-failure message); user-facing Markdown is formatted in @storybook/mcp.
  • The docs tools are registered when local manifests exist or composed remote sources are available, including remote-only setups without local manifests.
  • If auth is discovered during manifest fetch rather than startup probing, proxy-originated requests still return the same private-source guidance instead of turning into a 401/error path.

Review Order

  1. Proxy marker and route auth: packages/mcp-proxy/src/utils/proxy-client.ts adds the request header; packages/addon-mcp/src/preset.ts only bypasses the top-level OAuth challenge for verified local proxy requests.
  2. Access/state model: packages/addon-mcp/src/auth/composition-auth.ts owns per-source auth state and creates a per-request manifest provider from explicit request access mode.
  3. Source failure contract: packages/mcp/src/types.ts and packages/mcp/src/utils/get-manifest.ts model source failures as result data via ManifestProviderResult / getManifestResult.
  4. Tool rendering: packages/mcp/src/tools/*documentation*.ts and packages/mcp/src/utils/manifest-formatter/* render source-failure data as normal tool content/list entries.
  5. Remote docs registration: packages/addon-mcp/src/mcp-handler.ts and packages/addon-mcp/src/preset.ts enable docs tools/status for composed remote sources even without local manifests.
  6. Test decomposition: packages/addon-mcp/src/auth/composition-auth.fetch.test.ts splits fetch/cache behavior out of the auth-state tests.

Test Plan

  • pnpm exec oxfmt --check packages/addon-mcp/src/auth/composition-auth.ts packages/addon-mcp/src/auth/composition-auth.test.ts packages/addon-mcp/src/auth/composition-auth.fetch.test.ts packages/addon-mcp/src/auth/index.ts packages/addon-mcp/src/mcp-handler.ts packages/addon-mcp/src/mcp-handler.test.ts packages/addon-mcp/src/preset.ts packages/addon-mcp/src/preset.test.ts packages/mcp/src/types.ts packages/mcp/src/index.ts packages/mcp/src/utils/get-manifest.ts packages/mcp/src/tools/get-documentation.ts packages/mcp/src/tools/get-documentation-for-story.ts packages/mcp/src/tools/list-all-documentation.ts packages/mcp/src/tools/get-documentation.test.ts packages/mcp/src/tools/get-documentation-for-story.test.ts packages/mcp/src/tools/list-all-documentation.test.ts packages/mcp/src/utils/get-manifest.test.ts packages/mcp/README.md
  • git diff --check
  • pnpm turbo run typecheck --filter=@storybook/addon-mcp --filter=@storybook/mcp
  • pnpm vitest --project=@storybook/mcp --run
  • pnpm vitest --project=@storybook/addon-mcp --run

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: c3059b2

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

This PR includes changesets to release 2 packages
Name Type
@storybook/addon-mcp Patch
@storybook/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
Copy link
Copy Markdown

pkg-pr-new Bot commented May 21, 2026

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

commit: c3059b2

@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from ff4a46e to 17acd0e Compare May 21, 2026 15:20
@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Bundle Report

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

Bundle name Size Change
@storybook/mcp-esm 52.85kB 4.37kB (9.02%) ⬆️⚠️
@storybook/mcp-proxy-esm 23.86kB 328 bytes (1.39%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: @storybook/mcp-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
index.js 3.35kB 35.31kB 10.5% ⚠️
index.d.ts 1.02kB 16.34kB 6.64% ⚠️

Files in index.js:

  • ./src/tools/list-all-documentation.ts → Total Size: 2.14kB

  • ./src/utils/get-manifest.ts → Total Size: 7.12kB

  • ./src/types.ts → Total Size: 2.06kB

  • ./src/tools/get-documentation-for-story.ts → Total Size: 2.5kB

  • ./src/index.ts → Total Size: 799 bytes

  • ./src/tools/get-documentation.ts → Total Size: 3.29kB

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

Assets Changed:

Asset Name Size Change Total Size Change (%)
src-u75RQtk2.js (New) 20.77kB 20.77kB 100.0% 🚀
src-tbfJ4opt.js (Deleted) -20.44kB 0 bytes -100.0% 🗑️

Files in src-u75RQtk2.js:

  • ./src/index.ts → Total Size: 422 bytes

  • ./src/utils/proxy-client.ts → Total Size: 3.09kB

  • ./src/instructions.md → Total Size: 2.98kB

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 80.11696% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.79%. Comparing base (51afdf7) to head (c3059b2).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../mcp/src/utils/manifest-formatter/source-errors.ts 28.57% 7 Missing and 3 partials ⚠️
packages/mcp/src/utils/get-manifest.ts 70.37% 4 Missing and 4 partials ⚠️
...kages/mcp/src/utils/manifest-formatter/markdown.ts 73.07% 4 Missing and 3 partials ⚠️
packages/addon-mcp/src/auth/composition-auth.ts 95.71% 2 Missing and 1 partial ⚠️
packages/addon-mcp/src/mcp-handler.ts 75.00% 0 Missing and 2 partials ⚠️
packages/addon-mcp/src/preset.ts 81.81% 0 Missing and 2 partials ⚠️
packages/mcp/src/tools/list-all-documentation.ts 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #235      +/-   ##
==========================================
+ Coverage   76.06%   76.79%   +0.73%     
==========================================
  Files          52       53       +1     
  Lines        1412     1504      +92     
  Branches      391      420      +29     
==========================================
+ Hits         1074     1155      +81     
- Misses        205      214       +9     
- Partials      133      135       +2     

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

@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from 17acd0e to d762a64 Compare May 22, 2026 04:42
@kasperpeulen kasperpeulen marked this pull request as ready for review May 22, 2026 07:16
@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from d762a64 to 1ec40a4 Compare May 22, 2026 07:21
@kasperpeulen kasperpeulen changed the base branch from kasper/ade-canary-smoke to main May 22, 2026 07:21
@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from 1ec40a4 to 464cfbc Compare May 22, 2026 07:24
@netlify
Copy link
Copy Markdown

netlify Bot commented May 22, 2026

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

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

@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from 464cfbc to d6c19d0 Compare May 22, 2026 07:59
Copilot AI review requested due to automatic review settings May 22, 2026 07:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Storybook MCP proxy + addon-mcp + @storybook/mcp docs tooling to handle private composed Storybooks without hiding sources or surfacing tool errors. Instead, proxy-originated requests can reach the tools, and private sources render as normal MCP content notices instructing the agent to use the private Storybook’s own /mcp endpoint.

Changes:

  • Add a proxy marker header (X-Storybook-MCP-Proxy: true) and use it in addon-mcp to bypass only the top-level OAuth 401 challenge for proxy-originated requests.
  • Model “private source” as a ManifestSourceNotice value returned by manifestProvider, and render it as normal tool output (single- and multi-source).
  • Update unit/e2e tests and increase internal-storybook startup/registry timeouts for composed Storybooks.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/mcp/src/utils/manifest-formatter/markdown.ts Render per-source notice as normal markdown output in multi-source lists.
packages/mcp/src/utils/get-manifest.ts Extend manifest fetching to support ManifestSourceNotice results and convert notices/errors to MCP content.
packages/mcp/src/utils/get-manifest.test.ts Add coverage for preserving notices and multi-source notice handling.
packages/mcp/src/types.ts Introduce ManifestSourceNotice, ManifestProviderResult, notice?: string on SourceManifests, and a type guard.
packages/mcp/src/tools/list-all-documentation.ts Return notice content (non-error) in single-source mode; support notice-aware utilities.
packages/mcp/src/tools/list-all-documentation.test.ts Verify notices appear in multi-source listing without error: entries.
packages/mcp/src/tools/get-documentation.ts Return notice content (non-error) when manifests resolve to a notice.
packages/mcp/src/tools/get-documentation.test.ts Add test asserting notices are returned as normal content.
packages/mcp/src/tools/get-documentation-for-story.ts Return notice content (non-error) when manifests resolve to a notice.
packages/mcp/src/tools/get-documentation-for-story.test.ts Add test asserting notices are returned as normal content.
packages/mcp/src/index.ts Export new types/helpers for reuse (ManifestSourceNotice, ManifestProviderResult, sourceNoticeToMCPContent, isManifestSourceNotice).
packages/mcp-proxy/src/utils/proxy-client.ts Attach X-Storybook-MCP-Proxy: true to downstream tool-call requests.
packages/mcp-proxy/src/utils/proxy-client.test.ts Assert the proxy marker header is sent.
packages/mcp-proxy/src/instructions.md Document how agents should respond to private-source notices.
packages/addon-mcp/src/preset.ts Bypass top-level OAuth challenge when proxy marker header is present; update manifestProvider typing.
packages/addon-mcp/src/preset.test.ts Add tests for 401 challenge behavior vs proxy-marked requests.
packages/addon-mcp/src/mcp-handler.ts Update manifestProvider typing to ManifestProviderResult.
packages/addon-mcp/src/auth/index.ts Re-export proxy marker constants and proxy-request helper.
packages/addon-mcp/src/auth/composition-auth.ts Keep inconclusive refs in source list; return private-source notices for proxy requests; avoid serving cached manifests to proxy requests.
packages/addon-mcp/src/auth/composition-auth.test.ts Add tests for proxy marker detection and notice/caching behavior.
apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts Increase Storybook startup timeout to 60s.
apps/internal-storybook/tests/mcp-composition.e2e.test.ts Increase composed Storybook startup timeout to 60s.
apps/internal-storybook/tests/mcp-composition-auth.e2e.test.ts Increase composed+auth Storybook startup timeout to 60s.
apps/internal-storybook/tests/check-deps.e2e.test.ts Increase dependency registry check test timeout to 60s.
.changeset/private-composition-proxy.md Changeset documenting the user-facing behavior change in addon-mcp and @storybook/mcp.

Comment thread packages/mcp/src/utils/get-manifest.ts Outdated
Comment thread packages/mcp-proxy/src/utils/proxy-client.ts
Comment thread apps/internal-storybook/tests/mcp-composition-auth.e2e.test.ts
Comment thread packages/addon-mcp/src/auth/composition-auth.ts Outdated
@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from d6c19d0 to 924c7fd Compare May 22, 2026 08:34
Copilot AI review requested due to automatic review settings May 22, 2026 08:40
@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from 924c7fd to 2632c4d Compare May 22, 2026 08:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

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

Comment thread packages/mcp/src/utils/get-manifest.ts Outdated
Comment thread packages/addon-mcp/src/preset.ts Outdated
Comment thread packages/addon-mcp/src/auth/composition-auth.ts
@kasperpeulen kasperpeulen force-pushed the kasper/mcp-proxy-private-composition branch from 2632c4d to e9bb785 Compare May 22, 2026 09:13
Copilot AI review requested due to automatic review settings May 22, 2026 09:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

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

Comment thread packages/mcp/src/utils/get-manifest.ts Outdated
Comment on lines +73 to +83
@@ -42,7 +76,11 @@ type MCPErrorResult = {
* @param error - The error to convert (can be any type)
* @returns A tool result with error content and isError flag
*/
export const errorToMCPContent = (error: unknown): MCPErrorResult => {
export const errorToMCPContent = (error: unknown): MCPTextResult => {
if (error instanceof SourceManifestError) {
return sourceManifestFailureToMCPContent(error.failure);
}

Comment on lines +405 to +418
function formatSourceError(error: Extract<SourceManifests, { kind: 'error' }>['error']): string {
switch (error.kind) {
case 'requires-own-mcp':
return error.message;
case 'fetch-failed':
return `error: ${error.message}`;
default:
return assertNever(error);
}
}

function assertNever(value: never): never {
throw new Error(`Unhandled source manifest result: ${JSON.stringify(value)}`);
}
Comment on lines 255 to +276
@@ -230,8 +271,9 @@ export class CompositionAuth {
}

// Invalid manifest — check /mcp to see if it's an auth issue
if (await this.#isMcpUnauthorized(new URL(url).origin)) {
throw new AuthenticationError(url);
const mcpAuth = await this.#tryCheckMcpAuth(new URL(url).origin);
if (mcpAuth.unauthorized) {
throw new AuthenticationError(url, mcpAuth.authRequirement);
Copilot AI review requested due to automatic review settings May 22, 2026 10:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 28 changed files in this pull request and generated no new comments.

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

Copilot AI review requested due to automatic review settings May 22, 2026 14:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 33 changed files in this pull request and generated 1 comment.

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

Comment on lines +316 to +318
const hasDisplayableResult = results.some(
(result) => result.kind === 'manifest' || result.error.kind === 'requires-own-mcp',
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants