Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tame-taxis-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@frontify/fondue-components": patch
---

feat: add entrypoint for ai agents
2 changes: 0 additions & 2 deletions packages/components/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default {
'@storybook/addon-links',
'@storybook/addon-a11y',
'@storybook/addon-docs',
'@storybook/addon-mcp',
],
framework: {
name: '@storybook/react-vite',
Expand All @@ -27,7 +26,6 @@ export default {
backgrounds: false,
outline: false,
measure: false,
componentsManifest: true,
experimentalCodeExamples: true,
},
docs: {
Expand Down
1 change: 0 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@
"@storybook/addon-a11y": "^10.6.0",
"@storybook/addon-docs": "^10.6.0",
"@storybook/addon-links": "^10.6.0",
"@storybook/addon-mcp": "^10.6.0",
"@storybook/react-vite": "^10.6.0",
"@tailwindcss/forms": "^0.5.11",
"@testing-library/dom": "^10.4.1",
Expand Down
453 changes: 282 additions & 171 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions storybook-docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
storybook-static
node_modules
stories/guides/
static
2 changes: 1 addition & 1 deletion storybook-docs/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const config: StorybookConfig = {
'@etchteam/storybook-addon-status',
'@storybook/addon-docs',
],
staticDirs: ['assets'],
staticDirs: ['assets', '../static'],
framework: {
name: '@storybook/react-vite',
options: {},
Expand Down
11 changes: 11 additions & 0 deletions storybook-docs/.storybook/manager-head.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
<!-- Pointer for agents. Browsers run scripts, so they parse <noscript> as inert text and never render it.
Fetchers and HTML-to-markdown tools parse without scripting, so they see the paragraph in the page body. -->
<link rel="alternate" type="text/markdown" href="./llms.txt" title="Fondue docs for AI agents" />
<noscript>
<p>
AI agents: this Storybook needs JavaScript and is not meant to be scraped. Read
<a href="https://fondue-components.frontify.com/llms.txt">https://fondue-components.frontify.com/llms.txt</a>
for how to use Fondue (install the Fondue agent skill or query <code>@frontify/fondue/sdk</code>).
</p>
</noscript>

<!-- Custom CSS for Storybook interface goes here -->
<style>
.sidebar-header {
Expand Down
107 changes: 93 additions & 14 deletions storybook-docs/netlify-functions/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,103 @@
/* (c) Copyright Frontify Ltd., all rights reserved. */

import { basename } from 'node:path';
/**
* Deprecated MCP endpoint. The Storybook MCP that used to live here was replaced by
* the Fondue agent skill, which queries the locally installed `@frontify/fondue/sdk`.
* This stub stays so existing client configs get a pointer to the skill instead of a 404.
*/

import { createStorybookMcpHandler } from '@storybook/mcp';
const DEPRECATION_MESSAGE = [
'The Fondue Storybook MCP server has been deprecated and no longer serves component documentation.',
'Use the Fondue agent skill instead. It queries the `@frontify/fondue` version installed in your project:',
'',
' npx skills add frontify/fondue/packages/sdk',
'',
'Then remove this MCP server from your agent configuration.',
'Docs: https://fondue-components.frontify.com/llms.txt',
].join('\n');

const MANIFESTS_BASE = 'https://components.fondue-components.frontify.com/manifests';
// Kept under the names the Storybook MCP exposed, so agents calling them get the notice.
const TOOL_NAMES = ['docs-list', 'docs-show', 'docs-show-story'];

const handler = await createStorybookMcpHandler({
manifestProvider: async (_request: Request | undefined, path: string): Promise<string> => {
const url = `${MANIFESTS_BASE}/${basename(path)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
return await response.text();
},
});
const PROTOCOL_VERSION = '2025-06-18';

const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept, Mcp-Session-Id, Mcp-Protocol-Version',
};

type JsonRpcRequest = {
jsonrpc: '2.0';
id?: string | number | null;
method: string;
params?: { protocolVersion?: string };
};

const handleRequest = ({ id, method, params }: JsonRpcRequest) => {
switch (method) {
case 'initialize':
return {
jsonrpc: '2.0',
id,
result: {
protocolVersion: params?.protocolVersion ?? PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: 'fondue-storybook-mcp (deprecated)', version: '0.0.0' },
instructions: DEPRECATION_MESSAGE,
},
};
case 'ping':
return { jsonrpc: '2.0', id, result: {} };
case 'tools/list':
return {
jsonrpc: '2.0',
id,
result: {
tools: TOOL_NAMES.map((name) => ({
name,
description: `Deprecated. ${DEPRECATION_MESSAGE}`,
inputSchema: { type: 'object', properties: {}, additionalProperties: true },
})),
},
};
case 'tools/call':
return {
jsonrpc: '2.0',
id,
result: { content: [{ type: 'text', text: DEPRECATION_MESSAGE }], isError: true },
};
default:
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
}
};

const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' } });

export default async (req: Request): Promise<Response> => {
return await handler(req);
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
if (req.method !== 'POST') {
return new Response(DEPRECATION_MESSAGE, { status: 405, headers: { ...CORS_HEADERS, Allow: 'POST, OPTIONS' } });
}

let body: JsonRpcRequest | JsonRpcRequest[];
try {
body = (await req.json()) as JsonRpcRequest | JsonRpcRequest[];
} catch {
return json({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }, 400);
}

// Notifications (no id) get no response body.
const responses = (Array.isArray(body) ? body : [body])
.filter((message) => message.id !== undefined && message.id !== null)
.map(handleRequest);
if (responses.length === 0) {
return new Response(null, { status: 202, headers: CORS_HEADERS });
}
return json(Array.isArray(body) ? responses : responses[0]);
};

export const config = {
Expand Down
3 changes: 1 addition & 2 deletions storybook-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.2",
"private": true,
"scripts": {
"generate:guides": "node ./scripts/generateGuideStories.mjs",
"generate:guides": "node ./scripts/generateGuideStories.mjs && node ./scripts/generateLlmsTxt.mjs",
"storybook": "pnpm generate:guides && storybook dev -p 6010",
"build:storybook": "pnpm generate:guides && storybook build",
"lint": "eslint .",
Expand All @@ -23,7 +23,6 @@
"@storybook/addon-a11y": "^10.6.0",
"@storybook/addon-docs": "^10.6.0",
"@storybook/addon-links": "^10.6.0",
"@storybook/mcp": "^10.6.0",
"@storybook/react-vite": "^10.6.0",
"@tony.ganchev/eslint-plugin-header": "^3.4.4",
"@types/node": "^22.20.1",
Expand Down
62 changes: 62 additions & 0 deletions storybook-docs/scripts/generateLlmsTxt.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* (c) Copyright Frontify Ltd., all rights reserved. */

// Generates /llms.txt (https://llmstxt.org) and an /agents.txt alias for the
// deployed Storybook. Both serve the SDK usage guide, so an agent landing on
// the docs site learns to install the Fondue skill or query the SDK.

import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { guides } from '@frontify/fondue-sdk';

const outDir = join(dirname(fileURLToPath(import.meta.url)), '../static');

const usage = guides.get('sdk/Usage');
if (!usage) {
throw new Error('Guide "sdk/Usage" not found in @frontify/fondue-sdk');
}

/**
* Pushes every markdown heading one level down so the guide nests under the
* llms.txt H1. Fenced code blocks are left alone.
* @param {string} markdown
*/
const demoteHeadings = (markdown) => {
let inFence = false;
return markdown
.split('\n')
.map((line) => {
if (line.startsWith('```')) {
inFence = !inFence;
}
return !inFence && /^#{1,5} /.test(line) ? `#${line}` : line;
})
.join('\n');
};

const otherGuides = guides
.list()
.filter((guide) => guide.id !== usage.id)
.map((guide) => `- \`${guide.id}\`: ${guide.title}`)
.join('\n');

const content = `# Fondue

> Fondue is Frontify's React design system: components, icons and design tokens, published as \`@frontify/fondue\`. Don't scrape this Storybook. Install the Fondue agent skill, or query \`@frontify/fondue/sdk\` from the project, which describes the exact version installed there.

${demoteHeadings(usage.content).trim()}

## All guides

Read any of these from code with \`guides.get('<id>')?.content\` (\`import { guides } from '@frontify/fondue/sdk'\`):

${otherGuides}
`;

rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });
writeFileSync(join(outDir, 'llms.txt'), content);
writeFileSync(join(outDir, 'agents.txt'), content);

console.log('Generated static/llms.txt and static/agents.txt');
Loading