docs: add static GitHub Pages site
Some checks failed
CI / build (macos-latest) (push) Waiting to run
CI / build (ubuntu-latest) (push) Waiting to run
CI / build (windows-latest) (push) Waiting to run
pages / Deploy docs (push) Has been cancelled

This commit is contained in:
Peter Steinberger 2026-05-06 05:56:02 +01:00
parent 026eb28cf4
commit 7d345bc7db
No known key found for this signature in database
10 changed files with 1313 additions and 0 deletions

53
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: pages
on:
push:
branches:
- main
paths:
- 'docs/**'
- 'scripts/build-docs-site.mjs'
- 'scripts/docs-site-assets.mjs'
- '.github/workflows/pages.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
name: Deploy docs
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Check out
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Build docs site
run: node scripts/build-docs-site.mjs
- name: Configure Pages
uses: actions/configure-pages@v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
path: dist/docs-site
- name: Deploy
id: deployment
uses: actions/deploy-pages@v5

1
docs/CNAME Normal file
View File

@ -0,0 +1 @@
mcporter.sh

69
docs/index.md Normal file
View File

@ -0,0 +1,69 @@
---
title: Overview
permalink: /
summary: 'Overview of mcporter as a portable MCP runtime, CLI, generated-CLI toolkit, and typed-client layer.'
description: 'mcporter is a TypeScript runtime, CLI, and code-generation toolkit for the Model Context Protocol — built so AI agents and developers can call any MCP server without boilerplate.'
---
## Try it
mcporter auto-discovers the MCP servers already configured in Cursor, Claude Code/Desktop, Codex, Windsurf, OpenCode, and VS Code. Try it without installing anything:
```bash
# List every MCP server you already have configured.
npx mcporter list
# Inspect a single server with TypeScript-style signatures.
npx mcporter list linear --schema
# Call a tool — colon flags, function-call syntax, or trailing positional values.
npx mcporter call linear.create_comment issueId:ENG-123 body:'Looks good!'
npx mcporter call 'linear.create_comment(issueId: "ENG-123", body: "Looks good!")'
# Read or list MCP resources.
npx mcporter resource docs
npx mcporter resource docs file:///path/to/spec.md
# Mint a standalone CLI for any MCP server, ready to ship.
npx mcporter generate-cli linear --bundle dist/linear.js
# Emit `.d.ts` types or a typed client for agents and tests.
npx mcporter emit-ts linear --mode client --out src/linear-client.ts
```
`--json` produces a stable JSON envelope on stdout; human progress, prompts, and warnings always go to stderr so pipes stay parseable.
## What mcporter does
mcporter leans into the **code-execution-with-MCP** pattern Anthropic recommends: skip the giant tool-schema prompt, generate a small typed surface, and let the agent or the human call MCP servers like normal functions.
- **Zero-config discovery.** Reads your home config (`~/.mcporter/mcporter.json[c]`, or `$XDG_CONFIG_HOME/mcporter/mcporter.json[c]`), then `config/mcporter.json`, then imports from Cursor / Claude / Codex / Windsurf / OpenCode / VS Code. `${ENV}` placeholders are expanded; transports are pooled across calls.
- **One-command CLI generation.** [`mcporter generate-cli`](cli-generator.md) turns any MCP server into a ready-to-run CLI with embedded schemas, optional Rolldown/Bun bundling, and Bun-compiled binaries.
- **Typed clients.** [`mcporter emit-ts`](emit-ts.md) emits `.d.ts` interfaces or a ready-to-run client wrapping `createServerProxy()` so agents call MCP tools with full TypeScript types.
- **Friendly composable API.** [`createServerProxy()`](tool-calling.md) maps tools to camelCase methods, applies JSON-schema defaults, validates required arguments, and returns a `CallResult` with `.text()`, `.markdown()`, `.json()`, `.images()`, `.content()` helpers.
- **Ad-hoc connections + auto-OAuth.** Point the CLI at any MCP endpoint (HTTP, SSE, stdio) without touching config. Hosted MCPs that need a browser login (Supabase, Vercel, etc.) are auto-detected — `mcporter auth <url>` promotes the definition to OAuth on the fly. See [Ad-hoc connections](adhoc.md).
- **OAuth & stdio ergonomics.** Built-in OAuth caching, token refresh, log tailing, and stdio wrappers — same interface across HTTP, SSE, and stdio transports.
## Built for agents
mcporter is designed to be the layer between an MCP server and a coding agent. The pattern we recommend:
1. Configure the server once (or import from your editor of choice).
2. Run [`mcporter emit-ts <server>`](emit-ts.md) to get a `.d.ts` of the tool surface.
3. Wire small per-server [agent skills](agent-skills.md) instead of one mega-schema prompt — small prompts, named tools, no unrelated schemas loaded.
4. For shareable workflows, generate a standalone CLI with [`mcporter generate-cli`](cli-generator.md).
Because every transport flows through the same runtime, an agent that knows how to spawn `mcporter call` works with stdio servers, hosted HTTP MCPs, OAuth-gated services, and one-off URLs alike.
## Why a porter?
A _porter_ carries luggage between trains. mcporter does the same for MCP servers: it carries tool calls, schemas, OAuth tokens, and stdio handles between your agent (or your terminal) and whichever MCP server happens to be at the other end of the line. You don't have to know the shape of the server ahead of time, and the runtime keeps the connection warm so repeat calls are cheap.
## Where to next
- [Install](install.md) — npm, npx, Homebrew, or the standalone Bun-compiled binary.
- [Quickstart](quickstart.md) — your first list/call/resource in five minutes.
- [Configuration](config.md) — `mcporter.json`, imports, env interpolation, OAuth.
- [CLI reference](cli-reference.md) — every subcommand and flag.
- [Ad-hoc connections](adhoc.md) — point at any MCP endpoint without editing config.
- [Agent skills](agent-skills.md) — exposing servers to agents the right way.

68
docs/install.md Normal file
View File

