diff --git a/.changeset/private-composition-proxy.md b/.changeset/private-composition-proxy.md new file mode 100644 index 00000000..dd22f54c --- /dev/null +++ b/.changeset/private-composition-proxy.md @@ -0,0 +1,7 @@ +--- +"@storybook/addon-mcp": patch +"@storybook/mcp": patch +--- + +Handle private composed Storybooks when documentation requests come through the Storybook MCP proxy. +Private composed refs now stay visible and return normal guidance to use the source Storybook's MCP endpoint instead of surfacing as tool errors. diff --git a/apps/internal-storybook/pnpm-lock.yaml b/apps/internal-storybook/pnpm-lock.yaml index 3aae054b..e11dfbc6 100644 --- a/apps/internal-storybook/pnpm-lock.yaml +++ b/apps/internal-storybook/pnpm-lock.yaml @@ -7,20 +7,20 @@ settings: catalogs: default: '@storybook/addon-a11y': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@storybook/addon-docs': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@storybook/addon-themes': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@storybook/addon-vitest': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@storybook/react-vite': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@vitest/browser-playwright': specifier: 4.0.6 version: 4.0.6 @@ -28,8 +28,8 @@ catalogs: specifier: 1.56.1 version: 1.56.1 storybook: - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 vite: specifier: 7.2.2 version: 7.2.2 @@ -43,22 +43,22 @@ importers: devDependencies: '@storybook/addon-a11y': specifier: 'catalog:' - version: 10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) + version: 10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) '@storybook/addon-mcp': specifier: workspace:* version: link:../../packages/addon-mcp '@storybook/addon-themes': specifier: 'catalog:' - version: 10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.5.0-alpha.1(@vitest/browser-playwright@4.0.6)(@vitest/browser@4.0.6(vite@7.2.2)(vitest@4.0.6))(@vitest/runner@4.0.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.6) + version: 10.5.0-alpha.2(@vitest/browser-playwright@4.0.6)(@vitest/browser@4.0.6(vite@7.2.2)(vitest@4.0.6))(@vitest/runner@4.0.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.6) '@storybook/react-vite': specifier: 'catalog:' - version: 10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2) + version: 10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2) '@types/react': specifier: ^18.2.65 version: 18.3.28 @@ -82,7 +82,7 @@ importers: version: 18.3.1(react@18.3.1) storybook: specifier: 'catalog:' - version: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tinyexec: specifier: ^1.0.2 version: 1.0.2 @@ -937,32 +937,32 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-a11y@10.5.0-alpha.1': - resolution: {integrity: sha512-PnjJgl0wlpH/w7P4hR1znz1exUrAt6A5DKmLRUtoMQ0cjOvckaFcyJs3SoSfQTe5qqseeTkhFoXbb523pSedog==} + '@storybook/addon-a11y@10.5.0-alpha.2': + resolution: {integrity: sha512-Nw4DwvekQrGJ01lARHTBg+jDWaRo1+nF1XNP60gtKAdvMdo4u8Rk4sOykT3RfyH3KPj5exq8G9d9S+/gWfWblw==} peerDependencies: - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 - '@storybook/addon-docs@10.5.0-alpha.1': - resolution: {integrity: sha512-FhWkTIHtjCg4joIVkshwQPiqYUTPvnqNkpG/3B75GffluzL5YX2Qu9cftpCDql86ImgI1ctKIoDXZWmT40lovA==} + '@storybook/addon-docs@10.5.0-alpha.2': + resolution: {integrity: sha512-Hd4c35sgLe3CjRtmCD1v/ZURuF8yrWoWVS+r9BcCuwwDPDySQInGHnfGrLOI5X1h0CiVdBJqjLPBbrzagtrkdQ==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 peerDependenciesMeta: '@types/react': optional: true - '@storybook/addon-themes@10.5.0-alpha.1': - resolution: {integrity: sha512-HYazKMCn9Qq9oS0H4+nVH4fCJdxjCS/gcuyLlI0e+ew+MR6BvM4rbC+V7fhl9hj2u9R86cJKz4nwGC0ASfywuQ==} + '@storybook/addon-themes@10.5.0-alpha.2': + resolution: {integrity: sha512-JoNpMnqbUwXLo2+U5JmMar7JQVv00G6v7Eu1M1D/CAo2EN4bKK3g4ovbCjdulXeQ21G8OpVGatai+OVt7SWvhw==} peerDependencies: - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 - '@storybook/addon-vitest@10.5.0-alpha.1': - resolution: {integrity: sha512-mbY8a6wcteNnR9p+09uBIxGklc4Qzf7gk0CsOoGUT5re4Q4V7N7lXXoM94MX5uYEZsyKsrOXfBc4FV/H/wb+sg==} + '@storybook/addon-vitest@10.5.0-alpha.2': + resolution: {integrity: sha512-agNzDQGXydQVmmAXLN6NkQ6XFo1ONLBLCgcBBoBpKfPZk27EiYxDJNfAilky5cPv/4+jZQL5veZtdkSdKSK2Ag==} peerDependencies: '@vitest/browser': ^3.0.0 || ^4.0.0 '@vitest/browser-playwright': ^4.0.0 '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: '@vitest/browser': @@ -974,18 +974,18 @@ packages: vitest: optional: true - '@storybook/builder-vite@10.5.0-alpha.1': - resolution: {integrity: sha512-3kK4MNFXVfoqXOFOsUViCrZEQId50d9QE17UxV1l9jQ+nB787AlAKafEBnpaWtmkLpY11g0GDOgwoFVE1N5m4A==} + '@storybook/builder-vite@10.5.0-alpha.2': + resolution: {integrity: sha512-yzjn/TcKNSOXvdn5S0A5RrkSCtsYa0Ath3aL0EVfgkWbPuwHwOD/u3h91CVWRpjGHAOctjhk6cX1nJJQwDx1kQ==} peerDependencies: - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.5.0-alpha.1': - resolution: {integrity: sha512-W05MKCtFe8uqUruIK0qx250JTT8Xc/OdsvX7ayibdz1JwqDHCpuUw3dAqU9s3WRlhEqckhCSVD2LfMBkM27+OQ==} + '@storybook/csf-plugin@10.5.0-alpha.2': + resolution: {integrity: sha512-U2eLlrx3PrzwSbUSa8N1rScUqo+QD3T0yATfneWjiKfpQ5gL1O5dK2U/AbfqIdXzLEgco7NDz8iGx45CtFJlmA==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vite: '*' webpack: '*' peerDependenciesMeta: @@ -1007,36 +1007,36 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@10.5.0-alpha.1': - resolution: {integrity: sha512-pkaFTxlRXMs5gpszYn6qiJSFun2aVx5lZHgIFIMLKmCcO56LgQ5GGXDMwuQBCLb9ybblIxYZ3G/7hoVoTJn1PQ==} + '@storybook/react-dom-shim@10.5.0-alpha.2': + resolution: {integrity: sha512-sgi/5dpDKRKqWCizV1GAbpcz/EjeccHqIq1AlD4kVy+3mKVPMhwwSwvRiq0ihYig+2hhw/fQgHwjamjP2wFHUQ==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@storybook/react-vite@10.5.0-alpha.1': - resolution: {integrity: sha512-8IvABs5m8j+sny2hkMkGXnaqSzAHJAX0OBVk5yhZkecGQj2wOgVXYgjkizWAL5DH73cM82pGUPblo2rfICP70w==} + '@storybook/react-vite@10.5.0-alpha.2': + resolution: {integrity: sha512-FYAuy/k6EQxgxFP7eL/t0phhQN/1ksHYE+lr7pGitdC+ZI/QVts2lS576AoLg/Bm58MoSxz3zjDpJ44q7oCQNg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.5.0-alpha.1': - resolution: {integrity: sha512-hXyawu9EraXjlpx0+52X97nIZXkoGpAn1xbsvTZ+pdg3Cjn1nbrSmRxqd89FY9lJgPdahxksZVopsZSlVJGz4A==} + '@storybook/react@10.5.0-alpha.2': + resolution: {integrity: sha512-ewlAlot/+cvylBaMFbtok1WMlZz4B3ZZ/ebIa9cG8e958pClqVWxvkyA+SZ8oYvJJTNwrcK0HMkaRCdS8RxOMg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 typescript: '>= 4.9.x' peerDependenciesMeta: '@types/react': @@ -1585,8 +1585,8 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - storybook@10.5.0-alpha.1: - resolution: {integrity: sha512-5iTRa0fS1YLF14ZqiePabiCrLp6AnEvF1EiFLjhvXLWH7AUZTxd6BrUXAqD11Ql0R0gPGhST8GSrXknG6kvfEA==} + storybook@10.5.0-alpha.2: + resolution: {integrity: sha512-bxdnOntnglD9loavf1AGELgo36QvIpBFEkypHBHmEEausSZkdyxSHZ4QlC1CTv+5oeg8B7CEo9sGq6JGgIuZzw==} hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2331,21 +2331,21 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@storybook/addon-a11y@10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.1 - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/addon-docs@10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2)': + '@storybook/addon-docs@10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2)': dependencies: '@mdx-js/react': 3.1.1(@types/react@18.3.28)(react@18.3.1) - '@storybook/csf-plugin': 10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) + '@storybook/csf-plugin': 10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) '@storybook/icons': 2.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@storybook/react-dom-shim': 10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 optionalDependencies: '@types/react': 18.3.28 @@ -2356,16 +2356,16 @@ snapshots: - vite - webpack - '@storybook/addon-themes@10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@storybook/addon-themes@10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 - '@storybook/addon-vitest@10.5.0-alpha.1(@vitest/browser-playwright@4.0.6)(@vitest/browser@4.0.6(vite@7.2.2)(vitest@4.0.6))(@vitest/runner@4.0.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.6)': + '@storybook/addon-vitest@10.5.0-alpha.2(@vitest/browser-playwright@4.0.6)(@vitest/browser@4.0.6(vite@7.2.2)(vitest@4.0.6))(@vitest/runner@4.0.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.0.6)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: '@vitest/browser': 4.0.6(vite@7.2.2)(vitest@4.0.6) '@vitest/browser-playwright': 4.0.6(playwright@1.56.1)(vite@7.2.2)(vitest@4.0.6) @@ -2375,10 +2375,10 @@ snapshots: - react - react-dom - '@storybook/builder-vite@10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2)': + '@storybook/builder-vite@10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2)': dependencies: - '@storybook/csf-plugin': 10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/csf-plugin': 10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-dedent: 2.2.0 vite: 7.2.2 transitivePeerDependencies: @@ -2386,9 +2386,9 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2)': + '@storybook/csf-plugin@10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2)': dependencies: - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 @@ -2402,28 +2402,28 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-dom-shim@10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@storybook/react-dom-shim@10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) - '@storybook/react-vite@10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2)': + '@storybook/react-vite@10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@7.2.2)': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@7.2.2) '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - '@storybook/builder-vite': 10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) - '@storybook/react': 10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) + '@storybook/builder-vite': 10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.2.2) + '@storybook/react': 10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 18.3.1 react-docgen: 8.0.2 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.11 - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tsconfig-paths: 4.2.0 vite: 7.2.2 transitivePeerDependencies: @@ -2435,15 +2435,15 @@ snapshots: - typescript - webpack - '@storybook/react@10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)': + '@storybook/react@10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.5.0-alpha.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@storybook/react-dom-shim': 10.5.0-alpha.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: 18.3.1 react-docgen: 8.0.2 react-docgen-typescript: 2.4.0(typescript@5.9.3) react-dom: 18.3.1(react@18.3.1) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: '@types/react': 18.3.28 '@types/react-dom': 18.3.7(@types/react@18.3.28) @@ -3087,7 +3087,7 @@ snapshots: std-env@3.10.0: {} - storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/apps/internal-storybook/tests/check-deps.e2e.test.ts b/apps/internal-storybook/tests/check-deps.e2e.test.ts index 269da2b9..fb3d96ac 100644 --- a/apps/internal-storybook/tests/check-deps.e2e.test.ts +++ b/apps/internal-storybook/tests/check-deps.e2e.test.ts @@ -43,5 +43,5 @@ describe('Storybook Dependencies', () => { `Storybook dependencies are outdated. Update the catalog in pnpm-workspace.yaml:\n\n sed -i '' 's/${currentVersion}/${latestVersion}/g' pnpm-workspace.yaml && pnpm install\n\nOutdated packages:\n${outdatedList}`, ); } - }); + }, 60_000); }); diff --git a/apps/internal-storybook/tests/mcp-composition-auth.e2e.test.ts b/apps/internal-storybook/tests/mcp-composition-auth.e2e.test.ts index b3a3e9b6..23b74d4c 100644 --- a/apps/internal-storybook/tests/mcp-composition-auth.e2e.test.ts +++ b/apps/internal-storybook/tests/mcp-composition-auth.e2e.test.ts @@ -12,7 +12,7 @@ import { const PORT = 6008; const MCP_ENDPOINT = `http://localhost:${PORT}/mcp`; const WELL_KNOWN_ENDPOINT = `http://localhost:${PORT}/.well-known/oauth-protected-resource`; -const STARTUP_TIMEOUT = 30_000; +const STARTUP_TIMEOUT = 60_000; let storybookProcess: ReturnType | null = null; diff --git a/apps/internal-storybook/tests/mcp-composition.e2e.test.ts b/apps/internal-storybook/tests/mcp-composition.e2e.test.ts index 3ded43f5..24d12838 100644 --- a/apps/internal-storybook/tests/mcp-composition.e2e.test.ts +++ b/apps/internal-storybook/tests/mcp-composition.e2e.test.ts @@ -11,7 +11,7 @@ import { const PORT = 6007; const MCP_ENDPOINT = `http://localhost:${PORT}/mcp`; -const STARTUP_TIMEOUT = 30_000; +const STARTUP_TIMEOUT = 60_000; let storybookProcess: ReturnType | null = null; diff --git a/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts b/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts index f4a4b09a..5a5ebca1 100644 --- a/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts +++ b/apps/internal-storybook/tests/mcp-endpoint.e2e.test.ts @@ -11,7 +11,7 @@ import { const PORT = 6006; const MCP_ENDPOINT = `http://localhost:${PORT}/mcp`; -const STARTUP_TIMEOUT = 30_000; +const STARTUP_TIMEOUT = 60_000; let storybookProcess: ReturnType | null = null; diff --git a/eval/pnpm-lock.yaml b/eval/pnpm-lock.yaml index 910f1dff..a5c8af52 100644 --- a/eval/pnpm-lock.yaml +++ b/eval/pnpm-lock.yaml @@ -7,17 +7,17 @@ settings: catalogs: default: '@storybook/addon-a11y': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@storybook/react-vite': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 playwright: specifier: 1.56.1 version: 1.56.1 storybook: - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 valibot: specifier: 1.2.0 version: 1.2.0 @@ -49,13 +49,13 @@ importers: version: 1.1.11(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@storybook/addon-a11y': specifier: 'catalog:' - version: 10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/mcp': specifier: workspace:* version: link:../packages/mcp '@storybook/react-vite': specifier: 'catalog:' - version: 10.5.0-alpha.1(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.12)) + version: 10.5.0-alpha.2(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.12)) '@tsconfig/node-ts': specifier: ^23.6.1 version: 23.6.3 @@ -127,10 +127,10 @@ importers: version: 5.83.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) storybook: specifier: 'catalog:' - version: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) storybook-addon-test-codegen: specifier: ^3.0.0 - version: 3.0.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 3.0.1(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) tinyexec: specifier: ^1.0.1 version: 1.0.2 @@ -1762,23 +1762,23 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@storybook/addon-a11y@10.5.0-alpha.1': - resolution: {integrity: sha512-PnjJgl0wlpH/w7P4hR1znz1exUrAt6A5DKmLRUtoMQ0cjOvckaFcyJs3SoSfQTe5qqseeTkhFoXbb523pSedog==} + '@storybook/addon-a11y@10.5.0-alpha.2': + resolution: {integrity: sha512-Nw4DwvekQrGJ01lARHTBg+jDWaRo1+nF1XNP60gtKAdvMdo4u8Rk4sOykT3RfyH3KPj5exq8G9d9S+/gWfWblw==} peerDependencies: - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 - '@storybook/builder-vite@10.5.0-alpha.1': - resolution: {integrity: sha512-3kK4MNFXVfoqXOFOsUViCrZEQId50d9QE17UxV1l9jQ+nB787AlAKafEBnpaWtmkLpY11g0GDOgwoFVE1N5m4A==} + '@storybook/builder-vite@10.5.0-alpha.2': + resolution: {integrity: sha512-yzjn/TcKNSOXvdn5S0A5RrkSCtsYa0Ath3aL0EVfgkWbPuwHwOD/u3h91CVWRpjGHAOctjhk6cX1nJJQwDx1kQ==} peerDependencies: - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.5.0-alpha.1': - resolution: {integrity: sha512-W05MKCtFe8uqUruIK0qx250JTT8Xc/OdsvX7ayibdz1JwqDHCpuUw3dAqU9s3WRlhEqckhCSVD2LfMBkM27+OQ==} + '@storybook/csf-plugin@10.5.0-alpha.2': + resolution: {integrity: sha512-U2eLlrx3PrzwSbUSa8N1rScUqo+QD3T0yATfneWjiKfpQ5gL1O5dK2U/AbfqIdXzLEgco7NDz8iGx45CtFJlmA==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vite: '*' webpack: '*' peerDependenciesMeta: @@ -1800,36 +1800,36 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@10.5.0-alpha.1': - resolution: {integrity: sha512-pkaFTxlRXMs5gpszYn6qiJSFun2aVx5lZHgIFIMLKmCcO56LgQ5GGXDMwuQBCLb9ybblIxYZ3G/7hoVoTJn1PQ==} + '@storybook/react-dom-shim@10.5.0-alpha.2': + resolution: {integrity: sha512-sgi/5dpDKRKqWCizV1GAbpcz/EjeccHqIq1AlD4kVy+3mKVPMhwwSwvRiq0ihYig+2hhw/fQgHwjamjP2wFHUQ==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@storybook/react-vite@10.5.0-alpha.1': - resolution: {integrity: sha512-8IvABs5m8j+sny2hkMkGXnaqSzAHJAX0OBVk5yhZkecGQj2wOgVXYgjkizWAL5DH73cM82pGUPblo2rfICP70w==} + '@storybook/react-vite@10.5.0-alpha.2': + resolution: {integrity: sha512-FYAuy/k6EQxgxFP7eL/t0phhQN/1ksHYE+lr7pGitdC+ZI/QVts2lS576AoLg/Bm58MoSxz3zjDpJ44q7oCQNg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.5.0-alpha.1': - resolution: {integrity: sha512-hXyawu9EraXjlpx0+52X97nIZXkoGpAn1xbsvTZ+pdg3Cjn1nbrSmRxqd89FY9lJgPdahxksZVopsZSlVJGz4A==} + '@storybook/react@10.5.0-alpha.2': + resolution: {integrity: sha512-ewlAlot/+cvylBaMFbtok1WMlZz4B3ZZ/ebIa9cG8e958pClqVWxvkyA+SZ8oYvJJTNwrcK0HMkaRCdS8RxOMg==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 typescript: '>= 4.9.x' peerDependenciesMeta: '@types/react': @@ -3152,8 +3152,8 @@ packages: peerDependencies: storybook: ^0.0.0-0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 - storybook@10.5.0-alpha.1: - resolution: {integrity: sha512-5iTRa0fS1YLF14ZqiePabiCrLp6AnEvF1EiFLjhvXLWH7AUZTxd6BrUXAqD11Ql0R0gPGhST8GSrXknG6kvfEA==} + storybook@10.5.0-alpha.2: + resolution: {integrity: sha512-bxdnOntnglD9loavf1AGELgo36QvIpBFEkypHBHmEEausSZkdyxSHZ4QlC1CTv+5oeg8B7CEo9sGq6JGgIuZzw==} hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4748,16 +4748,16 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/addon-a11y@10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-a11y@10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.1 - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/builder-vite@10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12))': + '@storybook/builder-vite@10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12))': dependencies: - '@storybook/csf-plugin': 10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@storybook/csf-plugin': 10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ts-dedent: 2.2.0 vite: 7.3.1(@types/node@24.10.12) transitivePeerDependencies: @@ -4765,9 +4765,9 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12))': + '@storybook/csf-plugin@10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12))': dependencies: - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 @@ -4781,27 +4781,27 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@storybook/react-dom-shim@10.5.0-alpha.1(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/react-dom-shim@10.5.0-alpha.2(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: '@types/react': 18.3.28 - '@storybook/react-vite@10.5.0-alpha.1(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.12))': + '@storybook/react-vite@10.5.0-alpha.2(@types/react@18.3.28)(esbuild@0.27.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.12))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.12)) '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - '@storybook/builder-vite': 10.5.0-alpha.1(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)) - '@storybook/react': 10.5.0-alpha.1(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@storybook/builder-vite': 10.5.0-alpha.2(esbuild@0.27.3)(rollup@4.57.1)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.10.12)) + '@storybook/react': 10.5.0-alpha.2(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.4 react-docgen: 8.0.2 react-dom: 19.2.4(react@19.2.4) resolve: 1.22.11 - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tsconfig-paths: 4.2.0 vite: 7.3.1(@types/node@24.10.12) transitivePeerDependencies: @@ -4813,15 +4813,15 @@ snapshots: - typescript - webpack - '@storybook/react@10.5.0-alpha.1(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@storybook/react@10.5.0-alpha.2(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.5.0-alpha.1(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@storybook/react-dom-shim': 10.5.0-alpha.2(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) react: 19.2.4 react-docgen: 8.0.2 react-docgen-typescript: 2.4.0(typescript@5.9.3) react-dom: 19.2.4(react@19.2.4) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) optionalDependencies: '@types/react': 18.3.28 typescript: 5.9.3 @@ -6222,11 +6222,11 @@ snapshots: space-separated-tokens@2.0.2: {} - storybook-addon-test-codegen@3.0.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): + storybook-addon-test-codegen@3.0.1(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) diff --git a/packages/addon-mcp/pnpm-lock.yaml b/packages/addon-mcp/pnpm-lock.yaml index a9a726c8..d2402df6 100644 --- a/packages/addon-mcp/pnpm-lock.yaml +++ b/packages/addon-mcp/pnpm-lock.yaml @@ -7,11 +7,11 @@ settings: catalogs: default: '@storybook/addon-a11y': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@storybook/addon-vitest': - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 '@tmcp/adapter-valibot': specifier: ^0.1.5 version: 0.1.5 @@ -19,8 +19,8 @@ catalogs: specifier: ^0.8.5 version: 0.8.5 storybook: - specifier: 10.5.0-alpha.1 - version: 10.5.0-alpha.1 + specifier: 10.5.0-alpha.2 + version: 10.5.0-alpha.2 tmcp: specifier: ^1.19.4 version: 1.19.4 @@ -53,13 +53,13 @@ importers: devDependencies: '@storybook/addon-a11y': specifier: 'catalog:' - version: 10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.5.0-alpha.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.5.0-alpha.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) storybook: specifier: 'catalog:' - version: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) packages: @@ -490,18 +490,18 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-a11y@10.5.0-alpha.1': - resolution: {integrity: sha512-PnjJgl0wlpH/w7P4hR1znz1exUrAt6A5DKmLRUtoMQ0cjOvckaFcyJs3SoSfQTe5qqseeTkhFoXbb523pSedog==} + '@storybook/addon-a11y@10.5.0-alpha.2': + resolution: {integrity: sha512-Nw4DwvekQrGJ01lARHTBg+jDWaRo1+nF1XNP60gtKAdvMdo4u8Rk4sOykT3RfyH3KPj5exq8G9d9S+/gWfWblw==} peerDependencies: - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 - '@storybook/addon-vitest@10.5.0-alpha.1': - resolution: {integrity: sha512-mbY8a6wcteNnR9p+09uBIxGklc4Qzf7gk0CsOoGUT5re4Q4V7N7lXXoM94MX5uYEZsyKsrOXfBc4FV/H/wb+sg==} + '@storybook/addon-vitest@10.5.0-alpha.2': + resolution: {integrity: sha512-agNzDQGXydQVmmAXLN6NkQ6XFo1ONLBLCgcBBoBpKfPZk27EiYxDJNfAilky5cPv/4+jZQL5veZtdkSdKSK2Ag==} peerDependencies: '@vitest/browser': ^3.0.0 || ^4.0.0 '@vitest/browser-playwright': ^4.0.0 '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^10.5.0-alpha.1 + storybook: ^10.5.0-alpha.2 vitest: ^3.0.0 || ^4.0.0 peerDependenciesMeta: '@vitest/browser': @@ -768,8 +768,8 @@ packages: sqids@0.3.0: resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} - storybook@10.5.0-alpha.1: - resolution: {integrity: sha512-5iTRa0fS1YLF14ZqiePabiCrLp6AnEvF1EiFLjhvXLWH7AUZTxd6BrUXAqD11Ql0R0gPGhST8GSrXknG6kvfEA==} + storybook@10.5.0-alpha.2: + resolution: {integrity: sha512-bxdnOntnglD9loavf1AGELgo36QvIpBFEkypHBHmEEausSZkdyxSHZ4QlC1CTv+5oeg8B7CEo9sGq6JGgIuZzw==} hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1084,17 +1084,17 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@10.5.0-alpha.1(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-a11y@10.5.0-alpha.2(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.1 - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/addon-vitest@10.5.0-alpha.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-vitest@10.5.0-alpha.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - storybook: 10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - react - react-dom @@ -1397,7 +1397,7 @@ snapshots: sqids@0.3.0: {} - storybook@10.5.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.5.0-alpha.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) diff --git a/packages/addon-mcp/src/auth/composition-auth.fetch.test.ts b/packages/addon-mcp/src/auth/composition-auth.fetch.test.ts new file mode 100644 index 00000000..9696bf5f --- /dev/null +++ b/packages/addon-mcp/src/auth/composition-auth.fetch.test.ts @@ -0,0 +1,269 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { CompositionAuth } from './composition-auth.ts'; + +function createManifestProvider( + auth: CompositionAuth, + request = new Request('http://localhost:6006/mcp'), + kind: 'oauth-client' | 'local-proxy' = 'oauth-client', +) { + const access = auth.createRequestAccess(request, kind); + return { + access, + provider: auth.createManifestProvider('http://localhost:6006', access), + }; +} + +describe('CompositionAuth manifest fetching', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns manifest content when response is valid', async () => { + const auth = new CompositionAuth(); + const manifestJson = + '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; + + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(manifestJson), + }), + ); + + const { provider } = createManifestProvider(auth); + const request = new Request('http://localhost:6006/mcp'); + + const result = await provider(request, './manifests/components.json'); + expect(result).toBe(manifestJson); + }); + + it('throws when fetch returns non-ok response', async () => { + const auth = new CompositionAuth(); + + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 403, + }), + ); + + const { provider } = createManifestProvider(auth); + const request = new Request('http://localhost:6006/mcp'); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( + 'Failed to fetch', + ); + }); + + it('throws auth error when remote returns 401 directly', async () => { + const auth = new CompositionAuth(); + + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 401, + }), + ); + + const request = new Request('http://localhost:6006/mcp', { + headers: { Authorization: 'Bearer expired-token' }, + }); + const { provider } = createManifestProvider(auth, request); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( + 'Authentication failed', + ); + }); + + it('throws auth error when response is invalid manifest and /mcp returns 401', async () => { + const auth = new CompositionAuth(); + + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve('{"some":"unexpected"}'), + }) + .mockResolvedValueOnce({ + status: 401, + }), + ); + + const request = new Request('http://localhost:6006/mcp', { + headers: { Authorization: 'Bearer invalid-token' }, + }); + const { provider } = createManifestProvider(auth, request); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( + 'Authentication failed', + ); + }); + + it('checks the source /mcp endpoint without dropping a composed ref base path', async () => { + const auth = new CompositionAuth(); + const mockFetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve('{"some":"unexpected"}'), + }) + .mockResolvedValueOnce({ + status: 401, + }); + vi.stubGlobal('fetch', mockFetch); + + const { provider } = createManifestProvider(auth); + const request = new Request('http://localhost:6006/mcp'); + const source = { + id: 'remote', + title: 'Remote', + url: 'https://host.example.com/storybook', + }; + + await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( + 'Authentication failed', + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 2, + 'https://host.example.com/storybook/mcp', + expect.any(Object), + ); + }); + + it('throws when response is invalid manifest and /mcp does not return 401', async () => { + const auth = new CompositionAuth(); + + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve('{"some":"unexpected"}'), + }) + .mockResolvedValueOnce({ + status: 200, + }), + ); + + const { provider } = createManifestProvider(auth); + const request = new Request('http://localhost:6006/mcp'); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( + 'Invalid manifest response', + ); + }); + + it('caches remote manifest responses and revalidates in background', async () => { + const auth = new CompositionAuth(); + const manifestJson = + '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(manifestJson), + }); + vi.stubGlobal('fetch', mockFetch); + + const request = new Request('http://localhost:6006/mcp', { + headers: { Authorization: 'Bearer token' }, + }); + const { provider } = createManifestProvider(auth, request); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + await provider(request, './manifests/components.json', source); + expect(mockFetch).toHaveBeenCalledTimes(1); + + const result = await provider(request, './manifests/components.json', source); + expect(result).toBe(manifestJson); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it('fetches fresh when cache is expired', async () => { + vi.useFakeTimers(); + const auth = new CompositionAuth(); + const oldManifest = + '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; + const newManifest = + '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button","description":"updated"}}}'; + + const mockFetch = vi + .fn() + .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(oldManifest) }) + .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(newManifest) }); + vi.stubGlobal('fetch', mockFetch); + + const request = new Request('http://localhost:6006/mcp', { + headers: { Authorization: 'Bearer token' }, + }); + const { provider } = createManifestProvider(auth, request); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + const first = await provider(request, './manifests/components.json', source); + expect(first).toBe(oldManifest); + expect(mockFetch).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(61 * 60 * 1000); + + const second = await provider(request, './manifests/components.json', source); + expect(second).toBe(newManifest); + expect(mockFetch).toHaveBeenCalledTimes(2); + + vi.useRealTimers(); + }); + + it('does not cache local manifest responses', async () => { + const auth = new CompositionAuth(); + const manifestJson = + '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; + + const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(manifestJson), + }); + vi.stubGlobal('fetch', mockFetch); + + const { provider } = createManifestProvider(auth); + const request = new Request('http://localhost:6006/mcp'); + + await provider(request, './manifests/components.json'); + await provider(request, './manifests/components.json'); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it('does not cache error responses', async () => { + const auth = new CompositionAuth(); + const manifestJson = + '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; + + const mockFetch = vi + .fn() + .mockResolvedValueOnce({ ok: false, status: 500 }) + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(manifestJson), + }); + vi.stubGlobal('fetch', mockFetch); + + const request = new Request('http://localhost:6006/mcp', { + headers: { Authorization: 'Bearer token' }, + }); + const { provider } = createManifestProvider(auth, request); + const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + + await expect(provider(request, './manifests/components.json', source)).rejects.toThrow(); + + const result = await provider(request, './manifests/components.json', source); + expect(result).toBe(manifestJson); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/addon-mcp/src/auth/composition-auth.test.ts b/packages/addon-mcp/src/auth/composition-auth.test.ts index 61f02c90..18ff0686 100644 --- a/packages/addon-mcp/src/auth/composition-auth.test.ts +++ b/packages/addon-mcp/src/auth/composition-auth.test.ts @@ -1,5 +1,79 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { CompositionAuth, extractBearerToken } from './composition-auth.ts'; +import { + CompositionAuth, + extractBearerToken, + isStorybookMcpProxyRequest, +} from './composition-auth.ts'; + +const remoteRef = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; +const chromaticRef = { + id: 'chromatic', + title: 'Chromatic', + url: 'https://main--abc.chromatic.com', +}; +const remoteRequiresOwnMcpFailure = { + kind: 'requires-own-mcp', + endpoint: 'http://remote.example.com/mcp', + authProvider: 'unknown', +}; + +function stubRemoteAuthDiscovery() { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers({ + 'WWW-Authenticate': + 'Bearer resource_metadata="http://remote.example.com/.well-known/oauth-protected-resource"', + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + resource: 'http://remote.example.com/mcp', + authorization_servers: ['http://auth.example.com'], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + issuer: 'http://auth.example.com', + authorization_endpoint: 'http://auth.example.com/authorize', + token_endpoint: 'http://auth.example.com/token', + }), + }), + ); +} + +async function expectRemoteSourceFailure(promise: Promise) { + await expect(promise).resolves.toMatchObject({ + kind: 'source-failure', + failure: remoteRequiresOwnMcpFailure, + }); +} + +function createProxyRequest(): Request { + return new Request('http://localhost:6006/mcp', { + headers: { 'X-Storybook-MCP-Proxy': 'true' }, + }); +} + +function createManifestProvider( + auth: CompositionAuth, + request = new Request('http://localhost:6006/mcp'), + kind: 'oauth-client' | 'local-proxy' = 'oauth-client', +) { + const access = auth.createRequestAccess(request, kind); + return { + access, + provider: auth.createManifestProvider('http://localhost:6006', access), + }; +} describe('CompositionAuth', () => { beforeEach(() => { @@ -7,32 +81,29 @@ describe('CompositionAuth', () => { }); describe('extractBearerToken', () => { - it('extracts token from valid Bearer header', () => { - expect(extractBearerToken('Bearer abc123')).toBe('abc123'); - }); - - it('returns null for non-Bearer header', () => { - expect(extractBearerToken('Basic abc123')).toBeNull(); - }); - - it('returns null for undefined', () => { - expect(extractBearerToken(undefined)).toBeNull(); - }); - - it('returns null for null', () => { - expect(extractBearerToken(null)).toBeNull(); - }); - - it('extracts token from array header', () => { - expect(extractBearerToken(['Bearer abc'])).toBe('abc'); - }); - - it('finds Bearer in mixed array', () => { - expect(extractBearerToken(['Basic xyz', 'Bearer abc'])).toBe('abc'); - }); + it.each([ + ['Bearer abc123', 'abc123'], + [['Bearer abc'], 'abc'], + [['Basic xyz', 'Bearer abc'], 'abc'], + ])('extracts token from Bearer header %#', (header, token) => { + expect(extractBearerToken(header)).toBe(token); + }); + + it.each(['Basic abc123', undefined, null, ['Basic xyz']])( + 'returns null for non-Bearer header %#', + (header) => { + expect(extractBearerToken(header)).toBeNull(); + }, + ); + }); - it('returns null for array without Bearer', () => { - expect(extractBearerToken(['Basic xyz'])).toBeNull(); + describe('isStorybookMcpProxyRequest', () => { + it('detects proxy marker header values', () => { + expect(isStorybookMcpProxyRequest('true')).toBe(true); + expect(isStorybookMcpProxyRequest('TRUE')).toBe(true); + expect(isStorybookMcpProxyRequest(['false', 'true'])).toBe(true); + expect(isStorybookMcpProxyRequest(undefined)).toBe(false); + expect(isStorybookMcpProxyRequest('1')).toBe(false); }); }); @@ -145,12 +216,26 @@ describe('CompositionAuth', () => { expect(sources).toHaveLength(2); expect(sources.map((s) => s.id)).toEqual(['local', 'ref-a']); }); + + it('keeps refs with inconclusive startup probes in the source list', async () => { + const auth = new CompositionAuth(); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network timeout'))); + + await auth.initialize([{ id: 'remote', title: 'Remote', url: 'http://remote.example.com' }]); + const sources = auth.buildSources(); + + expect(sources).toEqual([ + { id: 'local', title: 'Local' }, + { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }, + ]); + }); }); describe('createManifestProvider', () => { it('creates a manifest provider function', () => { const auth = new CompositionAuth(); - const provider = auth.createManifestProvider('http://localhost:6006'); + const { provider } = createManifestProvider(auth); expect(typeof provider).toBe('function'); }); @@ -163,7 +248,7 @@ describe('CompositionAuth', () => { }); vi.stubGlobal('fetch', mockFetch); - const provider = auth.createManifestProvider('http://localhost:6006'); + const { provider } = createManifestProvider(auth); const request = new Request('http://localhost:6006/mcp'); await provider(request, './manifests/components.json'); @@ -183,7 +268,7 @@ describe('CompositionAuth', () => { }); vi.stubGlobal('fetch', mockFetch); - const provider = auth.createManifestProvider('http://localhost:6006'); + const { provider } = createManifestProvider(auth); const request = new Request('http://localhost:6006/mcp'); const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; @@ -198,55 +283,21 @@ describe('CompositionAuth', () => { it('extracts token from request headers for auth-required sources', async () => { const auth = new CompositionAuth(); - // Initialize with a ref that requires auth (401 → resource metadata → server metadata) - vi.stubGlobal( - 'fetch', - vi - .fn() - .mockResolvedValueOnce({ - ok: false, - status: 401, - headers: new Headers({ - 'WWW-Authenticate': - 'Bearer resource_metadata="http://remote.example.com/.well-known/oauth-protected-resource"', - }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - resource: 'http://remote.example.com/mcp', - authorization_servers: ['http://auth.example.com'], - }), - }) - .mockResolvedValueOnce({ - ok: true, - json: () => - Promise.resolve({ - issuer: 'http://auth.example.com', - authorization_endpoint: 'http://auth.example.com/authorize', - token_endpoint: 'http://auth.example.com/token', - }), - }), - ); + stubRemoteAuthDiscovery(); + await auth.initialize([remoteRef]); - await auth.initialize([{ id: 'remote', title: 'Remote', url: 'http://remote.example.com' }]); - - // Now set up mock for manifest fetching const mockFetch = vi.fn().mockResolvedValue({ ok: true, text: () => Promise.resolve('{"v":1,"components":{}}'), }); vi.stubGlobal('fetch', mockFetch); - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp', { headers: { Authorization: 'Bearer test-token-123' }, }); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + const { provider } = createManifestProvider(auth, request); - await provider(request, './manifests/components.json', source); + await provider(request, './manifests/components.json', remoteRef); expect(mockFetch).toHaveBeenCalledWith( 'http://remote.example.com/manifests/components.json', @@ -257,239 +308,161 @@ describe('CompositionAuth', () => { }), ); }); - }); - describe('fetchManifest (via createManifestProvider)', () => { - it('returns manifest content when response is valid', async () => { + it('returns a typed source failure for private refs requested through the Storybook MCP proxy', async () => { const auth = new CompositionAuth(); - const manifestJson = - '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(manifestJson), - }), - ); + stubRemoteAuthDiscovery(); + await auth.initialize([remoteRef]); - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp'); + const mockFetch = vi.fn(); + vi.stubGlobal('fetch', mockFetch); - const result = await provider(request, './manifests/components.json'); - expect(result).toBe(manifestJson); + const request = createProxyRequest(); + const { provider } = createManifestProvider(auth, request, 'local-proxy'); + await expectRemoteSourceFailure(provider(request, './manifests/components.json', remoteRef)); + expect(mockFetch).not.toHaveBeenCalled(); }); - it('throws when fetch returns non-ok response', async () => { + it('does not trust proxy marker headers unless the server marks the request as trusted', async () => { const auth = new CompositionAuth(); - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: false, - status: 403, - }), - ); - - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp'); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; - - await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( - 'Failed to fetch', - ); - }); - - it('throws auth error when remote returns 401 directly', async () => { - const auth = new CompositionAuth(); - - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: false, - status: 401, - }), - ); - - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp', { - headers: { Authorization: 'Bearer expired-token' }, - }); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; - - await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( - 'Authentication failed', - ); - }); - - it('throws auth error when response is invalid manifest and /mcp returns 401', async () => { - const auth = new CompositionAuth(); + stubRemoteAuthDiscovery(); + await auth.initialize([remoteRef]); vi.stubGlobal( 'fetch', vi .fn() - // First call: manifest fetch returns 200 with unexpected JSON .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve('{"some":"unexpected"}'), + ok: false, + status: 401, + headers: new Headers(), }) - // Second call: /mcp returns 401 .mockResolvedValueOnce({ + ok: false, status: 401, + headers: new Headers(), }), ); - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp', { - headers: { Authorization: 'Bearer invalid-token' }, - }); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + const request = createProxyRequest(); + const { access, provider } = createManifestProvider(auth, request); - await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( + await expect(provider(request, './manifests/components.json', remoteRef)).rejects.toThrow( 'Authentication failed', ); + expect(access.authError).toBeTruthy(); + }); + + it('records the Chromatic auth provider for private Chromatic refs', async () => { + const auth = new CompositionAuth(); + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + }); + vi.stubGlobal('fetch', mockFetch); + + const request = createProxyRequest(); + const { access, provider } = createManifestProvider(auth, request, 'local-proxy'); + + await expect( + provider(request, './manifests/components.json', chromaticRef), + ).resolves.toMatchObject({ + kind: 'source-failure', + failure: { + kind: 'requires-own-mcp', + endpoint: 'https://main--abc.chromatic.com/mcp', + authProvider: 'chromatic', + }, + }); + expect(access.authError).toBeNull(); }); - it('throws when response is invalid manifest and /mcp does not return 401', async () => { + it('records OAuth metadata when auth is discovered after an inconclusive startup probe', async () => { const auth = new CompositionAuth(); + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network timeout'))); + + await auth.initialize([remoteRef]); + + expect(auth.requiresAuth).toBe(false); + expect(auth.buildWellKnown('http://localhost:6006')).toBeNull(); vi.stubGlobal( 'fetch', vi .fn() - // First call: manifest fetch returns 200 with unexpected JSON + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers({ + 'WWW-Authenticate': + 'Bearer resource_metadata="http://remote.example.com/.well-known/oauth-protected-resource"', + }), + }) .mockResolvedValueOnce({ ok: true, - text: () => Promise.resolve('{"some":"unexpected"}'), + json: () => + Promise.resolve({ + resource: 'http://remote.example.com/mcp', + authorization_servers: ['http://auth.example.com'], + scopes_supported: ['storybook:read'], + }), }) - // Second call: /mcp returns 200 (no auth issue) .mockResolvedValueOnce({ - status: 200, + ok: true, + json: () => + Promise.resolve({ + issuer: 'http://auth.example.com', + authorization_endpoint: 'http://auth.example.com/authorize', + token_endpoint: 'http://auth.example.com/token', + }), }), ); - const provider = auth.createManifestProvider('http://localhost:6006'); const request = new Request('http://localhost:6006/mcp'); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + const { access, provider } = createManifestProvider(auth, request); - await expect(provider(request, './manifests/components.json', source)).rejects.toThrow( - 'Invalid manifest response', + await expect(provider(request, './manifests/components.json', remoteRef)).rejects.toThrow( + 'Authentication failed', ); - }); - - it('caches remote manifest responses and revalidates in background', async () => { - const auth = new CompositionAuth(); - const manifestJson = - '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(manifestJson), - }); - vi.stubGlobal('fetch', mockFetch); - - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp', { - headers: { Authorization: 'Bearer token' }, + expect(auth.requiresAuth).toBe(true); + expect(auth.authUrls).toContain('http://remote.example.com'); + expect(access.authError).toBeTruthy(); + expect(auth.buildWellKnown('http://localhost:6006')).toEqual({ + resource: 'http://localhost:6006/mcp', + authorization_servers: ['http://auth.example.com'], + scopes_supported: ['storybook:read'], }); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; - - // First call — fetches (blocking) - await provider(request, './manifests/components.json', source); - expect(mockFetch).toHaveBeenCalledTimes(1); - - // Second call — served from cache, triggers background revalidation - const result = await provider(request, './manifests/components.json', source); - expect(result).toBe(manifestJson); - expect(mockFetch).toHaveBeenCalledTimes(2); // background fetch started }); - it('fetches fresh when cache is expired', async () => { - vi.useFakeTimers(); + it('does not serve cached private manifests to Storybook MCP proxy requests', async () => { const auth = new CompositionAuth(); - const oldManifest = - '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; - const newManifest = - '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button","description":"updated"}}}'; - - const mockFetch = vi - .fn() - .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(oldManifest) }) - .mockResolvedValueOnce({ ok: true, text: () => Promise.resolve(newManifest) }); - vi.stubGlobal('fetch', mockFetch); - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp', { - headers: { Authorization: 'Bearer token' }, - }); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; - - // First call — fetches and caches - const first = await provider(request, './manifests/components.json', source); - expect(first).toBe(oldManifest); - expect(mockFetch).toHaveBeenCalledTimes(1); - - // Advance time past cache TTL (61 minutes) - vi.advanceTimersByTime(61 * 60 * 1000); - - // Second call — cache expired, fetches fresh (blocking) - const second = await provider(request, './manifests/components.json', source); - expect(second).toBe(newManifest); - expect(mockFetch).toHaveBeenCalledTimes(2); - - vi.useRealTimers(); - }); - - it('does not cache local manifest responses', async () => { - const auth = new CompositionAuth(); - const manifestJson = - '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; + stubRemoteAuthDiscovery(); + await auth.initialize([remoteRef]); const mockFetch = vi.fn().mockResolvedValue({ ok: true, - text: () => Promise.resolve(manifestJson), + text: () => Promise.resolve('{"v":1,"components":{}}'), }); vi.stubGlobal('fetch', mockFetch); - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp'); - - // No source = local - await provider(request, './manifests/components.json'); - await provider(request, './manifests/components.json'); - expect(mockFetch).toHaveBeenCalledTimes(2); - }); - - it('does not cache error responses', async () => { - const auth = new CompositionAuth(); - const manifestJson = - '{"v":1,"components":{"button":{"id":"button","path":"src/Button.tsx","name":"Button"}}}'; - - const mockFetch = vi - .fn() - // First call: fails - .mockResolvedValueOnce({ ok: false, status: 500 }) - // Second call: succeeds - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(manifestJson), - }); - vi.stubGlobal('fetch', mockFetch); - - const provider = auth.createManifestProvider('http://localhost:6006'); - const request = new Request('http://localhost:6006/mcp', { - headers: { Authorization: 'Bearer token' }, + const authenticatedRequest = new Request('http://localhost:6006/mcp', { + headers: { Authorization: 'Bearer test-token-123' }, }); - const source = { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }; + const { provider } = createManifestProvider(auth, authenticatedRequest); + await provider(authenticatedRequest, './manifests/components.json', remoteRef); - // First call fails — should not cache - await expect(provider(request, './manifests/components.json', source)).rejects.toThrow(); + const proxyRequest = createProxyRequest(); + const { provider: proxyProvider } = createManifestProvider(auth, proxyRequest, 'local-proxy'); + await expectRemoteSourceFailure( + proxyProvider(proxyRequest, './manifests/components.json', remoteRef), + ); - // Second call should fetch again (not cached) - const result = await provider(request, './manifests/components.json', source); - expect(result).toBe(manifestJson); - expect(mockFetch).toHaveBeenCalledTimes(2); + expect(mockFetch).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/addon-mcp/src/auth/composition-auth.ts b/packages/addon-mcp/src/auth/composition-auth.ts index 1a256466..ea492a59 100644 --- a/packages/addon-mcp/src/auth/composition-auth.ts +++ b/packages/addon-mcp/src/auth/composition-auth.ts @@ -6,7 +6,13 @@ * MCP clients like VS Code to handle the OAuth flow with Chromatic. */ -import { ComponentManifestMap, DocsManifestMap, type Source } from '@storybook/mcp'; +import { + ComponentManifestMap, + DocsManifestMap, + type ManifestProvider, + type Source, + type SourceManifestFailure, +} from '@storybook/mcp'; import * as v from 'valibot'; export interface ComposedRef { @@ -15,6 +21,8 @@ export interface ComposedRef { url: string; } +type RemoteSource = Source & { url: string }; + const OAuthResourceMetadata = v.object({ resource: v.optional(v.string()), authorization_servers: v.pipe(v.array(v.string()), v.minLength(1)), @@ -36,14 +44,38 @@ interface AuthRequirement { serverMetadata: OAuthServerMetadata; } -export type ManifestProvider = ( - request: Request | undefined, - path: string, - source?: Source, -) => Promise; +interface McpAuthCheck { + unauthorized: boolean; + authRequirement: AuthRequirement | null; +} + +type SourceState = + | { + kind: 'public'; + ref: ComposedRef; + } + | { + kind: 'requires-auth'; + ref: ComposedRef; + authRequirement: AuthRequirement; + } + | { + kind: 'unknown'; + ref: ComposedRef; + }; + +export type RequestAccessKind = 'oauth-client' | 'local-proxy'; + +export interface RequestAccess { + kind: RequestAccessKind; + token: string | null; + authError: AuthenticationError | null; +} const MANIFEST_CACHE_TTL = 60 * 60 * 1000; // 60 minutes const REVALIDATION_TTL = 60 * 1000; // 60 seconds +export const STORYBOOK_MCP_PROXY_HEADER = 'X-Storybook-MCP-Proxy'; +export const STORYBOOK_MCP_PROXY_HEADER_VALUE = 'true'; interface CacheEntry { text: string; @@ -52,19 +84,19 @@ interface CacheEntry { } export class AuthenticationError extends Error { - constructor(url: string) { + constructor( + url: string, + public readonly authRequirement: AuthRequirement | null = null, + ) { super(`Authentication failed for ${url}. Your token may be invalid or expired.`); this.name = 'AuthenticationError'; } } export class CompositionAuth { - #authRequirement: AuthRequirement | null = null; - #authRequiredUrls: string[] = []; - #refsWithManifests: ComposedRef[] = []; + #sourceStates = new Map(); #manifestCache = new Map(); #lastToken: string | null = null; - #authErrors = new WeakMap(); /** Initialize by checking which refs require authentication and have manifests. */ async initialize(refs: ComposedRef[]): Promise { @@ -73,56 +105,75 @@ export class CompositionAuth { const result = await this.#checkRef(ref.url); if (result === 'no-manifest') continue; - this.#refsWithManifests.push(ref); - - if (result === 'public') continue; - - // Auth required - this.#authRequiredUrls.push(ref.url); - if (!this.#authRequirement) { - this.#authRequirement = result; - } else { - const existingServer = this.#authRequirement.resourceMetadata.authorization_servers[0]; - const newServer = result.resourceMetadata.authorization_servers[0]; - if (existingServer !== newServer) { - console.warn( - `[addon-mcp] Composed ref "${ref.title}" uses a different OAuth server (${newServer}) than the first authenticated ref (${existingServer}). Only the first OAuth server will be used for authentication.`, - ); - } + if (result === 'public') { + this.#sourceStates.set(ref.id, { kind: 'public', ref }); + continue; } + + this.#recordAuthRequirement(ref, result); } catch (error) { console.warn( - `[addon-mcp] Failed to check auth for composed ref "${ref.title}" (${ref.url}): ${error instanceof Error ? error.message : String(error)}. Skipping this ref.`, + `[addon-mcp] Failed to check auth for composed ref "${ref.title}" (${ref.url}): ${error instanceof Error ? error.message : String(error)}. Keeping this ref in the MCP source list for request-time resolution.`, ); + this.#sourceStates.set(ref.id, { kind: 'unknown', ref }); } } } get requiresAuth(): boolean { - return this.#authRequiredUrls.length > 0; + return [...this.#sourceStates.values()].some((state) => state.kind === 'requires-auth'); } get authUrls(): string[] { - return this.#authRequiredUrls; + return [...this.#sourceStates.values()] + .filter((state) => state.kind === 'requires-auth') + .map((state) => state.ref.url); } - /** Check if a request encountered an auth error during manifest fetching. */ - hadAuthError(request: Request): boolean { - return this.#authErrors.has(request); + createRequestAccess(request: Request, kind: RequestAccessKind): RequestAccess { + return { + kind, + token: extractBearerToken(request.headers.get('Authorization')), + authError: null, + }; } /** Check if a URL requires authentication based on discovered auth requirements. */ #isAuthRequiredUrl(url: string): boolean { - return this.#authRequiredUrls.some((authUrl) => url.startsWith(authUrl)); + return [...this.#sourceStates.values()].some( + (state) => state.kind === 'requires-auth' && url.startsWith(state.ref.url), + ); + } + + #recordAuthRequirement(ref: ComposedRef, result: AuthRequirement): void { + const existingRequirement = this.#firstAuthRequirement(); + const existingServer = existingRequirement?.resourceMetadata.authorization_servers[0]; + const newServer = result.resourceMetadata.authorization_servers[0]; + if (existingServer && existingServer !== newServer) { + console.warn( + `[addon-mcp] Composed ref "${ref.title}" uses a different OAuth server (${newServer}) than the first authenticated ref (${existingServer}). Only the first OAuth server will be used for authentication.`, + ); + } + this.#sourceStates.set(ref.id, { kind: 'requires-auth', ref, authRequirement: result }); + } + + #firstAuthRequirement(): AuthRequirement | null { + for (const state of this.#sourceStates.values()) { + if (state.kind === 'requires-auth') { + return state.authRequirement; + } + } + return null; } /** Build .well-known/oauth-protected-resource response. */ buildWellKnown(origin: string): object | null { - if (!this.#authRequirement) return null; + const authRequirement = this.#firstAuthRequirement(); + if (!authRequirement) return null; return { resource: `${origin}/mcp`, - authorization_servers: this.#authRequirement.resourceMetadata.authorization_servers, - scopes_supported: this.#authRequirement.resourceMetadata.scopes_supported, + authorization_servers: authRequirement.resourceMetadata.authorization_servers, + scopes_supported: authRequirement.resourceMetadata.scopes_supported, }; } @@ -131,11 +182,11 @@ export class CompositionAuth { return `Bearer error="unauthorized", error_description="Authorization needed for composed Storybooks", resource_metadata="${origin}/.well-known/oauth-protected-resource"`; } - /** Build sources configuration: local first, then refs that have manifests. */ + /** Build sources configuration: local first, then refs that have manifests or inconclusive startup probes. */ buildSources(): Source[] { return [ { id: 'local', title: 'Local' }, - ...this.#refsWithManifests.map((ref) => ({ + ...[...this.#sourceStates.values()].map(({ ref }) => ({ id: ref.id, title: ref.title, url: ref.url, @@ -144,19 +195,26 @@ export class CompositionAuth { } /** Create a manifest provider for multi-source mode. */ - createManifestProvider(localOrigin: string): ManifestProvider { + createManifestProvider(localOrigin: string, access: RequestAccess): ManifestProvider { return async (request, path, source) => { - const token = extractBearerToken(request?.headers.get('Authorization')); - const baseUrl = source?.url ?? localOrigin; + const remoteSource = isRemoteSource(source) ? source : undefined; + const baseUrl = remoteSource?.url ?? localOrigin; const manifestUrl = `${baseUrl}${path.replace('./', '/')}`; - const isRemote = !!source?.url; + const isRemote = !!remoteSource; const needsAuth = isRemote && this.#isAuthRequiredUrl(baseUrl); - const tokenForRequest = needsAuth ? token : null; + const tokenForRequest = needsAuth ? access.token : null; + + if (needsAuth && !access.token && access.kind === 'local-proxy' && remoteSource) { + return { + kind: 'source-failure', + failure: createRequiresOwnMcpFailure(remoteSource), + }; + } // New token = user re-authenticated, invalidate all cached manifests - if (token && token !== this.#lastToken) { + if (access.token && access.token !== this.#lastToken) { this.#manifestCache.clear(); - this.#lastToken = token; + this.#lastToken = access.token; } if (isRemote) { @@ -197,7 +255,16 @@ export class CompositionAuth { return text; } catch (error) { if (error instanceof AuthenticationError && request) { - this.#authErrors.set(request, error); + if (remoteSource && error.authRequirement) { + this.#recordAuthRequirement(remoteSource, error.authRequirement); + } + if (access.kind === 'local-proxy' && !access.token && remoteSource) { + return { + kind: 'source-failure', + failure: createRequiresOwnMcpFailure(remoteSource), + }; + } + access.authError = error; } throw error; } @@ -215,7 +282,10 @@ export class CompositionAuth { const response = await fetch(url, { headers }); if (response.status === 401) { - throw new AuthenticationError(url); + const authRequirement = + (await this.#parseAuthFromResponse(response)) ?? + (await this.#tryCheckMcpAuth(getStorybookUrlFromManifestUrl(url))).authRequirement; + throw new AuthenticationError(url, authRequirement); } if (!response.ok) { @@ -230,8 +300,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(getStorybookUrlFromManifestUrl(url)); + if (mcpAuth.unauthorized) { + throw new AuthenticationError(url, mcpAuth.authRequirement); } throw new Error( @@ -279,24 +350,30 @@ export class CompositionAuth { return this.#parseAuthFromResponse(response); } - /** Quick check: does the remote /mcp return 401? */ - async #isMcpUnauthorized(origin: string): Promise { + /** Best-effort /mcp auth check for request-time discovery. */ + async #tryCheckMcpAuth(storybookUrl: string): Promise { try { - const response = await fetch(`${origin}/mcp`, { + const response = await fetch(`${storybookUrl}/mcp`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }), }); - return response.status === 401; + if (response.status !== 401) { + return { unauthorized: false, authRequirement: null }; + } + return { + unauthorized: true, + authRequirement: await this.#parseAuthFromResponse(response), + }; } catch { - return false; + return { unauthorized: false, authRequirement: null }; } } /** Extract auth requirement from a 401 response's WWW-Authenticate header. */ async #parseAuthFromResponse(response: Response): Promise { if (response.status !== 401) return null; - const wwwAuth = response.headers.get('WWW-Authenticate'); + const wwwAuth = response.headers?.get('WWW-Authenticate'); if (!wwwAuth) return null; const match = wwwAuth.match(/resource_metadata="([^"]+)"/); @@ -344,6 +421,37 @@ export class CompositionAuth { } } +function isRemoteSource(source: Source | undefined): source is RemoteSource { + return typeof source?.url === 'string' && source.url.length > 0; +} + +function isChromaticUrl(url: string): boolean { + try { + const hostname = new URL(url).hostname; + return hostname === 'chromatic.com' || hostname.endsWith('.chromatic.com'); + } catch { + return false; + } +} + +function getStorybookUrlFromManifestUrl(manifestUrl: string): string { + const url = new URL(manifestUrl); + url.pathname = url.pathname.replace(/\/manifests\/(?:components|docs)\.json$/, ''); + url.search = ''; + url.hash = ''; + return url.toString().replace(/\/$/, ''); +} + +function createRequiresOwnMcpFailure(source: RemoteSource): SourceManifestFailure { + const mcpEndpoint = `${source.url.replace(/\/$/, '')}/mcp`; + + return { + kind: 'requires-own-mcp', + endpoint: mcpEndpoint, + authProvider: isChromaticUrl(source.url) ? 'chromatic' : 'unknown', + }; +} + /** * Extract Bearer token from Authorization header. * Handles both Node.js (string | string[] | undefined) and Web API (string | null) headers. @@ -355,3 +463,13 @@ export function extractBearerToken( const bearer = values.find((value) => typeof value === 'string' && value.startsWith('Bearer ')); return bearer ? bearer.slice(7) : null; } + +export function isStorybookMcpProxyRequest( + headerValue: string | string[] | null | undefined, +): boolean { + const values = Array.isArray(headerValue) ? headerValue : [headerValue]; + return values.some( + (value) => + typeof value === 'string' && value.trim().toLowerCase() === STORYBOOK_MCP_PROXY_HEADER_VALUE, + ); +} diff --git a/packages/addon-mcp/src/auth/index.ts b/packages/addon-mcp/src/auth/index.ts index e507688c..8f8d3931 100644 --- a/packages/addon-mcp/src/auth/index.ts +++ b/packages/addon-mcp/src/auth/index.ts @@ -5,7 +5,11 @@ export { CompositionAuth, AuthenticationError, + STORYBOOK_MCP_PROXY_HEADER, + STORYBOOK_MCP_PROXY_HEADER_VALUE, extractBearerToken, + isStorybookMcpProxyRequest, type ComposedRef, - type ManifestProvider, + type RequestAccess, + type RequestAccessKind, } from './composition-auth.ts'; diff --git a/packages/addon-mcp/src/mcp-handler.test.ts b/packages/addon-mcp/src/mcp-handler.test.ts index 0f266f25..1ab671e3 100644 --- a/packages/addon-mcp/src/mcp-handler.test.ts +++ b/packages/addon-mcp/src/mcp-handler.test.ts @@ -233,6 +233,12 @@ describe('mcpServerHandler', () => { }; } + function parseSseResponse(body: string) { + const dataLine = body.split('\n').find((line) => line.startsWith('data: ')); + const responseText = dataLine!.replace(/^data: /, '').trim(); + return JSON.parse(responseText); + } + it('should initialize MCP server and handle requests', async () => { const mockOptions = createMockOptions(); const mockReq = createMockIncomingMessage({ @@ -258,9 +264,7 @@ describe('mcpServerHandler', () => { const { body } = getResponseData(); expect(response.end).toHaveBeenCalled(); - const dataLine = body.split('\n').find((line) => line.startsWith('data: ')); - const responseText = dataLine!.replace(/^data: /, '').trim(); - const parsedResponse = JSON.parse(responseText); + const parsedResponse = parseSseResponse(body); expect(parsedResponse).toMatchObject({ jsonrpc: '2.0', @@ -333,9 +337,7 @@ describe('mcpServerHandler', () => { }); const { body } = getResponseData(); - const dataLine = body.split('\n').find((line) => line.startsWith('data: ')); - const responseText = dataLine!.replace(/^data: /, '').trim(); - const parsedResponse = JSON.parse(responseText); + const parsedResponse = parseSseResponse(body); expect(parsedResponse.result.instructions).toContain( 'Follow these workflows when working with UI and/or Storybook.', @@ -454,9 +456,7 @@ describe('mcpServerHandler', () => { // Parse the SSE response const { body } = getResponseData(); - const dataLine = body.split('\n').find((line) => line.startsWith('data: ')); - const responseText = dataLine!.replace(/^data: /, '').trim(); - const parsedResponse = JSON.parse(responseText); + const parsedResponse = parseSseResponse(body); // Verify component manifest tools are included const toolNames = parsedResponse.result.tools.map((t: any) => t.name); @@ -464,6 +464,77 @@ describe('mcpServerHandler', () => { expect(toolNames).toContain('get-documentation'); expect(toolNames).toContain('get-documentation-for-story'); }); + + it('should register documentation tools when remote composed sources exist without local manifests', async () => { + const applyMock = vi.fn(async (key: string, defaultValue?: any) => { + if (key === 'core') { + return { disableTelemetry: false }; + } + if (key === 'features') { + return {}; + } + if (key === 'experimental_manifests') { + return undefined; + } + return defaultValue; + }); + + const mockOptions = createMockOptions({ + port: 6012, + presets: { apply: applyMock }, + }); + const sources = [ + { id: 'local', title: 'Local' }, + { id: 'remote', title: 'Remote', url: 'http://remote.example.com' }, + ]; + + const initReq = createMockIncomingMessage({ + method: 'POST', + headers: { 'content-type': 'application/json', host: 'localhost:6012' }, + body: createMCPInitializeRequest(), + }); + const { response: initResponse } = createMockServerResponse(); + + await mcpServerHandler({ + req: initReq, + res: initResponse, + options: mockOptions as any, + addonOptions: { + toolsets: { dev: true, docs: true }, + }, + sources, + compositionAuth: new CompositionAuth(), + }); + + const listToolsReq = createMockIncomingMessage({ + method: 'POST', + headers: { 'content-type': 'application/json', host: 'localhost:6012' }, + body: { + jsonrpc: '2.0', + id: 2, + method: 'tools/list', + params: {}, + }, + }); + const { response: listResponse, getResponseData } = createMockServerResponse(); + + await mcpServerHandler({ + req: listToolsReq, + res: listResponse, + options: mockOptions as any, + addonOptions: { + toolsets: { dev: true, docs: true }, + }, + sources, + compositionAuth: new CompositionAuth(), + }); + + const parsedResponse = parseSseResponse(getResponseData().body); + const toolNames = parsedResponse.result.tools.map((t: any) => t.name); + expect(toolNames).toContain('list-all-documentation'); + expect(toolNames).toContain('get-documentation'); + expect(toolNames).toContain('get-documentation-for-story'); + }); }); describe('getToolsets', () => { diff --git a/packages/addon-mcp/src/mcp-handler.ts b/packages/addon-mcp/src/mcp-handler.ts index 0a831a83..c9285605 100644 --- a/packages/addon-mcp/src/mcp-handler.ts +++ b/packages/addon-mcp/src/mcp-handler.ts @@ -9,6 +9,7 @@ import { addListAllDocumentationTool, addGetDocumentationTool, addGetStoryDocumentationTool, + type ManifestProvider, type Source, } from '@storybook/mcp'; import type { Options } from 'storybook/internal/types'; @@ -21,7 +22,7 @@ import { getManifestStatus } from './tools/is-manifest-available.ts'; import { addRunStoryTestsTool, getAddonVitestConstants } from './tools/run-story-tests.ts'; import { estimateTokens } from './utils/estimate-tokens.ts'; import { isAddonA11yEnabled } from './utils/is-addon-a11y-enabled.ts'; -import type { CompositionAuth } from './auth/index.ts'; +import type { CompositionAuth, RequestAccess } from './auth/index.ts'; import { buildServerInstructions } from './instructions/build-server-instructions.ts'; let transport: HttpTransport | undefined; @@ -41,6 +42,7 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { const addonVitestConstants = await getAddonVitestConstants(); const manifestStatus = await getManifestStatus(options); a11yEnabled = await isAddonA11yEnabled(options); + const docsAvailable = manifestStatus.available || !!multiSource; let server: McpServer; @@ -50,7 +52,7 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { return buildServerInstructions({ devEnabled: server?.ctx.custom?.toolsets?.dev ?? true, testEnabled: (server?.ctx.custom?.toolsets?.test ?? true) && !!addonVitestConstants, - docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? true) && manifestStatus.available, + docsEnabled: (server?.ctx.custom?.toolsets?.docs ?? true) && docsAvailable, changeDetectionEnabled, }); }, @@ -86,9 +88,9 @@ const initializeMCPServer = async (options: Options, multiSource?: boolean) => { // Register test addon tools await addRunStoryTestsTool(server, { a11yEnabled }); - // Only register the additional tools if the component manifest feature is enabled - if (manifestStatus.available) { - logger.info('Experimental components manifest feature detected - registering component tools'); + // Register documentation tools when local manifests exist or remote composed sources can provide them. + if (docsAvailable) { + logger.info('Documentation sources detected - registering component tools'); const contextAwareEnabled = () => server.ctx.custom?.toolsets?.docs ?? true; await addListAllDocumentationTool(server, contextAwareEnabled); await addGetDocumentationTool(server, contextAwareEnabled, { multiSource }); @@ -113,14 +115,12 @@ type McpServerHandlerParams = { addonOptions: AddonOptionsOutput; /** Sources for multi-source mode (when refs are configured) */ sources?: Source[]; - /** Optional custom manifest provider, receives source as third param in multi-source mode */ - manifestProvider?: ( - request: Request | undefined, - path: string, - source?: Source, - ) => Promise; + /** Builds a manifest provider with the current request's access policy baked in. */ + manifestProviderFactory?: (access: RequestAccess) => ManifestProvider; /** Composition auth handler for multi-source mode */ compositionAuth: CompositionAuth; + /** True when the Node request was verified as local Storybook MCP proxy traffic. */ + trustedProxyRequest?: boolean; }; export const mcpServerHandler = async ({ @@ -129,8 +129,9 @@ export const mcpServerHandler = async ({ options, addonOptions, sources, - manifestProvider, + manifestProviderFactory, compositionAuth, + trustedProxyRequest, }: McpServerHandlerParams) => { // Initialize MCP server and transport on first request, with concurrency safety if (!initialize) { @@ -143,6 +144,11 @@ export const mcpServerHandler = async ({ // Convert Node.js request to Web API Request const webRequest = await incomingMessageToWebRequest(req); + const requestAccess = compositionAuth.createRequestAccess( + webRequest, + trustedProxyRequest ? 'local-proxy' : 'oauth-client', + ); + const manifestProvider = manifestProviderFactory?.(requestAccess); const addonContext: AddonContext = { options, @@ -186,7 +192,7 @@ export const mcpServerHandler = async ({ // is fully consumed can we check whether a tool hit an auth error. const body = await response.arrayBuffer(); - const finalResponse = compositionAuth.hadAuthError(webRequest) + const finalResponse = requestAccess.authError ? new Response('401 - Unauthorized', { status: 401, headers: { diff --git a/packages/addon-mcp/src/preset.test.ts b/packages/addon-mcp/src/preset.test.ts index 2dba0538..aaaf1438 100644 --- a/packages/addon-mcp/src/preset.test.ts +++ b/packages/addon-mcp/src/preset.test.ts @@ -68,6 +68,71 @@ describe('experimental_devServer', () => { expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining(' { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + text: () => + Promise.resolve( + JSON.stringify({ + v: 1, + components: { + button: { + id: 'button', + path: 'src/Button.tsx', + name: 'Button', + }, + }, + }), + ), + }), + ); + + const handlers: Record = {}; + mockApp.get = vi.fn((path: string, handler: any) => { + handlers[path] = handler; + }); + + const optionsWithRemoteRef = { + port: 6006, + presets: { + apply: vi.fn((key: string) => { + if (key === 'refs') { + return Promise.resolve({ + remote: { title: 'Remote', url: 'https://remote.example.com' }, + }); + } + if (key === 'features') { + return Promise.resolve({}); + } + return Promise.resolve(undefined); + }), + }, + } as unknown as Options; + + await (experimental_devServer as any)(mockApp, optionsWithRemoteRef); + + const mockReq = { + headers: { + accept: 'text/html', + }, + } as any; + const mockRes = { + writeHead: vi.fn(), + end: vi.fn(), + } as any; + + await handlers['/mcp'](mockReq, mockRes); + + const html = mockRes.end.mock.calls[0][0]; + expect(html).toMatch( + /docs<\/span>\s*enabled<\/span>/, + ); + expect(html).not.toContain('This toolset is only supported in React-based setups.'); + expect(html).not.toContain('This toolset requires enabling the component manifest feature.'); + }); + it('should show Storybook version requirement for addon-vitest and a manual manifest link', async () => { vi.spyOn(runStoryTests, 'getAddonVitestConstants').mockResolvedValue(undefined); const manifestEnabledOptions = { @@ -206,6 +271,233 @@ describe('experimental_devServer', () => { expect(mockRes.end).toHaveBeenCalledWith('Not found'); }); + it('should challenge regular requests when composed refs require auth', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers({ + 'WWW-Authenticate': + 'Bearer resource_metadata="https://remote.example.com/.well-known/oauth-protected-resource"', + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + resource: 'https://remote.example.com/mcp', + authorization_servers: ['https://auth.example.com'], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + }), + }), + ); + + const optionsWithPrivateRef = { + port: 6006, + presets: { + apply: vi.fn((key: string) => { + if (key === 'refs') { + return Promise.resolve({ + private: { title: 'Private', url: 'https://remote.example.com' }, + }); + } + if (key === 'features') { + return Promise.resolve({ componentsManifest: false }); + } + return Promise.resolve(undefined); + }), + }, + } as unknown as Options; + + await (experimental_devServer as any)(mockApp, optionsWithPrivateRef); + + const mockReq = { headers: {} } as any; + const mockRes = { writeHead: vi.fn(), end: vi.fn() } as any; + await mcpHandler(mockReq, mockRes); + + expect(mockRes.writeHead).toHaveBeenCalledWith( + 401, + expect.objectContaining({ + 'WWW-Authenticate': expect.stringContaining('resource_metadata='), + }), + ); + expect(mockRes.end).toHaveBeenCalledWith('401 - Unauthorized'); + }); + + it('should allow Storybook MCP proxy requests when composed refs require auth', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers({ + 'WWW-Authenticate': + 'Bearer resource_metadata="https://remote.example.com/.well-known/oauth-protected-resource"', + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + resource: 'https://remote.example.com/mcp', + authorization_servers: ['https://auth.example.com'], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + }), + }), + ); + + const optionsWithPrivateRef = { + port: 6006, + presets: { + apply: vi.fn((key: string) => { + if (key === 'refs') { + return Promise.resolve({ + private: { title: 'Private', url: 'https://remote.example.com' }, + }); + } + if (key === 'features') { + return Promise.resolve({ componentsManifest: false }); + } + return Promise.resolve(undefined); + }), + }, + } as unknown as Options; + + await (experimental_devServer as any)(mockApp, optionsWithPrivateRef); + + const initializeRequest = JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'test-client', + version: '1.0.0', + }, + }, + }); + const mockReq = { + method: 'POST', + headers: { + 'content-type': 'application/json', + host: 'localhost:6006', + 'x-storybook-mcp-proxy': 'true', + }, + socket: { encrypted: false, remoteAddress: '127.0.0.1' }, + url: '/mcp', + [Symbol.asyncIterator]: async function* () { + yield Buffer.from(initializeRequest); + }, + } as any; + const mockRes = { + writeHead: vi.fn(), + write: vi.fn(), + end: vi.fn(), + setHeader: vi.fn(), + statusCode: 0, + } as any; + + await mcpHandler(mockReq, mockRes); + + expect(mockRes.writeHead).not.toHaveBeenCalledWith( + 401, + expect.objectContaining({ 'WWW-Authenticate': expect.any(String) }), + ); + expect(mockRes.end).toHaveBeenCalled(); + }); + + it('should challenge spoofed Storybook MCP proxy requests from non-loopback clients', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers({ + 'WWW-Authenticate': + 'Bearer resource_metadata="https://remote.example.com/.well-known/oauth-protected-resource"', + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + resource: 'https://remote.example.com/mcp', + authorization_servers: ['https://auth.example.com'], + }), + }) + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + issuer: 'https://auth.example.com', + authorization_endpoint: 'https://auth.example.com/authorize', + token_endpoint: 'https://auth.example.com/token', + }), + }), + ); + + const optionsWithPrivateRef = { + port: 6006, + presets: { + apply: vi.fn((key: string) => { + if (key === 'refs') { + return Promise.resolve({ + private: { title: 'Private', url: 'https://remote.example.com' }, + }); + } + if (key === 'features') { + return Promise.resolve({ componentsManifest: false }); + } + return Promise.resolve(undefined); + }), + }, + } as unknown as Options; + + await (experimental_devServer as any)(mockApp, optionsWithPrivateRef); + + const mockReq = { + headers: { + 'x-storybook-mcp-proxy': 'true', + }, + socket: { remoteAddress: '10.0.0.5' }, + } as any; + const mockRes = { writeHead: vi.fn(), end: vi.fn() } as any; + await mcpHandler(mockReq, mockRes); + + expect(mockRes.writeHead).toHaveBeenCalledWith( + 401, + expect.objectContaining({ + 'WWW-Authenticate': expect.stringContaining('resource_metadata='), + }), + ); + expect(mockRes.end).toHaveBeenCalledWith('401 - Unauthorized'); + }); + it('should forward non-HTML GET /mcp requests to MCP handler', async () => { const handlers: Record = {}; mockApp.get = vi.fn((path: string, handler: any) => { @@ -254,28 +546,38 @@ describe('experimental_devServer', () => { }); it('should parse refs from storybook config', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: false, + status: 404, + headers: new Headers(), + }), + ); + + const apply = vi.fn((key: string) => { + if (key === 'refs') { + return Promise.resolve({ + 'my-lib': { title: 'My Library', url: 'https://my-lib.example.com' }, + 'design-system': { url: 'https://ds.example.com' }, + }); + } + if (key === 'features') { + return Promise.resolve({ componentsManifest: false }); + } + return Promise.resolve(undefined); + }); const optionsWithRefs = { port: 6006, presets: { - apply: vi.fn((key: string) => { - if (key === 'refs') { - return Promise.resolve({ - 'my-lib': { title: 'My Library', url: 'https://my-lib.example.com' }, - 'design-system': { url: 'https://ds.example.com' }, - }); - } - if (key === 'features') { - return Promise.resolve({ componentsManifest: false }); - } - return Promise.resolve(undefined); - }), + apply, }, } as unknown as Options; await (experimental_devServer as any)(mockApp, optionsWithRefs); // The preset should have called presets.apply('refs') - expect(optionsWithRefs.presets.apply).toHaveBeenCalledWith('refs', {}); + expect(apply).toHaveBeenCalledWith('refs', {}); }); it('should handle refs config returning non-object gracefully', async () => { diff --git a/packages/addon-mcp/src/preset.ts b/packages/addon-mcp/src/preset.ts index c30e13cb..9469c402 100644 --- a/packages/addon-mcp/src/preset.ts +++ b/packages/addon-mcp/src/preset.ts @@ -7,9 +7,18 @@ import { getAddonVitestConstants } from './tools/run-story-tests.ts'; import { isAddonA11yEnabled } from './utils/is-addon-a11y-enabled.ts'; import htmlTemplate from './template.html'; import path from 'node:path'; -import { CompositionAuth, extractBearerToken, type ComposedRef } from './auth/index.ts'; +import { + CompositionAuth, + STORYBOOK_MCP_PROXY_HEADER, + extractBearerToken, + isStorybookMcpProxyRequest, + type ComposedRef, +} from './auth/index.ts'; import { logger } from 'storybook/internal/node-logger'; import type { Source } from '@storybook/mcp'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +const STORYBOOK_MCP_PROXY_HEADER_KEY = STORYBOOK_MCP_PROXY_HEADER.toLowerCase(); export const previewAnnotations: PresetPropertyFn<'previewAnnotations'> = async ( existingAnnotations = [], @@ -35,8 +44,8 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> // Build sources and manifest provider only if refs are configured let sources: Source[] | undefined; - let manifestProvider: - | ((request: Request | undefined, path: string, source?: Source) => Promise) + let manifestProviderFactory: + | Parameters[0]['manifestProviderFactory'] | undefined; if (refs.length > 0) { @@ -51,7 +60,7 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> logger.info(`Sources: ${sources.map((s) => s.id).join(', ')}`); // Create manifest provider that handles multi-source - manifestProvider = compositionAuth.createManifestProvider(origin); + manifestProviderFactory = (access) => compositionAuth.createManifestProvider(origin, access); } // Serve .well-known/oauth-protected-resource for MCP auth @@ -67,12 +76,9 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> res.end(JSON.stringify(wellKnown)); }); - const requireAuth = ( - req: import('node:http').IncomingMessage, - res: import('node:http').ServerResponse, - ): boolean => { - const token = extractBearerToken(req.headers['authorization']); - if (compositionAuth.requiresAuth && !token) { + const requireAuth = (req: IncomingMessage, res: ServerResponse): boolean => { + const token = extractBearerToken(req.headers.authorization); + if (compositionAuth.requiresAuth && !token && !isLocalStorybookMcpProxyRequest(req)) { res.writeHead(401, { 'Content-Type': 'text/plain', 'WWW-Authenticate': compositionAuth.buildWwwAuthenticate(origin), @@ -92,8 +98,9 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> options, addonOptions, sources, - manifestProvider, + manifestProviderFactory, compositionAuth, + trustedProxyRequest: isLocalStorybookMcpProxyRequest(req), }); }); @@ -101,8 +108,10 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> const addonVitestConstants = await getAddonVitestConstants(); const a11yEnabled = await isAddonA11yEnabled(options); + const hasRemoteSources = sources?.some((source) => source.url) ?? false; const isDevEnabled = addonOptions.toolsets?.dev ?? true; - const isDocsEnabled = manifestStatus.available && (addonOptions.toolsets?.docs ?? true); + const isDocsEnabled = + (manifestStatus.available || hasRemoteSources) && (addonOptions.toolsets?.docs ?? true); const isTestEnabled = !!addonVitestConstants && (addonOptions.toolsets?.test ?? true); app!.get('/mcp', (req, res) => { @@ -115,8 +124,9 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> options, addonOptions, sources, - manifestProvider, + manifestProviderFactory, compositionAuth, + trustedProxyRequest: isLocalStorybookMcpProxyRequest(req), }); } @@ -124,14 +134,14 @@ export const experimental_devServer: PresetPropertyFn<'experimental_devServer'> res.writeHead(200, { 'Content-Type': 'text/html' }); let docsNotice = ''; - if (!manifestStatus.hasManifests) { + if (!hasRemoteSources && !manifestStatus.hasManifests) { docsNotice = `
- This toolset is only supported in React-based setups. -
`; - } else if (!manifestStatus.hasFeatureFlag) { + This toolset is only supported in React-based setups. + `; + } else if (!hasRemoteSources && !manifestStatus.hasFeatureFlag) { docsNotice = `
- This toolset requires enabling the component manifest feature. - Learn how to enable it + This toolset requires enabling the component manifest feature. + Learn how to enable it
`; } @@ -174,6 +184,17 @@ export const features: PresetPropertyFn<'features'> = async (existingFeatures) = }; }; +function isLocalStorybookMcpProxyRequest(req: IncomingMessage): boolean { + return ( + isStorybookMcpProxyRequest(req.headers[STORYBOOK_MCP_PROXY_HEADER_KEY]) && + isLoopbackAddress(req.socket.remoteAddress) + ); +} + +function isLoopbackAddress(address: string | undefined): boolean { + return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'; +} + /** * Get composed Storybook refs from Storybook config. * See: https://storybook.js.org/docs/sharing/storybook-composition diff --git a/packages/mcp-proxy/src/instructions.md b/packages/mcp-proxy/src/instructions.md index e48e62e6..306cb73f 100644 --- a/packages/mcp-proxy/src/instructions.md +++ b/packages/mcp-proxy/src/instructions.md @@ -33,3 +33,4 @@ Use `withStoryIds: true` on **list-all-documentation** when you also need story - When the downstream Storybook composes multiple sources, **list-all-documentation** returns entries from all of them. - Use `storybookId` in **get-documentation** when you need to scope a request to one source. +- If a composed Storybook source says it requires authentication, use the MCP endpoint shown for that source instead of trying to access that source through this proxy. diff --git a/packages/mcp-proxy/src/utils/proxy-client.test.ts b/packages/mcp-proxy/src/utils/proxy-client.test.ts index 43f67eba..774dbabe 100644 --- a/packages/mcp-proxy/src/utils/proxy-client.test.ts +++ b/packages/mcp-proxy/src/utils/proxy-client.test.ts @@ -47,6 +47,7 @@ describe('proxyToolCall', () => { const init = call[1] as RequestInit; const headers = init.headers as Record; expect(headers.Accept).toBe('application/json, text/event-stream'); + expect(headers['X-Storybook-MCP-Proxy']).toBe('true'); const body = JSON.parse(init.body as string); expect(body).toMatchObject({ jsonrpc: '2.0', diff --git a/packages/mcp-proxy/src/utils/proxy-client.ts b/packages/mcp-proxy/src/utils/proxy-client.ts index 213598c9..e71f38f7 100644 --- a/packages/mcp-proxy/src/utils/proxy-client.ts +++ b/packages/mcp-proxy/src/utils/proxy-client.ts @@ -4,14 +4,17 @@ import type { StorybookInstanceRecordV1, } from '../types/index.ts'; +const STORYBOOK_MCP_PROXY_HEADER = 'X-Storybook-MCP-Proxy'; +const STORYBOOK_MCP_PROXY_HEADER_VALUE = 'true'; + /** * Forward an MCP `tools/call` JSON-RPC request to a local Storybook MCP server. * * The downstream is `@storybook/addon-mcp` over HTTP at `record.mcp.endpoint`. * tmcp's HttpTransport hardcodes `text/event-stream` for any request with an * id, so we accept both content-types and parse the SSE envelope when needed. - * The proxy is stdio-fronted and stateless; every call is independent and we - * don't need session bookkeeping. + * The proxy is stateless; every call is independent and we don't need session + * bookkeeping. */ export async function proxyToolCall( record: StorybookInstanceRecordV1, @@ -28,6 +31,7 @@ export async function proxyToolCall( headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', + [STORYBOOK_MCP_PROXY_HEADER]: STORYBOOK_MCP_PROXY_HEADER_VALUE, }, body: JSON.stringify({ jsonrpc: '2.0', diff --git a/packages/mcp/README.md b/packages/mcp/README.md index f32c5921..9b60aad8 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -128,7 +128,7 @@ type StorybookContext = { request: Request | undefined, path: string, source?: Source, - ) => Promise; + ) => Promise; sources?: Source[]; onListAllDocumentation?: (params: { context: StorybookContext; @@ -166,12 +166,12 @@ type StorybookMcpHandlerOptions = StorybookContext & { Type: ```ts -(request: Request | undefined, path: string, source?: Source) => Promise; +(request: Request | undefined, path: string, source?: Source) => Promise; ``` Primary extension point for production setups. -Use this when manifests are not available at the default same-origin paths. Your function returns the raw JSON string for each requested manifest path. +Use this when manifests are not available at the default same-origin paths. Your function returns the raw JSON string for each requested manifest path. It may also return a `source-failure` result when a source should be shown as guidance instead of fetched as a manifest. For a real customization example (switching between HTTP and filesystem-backed manifest loading), see the [Example implementation](#example-implementation) section above. @@ -255,12 +255,33 @@ Represents one Storybook source in multi-source mode. Type: ```ts -type SourceManifests = { - source: Source; - componentManifest: ComponentManifestMap; - docsManifest?: DocsManifestMap; - error?: string; -}; +type SourceManifests = + | { + kind: 'manifest'; + source: Source; + componentManifest: ComponentManifestMap; + docsManifest?: DocsManifestMap; + } + | { + kind: 'error'; + source: Source; + error: SourceManifestFailure; + }; + +type SourceManifestFailure = + | { kind: 'fetch-failed'; message: string } + | { + kind: 'requires-own-mcp'; + endpoint: string; + authProvider: 'chromatic' | 'unknown'; + }; + +type ManifestProviderResult = + | string + | { + kind: 'source-failure'; + failure: SourceManifestFailure; + }; ``` Represents fetched manifests (or an error) for a single source. diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index ffe9f378..4ba50689 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -22,11 +22,20 @@ export { export { COMPONENT_MANIFEST_PATH, DOCS_MANIFEST_PATH, + getManifestResult, getMultiSourceManifests, + sourceManifestFailureToMCPContent, } from './utils/get-manifest.ts'; // Export types for reuse -export type { StorybookContext, Source, SourceManifests } from './types.ts'; +export type { + ManifestProvider, + ManifestProviderResult, + StorybookContext, + Source, + SourceManifestFailure, + SourceManifests, +} from './types.ts'; // copied from tmcp internals as it's not exposed type InitializeRequestParams = { diff --git a/packages/mcp/src/tools/get-documentation-for-story.test.ts b/packages/mcp/src/tools/get-documentation-for-story.test.ts index f08399cb..dae9a7d9 100644 --- a/packages/mcp/src/tools/get-documentation-for-story.test.ts +++ b/packages/mcp/src/tools/get-documentation-for-story.test.ts @@ -46,7 +46,7 @@ describe('getComponentStoryDocumentationTool', () => { await addGetStoryDocumentationTool(server); // Mock getManifest to return the fixture - getManifestSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestSpy = vi.spyOn(getManifest, 'getManifestResult'); getManifestSpy.mockResolvedValue({ componentManifest: smallManifestFixture, }); @@ -189,7 +189,50 @@ describe('getComponentStoryDocumentationTool', () => { ], "isError": true, } - `); + `); + }); + + it('should return requires-own-mcp source errors as guidance content', async () => { + getManifestSpy.mockResolvedValue({ + kind: 'source-failure', + failure: { + kind: 'requires-own-mcp', + endpoint: 'https://example.com/mcp', + authProvider: 'chromatic', + }, + }); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_STORY_TOOL_NAME, + arguments: { + componentId: 'button', + storyName: 'Primary', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toEqual({ + content: [ + { + type: 'text', + text: `# Private Storybook + +This composed Storybook is private and requires Chromatic authentication. + +To access documentation from this source, register or use its MCP endpoint: +https://example.com/mcp`, + }, + ], + }); }); it('should include import statement when available', async () => { @@ -285,7 +328,7 @@ describe('getComponentStoryDocumentationTool', () => { ); await addGetStoryDocumentationTool(server, undefined, { multiSource: true }); - getManifestSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestSpy = vi.spyOn(getManifest, 'getManifestResult'); getManifestSpy.mockResolvedValue({ componentManifest: smallManifestFixture, }); diff --git a/packages/mcp/src/tools/get-documentation-for-story.ts b/packages/mcp/src/tools/get-documentation-for-story.ts index 335907c4..707d0e9f 100644 --- a/packages/mcp/src/tools/get-documentation-for-story.ts +++ b/packages/mcp/src/tools/get-documentation-for-story.ts @@ -2,7 +2,11 @@ import * as v from 'valibot'; import type { McpServer } from 'tmcp'; import type { StorybookContext } from '../types.ts'; import { StorybookIdField } from '../types.ts'; -import { errorToMCPContent, getManifests } from '../utils/get-manifest.ts'; +import { + errorToMCPContent, + getManifestResult, + sourceManifestFailureToMCPContent, +} from '../utils/get-manifest.ts'; import { formatStoryDocumentation } from '../utils/manifest-formatter/markdown.ts'; import { LIST_TOOL_NAME } from './list-all-documentation.ts'; @@ -69,7 +73,10 @@ export async function addGetStoryDocumentationTool( } } - const manifest = await getManifests(ctx?.request, ctx?.manifestProvider, source); + const manifest = await getManifestResult(ctx?.request, ctx?.manifestProvider, source); + if (manifest.kind === 'source-failure') { + return sourceManifestFailureToMCPContent(manifest.failure, manifest.source); + } const component = manifest.componentManifest?.components[componentId]; diff --git a/packages/mcp/src/tools/get-documentation.test.ts b/packages/mcp/src/tools/get-documentation.test.ts index be5e12e6..98294020 100644 --- a/packages/mcp/src/tools/get-documentation.test.ts +++ b/packages/mcp/src/tools/get-documentation.test.ts @@ -44,7 +44,7 @@ describe('getDocumentationTool', () => { await addGetDocumentationTool(server); // Mock getManifests to return the fixture - getManifestsSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestsSpy = vi.spyOn(getManifest, 'getManifestResult'); getManifestsSpy.mockResolvedValue({ componentManifest: smallManifestFixture, }); @@ -160,7 +160,49 @@ describe('getDocumentationTool', () => { ], "isError": true, } - `); + `); + }); + + it('should return requires-own-mcp source errors as guidance content', async () => { + getManifestsSpy.mockResolvedValue({ + kind: 'source-failure', + failure: { + kind: 'requires-own-mcp', + endpoint: 'https://example.com/mcp', + authProvider: 'chromatic', + }, + }); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: GET_TOOL_NAME, + arguments: { + id: 'button', + }, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest }, + }); + + expect(response.result).toEqual({ + content: [ + { + type: 'text', + text: `# Private Storybook + +This composed Storybook is private and requires Chromatic authentication. + +To access documentation from this source, register or use its MCP endpoint: +https://example.com/mcp`, + }, + ], + }); }); it('should call onGetDocumentation handler when provided', async () => { @@ -523,7 +565,7 @@ describe('getDocumentationTool', () => { ); await addGetDocumentationTool(server, undefined, { multiSource: true }); - getManifestsSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestsSpy = vi.spyOn(getManifest, 'getManifestResult'); getManifestsSpy.mockResolvedValue({ componentManifest: smallManifestFixture, }); diff --git a/packages/mcp/src/tools/get-documentation.ts b/packages/mcp/src/tools/get-documentation.ts index 2a224dac..538b6ed3 100644 --- a/packages/mcp/src/tools/get-documentation.ts +++ b/packages/mcp/src/tools/get-documentation.ts @@ -2,7 +2,11 @@ import * as v from 'valibot'; import type { McpServer } from 'tmcp'; import type { ComponentManifest, Doc, StorybookContext } from '../types.ts'; import { StorybookIdField } from '../types.ts'; -import { getManifests, errorToMCPContent } from '../utils/get-manifest.ts'; +import { + errorToMCPContent, + getManifestResult, + sourceManifestFailureToMCPContent, +} from '../utils/get-manifest.ts'; import { LIST_TOOL_NAME } from './list-all-documentation.ts'; import { formatComponentManifest, @@ -76,11 +80,11 @@ Example: id="button" returns Primary, Secondary, Large stories with code like { await addListAllDocumentationTool(server); // Mock getManifests to return the fixture - getManifestsSpy = vi.spyOn(getManifest, 'getManifests'); + getManifestsSpy = vi.spyOn(getManifest, 'getManifestResult'); getManifestsSpy.mockResolvedValue({ componentManifest: smallManifestFixture, }); @@ -127,10 +127,12 @@ describe('listAllDocumentationTool', () => { const getMultiSourceManifestsSpy = vi.spyOn(getManifest, 'getMultiSourceManifests'); getMultiSourceManifestsSpy.mockResolvedValue([ { + kind: 'manifest', source: sources[0]!, componentManifest: smallManifestFixture, }, { + kind: 'manifest', source: sources[1]!, componentManifest: remoteManifest, }, @@ -166,10 +168,12 @@ describe('listAllDocumentationTool', () => { const getMultiSourceManifestsSpy = vi.spyOn(getManifest, 'getMultiSourceManifests'); getMultiSourceManifestsSpy.mockResolvedValue([ { + kind: 'manifest', source: sources[0]!, componentManifest: smallManifestFixture, }, { + kind: 'manifest', source: sources[1]!, componentManifest: remoteManifest, }, @@ -212,13 +216,17 @@ describe('listAllDocumentationTool', () => { const getMultiSourceManifestsSpy = vi.spyOn(getManifest, 'getMultiSourceManifests'); getMultiSourceManifestsSpy.mockResolvedValue([ { + kind: 'manifest', source: sources[0]!, componentManifest: smallManifestFixture, }, { + kind: 'error', source: sources[1]!, - componentManifest: { v: 1, components: {} }, - error: 'Failed to fetch manifest: 401 Unauthorized', + error: { + kind: 'fetch-failed', + message: 'Failed to fetch manifest: 401 Unauthorized', + }, }, ]); @@ -245,6 +253,49 @@ describe('listAllDocumentationTool', () => { getMultiSourceManifestsSpy.mockRestore(); }); + + it('should show auth guidance for sources that should be accessed through another MCP endpoint', async () => { + const getMultiSourceManifestsSpy = vi.spyOn(getManifest, 'getMultiSourceManifests'); + getMultiSourceManifestsSpy.mockResolvedValue([ + { + kind: 'manifest', + source: sources[0]!, + componentManifest: smallManifestFixture, + }, + { + kind: 'error', + source: sources[1]!, + error: { + kind: 'requires-own-mcp', + endpoint: 'http://remote.example.com/mcp', + authProvider: 'chromatic', + }, + }, + ]); + + const request = { + jsonrpc: '2.0' as const, + id: 1, + method: 'tools/call', + params: { + name: LIST_TOOL_NAME, + arguments: {}, + }, + }; + + const mockHttpRequest = new Request('https://example.com/mcp'); + const response = await server.receive(request, { + custom: { request: mockHttpRequest, sources }, + }); + + const text = (response.result as any).content[0].text; + expect(text).toContain('# Remote'); + expect(text).toContain('id: remote'); + expect(text).toContain('Use its MCP endpoint: http://remote.example.com/mcp'); + expect(text).not.toContain('error:'); + + getMultiSourceManifestsSpy.mockRestore(); + }); }); it('should handle fetch errors gracefully', async () => { diff --git a/packages/mcp/src/tools/list-all-documentation.ts b/packages/mcp/src/tools/list-all-documentation.ts index baefcabc..8aae35e7 100644 --- a/packages/mcp/src/tools/list-all-documentation.ts +++ b/packages/mcp/src/tools/list-all-documentation.ts @@ -1,7 +1,12 @@ import type { McpServer } from 'tmcp'; import * as v from 'valibot'; import type { StorybookContext } from '../types.ts'; -import { getManifests, getMultiSourceManifests, errorToMCPContent } from '../utils/get-manifest.ts'; +import { + errorToMCPContent, + getManifestResult, + getMultiSourceManifests, + sourceManifestFailureToMCPContent, +} from '../utils/get-manifest.ts'; import { formatMultiSourceManifestsToLists, formatManifestsToLists, @@ -50,7 +55,7 @@ export async function addListAllDocumentationTool( withStoryIds, }); - const firstSuccess = multiSourceManifests.find((m) => !m.error); + const firstSuccess = multiSourceManifests.find((m) => m.kind === 'manifest'); if (firstSuccess) { await ctx.onListAllDocumentation?.({ context: ctx, @@ -74,15 +79,22 @@ export async function addListAllDocumentationTool( } // Single-source mode: existing behavior - const manifests = await getManifests(ctx?.request, ctx?.manifestProvider); + const manifests = await getManifestResult(ctx?.request, ctx?.manifestProvider); + if (manifests.kind === 'source-failure') { + return sourceManifestFailureToMCPContent(manifests.failure, manifests.source); + } + const allManifests = { + componentManifest: manifests.componentManifest, + docsManifest: manifests.docsManifest, + }; - const lists = formatManifestsToLists(manifests, { + const lists = formatManifestsToLists(allManifests, { withStoryIds, }); await ctx?.onListAllDocumentation?.({ context: ctx, - manifests, + manifests: allManifests, resultText: lists, }); diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index 9adbbf05..6eaee326 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -13,16 +13,45 @@ export type Source = { url?: string; }; +export type SourceManifestFailure = + | { + kind: 'fetch-failed'; + message: string; + } + | { + kind: 'requires-own-mcp'; + endpoint: string; + authProvider: 'chromatic' | 'unknown'; + }; + +export type ManifestProviderResult = + | string + | { + kind: 'source-failure'; + failure: SourceManifestFailure; + }; + +export type ManifestProvider = ( + request: Request | undefined, + path: string, + source?: Source, +) => Promise; + /** - * All manifests for a single source. + * Result for a single source in multi-source manifest loading. */ -export type SourceManifests = { - source: Source; - componentManifest: ComponentManifestMap; - docsManifest?: DocsManifestMap; - /** Error message if fetching this source failed */ - error?: string; -}; +export type SourceManifests = + | { + kind: 'manifest'; + source: Source; + componentManifest: ComponentManifestMap; + docsManifest?: DocsManifestMap; + } + | { + kind: 'error'; + source: Source; + error: SourceManifestFailure; + }; /** * Custom context passed to MCP server and tools. @@ -42,11 +71,7 @@ export type StorybookContext = { * replacing /mcp with /manifests/components.json. * Custom providers can use the request parameter to determine the manifest source, or ignore it entirely. */ - manifestProvider?: ( - request: Request | undefined, - path: string, - source?: Source, - ) => Promise; + manifestProvider?: ManifestProvider; /** * Sources configuration for multi-source mode. * When provided, tools will fetch and display manifests grouped by source. diff --git a/packages/mcp/src/utils/get-manifest.test.ts b/packages/mcp/src/utils/get-manifest.test.ts index bc865608..5057bd71 100644 --- a/packages/mcp/src/utils/get-manifest.test.ts +++ b/packages/mcp/src/utils/get-manifest.test.ts @@ -1,6 +1,16 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { getManifests, getMultiSourceManifests, ManifestGetError } from './get-manifest.ts'; -import type { ComponentManifestMap, DocsManifestMap, Source } from '../types.ts'; +import { + getManifests, + getManifestResult, + getMultiSourceManifests, + ManifestGetError, +} from './get-manifest.ts'; +import type { + ComponentManifestMap, + DocsManifestMap, + ManifestProviderResult, + Source, +} from '../types.ts'; global.fetch = vi.fn(); @@ -68,8 +78,8 @@ function createFetchMock(responses: { components?: unknown; docs?: unknown }) { * Helper to create a manifestProvider mock that returns different responses based on path */ function createManifestProviderMock(responses: { - components?: string | Error; - docs?: string | Error; + components?: ManifestProviderResult | Error; + docs?: ManifestProviderResult | Error; }) { return vi.fn().mockImplementation((_request: Request | undefined, path: string) => { if (path.includes('components.json')) { @@ -410,6 +420,57 @@ Invalid key: Expected "v" but received undefined]`); ); }); + it('should return source failures from manifestProvider as data', async () => { + const request = createMockRequest('https://example.com/mcp'); + const sourceFailure = { + kind: 'source-failure', + failure: { + kind: 'requires-own-mcp', + endpoint: 'https://example.com/mcp', + authProvider: 'chromatic', + }, + } satisfies ManifestProviderResult; + const manifestProvider = createManifestProviderMock({ + components: sourceFailure, + }); + + await expect(getManifestResult(request, manifestProvider)).resolves.toEqual({ + kind: 'source-failure', + failure: sourceFailure.failure, + }); + }); + + it('should preserve fetched component documentation when only the optional docs manifest returns a source error', async () => { + const validManifest: ComponentManifestMap = { + v: 1, + components: { + button: { + id: 'button', + path: 'src/components/Button.tsx', + name: 'Button', + description: 'A button component', + }, + }, + }; + const request = createMockRequest('https://example.com/mcp'); + const sourceFailure = { + kind: 'source-failure', + failure: { + kind: 'requires-own-mcp', + endpoint: 'https://example.com/mcp', + authProvider: 'chromatic', + }, + } satisfies ManifestProviderResult; + const manifestProvider = createManifestProviderMock({ + components: JSON.stringify(validManifest), + docs: sourceFailure, + }); + + await expect(getManifests(request, manifestProvider)).resolves.toEqual({ + componentManifest: validManifest, + }); + }); + it('should handle invalid JSON from manifestProvider', async () => { const request = createMockRequest('https://example.com/mcp'); const manifestProvider = createManifestProviderMock({ @@ -468,12 +529,16 @@ Invalid key: Expected "v" but received undefined]`); ); expect(results).toHaveLength(2); - expect(results[0]!.source.id).toBe('local'); - expect(results[0]!.componentManifest).toEqual(localManifest); - expect(results[0]!.error).toBeUndefined(); - expect(results[1]!.source.id).toBe('remote'); - expect(results[1]!.componentManifest).toEqual(remoteManifest); - expect(results[1]!.error).toBeUndefined(); + expect(results[0]).toMatchObject({ + kind: 'manifest', + source: { id: 'local' }, + componentManifest: localManifest, + }); + expect(results[1]).toMatchObject({ + kind: 'manifest', + source: { id: 'remote' }, + componentManifest: remoteManifest, + }); }); it('should capture errors for individual sources without failing', async () => { @@ -496,10 +561,54 @@ Invalid key: Expected "v" but received undefined]`); ); expect(results).toHaveLength(2); - expect(results[0]!.error).toBeUndefined(); - expect(results[0]!.componentManifest).toEqual(localManifest); - expect(results[1]!.error).toContain('401 Unauthorized'); - expect(results[1]!.componentManifest).toEqual({ v: 1, components: {} }); + expect(results[0]).toMatchObject({ + kind: 'manifest', + componentManifest: localManifest, + }); + expect(results[1]).toMatchObject({ + kind: 'error', + error: { + kind: 'fetch-failed', + message: 'Failed to get component manifest: 401 Unauthorized', + }, + }); + }); + + it('should capture requires-own-mcp source errors without failing the whole request', async () => { + const sourceFailure = { + kind: 'source-failure', + failure: { + kind: 'requires-own-mcp', + endpoint: 'http://remote.example.com/mcp', + authProvider: 'chromatic', + }, + } satisfies ManifestProviderResult; + const manifestProvider = vi + .fn() + .mockImplementation((_req: Request | undefined, path: string, source?: Source) => { + if (source?.id === 'remote') { + return Promise.resolve(sourceFailure); + } + if (path.includes('docs.json')) { + return Promise.reject(new Error('Not found')); + } + return Promise.resolve(JSON.stringify(localManifest)); + }); + + const results = await getMultiSourceManifests( + [localSource, remoteSource], + undefined, + manifestProvider, + ); + + expect(results[0]).toMatchObject({ kind: 'manifest' }); + expect(results[1]).toMatchObject({ + kind: 'error', + error: { + kind: 'requires-own-mcp', + endpoint: 'http://remote.example.com/mcp', + }, + }); }); it('should throw when all sources fail', async () => { diff --git a/packages/mcp/src/utils/get-manifest.ts b/packages/mcp/src/utils/get-manifest.ts index 6d764db8..048dfd55 100644 --- a/packages/mcp/src/utils/get-manifest.ts +++ b/packages/mcp/src/utils/get-manifest.ts @@ -2,9 +2,16 @@ import { ComponentManifestMap, DocsManifestMap, type AllManifests, + type ManifestProvider, + type ManifestProviderResult, type Source, + type SourceManifestFailure, type SourceManifests, } from '../types.ts'; +import { + formatSourceManifestFailureDetail, + formatSourceManifestFailureSummary, +} from './manifest-formatter/source-errors.ts'; import * as v from 'valibot'; /** @@ -29,20 +36,49 @@ export class ManifestGetError extends Error { } /** - * MCP tool result type for error responses + * MCP tool result type for text responses */ -type MCPErrorResult = { +type MCPTextResult = { content: Array<{ type: 'text'; text: string }>; - isError: true; + isError?: true; +}; + +export const sourceManifestFailureToMCPContent = ( + failure: SourceManifestFailure, + source?: Source, +): MCPTextResult => { + switch (failure.kind) { + case 'requires-own-mcp': + return { + content: [ + { + type: 'text', + text: formatSourceManifestFailureDetail(failure, source), + }, + ], + }; + case 'fetch-failed': + return { + content: [ + { + type: 'text', + text: formatSourceManifestFailureDetail(failure, source), + }, + ], + isError: true, + }; + default: + return assertNever(failure); + } }; /** - * Converts an error to MCP-compatible content format + * Converts an error to MCP-compatible content format. * * @param error - The error to convert (can be any type) - * @returns A tool result with error content and isError flag + * @returns A tool result with error content */ -export const errorToMCPContent = (error: unknown): MCPErrorResult => { +export const errorToMCPContent = (error: unknown): MCPTextResult => { const errorPrefix = error instanceof ManifestGetError ? 'Error getting manifest' : 'Unexpected error'; const errorMessage = error instanceof Error ? error.message : String(error); @@ -90,6 +126,14 @@ ${error instanceof v.ValiError ? error.issues.map((i) => i.message).join('\n') : } } +export type ManifestLoadResult = + | ({ kind: 'manifest' } & AllManifests) + | { + kind: 'source-failure'; + source?: Source; + failure: SourceManifestFailure; + }; + /** * Gets component and docs manifest from a request or using a custom provider * @@ -101,13 +145,24 @@ ${error instanceof v.ValiError ? error.issues.map((i) => i.message).join('\n') : */ export async function getManifests( request?: Request, - manifestProvider?: ( - request: Request | undefined, - path: string, - source?: Source, - ) => Promise, + manifestProvider?: ManifestProvider, source?: Source, ): Promise { + const result = await getManifestResult(request, manifestProvider, source); + if (result.kind === 'source-failure') { + throw new ManifestGetError(formatSourceManifestFailureSummary(result.failure)); + } + return { + componentManifest: result.componentManifest, + docsManifest: result.docsManifest, + }; +} + +export async function getManifestResult( + request?: Request, + manifestProvider?: ManifestProvider, + source?: Source, +): Promise { const provider = manifestProvider ?? defaultManifestProvider; // Fetch both component and docs manifests in parallel @@ -131,6 +186,9 @@ export async function getManifests( reason instanceof Error ? reason : undefined, ); } + if (isSourceManifestFailureResult(componentResult.value)) { + return { kind: 'source-failure', source, failure: componentResult.value.failure }; + } const componentManifest = parseManifest({ jsonString: componentResult.value, @@ -147,7 +205,10 @@ export async function getManifests( } if (docsResult.status === 'rejected') { - return { componentManifest }; + return { kind: 'manifest', componentManifest }; + } + if (isSourceManifestFailureResult(docsResult.value)) { + return { kind: 'manifest', componentManifest }; } // Handle docs manifest result (optional - only exists when addon-docs is used) @@ -158,7 +219,7 @@ export async function getManifests( url: getUrl(DOCS_MANIFEST_PATH), }); - return { componentManifest, docsManifest }; + return { kind: 'manifest', componentManifest, docsManifest }; } /** @@ -180,7 +241,7 @@ function getManifestUrlFromRequest(request: Request, path: string): string { async function defaultManifestProvider( request: Request | undefined, path: string, -): Promise { +): Promise { if (!request) { throw new ManifestGetError( "Request is required when using the default manifest provider. You must either pass the original request forward to the server context, or set a custom manifestProvider that doesn't need the request.", @@ -220,41 +281,63 @@ async function defaultManifestProvider( export async function getMultiSourceManifests( sources: Source[], request?: Request, - manifestProvider?: ( - request: Request | undefined, - path: string, - source?: Source, - ) => Promise, + manifestProvider?: ManifestProvider, ): Promise { // Fetch all sources in parallel const results = await Promise.all( sources.map(async (source) => { try { - const manifests = await getManifests(request, manifestProvider, source); + const manifests = await getManifestResult(request, manifestProvider, source); + if (manifests.kind === 'source-failure') { + return { + kind: 'error', + source, + error: manifests.failure, + } satisfies SourceManifests; + } return { + kind: 'manifest', source, componentManifest: manifests.componentManifest, docsManifest: manifests.docsManifest, - }; + } satisfies SourceManifests; } catch (error) { // Capture error but don't fail the entire request - const errorMessage = error instanceof Error ? error.message : String(error); return { + kind: 'error', source, - componentManifest: { v: 1, components: {} }, - error: errorMessage, - }; + error: sourceManifestFailureFromError(error), + } satisfies SourceManifests; } }), ); - // Check if at least one source succeeded - const successCount = results.filter((r) => !r.error).length; - if (successCount === 0) { + // Check if at least one source can produce useful tool output. + const hasDisplayableResult = results.some( + (result) => result.kind === 'manifest' || result.error.kind === 'requires-own-mcp', + ); + if (!hasDisplayableResult) { throw new ManifestGetError( - `Failed to fetch manifests from any source. Errors:\n${results.map((r) => `- ${r.source.title}: ${r.error}`).join('\n')}`, + `Failed to fetch manifests from any source. Errors:\n${results.map((r) => `- ${r.source.title}: ${r.kind === 'error' ? formatSourceManifestFailureSummary(r.error) : 'unknown error'}`).join('\n')}`, ); } return results; } + +function isSourceManifestFailureResult( + result: ManifestProviderResult, +): result is Extract { + return typeof result !== 'string' && result.kind === 'source-failure'; +} + +function sourceManifestFailureFromError(error: unknown): SourceManifestFailure { + return { + kind: 'fetch-failed', + message: error instanceof Error ? error.message : String(error), + }; +} + +function assertNever(value: never): never { + throw new Error(`Unhandled source manifest failure: ${JSON.stringify(value)}`); +} diff --git a/packages/mcp/src/utils/manifest-formatter/markdown.ts b/packages/mcp/src/utils/manifest-formatter/markdown.ts index 25f2799e..93bdaaa0 100644 --- a/packages/mcp/src/utils/manifest-formatter/markdown.ts +++ b/packages/mcp/src/utils/manifest-formatter/markdown.ts @@ -14,6 +14,7 @@ import { } from '../parse-react-docgen.ts'; import { dedent } from '../dedent.ts'; import { extractDocsSummary, MAX_SUMMARY_LENGTH } from './extract-docs-summary.ts'; +import { formatSourceManifestFailureSummary } from './source-errors.ts'; /** * Maximum number of stories to show in full detail in component manifests. @@ -357,45 +358,59 @@ export function formatMultiSourceManifestsToLists( ): string { const parts: string[] = []; - for (const { source, componentManifest, docsManifest, error } of manifests) { + for (const sourceResult of manifests) { + const { source } = sourceResult; parts.push(`# ${source.title}`); parts.push(`id: ${source.id}`); parts.push(''); - if (error) { - parts.push(`error: ${error}`); - parts.push(''); - continue; - } - - const components = Object.values(componentManifest.components); - if (components.length > 0) { - parts.push('## Components'); - parts.push(''); - for (const component of components) { - parts.push(formatComponentLine(component)); - if (options.withStoryIds) { - for (const story of component.stories ?? []) { - parts.push(formatStorySubLine(story)); + switch (sourceResult.kind) { + case 'manifest': { + const components = Object.values(sourceResult.componentManifest.components); + if (components.length > 0) { + parts.push('## Components'); + parts.push(''); + for (const component of components) { + parts.push(formatComponentLine(component)); + if (options.withStoryIds) { + for (const story of component.stories ?? []) { + parts.push(formatStorySubLine(story)); + } + } } + parts.push(''); } - } - parts.push(''); - } - if (docsManifest && Object.keys(docsManifest.docs).length > 0) { - parts.push('## Docs'); - parts.push(''); - for (const doc of Object.values(docsManifest.docs)) { - parts.push(formatDocLine(doc)); + if (sourceResult.docsManifest && Object.keys(sourceResult.docsManifest.docs).length > 0) { + parts.push('## Docs'); + parts.push(''); + for (const doc of Object.values(sourceResult.docsManifest.docs)) { + parts.push(formatDocLine(doc)); + } + parts.push(''); + } + break; } - parts.push(''); + case 'error': + parts.push(formatSourceError(sourceResult.error)); + parts.push(''); + break; + default: + assertNeverSourceResult(sourceResult); } } return parts.join('\n').trim(); } +function formatSourceError(error: Extract['error']): string { + return formatSourceManifestFailureSummary(error); +} + +function assertNeverSourceResult(value: never): never { + throw new Error(`Unhandled source manifest result: ${JSON.stringify(value)}`); +} + /** * Format a single story's documentation. */ diff --git a/packages/mcp/src/utils/manifest-formatter/source-errors.ts b/packages/mcp/src/utils/manifest-formatter/source-errors.ts new file mode 100644 index 00000000..1b2cd607 --- /dev/null +++ b/packages/mcp/src/utils/manifest-formatter/source-errors.ts @@ -0,0 +1,52 @@ +import type { Source, SourceManifestFailure } from '../../types.ts'; + +type RequiresOwnMcpFailure = Extract; + +export function formatSourceManifestFailureDetail( + failure: SourceManifestFailure, + source?: Source, +): string { + switch (failure.kind) { + case 'requires-own-mcp': + return `# ${source?.title ?? 'Private Storybook'} + +${formatRequiresOwnMcpMessage(failure)} + +To access documentation from this source, register or use its MCP endpoint: +${failure.endpoint}`; + case 'fetch-failed': + return `Error getting manifest: ${failure.message}`; + default: + return assertNeverSourceFailure(failure); + } +} + +export function formatSourceManifestFailureSummary(failure: SourceManifestFailure): string { + switch (failure.kind) { + case 'requires-own-mcp': + return `${formatRequiresOwnMcpMessage(failure)} Use its MCP endpoint: ${failure.endpoint}`; + case 'fetch-failed': + return `error: ${failure.message}`; + default: + return assertNeverSourceFailure(failure); + } +} + +function formatRequiresOwnMcpMessage(failure: RequiresOwnMcpFailure): string { + switch (failure.authProvider) { + case 'chromatic': + return 'This composed Storybook is private and requires Chromatic authentication.'; + case 'unknown': + return 'This composed Storybook requires authentication.'; + default: + return assertNeverAuthProvider(failure.authProvider); + } +} + +function assertNeverSourceFailure(value: never): never { + throw new Error(`Unhandled source manifest failure: ${JSON.stringify(value)}`); +} + +function assertNeverAuthProvider(value: never): never { + throw new Error(`Unhandled source auth provider: ${JSON.stringify(value)}`); +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 334d2977..13a8aed1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,18 +6,18 @@ packages: - '!eval/tasks/*/trials/*/project' catalog: - '@storybook/addon-a11y': 10.5.0-alpha.1 - '@storybook/addon-docs': 10.5.0-alpha.1 - '@storybook/addon-themes': 10.5.0-alpha.1 - '@storybook/addon-vitest': 10.5.0-alpha.1 - '@storybook/react-vite': 10.5.0-alpha.1 + '@storybook/addon-a11y': 10.5.0-alpha.2 + '@storybook/addon-docs': 10.5.0-alpha.2 + '@storybook/addon-themes': 10.5.0-alpha.2 + '@storybook/addon-vitest': 10.5.0-alpha.2 + '@storybook/react-vite': 10.5.0-alpha.2 '@tmcp/adapter-valibot': ^0.1.5 '@tmcp/transport-http': ^0.8.5 '@tmcp/transport-stdio': ^0.4.3 '@vitest/browser-playwright': 4.0.6 - eslint-plugin-storybook: 10.5.0-alpha.1 + eslint-plugin-storybook: 10.5.0-alpha.2 playwright: 1.56.1 - storybook: 10.5.0-alpha.1 + storybook: 10.5.0-alpha.2 tmcp: ^1.19.4 tsdown: ^0.15.12 typescript: ^5.9.3 @@ -28,10 +28,10 @@ catalog: catalogs: trials: '@eslint/js': 9.39.1 - '@storybook/addon-a11y': 10.5.0-alpha.1 - '@storybook/addon-docs': 10.5.0-alpha.1 - '@storybook/addon-vitest': 10.5.0-alpha.1 - '@storybook/react-vite': 10.5.0-alpha.1 + '@storybook/addon-a11y': 10.5.0-alpha.2 + '@storybook/addon-docs': 10.5.0-alpha.2 + '@storybook/addon-vitest': 10.5.0-alpha.2 + '@storybook/react-vite': 10.5.0-alpha.2 '@types/node': 24.10.1 '@types/react': 19.2.6 '@types/react-dom': 19.2.3 @@ -40,11 +40,11 @@ catalogs: eslint: 9.39.1 eslint-plugin-react-hooks: 7.0.1 eslint-plugin-react-refresh: 0.4.24 - eslint-plugin-storybook: 10.5.0-alpha.1 + eslint-plugin-storybook: 10.5.0-alpha.2 globals: 16.5.0 react: 19.2.0 react-dom: 19.2.0 - storybook: 10.5.0-alpha.1 + storybook: 10.5.0-alpha.2 typescript: 5.9.3 typescript-eslint: 8.47.0 vite: 7.2.2