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
35 changes: 28 additions & 7 deletions e2e/docs-ui-static.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,41 @@ test.describe('Cloudflare Static Assets export', () => {

test('supports client navigation between generated documentation pages', async ({ page }) => {
await page.goto('/docs/quickstart');
await expect(page.getByText('Getting Started').first()).toBeVisible();
await page
.getByRole('link', { name: /Configuration/ })
.last()
.click();
await expect(page).toHaveURL(/\/docs\/configuration/);
await expect(page.getByRole('heading', { name: 'Configuration' }).first()).toBeVisible();
await expect(page).toHaveTitle('Petstore Docs');
await expect(page.getByRole('heading', { name: 'Quickstart' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Configuration' })).toHaveCount(0);
});

test('serves generated MCP and SDK deep links', async ({ page }) => {
await page.goto('/mcp/docs_quickstart');
await expect(page.getByText('docs_quickstart').first()).toBeVisible();

await page.goto('/mcp/sdk_typescript_petstore_typescript_client_sdk');
await expect(
page.getByText('sdk_typescript_petstore_typescript_client_sdk').first(),
).toBeVisible();

await page.goto('/sdks/typescript');
await expect(page.getByText('TypeScript').first()).toBeVisible();
});

test('matches the local demo documentation and MCP SDK tools', async ({ request }) => {
const docsResponse = await request.get('/api/docs');
const docs = await docsResponse.json();
expect(docs.sections).toEqual([
expect.objectContaining({
section: 'Get started',
documents: [expect.objectContaining({ title: 'Quickstart', slug: 'quickstart' })],
}),
]);

const mcpResponse = await request.get('/api/mcp');
const mcp = await mcpResponse.json();
const toolNames = mcp.tools.map((tool: { name: string }) => tool.name);
expect(toolNames.filter((name: string) => name.startsWith('docs_'))).toEqual([
'docs_quickstart',
]);
expect(toolNames.filter((name: string) => name.startsWith('sdk_'))).toHaveLength(11);
expect(toolNames).toContain('sdk_typescript_petstore_typescript_client_sdk');
});
});
13 changes: 11 additions & 2 deletions packages/docs-ui/scripts/cloudflare.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ if (!['demo', 'docs'].includes(target)) {
const require = createRequire(import.meta.url);
const scriptDir = dirname(fileURLToPath(import.meta.url));
const docsUiDir = resolve(scriptDir, '..');
const workspaceRoot = resolve(docsUiDir, '..', '..');
const cliMain = join(workspaceRoot, 'packages', 'cli', 'dist', 'main.js');
const outputDir = join(docsUiDir, '.next-cloudflare');
const nextCli = require.resolve('next/dist/bin/next');
const wranglerPackagePath = require.resolve('wrangler/package.json');
Expand Down Expand Up @@ -61,10 +63,10 @@ const env = {
: {}),
};

function run(executable, args) {
function run(executable, args, cwd = docsUiDir) {
return new Promise((resolveCommand, rejectCommand) => {
const child = spawn(executable, args, {
cwd: docsUiDir,
cwd,
env,
stdio: 'inherit',
});
Expand Down Expand Up @@ -102,6 +104,13 @@ function validateStaticOutput() {

try {
rmSync(outputDir, { recursive: true, force: true });
if (target === 'demo') {
if (!existsSync(cliMain)) {
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
await run(npm, ['run', 'build:cli'], workspaceRoot);
}
await run(process.execPath, [cliMain, 'generate'], prepared.demoDir);
}
await run(process.execPath, [nextCli, 'build', '--webpack']);
validateStaticOutput();

Expand Down
106 changes: 80 additions & 26 deletions packages/docs-ui/scripts/prepare-demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,51 @@ const demoDir = join(docsUiDir, '.cortex-demo');
const fixturesDir = join(workspaceRoot, 'packages', 'core', '__fixtures__');
const docsSiteDir = join(workspaceRoot, 'packages', 'docs-site');

const quickstart = `# Quickstart

Welcome to your API documentation! This guide will help you get started.

## API Reference

Browse the full API reference to see all available endpoints, request/response schemas, and authentication details.

## SDKs

Cortex generates type-safe SDKs for your API in multiple languages. Install the SDK for your language of choice and start making API calls in minutes.

## MCP Server

An MCP (Model Context Protocol) server is generated alongside your SDKs, enabling AI assistants to interact with your API using structured tool calls.

## Next Steps

- Explore the **API Reference** tab for endpoint details
- Visit the **SDKs** tab to download generated clients
- Check the **MCP** tab for AI integration setup
`;

const apiReferenceIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>`;

function buildLogo(textColor) {
const name = 'Petstore';
const totalWidth = Math.ceil(22 + 4 + name.length * 8.5);
const fillOpacity = textColor === '#ffffff' ? '0.1' : '0.08';
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${totalWidth} 21">
<g stroke="${textColor}" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M9,2.5 Q11,1 13,2.5 L18,5 Q20,6 18,7 L13,9.5 Q11,11 9,9.5 L4,7 Q2,6 4,5 Z" stroke-width="1.5" fill="${textColor}" fill-opacity="${fillOpacity}"/>
<path d="M3,10 L9,13.5 Q11,14.8 13,13.5 L19,10" stroke-width="1.5"/>
<path d="M3,13.5 L9,17 Q11,18.3 13,17 L19,13.5" stroke-width="1.5" stroke-opacity="0.5"/>
</g>
<text x="26" y="15" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="16" font-weight="600" fill="${textColor}">${name}</text>
</svg>`;
}

function copyFixture(sourceName, targetName, transform = (content) => content) {
const content = readFileSync(join(fixturesDir, sourceName), 'utf8');
writeFileSync(join(demoDir, 'specs', targetName), transform(content), 'utf8');
Expand Down Expand Up @@ -61,7 +106,26 @@ export function prepareDemo(apiUrl = process.env.CORTEX_DEMO_API_URL || 'http://
});

cpSync(join(docsSiteDir, 'assets'), join(demoDir, 'assets'), { recursive: true });
cpSync(join(docsSiteDir, 'docs'), join(demoDir, 'docs'), { recursive: true });
writeFileSync(join(demoDir, 'docs', 'quickstart.md'), quickstart, 'utf8');
writeFileSync(
join(demoDir, 'docs', 'REST_INTRO.md'),
`Welcome to the Petstore API. This API provides endpoints for managing resources.

## Base URL

\`\`\`
${apiUrl}
\`\`\`

## Rate Limiting

API requests are rate-limited to **1000 requests per minute** per API key. When you exceed the limit, requests return a \`429 Too Many Requests\` response. The \`Retry-After\` header indicates how long to wait before retrying.
`,
'utf8',
);
writeFileSync(join(demoDir, 'assets', 'logo_dark.svg'), buildLogo('#ffffff'), 'utf8');
writeFileSync(join(demoDir, 'assets', 'logo_light.svg'), buildLogo('#0a0a0a'), 'utf8');
writeFileSync(join(demoDir, 'assets', 'api-reference-icon.svg'), apiReferenceIcon, 'utf8');
writeFileSync(
join(demoDir, 'assets', 'custom.css'),
':root { --cortex-custom-head-loaded: yes; }\n',
Expand All @@ -70,8 +134,8 @@ export function prepareDemo(apiUrl = process.env.CORTEX_DEMO_API_URL || 'http://

const languages = sourceLanguages();
const config = {
project: 'cortex-demo',
title: 'Cortex Docs Demo',
project: 'Petstore',
title: 'Petstore Docs',
logo_dark: './assets/logo_dark.svg',
logo_light: './assets/logo_light.svg',
logoHeight: 24,
Expand All @@ -85,56 +149,46 @@ export function prepareDemo(apiUrl = process.env.CORTEX_DEMO_API_URL || 'http://
theme: 'system',
primaryColor: '#ffffff',
home: {
title: 'Cortex Docs Demo',
title: 'Petstore Docs',
description:
'Explore API documentation, generated SDKs, and MCP tools for the Petstore example.',
cta: { label: 'Open API Reference', href: '/api-reference' },
'Explore the full API surface, grab a client SDK, or wire up AI coding agents via our MCP for faster integration.',
cta: { label: 'Getting Started', href: '/docs' },
sections: [
{
title: 'API Reference',
description: 'Send requests to the Worker-native Petstore API.',
badge: 'Live demo',
href: '/api-reference',
icon: 'assets/docs-icon.svg',
description: 'Try endpoints, visualize schema, and check out code samples.',
badge: 'Reference',
href: '/reference',
icon: 'assets/api-reference-icon.svg',
},
{
title: 'SDKs',
description: 'Review generated clients for all supported languages.',
description: 'Typed client libraries for every major language.',
badge: 'Libraries',
href: '/sdks',
icon: 'assets/sdks-icon.svg',
},
{
title: 'MCP',
description: 'Review the generated MCP server and tool definitions.',
badge: 'AI agents',
description: 'Hook up AI coding agents via our MCP in seconds.',
badge: 'AI Agents',
href: '/mcp',
icon: 'assets/mcp-icon.svg',
},
],
},
docs: [
{
section: 'Getting Started',
sources: [
{ title: 'Quickstart', document: 'docs/quickstart.md' },
{ title: 'Configuration', document: 'docs/configuration.md' },
],
},
{
section: 'Features',
sources: [
{ title: 'SDK Generation', document: 'docs/sdk-generation.md' },
{ title: 'MCP Servers', document: 'docs/mcp-servers.md' },
{ title: 'Publishing', document: 'docs/publishing.md' },
],
section: 'Get started',
sources: [{ title: 'Quickstart', document: 'docs/quickstart.md' }],
},
],
sources: [
{
title: 'REST API V1',
type: 'openapi-spec',
spec: './specs/petstore.yaml',
intro: './docs/REST_INTRO.md',
languages,
},
{
Expand Down
31 changes: 31 additions & 0 deletions scripts/check-demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ async function check(path, round, cacheBust = false) {
await response.arrayBuffer();
}

async function readJson(path) {
const response = await fetch(`${baseUrl}${path}?check=${Date.now()}`, {
headers: { 'user-agent': 'cortex-demo-health-check/1.0' },
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`${path} returned ${response.status}.`);
return response.json();
}

let propagationFailures = [];
for (let attempt = 1; attempt <= maximumPropagationAttempts; attempt += 1) {
const results = await Promise.allSettled(
Expand All @@ -53,6 +62,28 @@ if (propagationFailures.length > 0) {
throw propagationFailures[0].reason;
}

const [config, docs, mcp] = await Promise.all([
readJson('/api/config'),
readJson('/api/docs'),
readJson('/api/mcp'),
]);
if (config.project !== 'Petstore' || config.title !== 'Petstore Docs') {
throw new Error('The deployed demo does not use the local Petstore project configuration.');
}
const documents = docs.sections?.flatMap((section) => section.documents ?? []) ?? [];
if (documents.length !== 1 || documents[0]?.title !== 'Quickstart') {
throw new Error('The deployed demo must contain only the Quickstart documentation page.');
}
const toolNames = mcp.tools?.map((tool) => tool.name) ?? [];
const sdkTools = toolNames.filter((name) => name.startsWith('sdk_'));
const docsTools = toolNames.filter((name) => name.startsWith('docs_'));
if (sdkTools.length !== 11 || !sdkTools.includes('sdk_typescript_petstore_typescript_client_sdk')) {
throw new Error(`The deployed demo exposed ${sdkTools.length} sdk_* MCP tools instead of 11.`);
}
if (docsTools.length !== 1 || docsTools[0] !== 'docs_quickstart') {
throw new Error(`The deployed demo exposed unexpected documentation MCP tools: ${docsTools}.`);
}

for (let round = 1; round <= rounds; round += 1) {
await Promise.all(paths.map((path) => check(path, round)));
}
Expand Down
8 changes: 8 additions & 0 deletions scripts/smoke-cli-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ async function verifyGeneratedMcp() {
if (result.tools.length === 0) {
throw new Error('The MCP server generated by the packaged CLI exposed no tools.');
}
const sdkTools = result.tools.filter((tool) => tool.name.startsWith('sdk_'));
if (
!sdkTools.some((tool) => tool.name === 'sdk_typescript_registry_smoke_typescript_client_sdk')
) {
throw new Error(
'The MCP server generated by the packaged CLI exposed no TypeScript SDK tool.',
);
}
} finally {
await client.close();
}
Expand Down