@ -0,0 +1,68 @@
---
summary: 'How to install mcporter — npx, npm, pnpm, Homebrew, or a standalone Bun-compiled binary.'
---
# Install
mcporter ships as both a published npm package and a Homebrew formula. Most workflows can also run mcporter without installing anything via `npx`.
## Try without installing
```bash
npx mcporter --version
npx mcporter list
```
`npx` keeps the package in your npm cache, so subsequent runs are instant. This is the recommended first step.
## npm / pnpm / Bun
Install globally:
```bash
npm install -g mcporter
```
Or add it to a project:
```bash
pnpm add mcporter # or: npm install mcporter / bun add mcporter
```
mcporter targets Node 24+ and works under Bun. The package exposes both an importable runtime (`createRuntime`, `callOnce`, `createServerProxy`) and the `mcporter` CLI binary.
## Homebrew
```bash
brew install steipete/tap/mcporter
```
The tap publishes alongside npm. If you previously installed from an older tap, run `brew update` before reinstalling so Homebrew picks up the new formula path.
## Standalone binary
Each release also ships a Bun-compiled standalone binary you can drop on `$PATH` without a Node toolchain. Grab the asset for your OS/arch from the [GitHub releases page](https://github.com/steipete/mcporter/releases) and `chmod +x` it. The compiled CLI behaves the same as the Node build but boots noticeably faster and bundles its dependencies.
## Verify
```bash
mcporter --version
mcporter list
```
The first invocation will print every MCP server it discovered across your configs (Cursor, Claude Code/Desktop, Codex, Windsurf, OpenCode, VS Code). If nothing shows up, jump to [Configuration](config.md) to add a server.
## Updating
- `npm`: `npm install -g mcporter@latest`
- `pnpm`: `pnpm up -g mcporter@latest`
- `brew`: `brew upgrade steipete/tap/mcporter`
- Standalone binary: download a fresh release asset.
## Uninstall
- `npm uninstall -g mcporter`
- `brew uninstall steipete/tap/mcporter`
- Standalone binary: delete the file you copied onto `$PATH`.
mcporter stores OAuth tokens and cached schemas under `~/.mcporter/` (or `$XDG_CACHE_HOME/mcporter/` when set). Remove that directory if you want a fully clean slate.

79
docs/quickstart.md Normal file
View File

@ -0,0 +1,79 @@
---
summary: 'Five-minute walk through listing MCP servers, calling a tool, and emitting a typed client.'
---
# Quickstart
This walkthrough assumes you already have an MCP server configured in Cursor, Claude Code/Desktop, Codex, Windsurf, OpenCode, or VS Code. If not, copy [`config/mcporter.example.json`](https://github.com/steipete/mcporter/blob/main/config/mcporter.example.json) into `~/.mcporter/mcporter.json` and edit it — see [Configuration](config.md) for the full schema.
## 1. List the servers mcporter sees
```bash
npx mcporter list
```
You get one row per server with auth status, transport type, and tool count. Add `--json` for machine output, or `--verbose` to see which config files registered each server.
## 2. Inspect a single server
```bash
npx mcporter list linear
```
Single-server output reads like a TypeScript header file: dimmed `/** … */` doc comments above each `function name(...)` signature, with optional parameters summarised so the screen stays scannable. Add flags to drill in:
- `--brief` (alias `--signatures`) — compact signatures only.
- `--all-parameters` — show every optional parameter inline.
- `--schema` — pretty-print the JSON schema for each tool.
- `--json` — machine-readable schema payload.
`mcporter list shadcn.io/api/mcp.getComponents` works too — bare URLs (with or without a `.tool` suffix or scheme) auto-resolve.
## 3. Call a tool
```bash
# Colon-delimited flags (shell-friendly).
npx mcporter call linear.create_comment issueId:ENG-123 body:'Looks good!'
# Function-call style copy/pasted from `mcporter list`.
npx mcporter call 'linear.create_comment(issueId: "ENG-123", body: "Looks good!")'
# Anything after `--` is a literal positional value.
npx mcporter call docs.fetch -- --raw-string-with-leading-dashes
```
Pick the output format with `--output text|markdown|json|raw`. Use `--save-images <dir>` to persist binary content blocks. See [CLI reference](cli-reference.md) for the full flag list.
## 4. Read MCP resources
```bash
npx mcporter resource docs # list resources
npx mcporter resource docs file:///path/to/spec.md # read a resource
```
Output formatting is shared with `mcporter call` (`--output`, `--json`, `--raw`).
## 5. Generate a standalone CLI
When you want to share a tool with someone who shouldn't have to learn `mcporter call`:
```bash
npx mcporter generate-cli linear --bundle dist/linear.js
node dist/linear.js create-comment --issue-id ENG-123 --body 'Looks good!'
```
Add `--compile <path>` for a Bun-compiled binary, or `--include-tools a,b,c` to ship a subset. Full details in [CLI generator](cli-generator.md).
## 6. Emit typed clients for agents
```bash
npx mcporter emit-ts linear --mode client --out src/linear-client.ts
```
You get a `.d.ts` interface and a `createServerProxy()`-backed factory. Calls return `CallResult` objects with `.text()`, `.markdown()`, `.json()`, `.images()`, `.content()` helpers — see [Tool calling](tool-calling.md) for the proxy API and [emit-ts](emit-ts.md) for the generator.
## What next
- [Configuration](config.md) — `mcporter.json` schema, env interpolation, OAuth fields.
- [Ad-hoc connections](adhoc.md) — point at any MCP endpoint without editing config.
- [Agent skills](agent-skills.md) — wiring per-server skills into a coding agent.

BIN
docs/social-card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

129
docs/social-card.svg Normal file
View File

@ -0,0 +1,129 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
<title id="title">mcporter social card</title>
<desc id="desc">mcporter: MCP, made portable. TypeScript runtime, CLI, and code-generation toolkit for the Model Context Protocol.</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0a0c12"/>
<stop offset="0.55" stop-color="#10131c"/>
<stop offset="1" stop-color="#0c0f17"/>
</linearGradient>
<linearGradient id="brandSweep" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#7c3aed"/>
<stop offset="0.45" stop-color="#06b6d4"/>
<stop offset="0.85" stop-color="#10b981"/>
<stop offset="1" stop-color="#f59e0b"/>
</linearGradient>
<linearGradient id="logoGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7c3aed"/>
<stop offset="1" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="panel" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0e121b"/>
<stop offset="1" stop-color="#070a10"/>
</linearGradient>
<linearGradient id="panelBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#1a1f2c"/>
<stop offset="1" stop-color="#0f131c"/>
</linearGradient>
<radialGradient id="haze" cx="0.18" cy="0.22" r="0.6">
<stop offset="0" stop-color="#7c3aed" stop-opacity="0.28"/>
<stop offset="0.6" stop-color="#06b6d4" stop-opacity="0.08"/>
<stop offset="1" stop-color="#06b6d4" stop-opacity="0"/>
</radialGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="22" stdDeviation="34" flood-color="#000000" flood-opacity="0.55"/>
</filter>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<rect width="1200" height="630" fill="url(#haze)"/>
<!-- Top accent line -->
<rect x="0" y="0" width="1200" height="6" fill="url(#brandSweep)"/>
<!-- Brand mark: stylized suitcase icon -->
<g transform="translate(76 76)">
<rect x="0" y="0" width="118" height="118" rx="24" fill="url(#logoGrad)"/>
<rect x="22" y="42" width="74" height="56" rx="8" fill="#0a0c12"/>
<rect x="40" y="28" width="38" height="16" rx="4" fill="#0a0c12"/>
<rect x="22" y="58" width="74" height="3" fill="rgba(255,255,255,0.18)"/>
<circle cx="59" cy="74" r="6" fill="#a78bfa"/>
</g>
<!-- Title -->
<text x="76" y="276" fill="#f5f7fb" font-family="Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="100" font-weight="800" letter-spacing="-1">mcporter</text>
<!-- Tagline -->
<text x="80" y="338" fill="#cbd5e1" font-family="Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="34" font-weight="700" letter-spacing="0">MCP, made portable.</text>
<!-- Description -->
<text x="80" y="386" fill="#94a3b8" font-family="Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="500">TypeScript runtime + CLI for the Model Context Protocol.</text>
<text x="80" y="414" fill="#94a3b8" font-family="Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="500">Discover, call, and generate clients for any MCP server.</text>
<!-- Multi-color accent bar -->
<rect x="80" y="440" width="280" height="4" rx="2" fill="url(#brandSweep)"/>
<!-- Bottom row: install pill + URL pill -->
<g transform="translate(80 478)">
<rect x="0" y="0" width="320" height="48" rx="11" fill="#0e121b" stroke="#1f2937"/>
<text x="20" y="32" fill="#64748b" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="18" font-weight="500">$</text>
<text x="42" y="32" fill="#e6edf3" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="18" font-weight="600">npx mcporter list</text>
<rect x="340" y="0" width="180" height="48" rx="11" fill="#0e121b" stroke="#1f2937"/>
<text x="362" y="32" fill="#a78bfa" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="18" font-weight="600">mcporter.sh</text>
</g>
<!-- Right-side terminal mockup -->
<g transform="translate(670 142)" filter="url(#shadow)">
<rect x="0" y="0" width="464" height="346" rx="20" fill="url(#panel)" stroke="#1b2030"/>
<rect x="0" y="0" width="464" height="42" rx="20" fill="url(#panelBar)"/>
<rect x="0" y="22" width="464" height="20" fill="url(#panelBar)"/>
<circle cx="24" cy="21" r="6" fill="#ec4899"/>
<circle cx="46" cy="21" r="6" fill="#f59e0b"/>
<circle cx="68" cy="21" r="6" fill="#10b981"/>
<text x="232" y="27" fill="#94a3b8" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13" font-weight="500" text-anchor="middle">mcporter — agent ready</text>
<!-- Terminal content -->
<text x="22" y="80" fill="#a78bfa" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="15" font-weight="600">$ npx mcporter list linear</text>
<text x="22" y="108" fill="#64748b" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13">/**</text>
<text x="22" y="126" fill="#64748b" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13"> * Create a comment on a Linear issue</text>
<text x="22" y="144" fill="#64748b" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13"> * @param issueId The issue ID</text>
<text x="22" y="162" fill="#64748b" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13"> * @param body Markdown comment body</text>
<text x="22" y="180" fill="#64748b" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13"> */</text>
<text x="22" y="200" fill="#7dd3fc" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13" font-weight="600">function</text>
<text x="100" y="200" fill="#fde68a" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13" font-weight="600">create_comment</text>
<text x="226" y="200" fill="#cbd5e1" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13">(issueId, body);</text>
<text x="22" y="234" fill="#a78bfa" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="15" font-weight="600">$ mcporter call linear.create_comment \\</text>
<text x="36" y="252" fill="#cbd5e1" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13">issueId:ENG-123 body:'lgtm'</text>
<rect x="22" y="266" width="420" height="60" rx="10" fill="#06090f" stroke="#1f2937"/>
<text x="38" y="290" fill="#fbbf24" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13">{</text>
<text x="54" y="308" fill="#a7f3d0" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13">"ok": true, "comment": { "id": "abc123" }</text>
<text x="38" y="326" fill="#fbbf24" font-family="JetBrains Mono, SFMono-Regular, Menlo, monospace" font-size="13">}</text>
</g>
<!-- Capability pills below terminal -->
<g transform="translate(670 514)" font-family="Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="14" font-weight="600">
<g>
<rect x="0" y="0" width="92" height="30" rx="15" fill="#0e121b" stroke="#262a36"/>
<text x="46" y="20" fill="#a78bfa" text-anchor="middle">Runtime</text>
</g>
<g transform="translate(104 0)">
<rect x="0" y="0" width="56" height="30" rx="15" fill="#0e121b" stroke="#262a36"/>
<text x="28" y="20" fill="#06b6d4" text-anchor="middle">CLI</text>
</g>
<g transform="translate(174 0)">
<rect x="0" y="0" width="78" height="30" rx="15" fill="#0e121b" stroke="#262a36"/>
<text x="39" y="20" fill="#10b981" text-anchor="middle">OAuth</text>
</g>
<g transform="translate(266 0)">
<rect x="0" y="0" width="68" height="30" rx="15" fill="#0e121b" stroke="#262a36"/>
<text x="34" y="20" fill="#ec4899" text-anchor="middle">stdio</text>
</g>
<g transform="translate(348 0)">
<rect x="0" y="0" width="64" height="30" rx="15" fill="#0e121b" stroke="#262a36"/>
<text x="32" y="20" fill="#f59e0b" text-anchor="middle">HTTP</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -58,6 +58,7 @@
"dev": "tsgo -w -p tsconfig.build.json",
"prepublishOnly": "pnpm check && pnpm test && pnpm build",
"docs:list": "pnpm exec tsx scripts/docs-list.ts",
"docs:site": "node scripts/build-docs-site.mjs",
"generate:schema": "tsx scripts/generate-json-schema.ts",
"mcporter:list": "pnpm exec tsx src/cli.ts list",
"mcporter:call": "pnpm exec tsx src/cli.ts call"

639
scripts/build-docs-site.mjs Normal file
View File

@ -0,0 +1,639 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { css, faviconSvg, js, preThemeScript, themeToggleHtml } from './docs-site-assets.mjs';
const root = process.cwd();
const docsDir = path.join(root, 'docs');
const outDir = path.join(root, 'dist', 'docs-site');
const repoBase = 'https://github.com/steipete/mcporter';
const repoEditBase = `${repoBase}/edit/main/docs`;
const cname = readCname();
const siteBase = cname ? `https://${cname}` : '';
const productName = 'mcporter';
const productTagline = 'MCP, made portable.';
const productDescription =
'TypeScript runtime, CLI, and code-generation toolkit for the Model Context Protocol — built so AI agents and developers can call any MCP server without boilerplate.';
const brewInstall = 'npx mcporter list';
const sections = [
['Start', ['index.md', 'install.md', 'quickstart.md', 'config.md']],
['CLI', ['cli-reference.md', 'call-syntax.md', 'call-heuristic.md', 'shortcuts.md', 'logging.md']],
['Generators', ['cli-generator.md', 'emit-ts.md', 'tool-calling.md']],
['Connecting servers', ['adhoc.md', 'import.md', 'local.md', 'daemon.md', 'mcp.md']],
['Agents', ['agent-skills.md', 'subagent.md']],
[
'Operations',
[
'RELEASE.md',
'manual-testing.md',
'livetests.md',
'hang-debug.md',
'windows.md',
'tmux.md',
'known-issues.md',
'supabase-auth-issue.md',
],
],
['Reference', ['spec.md', 'migration.md', 'pnpm-mcp-migration.md', 'refactor.md']],
];
// Skip these from page generation (internal notes etc.). Pages excluded here are
// neither rendered nor link-validated.
const buildExcludes = [];
fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(outDir, { recursive: true });
const allPages = allMarkdown(docsDir).map((file) => {
const rel = path.relative(docsDir, file).replaceAll(path.sep, '/');
const raw = fs.readFileSync(file, 'utf8');
const { frontmatter, body } = parseFrontmatter(raw);
const cleaned = stripStrayDirectives(body);
const title = frontmatter.title || firstHeading(cleaned) || titleize(path.basename(rel, '.md'));
return { file, rel, title, outRel: outPath(rel, frontmatter), markdown: cleaned, frontmatter };
});
const pages = allPages.filter((page) => !buildExcludes.some((re) => re.test(page.rel)));
const pageMap = new Map(pages.map((page) => [page.rel, page]));
const permalinkMap = new Map();
for (const page of pages) {
if (page.frontmatter.permalink) {
permalinkMap.set(normalizePermalink(page.frontmatter.permalink), page);
}
}
const nav = sections
.map(([name, rels]) => ({
name,
pages: rels.map((rel) => pageMap.get(rel)).filter(Boolean),
}))
.filter((section) => section.pages.length);
// Catch-all section: any docs/*.md we didn't slot into the curated nav goes
// under "More". This keeps every doc reachable without forcing the author to
// hand-edit `sections` for every new file.
const navRels = new Set(nav.flatMap((s) => s.pages.map((p) => p.rel)));
const extras = pages
.filter((page) => !navRels.has(page.rel) && page.rel !== 'index.md')
.toSorted((a, b) => a.title.localeCompare(b.title));
if (extras.length) nav.push({ name: 'More', pages: extras });
const sectionByRel = new Map();
for (const section of nav) for (const page of section.pages) sectionByRel.set(page.rel, section.name);
const orderedPages = nav.flatMap((s) => s.pages);
for (const page of pages) {
const html = markdownToHtml(page.markdown, page.rel);
const toc = tocFromHtml(html);
const idx = orderedPages.findIndex((p) => p.rel === page.rel);
const prev = idx > 0 ? orderedPages[idx - 1] : null;
const next = idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : null;
const sectionName = sectionByRel.get(page.rel) || 'Reference';
const pageOut = path.join(outDir, page.outRel);
fs.mkdirSync(path.dirname(pageOut), { recursive: true });
fs.writeFileSync(pageOut, layout({ page, html, toc, prev, next, sectionName }), 'utf8');
}
fs.writeFileSync(path.join(outDir, 'favicon.svg'), faviconSvg(), 'utf8');
copyStaticAsset('social-card.svg');
copyStaticAsset('social-card.png');
fs.writeFileSync(path.join(outDir, '.nojekyll'), '', 'utf8');
if (cname) fs.writeFileSync(path.join(outDir, 'CNAME'), cname, 'utf8');
validateLinks(outDir);
console.log(`built docs site: ${path.relative(root, outDir)}`);
function readCname() {
for (const candidate of [path.join(docsDir, 'CNAME'), path.join(root, 'CNAME')]) {
if (fs.existsSync(candidate)) return fs.readFileSync(candidate, 'utf8').trim();
}
return '';
}
function copyStaticAsset(name) {
const source = path.join(docsDir, name);
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(outDir, name));
}
function parseFrontmatter(raw) {
const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
if (!match) return { frontmatter: {}, body: raw };
const fm = {};
for (const line of match[1].split('\n')) {
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
if (!m) continue;
let value = m[2];
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
fm[m[1]] = value;
}
return { frontmatter: fm, body: raw.slice(match[0].length) };
}
function stripStrayDirectives(body) {
return body
.replace(/\r\n/g, '\n')
.split('\n')
.filter((line) => !/^\s*\{:\s*[^}]*\}\s*$/.test(line))
.map((line) => line.replace(/\s*\{:\s*[^}]*\}\s*$/, ''))
.join('\n');
}
function normalizePermalink(value) {
let v = value.trim();
if (!v) return '/';
if (!v.startsWith('/')) v = `/${v}`;
if (v.length > 1 && v.endsWith('/')) v = v.slice(0, -1);
return v;
}
function allMarkdown(dir) {
return fs
.readdirSync(dir, { withFileTypes: true })
.flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return allMarkdown(full);
return entry.name.endsWith('.md') ? [full] : [];
})
.toSorted((a, b) => a.localeCompare(b));
}
function outPath(rel, frontmatter = {}) {
if (frontmatter.permalink) {
const permalink = normalizePermalink(frontmatter.permalink);
if (permalink === '/') return 'index.html';
return `${permalink.slice(1)}/index.html`;
}
if (rel === 'index.md') return 'index.html';
if (rel === 'README.md') return 'index.html';
if (rel.endsWith('/README.md')) return rel.replace(/README\.md$/, 'index.html');
return rel.replace(/\.md$/, '.html');
}
function firstHeading(markdown) {
return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim();
}
function titleize(input) {
return input.replaceAll('-', ' ').replace(/\b\w/g, (m) => m.toUpperCase());
}
function markdownToHtml(markdown, currentRel) {
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
const html = [];
let paragraph = [];
let list = null;
let fence = null;
let blockquote = [];
const flushParagraph = () => {
if (!paragraph.length) return;
html.push(`<p>${inline(paragraph.join(' '), currentRel)}</p>`);
paragraph = [];
};
const closeList = () => {
if (!list) return;
html.push(`</${list}>`);
list = null;
};
const flushBlockquote = () => {
if (!blockquote.length) return;
const inner = markdownToHtml(blockquote.join('\n'), currentRel);
html.push(`<blockquote>${inner}</blockquote>`);
blockquote = [];
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const fenceMatch = line.match(/^```([\w+-]+)?\s*$/);
if (fenceMatch) {
flushParagraph();
closeList();
flushBlockquote();
if (fence) {
html.push(
`<pre><code class="language-${escapeAttr(fence.lang)}">${escapeHtml(fence.lines.join('\n'))}</code></pre>`
);
fence = null;
} else {
fence = { lang: fenceMatch[1] || 'text', lines: [] };
}
continue;
}
if (fence) {
fence.lines.push(line);
continue;
}
if (/^>\s?/.test(line)) {
flushParagraph();
closeList();
blockquote.push(line.replace(/^>\s?/, ''));
continue;
}
flushBlockquote();
if (!line.trim()) {
flushParagraph();
closeList();
continue;
}
if (/^\s*---+\s*$/.test(line)) {
flushParagraph();
closeList();
html.push('<hr>');
continue;
}
const heading = line.match(/^(#{1,4})\s+(.+)$/);
if (heading) {
flushParagraph();
closeList();
const level = heading[1].length;
const text = heading[2].trim();
const id = slug(text);
const inner = inline(text, currentRel);
if (level === 1) {
html.push(`<h1 id="${id}">${inner}</h1>`);
} else {
html.push(
`<h${level} id="${id}"><a class="anchor" href="#${id}" aria-label="Anchor link">#</a>${inner}</h${level}>`
);
}
continue;
}
if (
line.trimStart().startsWith('|') &&
line.includes('|', line.indexOf('|') + 1) &&
isDivider(lines[i + 1] || '')
) {
flushParagraph();
closeList();
const header = splitRow(line);
const aligns = splitRow(lines[i + 1]).map((cell) => {
const left = cell.startsWith(':');
const right = cell.endsWith(':');
return right && left ? 'center' : right ? 'right' : left ? 'left' : '';
});
i += 1;
const rows = [];
while (i + 1 < lines.length && lines[i + 1].trimStart().startsWith('|')) {
i += 1;
rows.push(splitRow(lines[i]));
}
const th = header
.map((c, idx) => `<th${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ''}>${inline(c, currentRel)}</th>`)
.join('');
const tb = rows
.map(
(r) =>
`<tr>${r.map((c, idx) => `<td${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ''}>${inline(c, currentRel)}</td>`).join('')}</tr>`
)
.join('');
html.push(`<table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table>`);
continue;
}
const bullet = line.match(/^\s*-\s+(.+)$/);
const numbered = line.match(/^\s*\d+\.\s+(.+)$/);
if (bullet || numbered) {
flushParagraph();
const tag = bullet ? 'ul' : 'ol';
if (list && list !== tag) closeList();
if (!list) {
list = tag;
html.push(`<${tag}>`);
}
html.push(`<li>${inline((bullet || numbered)[1], currentRel)}</li>`);
continue;
}
paragraph.push(line.trim());
}
flushParagraph();
closeList();
flushBlockquote();
return html.join('\n');
}
function inline(text, currentRel) {
const stash = [];
let out = text.replace(/`([^`]+)`/g, (_, code) => {
stash.push(`<code>${escapeHtml(code)}</code>`);
return `\uE000${stash.length - 1}\uE000`;
});
out = escapeHtml(out)
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, '$1<em>$2</em>')
.replace(/(^|[^_])_([^_\s][^_]*?)_(?!_)/g, '$1<em>$2</em>')
.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
(_, label, href) => `<a href="${escapeAttr(rewriteHref(href, currentRel))}">${label}</a>`
)
.replace(/&lt;(https?:\/\/[^\s<>]+)&gt;/g, '<a href="$1">$1</a>');
out = out.replace(/\\\|/g, '|');
out = out.replace(/&lt;br&gt;/g, '<br>');
return out.replace(/\uE000(\d+)\uE000/g, (_, i) => stash[Number(i)]);
}
function splitRow(line) {
let trimmed = line.trim();
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
if (trimmed.endsWith('|') && !trimmed.endsWith('\\|')) trimmed = trimmed.slice(0, -1);
const cells = [];
let current = '';
for (let idx = 0; idx < trimmed.length; idx++) {
const char = trimmed[idx];
if (char === '\\' && trimmed[idx + 1] === '|') {
current += '\\|';
idx += 1;
continue;
}
if (char === '|') {
cells.push(current.trim().replace(/\\\|/g, '|'));
current = '';
continue;
}
current += char;
}
cells.push(current.trim().replace(/\\\|/g, '|'));
return cells;
}
function isDivider(line) {
return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line);
}
function rewriteHref(href, currentRel) {
if (/^(https?:|mailto:|tel:|#)/.test(href)) return href;
const [raw, hash = ''] = href.split('#');
if (!raw) return hash ? `#${hash}` : '';
if (raw.startsWith('/')) {
const target = permalinkMap.get(normalizePermalink(raw));
if (target) {
const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel);
const out = hrefToOutRel(target.outRel, currentOut);
return hash ? `${out}#${hash}` : out;
}
return href;
}
if (!raw.endsWith('.md')) return href;
const from = path.posix.dirname(currentRel);
const target = path.posix.normalize(path.posix.join(from, raw));
let rewritten = pageMap.get(target)?.outRel || outPath(target);
const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel);
rewritten = hrefToOutRel(rewritten, currentOut);
return `${rewritten}${hash ? `#${hash}` : ''}`;
}
function tocFromHtml(html) {
const items = [];
const re = /<h([23]) id="([^"]+)">([\s\S]*?)<\/h[23]>/g;
let m;
while ((m = re.exec(html))) {
const text = m[3]
.replace(/<a class="anchor"[^>]*>.*?<\/a>/, '')
.replace(/<[^>]+>/g, '')
.trim();
items.push({ level: Number(m[1]), id: m[2], text });
}
if (items.length < 2) return '';
return `<nav class="toc" aria-label="On this page"><h2>On this page</h2>${items
.map((i) => `<a class="toc-l${i.level}" href="#${i.id}">${escapeHtml(i.text)}</a>`)
.join('')}</nav>`;
}
function isHomePage(page) {
if (page.frontmatter.permalink && normalizePermalink(page.frontmatter.permalink) === '/') return true;
return page.rel === 'index.md' || page.rel === 'README.md';
}
function homeHero(page) {
const description = page.frontmatter.description || productDescription;
const installRel = pageMap.get('install.md')?.outRel
? hrefToOutRel(pageMap.get('install.md').outRel, page.outRel)
: 'install.html';
const quickstartRel = pageMap.get('quickstart.md')?.outRel
? hrefToOutRel(pageMap.get('quickstart.md').outRel, page.outRel)
: 'quickstart.html';
const features = ['TypeScript runtime', 'CLI', 'Generated CLIs', 'Typed clients', 'OAuth', 'stdio + HTTP + SSE'];
return `<header class="home-hero">
<p class="eyebrow">Model Context Protocol · Toolkit</p>
<h1>${escapeHtml(productTagline)}</h1>
<p class="lede">${escapeHtml(description)}</p>
<div class="home-cta">
<a class="btn btn-primary" href="${quickstartRel}">Quickstart</a>
<a class="btn btn-ghost" href="${repoBase}" rel="noopener">GitHub</a>
<div class="home-install" aria-label="Try with npx">
<span class="prompt" aria-hidden="true">$</span>
<code>${escapeHtml(brewInstall)}</code>
</div>
</div>
<div class="home-services" aria-label="Capabilities">
${features.map((s) => `<span>${escapeHtml(s)}</span>`).join('')}
</div>
<p class="muted"><a href="${installRel}">Install options </a></p>
</header>`;
}
function standardHero(page, sectionName, editUrl) {
return `<header class="hero">
<div class="hero-text">
<p class="eyebrow">${escapeHtml(sectionName)}</p>
<h1>${escapeHtml(page.title)}</h1>
</div>
<div class="hero-meta">
<a class="repo" href="${repoBase}" rel="noopener">GitHub</a>
<a class="edit" href="${escapeAttr(editUrl)}" rel="noopener">Edit page</a>
</div>
</header>`;
}
function layout({ page, html, toc, prev, next, sectionName }) {
const depth = page.outRel.split('/').length - 1;
const rootPrefix = depth ? '../'.repeat(depth) : '';
const editUrl = `${repoEditBase}/${page.rel}`;
const home = isHomePage(page);
const prevNext = !home && (prev || next) ? pageNavHtml(prev, next, page.outRel) : '';
const heroBlock = home ? homeHero(page) : standardHero(page, sectionName, editUrl);
const articleClass = home ? 'doc doc-home' : 'doc';
const tocBlock = home ? '' : toc;
const titleSuffix = home ? `${productName}${productTagline}` : `${page.title}${productName}`;
const description =
page.frontmatter.description || (home ? productDescription : `${page.title}${productName} documentation.`);
const canonicalUrl = pageCanonicalUrl(page);
const socialImage = siteBase ? `${siteBase}/social-card.png` : `${rootPrefix}social-card.png`;
const socialMeta = [
['link', 'rel', 'canonical', 'href', canonicalUrl],
['meta', 'property', 'og:type', 'content', 'website'],
['meta', 'property', 'og:site_name', 'content', productName],
['meta', 'property', 'og:title', 'content', titleSuffix],
['meta', 'property', 'og:description', 'content', description],
['meta', 'property', 'og:url', 'content', canonicalUrl],
['meta', 'property', 'og:image', 'content', socialImage],
['meta', 'property', 'og:image:width', 'content', '1200'],
['meta', 'property', 'og:image:height', 'content', '630'],
['meta', 'name', 'twitter:card', 'content', 'summary_large_image'],
['meta', 'name', 'twitter:title', 'content', titleSuffix],
['meta', 'name', 'twitter:description', 'content', description],
['meta', 'name', 'twitter:image', 'content', socialImage],
]
.map(tagHtml)
.join('\n ');
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(titleSuffix)}</title>
<meta name="description" content="${escapeAttr(description)}">
${socialMeta}
<link rel="icon" href="${rootPrefix}favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>${preThemeScript()}</script>
<style>${css()}</style>
</head>
<body${home ? ' class="home"' : ''}>
<button class="nav-toggle" type="button" aria-label="Toggle navigation" aria-expanded="false">
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
</button>
<div class="shell">
<aside class="sidebar">
<div class="sidebar-head">
<a class="brand" href="${hrefToOutRel('index.html', page.outRel)}" aria-label="${productName} docs home">
<span class="mark" aria-hidden="true"></span>
<span><strong>${escapeHtml(productName)}</strong><small>MCP toolkit docs</small></span>
</a>
${themeToggleHtml()}
</div>
<label class="search"><span>Search</span><input id="doc-search" type="search" placeholder="call, generate-cli, oauth"></label>
<nav>${navHtml(page)}</nav>
</aside>
<main>
${heroBlock}
<div class="doc-grid${home ? ' doc-grid-home' : ''}">
<article class="${articleClass}">${html}${prevNext}</article>
${tocBlock}
</div>
</main>
</div>
<script>${js()}</script>
</body>
</html>`;
}
function pageCanonicalUrl(page) {
if (!siteBase) return page.outRel;
if (page.outRel === 'index.html') return `${siteBase}/`;
const rel = page.outRel.endsWith('/index.html') ? page.outRel.slice(0, -'index.html'.length) : page.outRel;
return `${siteBase}/${rel}`;
}
function tagHtml([tag, k1, v1, k2, v2]) {
return tag === 'link'
? `<link ${k1}="${v1}" ${k2}="${escapeAttr(v2)}">`
: `<meta ${k1}="${v1}" ${k2}="${escapeAttr(v2)}">`;
}
function pageNavHtml(prev, next, currentOutRel) {
const cell = (page, dir) => {
if (!page) return '';
return `<a class="page-nav-${dir}" href="${hrefToOutRel(page.outRel, currentOutRel)}"><small>${dir === 'prev' ? 'Previous' : 'Next'}</small><span>${escapeHtml(page.title)}</span></a>`;
};
return `<nav class="page-nav" aria-label="Pager">${cell(prev, 'prev')}${cell(next, 'next')}</nav>`;
}
function navHtml(currentPage) {
return nav
.map(
(section) =>
`<section><h2>${escapeHtml(section.name)}</h2>${section.pages
.map((page) => {
const href = hrefToOutRel(page.outRel, currentPage.outRel);
const active = page.rel === currentPage.rel ? ' active' : '';
return `<a class="nav-link${active}" href="${href}">${escapeHtml(navTitle(page))}</a>`;
})
.join('')}</section>`
)
.join('');
}
function navTitle(page) {
if (page.rel === 'index.md') return 'Overview';
return page.title.replace(/^`mcporter\s*/, '').replace(/`$/, '');
}
function hrefToOutRel(targetOutRel, currentOutRel) {
const currentDir = path.posix.dirname(currentOutRel);
if (targetOutRel.endsWith('/index.html')) {
const targetDir = targetOutRel.slice(0, -'index.html'.length);
const rel = path.posix.relative(currentDir, targetDir || '.') || '.';
return rel.endsWith('/') ? rel : `${rel}/`;
}
if (targetOutRel === 'index.html') {
const rel = path.posix.relative(currentDir, '.') || '.';
return rel.endsWith('/') ? rel : `${rel}/`;
}
return path.posix.relative(currentDir, targetOutRel) || path.posix.basename(targetOutRel);
}
function slug(text) {
return text
.toLowerCase()
.replace(/`/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
function escapeHtml(value) {
return String(value ?? '').replace(
/[&<>"']/g,
(char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]
);
}
function escapeAttr(value) {
return escapeHtml(value);
}
function validateLinks(outputDir) {
const failures = [];
const placeholderHrefs = /^(url|path|file|dir|name)$/i;
for (const file of allHtml(outputDir)) {
const html = fs.readFileSync(file, 'utf8');
for (const match of html.matchAll(/href="([^"]+)"/g)) {
const href = match[1];
if (/^(#|https?:|mailto:|tel:|javascript:)/.test(href)) continue;
if (placeholderHrefs.test(href)) continue;
const [rawPath, anchor = ''] = href.split('#');
const targetPath = rawPath ? path.resolve(path.dirname(file), rawPath) : file;
const target =
fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()
? path.join(targetPath, 'index.html')
: targetPath;
if (!fs.existsSync(target)) {
failures.push(`${path.relative(outputDir, file)}: ${href} -> missing ${path.relative(outputDir, target)}`);
continue;
}
if (anchor) {
const targetHtml = fs.readFileSync(target, 'utf8');
if (!targetHtml.includes(`id="${anchor}"`) && !targetHtml.includes(`name="${anchor}"`)) {
failures.push(`${path.relative(outputDir, file)}: ${href} -> missing anchor`);
}
}
}
}
if (failures.length) {
throw new Error(`broken docs links:\n${failures.join('\n')}`);
}
}
function allHtml(dir) {
return fs
.readdirSync(dir, { withFileTypes: true })
.flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return allHtml(full);
return entry.name.endsWith('.html') ? [full] : [];
})
.toSorted((a, b) => a.localeCompare(b));
}

View File

@ -0,0 +1,274 @@
export function css() {
return `
:root{
--ink:#0d1116;
--text:#1f2530;
--muted:#5f6b7a;
--subtle:#94a0b1;
--bg:#f7f9fc;
--paper:#ffffff;
--accent:#7c3aed;
--accent-soft:rgba(124,58,237,.10);
--accent-strong:#5b21b6;
--brand-cyan:#06b6d4;
--brand-pink:#ec4899;
--brand-amber:#f59e0b;
--brand-emerald:#10b981;
--line:#e3e7ef;
--line-soft:#eef1f6;
--code-bg:#0b0d12;
--code-fg:#e6edf3;
--code-inline-fg:#1c2128;
--pill-border:#dbe2eb;
--shadow-card:0 4px 14px rgba(15,17,21,.08);
--scrollbar:#cbd5e1;
}
:root[data-theme="dark"]{
--ink:#f3f5f9;
--text:#cbd2dc;
--muted:#8d96a4;
--subtle:#5d6371;
--bg:#0a0c12;
--paper:#13161f;
--accent:#a78bfa;
--accent-soft:rgba(167,139,250,.16);
--accent-strong:#c4b5fd;
--line:#262a36;
--line-soft:#1d2029;
--code-bg:#06080d;
--code-fg:#e6edf3;
--code-inline-fg:#e6edf3;
--pill-border:#2a2f3c;
--shadow-card:0 4px 18px rgba(0,0,0,.45);
--scrollbar:#3a4154;
}
:root{color-scheme:light}
:root[data-theme="dark"]{color-scheme:dark}
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:24px}
body{margin:0;background:var(--bg);color:var(--text);font-family:"Inter",ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;line-height:1.65;overflow-x:hidden;-webkit-font-smoothing:antialiased;font-feature-settings:"cv02","cv03","cv04","cv11";transition:background-color .18s,color .18s}
::selection{background:var(--accent);color:#fff}
a{color:var(--accent);text-decoration:none;transition:color .12s}
a:hover{text-decoration:underline;text-underline-offset:.2em}
.shell{display:grid;grid-template-columns:268px minmax(0,1fr);min-height:100vh}
.sidebar{position:sticky;top:0;height:100vh;overflow:auto;padding:24px 22px;background:var(--paper);border-right:1px solid var(--line);scrollbar-width:thin;scrollbar-color:var(--line) transparent;transition:background-color .18s,border-color .18s}
.sidebar::-webkit-scrollbar{width:6px}
.sidebar::-webkit-scrollbar-thumb{background:var(--line);border-radius:6px}
.sidebar-head{display:flex;align-items:center;gap:10px;margin-bottom:24px}
.brand{display:flex;align-items:center;gap:11px;color:var(--ink);text-decoration:none;flex:1;min-width:0}
.brand:hover{text-decoration:none}
.brand .mark{position:relative;flex:0 0 32px;width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,var(--accent) 0%,var(--brand-cyan) 100%);box-shadow:0 1px 2px rgba(15,17,21,.18),inset 0 1px 0 rgba(255,255,255,.18)}
.brand .mark::before,.brand .mark::after{content:"";position:absolute;background:#fff;border-radius:2px}
.brand .mark::before{left:7px;right:7px;top:9px;height:3px;opacity:.95}
.brand .mark::after{left:13px;right:13px;top:14px;bottom:7px;border-radius:1px;opacity:.85}
.brand strong{display:block;font-size:1.05rem;line-height:1.1;font-weight:600;letter-spacing:0;color:var(--ink)}
.brand small{display:block;color:var(--muted);font-size:.74rem;margin-top:3px;font-weight:400}
.theme-toggle{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:34px;height:34px;border-radius:8px;border:1px solid var(--line);background:var(--paper);color:var(--muted);cursor:pointer;padding:0;transition:border-color .15s,color .15s,background-color .15s,transform .12s}
.theme-toggle:hover{border-color:var(--ink);color:var(--ink)}
.theme-toggle:active{transform:scale(.94)}
.theme-toggle svg{width:16px;height:16px;display:block}
.theme-icon-sun{display:none}
:root[data-theme="dark"] .theme-icon-sun{display:block}
:root[data-theme="dark"] .theme-icon-moon{display:none}
.search{display:block;margin:0 0 22px}
.search span{display:block;color:var(--muted);font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:0;margin-bottom:7px}
.search input{width:100%;border:1px solid var(--line);background:var(--paper);border-radius:8px;padding:9px 12px;font:inherit;font-size:.9rem;color:var(--text);outline:none;transition:border-color .15s,box-shadow .15s,background-color .18s}
.search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
nav section{margin:0 0 18px}
nav h2{font-size:.68rem;color:var(--muted);text-transform:uppercase;letter-spacing:0;margin:0 0 6px;font-weight:600}
.nav-link{display:block;color:var(--text);text-decoration:none;border-radius:6px;padding:5px 10px;margin:1px 0;font-size:.9rem;line-height:1.4;transition:background .12s,color .12s}
.nav-link:hover{background:var(--line-soft);color:var(--ink);text-decoration:none}
.nav-link.active{background:var(--accent-soft);color:var(--accent);font-weight:600}
main{min-width:0;padding:32px clamp(20px,4.5vw,56px) 80px;max-width:1180px;margin:0 auto;width:100%}
.hero{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;border-bottom:1px solid var(--line);padding:8px 0 22px;margin-bottom:8px;flex-wrap:wrap}
.hero-text{min-width:0;flex:1 1 320px}
.eyebrow{margin:0 0 8px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:0;font-size:.7rem}
.hero h1{font-size:2.25rem;line-height:1.1;letter-spacing:0;margin:0;font-weight:700;color:var(--ink)}
.hero-meta{display:flex;gap:8px;flex:0 0 auto;flex-wrap:wrap}
.repo,.edit,.btn-ghost{border:1px solid var(--line);color:var(--text);text-decoration:none;border-radius:7px;padding:6px 11px;font-weight:500;font-size:.83rem;background:var(--paper);transition:border-color .15s,color .15s,background .15s}
.repo:hover,.edit:hover,.btn-ghost:hover{border-color:var(--ink);color:var(--ink);text-decoration:none}
.edit{color:var(--muted)}
.home-hero{padding:14px 0 28px;margin-bottom:8px;border-bottom:1px solid var(--line)}
.home-hero h1{font-size:3.25rem;line-height:1.04;letter-spacing:0;margin:0 0 .35em;font-weight:700;color:var(--ink);background:linear-gradient(120deg,var(--accent) 0%,var(--brand-cyan) 60%,var(--brand-pink) 100%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.home-hero .lede{font-size:1.18rem;line-height:1.55;color:var(--text);margin:0 0 1.2em;max-width:60ch}
.home-cta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:0 0 18px}
.home-cta .btn{display:inline-flex;align-items:center;gap:7px;border-radius:8px;padding:10px 16px;font-weight:600;font-size:.92rem;text-decoration:none;transition:background .15s,border-color .15s,color .15s,transform .12s}
.home-cta .btn-primary{background:var(--accent);color:#fff;border:1px solid var(--accent)}
.home-cta .btn-primary:hover{background:var(--accent-strong);border-color:var(--accent-strong);text-decoration:none;color:#fff}
.home-cta .btn-ghost{padding:10px 16px}
.home-install{display:flex;align-items:center;gap:12px;background:var(--code-bg);color:var(--code-fg);border-radius:8px;padding:10px 10px 10px 16px;font:500 .9rem/1.2 "JetBrains Mono","SF Mono",ui-monospace,monospace;max-width:32em;border:1px solid #1f2937}
.home-install .prompt{color:#64748b;user-select:none;flex:0 0 auto}
.home-install code{flex:1;background:transparent;border:0;color:var(--code-fg);font:inherit;padding:0;white-space:pre;overflow:hidden;text-overflow:ellipsis}
.home-install .copy{flex:0 0 auto;background:rgba(255,255,255,.08);color:var(--code-fg);border:1px solid rgba(255,255,255,.16);border-radius:6px;padding:5px 11px;font:500 .72rem/1 "Inter",sans-serif;cursor:pointer;transition:background .15s,border-color .15s}
.home-install .copy:hover{background:rgba(255,255,255,.16)}
.home-install .copy.copied{background:var(--accent);border-color:var(--accent)}
.home-services{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 18px}
.home-services span{display:inline-block;padding:3px 9px;border:1px solid var(--line);border-radius:999px;font-size:.78rem;color:var(--muted);background:var(--paper)}
.doc-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:48px;margin-top:24px}
.doc-grid-home{margin-top:8px}
@media(min-width:1180px){.doc-grid{grid-template-columns:minmax(0,72ch) 200px;justify-content:start}.doc-grid-home{grid-template-columns:minmax(0,76ch);justify-content:start}}
.doc{min-width:0;max-width:72ch;overflow-wrap:break-word}
.doc-home{max-width:76ch}
.doc h1{font-size:2.6rem;line-height:1.08;letter-spacing:0;margin:0 0 .4em;font-weight:700;color:var(--ink)}
body:not(.home) .doc>h1:first-child{display:none}
.doc h2{font-size:1.45rem;line-height:1.2;margin:2em 0 .5em;font-weight:600;letter-spacing:0;color:var(--ink);position:relative}
.doc h3{font-size:1.1rem;margin:1.7em 0 .35em;position:relative;font-weight:600;color:var(--ink);letter-spacing:0}
.doc h4{font-size:.98rem;margin:1.4em 0 .25em;color:var(--ink);position:relative;font-weight:600}
.doc h2:first-child,.doc h3:first-child,.doc h4:first-child{margin-top:.2em}
.doc :is(h2,h3,h4) .anchor{position:absolute;left:-1.05em;top:0;color:var(--subtle);opacity:0;text-decoration:none;font-weight:400;padding-right:.3em;transition:opacity .12s,color .12s}
.doc :is(h2,h3,h4):hover .anchor{opacity:.7}
.doc :is(h2,h3,h4) .anchor:hover{opacity:1;color:var(--accent);text-decoration:none}
.doc p{margin:0 0 1.05em}
.doc ul,.doc ol{padding-left:1.3rem;margin:0 0 1.15em}
.doc li{margin:.25em 0}
.doc li>p{margin:0 0 .4em}
.doc strong{font-weight:600;color:var(--ink)}
.doc em{font-style:italic}
.doc code{font-family:"JetBrains Mono","SF Mono",ui-monospace,monospace;font-size:.84em;background:var(--line-soft);border:1px solid var(--line);border-radius:5px;padding:.08em .35em;color:var(--code-inline-fg)}
.doc pre{position:relative;overflow:auto;background:var(--code-bg);color:var(--code-fg);border-radius:8px;padding:14px 18px;margin:1.3em 0;font-size:.85em;line-height:1.6;scrollbar-width:thin;scrollbar-color:#334155 transparent;border:1px solid #1f2937}
.doc pre::-webkit-scrollbar{height:8px;width:8px}
.doc pre::-webkit-scrollbar-thumb{background:#334155;border-radius:8px}
.doc pre code{display:block;background:transparent;border:0;color:inherit;padding:0;font-size:1em;white-space:pre}
.doc pre .copy{position:absolute;top:8px;right:8px;background:rgba(255,255,255,.06);color:var(--code-fg);border:1px solid rgba(255,255,255,.16);border-radius:6px;padding:3px 9px;font:500 .7rem/1 "Inter",sans-serif;cursor:pointer;opacity:0;transition:opacity .15s,background .15s,border-color .15s}
.doc pre:hover .copy,.doc pre .copy:focus{opacity:1}
.doc pre .copy:hover{background:rgba(255,255,255,.12)}
.doc pre .copy.copied{background:var(--accent);border-color:var(--accent);opacity:1}
.doc blockquote{margin:1.4em 0;padding:10px 16px;border-left:3px solid var(--accent);background:var(--accent-soft);border-radius:0 8px 8px 0;color:var(--text)}
.doc blockquote p:last-child{margin-bottom:0}
.doc table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:.92em}
.doc th,.doc td{border-bottom:1px solid var(--line);padding:9px 10px;text-align:left;vertical-align:top}
.doc th{font-weight:600;color:var(--ink);background:var(--line-soft);border-bottom:1px solid var(--line)}
.doc hr{border:0;border-top:1px solid var(--line);margin:2.2em 0}
.toc{position:sticky;top:24px;align-self:start;font-size:.84rem;padding-left:14px;border-left:1px solid var(--line);max-height:calc(100vh - 48px);overflow:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}
.toc::-webkit-scrollbar{width:5px}
.toc::-webkit-scrollbar-thumb{background:var(--line);border-radius:5px}
.toc h2{font-size:.66rem;color:var(--muted);text-transform:uppercase;letter-spacing:0;margin:0 0 10px;font-weight:600}
.toc a{display:block;color:var(--muted);text-decoration:none;padding:4px 0 4px 10px;line-height:1.35;border-left:2px solid transparent;margin-left:-12px;transition:color .12s,border-color .12s}
.toc a:hover{color:var(--ink);text-decoration:none}
.toc a.active{color:var(--accent);border-left-color:var(--accent);font-weight:500}
.toc-l3{padding-left:22px!important;font-size:.94em}
@media(max-width:1179px){.toc{display:none}}
.page-nav{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:48px;border-top:1px solid var(--line);padding-top:20px}
.page-nav>a{display:block;border:1px solid var(--line);background:var(--paper);border-radius:9px;padding:13px 16px;text-decoration:none;color:var(--text);transition:border-color .15s,transform .15s,box-shadow .15s,background-color .18s}
.page-nav>a:hover{border-color:var(--accent);text-decoration:none;color:var(--ink)}
.page-nav small{display:block;color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:0;margin-bottom:5px;font-weight:600}
.page-nav span{display:block;font-weight:600;line-height:1.3;color:var(--ink)}
.page-nav-prev{text-align:left}
.page-nav-next{text-align:right;grid-column:2}
.page-nav-prev:only-child{grid-column:1}
.nav-toggle{display:none;position:fixed;top:14px;right:14px;top:calc(14px + env(safe-area-inset-top, 0px));right:calc(14px + env(safe-area-inset-right, 0px));z-index:20;width:40px;height:40px;border-radius:9px;background:var(--paper);border:1px solid var(--line);color:var(--ink);cursor:pointer;padding:10px 9px;flex-direction:column;align-items:stretch;justify-content:space-between;box-shadow:var(--shadow-card)}
.nav-toggle span{display:block;width:100%;height:2px;flex:0 0 2px;background:currentColor;border-radius:2px;transition:transform .2s,opacity .2s}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(8px) rotate(45deg)}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-8px) rotate(-45deg)}
@media(max-width:900px){
.shell{display:block}
.sidebar{position:fixed;inset:0 30% 0 0;max-width:320px;height:100vh;z-index:15;transform:translateX(-100%);transition:transform .25s ease,background-color .18s,border-color .18s;box-shadow:0 18px 40px rgba(0,0,0,.18);background:var(--paper);pointer-events:none}
.sidebar.open{transform:translateX(0);pointer-events:auto}
.nav-toggle{display:flex}
main{padding:64px 18px 56px}
.hero{padding-top:6px}
.hero h1{font-size:1.8rem}
.home-hero h1{font-size:2.45rem}
.doc h1{font-size:2.1rem}
.hero-meta{width:100%;justify-content:flex-start}
.home-hero{padding-top:8px}
.doc{padding:0}
.doc-grid{margin-top:18px;gap:24px}
.doc :is(h2,h3,h4) .anchor{display:none}
}
@media(max-width:520px){
main{padding:60px 14px 48px}
.doc pre{margin-left:-14px;margin-right:-14px;border-radius:0;border-left:0;border-right:0}
.home-install{flex-wrap:wrap}
}
`;
}
export function js() {
return `
const themeRoot=document.documentElement;
function applyTheme(mode){themeRoot.dataset.theme=mode;document.querySelectorAll('[data-theme-toggle]').forEach(b=>b.setAttribute('aria-pressed',mode==='dark'?'true':'false'))}
function storedTheme(){try{return localStorage.getItem('theme')}catch(e){return null}}
function persistTheme(mode){try{localStorage.setItem('theme',mode)}catch(e){}}
applyTheme(themeRoot.dataset.theme==='dark'?'dark':'light');
document.querySelectorAll('[data-theme-toggle]').forEach(btn=>{btn.addEventListener('click',()=>{const next=themeRoot.dataset.theme==='dark'?'light':'dark';applyTheme(next);persistTheme(next)})});
const systemDark=window.matchMedia&&matchMedia('(prefers-color-scheme: dark)');
function onSystemChange(e){if(storedTheme())return;applyTheme(e.matches?'dark':'light')}
if(systemDark){if(systemDark.addEventListener)systemDark.addEventListener('change',onSystemChange);else if(systemDark.addListener)systemDark.addListener(onSystemChange)}
const sidebar=document.querySelector('.sidebar');
const toggle=document.querySelector('.nav-toggle');
const mobileNav=window.matchMedia('(max-width: 900px)');
const sidebarFocusable='a[href],button,input,select,textarea,[tabindex]';
function setSidebarFocusable(enabled){
sidebar?.querySelectorAll(sidebarFocusable).forEach((el)=>{
if(enabled){
if(el.dataset.sidebarTabindex!==undefined){
if(el.dataset.sidebarTabindex)el.setAttribute('tabindex',el.dataset.sidebarTabindex);
else el.removeAttribute('tabindex');
delete el.dataset.sidebarTabindex;
}
}else if(el.dataset.sidebarTabindex===undefined){
el.dataset.sidebarTabindex=el.getAttribute('tabindex')??'';
el.setAttribute('tabindex','-1');
}
});
}
function setSidebarOpen(open){
if(!sidebar||!toggle)return;
sidebar.classList.toggle('open',open);
toggle.setAttribute('aria-expanded',open?'true':'false');
if(mobileNav.matches){
sidebar.inert=!open;
if(open)sidebar.removeAttribute('aria-hidden');
else sidebar.setAttribute('aria-hidden','true');
setSidebarFocusable(open);
}else{
sidebar.inert=false;
sidebar.removeAttribute('aria-hidden');
setSidebarFocusable(true);
}
}
setSidebarOpen(false);
toggle?.addEventListener('click',()=>setSidebarOpen(!sidebar?.classList.contains('open')));
document.addEventListener('click',(e)=>{if(!sidebar?.classList.contains('open'))return;if(sidebar.contains(e.target)||toggle?.contains(e.target))return;setSidebarOpen(false)});
document.addEventListener('keydown',(e)=>{if(e.key==='Escape')setSidebarOpen(false)});
const syncSidebarForViewport=()=>setSidebarOpen(sidebar?.classList.contains('open')??false);
if(mobileNav.addEventListener)mobileNav.addEventListener('change',syncSidebarForViewport);
else mobileNav.addListener?.(syncSidebarForViewport);
const input=document.getElementById('doc-search');
input?.addEventListener('input',()=>{const q=input.value.trim().toLowerCase();document.querySelectorAll('nav section').forEach(sec=>{let any=false;sec.querySelectorAll('.nav-link').forEach(a=>{const m=!q||a.textContent.toLowerCase().includes(q);a.style.display=m?'block':'none';if(m)any=true});sec.style.display=any?'block':'none'})});
function attachCopy(target,getText){const btn=document.createElement('button');btn.type='button';btn.className='copy';btn.textContent='Copy';btn.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(getText());btn.textContent='Copied';btn.classList.add('copied');setTimeout(()=>{btn.textContent='Copy';btn.classList.remove('copied')},1400)}catch{btn.textContent='Failed';setTimeout(()=>{btn.textContent='Copy'},1400)}});target.appendChild(btn)}
document.querySelectorAll('.doc pre').forEach(pre=>attachCopy(pre,()=>pre.querySelector('code')?.textContent??''));
document.querySelectorAll('.home-install').forEach(el=>attachCopy(el,()=>el.querySelector('code')?.textContent??''));
const tocLinks=document.querySelectorAll('.toc a');
if(tocLinks.length){const map=new Map();tocLinks.forEach(a=>{const id=a.getAttribute('href').slice(1);const el=document.getElementById(id);if(el)map.set(el,a)});const setActive=l=>{tocLinks.forEach(x=>x.classList.remove('active'));l.classList.add('active')};const obs=new IntersectionObserver(entries=>{const visible=entries.filter(e=>e.isIntersecting).sort((a,b)=>a.boundingClientRect.top-b.boundingClientRect.top);if(visible.length){const link=map.get(visible[0].target);if(link)setActive(link)}},{rootMargin:'-15% 0px -65% 0px',threshold:0});map.forEach((_,el)=>obs.observe(el))}
`;
}
export function preThemeScript() {
return `(function(){var s;try{s=localStorage.getItem('theme')}catch(e){}var d=window.matchMedia&&matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=s||(d?'dark':'light')})();`;
}
export function themeToggleHtml() {
return `<button class="theme-toggle" type="button" aria-label="Toggle dark mode" aria-pressed="false" data-theme-toggle>
<svg class="theme-icon-moon" viewBox="0 0 20 20" aria-hidden="true"><path d="M14.6 12.1A6.5 6.5 0 0 1 7.4 2.7a6.5 6.5 0 1 0 7.2 9.4z" fill="currentColor"/></svg>
<svg class="theme-icon-sun" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="3.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="10" y1="2" x2="10" y2="4"/><line x1="10" y1="16" x2="10" y2="18"/><line x1="2" y1="10" x2="4" y2="10"/><line x1="16" y1="10" x2="18" y2="10"/><line x1="4.2" y1="4.2" x2="5.6" y2="5.6"/><line x1="14.4" y1="14.4" x2="15.8" y2="15.8"/><line x1="4.2" y1="15.8" x2="5.6" y2="14.4"/><line x1="14.4" y1="5.6" x2="15.8" y2="4.2"/></g></svg>
</button>`;
}
export function faviconSvg() {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="mcporter">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7c3aed"/>
<stop offset="1" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="url(#g)"/>
<rect x="14" y="22" width="36" height="22" rx="4" fill="#0b0d12"/>
<rect x="22" y="16" width="20" height="8" rx="2" fill="#0b0d12"/>
<rect x="14" y="29" width="36" height="2" fill="rgba(255,255,255,.18)"/>
<circle cx="32" cy="38" r="3" fill="#7c3aed"/>
</svg>`;
}