docs: replace the typedoc site with VitePress (v2 + v1 reface) (#2395)

This commit is contained in:
Felix Weinberger 2026-06-30 16:03:19 +01:00 • committed by GitHub
commit 732abe0ac4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1996 additions and 270 deletions

View file

@ -0,0 +1,32 @@
/**
* Generate docs/index.md (the V2 site's landing page) from the repository README.
*
* The docs site lands on the README — the same model as the V1 site — so the README
* stays the single source of truth and the landing page can never drift from it.
* Links that only resolve on GitHub are rewritten for the site:
* - `docs/<page>.md` -> `./<page>.md` (index.md sits next to the guide pages)
* - other repo paths -> the GitHub blob URL
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
const GITHUB = 'https://github.com/modelcontextprotocol/typescript-sdk/blob/main';
let markdown = readFileSync(join(repoRoot, 'README.md'), 'utf8');
markdown = markdown
// The README's "API docs" link points at the old typedoc site; on the landing it is the
// in-site API Reference section.
.replaceAll('](https://modelcontextprotocol.github.io/typescript-sdk/)', '](./api/index.md)')
.replaceAll('](docs/', '](./')
.replaceAll('](packages/', `](${GITHUB}/packages/`)
.replaceAll('](examples/', `](${GITHUB}/examples/`)
.replaceAll('](LICENSE)', `](${GITHUB}/LICENSE)`)
.replaceAll('](CONTRIBUTING.md)', `](${GITHUB}/CONTRIBUTING.md)`)
.replaceAll('](SECURITY.md)', `](${GITHUB}/SECURITY.md)`)
.replaceAll('](CODE_OF_CONDUCT.md)', `](${GITHUB}/CODE_OF_CONDUCT.md)`);
writeFileSync(join(repoRoot, 'docs', 'index.md'), markdown);
console.log('docs/index.md generated from README.md');

147
scripts/build-docs-site.sh Executable file
View file

@ -0,0 +1,147 @@
#!/usr/bin/env bash
#
# Build the combined V1 + V2 documentation site (VitePress).
#
# V2 (from the current checkout, sources in docs/) is placed under /v2/.
# V1 (content from the v1.x branch, VitePress shell in docs/v1/) is placed at the root.
#
# Usage:
# scripts/build-docs-site.sh [output-dir]
#
# Default output directory: tmp/docs-combined
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT_DIR="$(cd "$REPO_ROOT" && realpath -m "${1:-tmp/docs-combined}")"
V1_WORKTREE="$REPO_ROOT/.worktrees/v1-docs"
V1_CONTENT="$REPO_ROOT/docs/v1/content"
V1_GITHUB="https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x"
cleanup() {
echo "Cleaning up worktree..."
cd "$REPO_ROOT"
git worktree remove --force "$V1_WORKTREE" 2>/dev/null || true
}
trap cleanup EXIT
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# ---------------------------------------------------------------------------
# Step 1: Build V2 site from the current checkout
# ---------------------------------------------------------------------------
echo "=== Building V2 site ==="
cd "$REPO_ROOT"
pnpm install
pnpm -r --filter='./packages/**' build
pnpm docs:build
mkdir -p "$OUTPUT_DIR/v2"
cp -r "$REPO_ROOT/docs/.vitepress/dist/." "$OUTPUT_DIR/v2/"
# ---------------------------------------------------------------------------
# Step 2: Build V1 site from the v1.x branch
# ---------------------------------------------------------------------------
echo "=== Building V1 site ==="
git fetch origin v1.x
git worktree remove --force "$V1_WORKTREE" 2>/dev/null || true
rm -rf "$V1_WORKTREE"
# FETCH_HEAD (set by the fetch above) rather than origin/v1.x: the remote-tracking ref
# does not exist in single-branch or refspec-restricted clones.
git worktree add "$V1_WORKTREE" FETCH_HEAD --detach
cd "$V1_WORKTREE"
npm install
npm install --no-save typedoc@^0.28.14 typedoc-plugin-markdown@^4.9.0 typedoc-vitepress-theme@^1.1.0
rm -rf "$V1_CONTENT"
mkdir -p "$V1_CONTENT"
# Same entry points as the previous typedoc HTML setup for v1, but rendered to
# markdown (one page per module) for the VitePress shell in docs/v1/.
cat > typedoc.v1-site.json << TYPEDOC_EOF
{
"name": "MCP TypeScript SDK",
"entryPoints": [
"src/client/index.ts",
"src/server/index.ts",
"src/shared/protocol.ts",
"src/shared/transport.ts",
"src/types.ts",
"src/inMemory.ts",
"src/validation/index.ts",
"src/experimental/index.ts"
],
"tsconfig": "tsconfig.json",
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
"outputFileStrategy": "modules",
"readme": "none",
"docsRoot": "$V1_CONTENT",
"out": "$V1_CONTENT/api",
"exclude": [
"**/*.test.ts",
"**/__fixtures__/**",
"**/__mocks__/**",
"src/examples/**"
],
"skipErrorChecking": true
}
TYPEDOC_EOF
npx typedoc --options typedoc.v1-site.json
# Copy the v1 markdown content (build copy only — the v1.x branch stays untouched).
cp docs/*.md "$V1_CONTENT/"
cp README.md "$V1_CONTENT/index.md"
# Rewrite links that don't resolve on the site:
# - source files -> GitHub
# - README links into docs/ -> site-local pages (docs/*.md sit next to index.md in content/)
sed -i "s|(src/examples/|(${V1_GITHUB}/src/examples/|g" "$V1_CONTENT/index.md"
sed -i "s|(LICENSE)|(${V1_GITHUB}/LICENSE)|g" "$V1_CONTENT/index.md"
sed -i "s|(docs/|(./|g" "$V1_CONTENT/index.md"
sed -i "s|(\.\./src/examples/|(${V1_GITHUB}/src/examples/|g" "$V1_CONTENT"/*.md
# Mechanical {@linkcode} -> backticks transform, mirroring the v2 docs transform.
# (The v1 docs currently contain none; this keeps the build robust if tags appear.)
node -e '
const fs = require("fs");
for (const f of process.argv.slice(1)) {
const src = fs.readFileSync(f, "utf8");
const out = src.replace(/\{@linkcode\s+([^}|]+?)(?:\s*\|\s*([^}]+?))?\s*\}/gs, (_m, path, label) =>
"`" + (label !== undefined ? label.replace(/\s+/g, " ").trim() : path.replace(/\s+/g, " ").trim().split(/[.#!/]/).pop()) + "`"
);
if (out !== src) fs.writeFileSync(f, out);
}
' "$V1_CONTENT"/*.md
# The v1.x guide pages start at "##" — the old typedoc shell supplied the page title,
# VitePress does not. Prepend a title heading (build copy only) so pages don't open
# with an orphaned section gap. index.md (the README) already has one.
for f in "$V1_CONTENT"/*.md; do
name="$(basename "$f" .md)"
[ "$name" = "index" ] && continue
head -n 1 "$f" | grep -q '^# ' && continue
case "$name" in
faq) title="FAQ" ;;
*) title="$(printf '%s' "${name:0:1}" | tr '[:lower:]' '[:upper:]')${name:1}" ;;
esac
if head -n 1 "$f" | grep -qi "^## *${title} *$"; then
# The page's first section heading already is the title — promote it.
sed -i '1s/^## */# /' "$f"
else
{ printf '# %s\n\n' "$title"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
fi
done
cd "$REPO_ROOT"
pnpm exec vitepress build docs/v1
cp -r "$REPO_ROOT/docs/v1/.vitepress/dist/." "$OUTPUT_DIR/"
echo "=== Combined docs generated at $OUTPUT_DIR ==="

View file

@ -1,115 +0,0 @@
#!/usr/bin/env bash
#
# Generate combined V1 + V2 TypeDoc documentation.
#
# V1 docs (from the v1.x branch) are placed at the root.
# V2 docs (from main) are placed under /v2/.
#
# Usage:
# scripts/generate-multidoc.sh [output-dir]
#
# Default output directory: tmp/docs-combined
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT_DIR="$(cd "$REPO_ROOT" && realpath -m "${1:-tmp/docs-combined}")"
V1_WORKTREE="$REPO_ROOT/.worktrees/v1-docs"
V2_WORKTREE="$REPO_ROOT/.worktrees/v2-docs"
cleanup() {
echo "Cleaning up worktrees..."
cd "$REPO_ROOT"
git worktree remove --force "$V1_WORKTREE" 2>/dev/null || true
git worktree remove --force "$V2_WORKTREE" 2>/dev/null || true
}
trap cleanup EXIT
rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# ---------------------------------------------------------------------------
# Step 1: Generate V1 docs from v1.x branch
# ---------------------------------------------------------------------------
echo "=== Generating V1 docs ==="
git fetch origin v1.x
git worktree remove --force "$V1_WORKTREE" 2>/dev/null || true
rm -rf "$V1_WORKTREE"
git worktree add "$V1_WORKTREE" "origin/v1.x" --detach
cd "$V1_WORKTREE"
npm install
npm install --save-dev typedoc@^0.28.14
cat > typedoc.json << 'TYPEDOC_EOF'
{
"name": "MCP TypeScript SDK",
"entryPoints": [
"src/client/index.ts",
"src/server/index.ts",
"src/shared/protocol.ts",
"src/shared/transport.ts",
"src/types.ts",
"src/inMemory.ts",
"src/validation/index.ts",
"src/experimental/index.ts"
],
"tsconfig": "tsconfig.json",
"out": "tmp/docs",
"exclude": [
"**/*.test.ts",
"**/__fixtures__/**",
"**/__mocks__/**",
"src/examples/**"
],
"projectDocuments": [
"docs/server.md",
"docs/client.md",
"docs/capabilities.md",
"docs/protocol.md",
"docs/faq.md"
],
"navigationLinks": {
"V2 Docs": "/v2/"
},
"headings": {
"readme": false
},
"skipErrorChecking": true
}
TYPEDOC_EOF
# Rewrite relative .ts links to point to GitHub source instead of media downloads
V1_GITHUB="https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x"
sed -i "s|(src/examples/|(${V1_GITHUB}/src/examples/|g" README.md
sed -i "s|(../src/examples/|(${V1_GITHUB}/src/examples/|g" docs/*.md
npx typedoc
cp -r "$V1_WORKTREE/tmp/docs/"* "$OUTPUT_DIR/"
# ---------------------------------------------------------------------------
# Step 2: Generate V2 docs from main branch
# ---------------------------------------------------------------------------
echo "=== Generating V2 docs ==="
git fetch origin main
git worktree remove --force "$V2_WORKTREE" 2>/dev/null || true
rm -rf "$V2_WORKTREE"
git worktree add "$V2_WORKTREE" "origin/main" --detach
cd "$V2_WORKTREE"
pnpm install
pnpm -r --filter='./packages/**' build
npx typedoc # outputs to tmp/docs/ per typedoc.config.mjs
mkdir -p "$OUTPUT_DIR/v2"
cp -r "$V2_WORKTREE/tmp/docs/"* "$OUTPUT_DIR/v2/"
cd "$REPO_ROOT"
echo "=== Combined docs generated at $OUTPUT_DIR ==="

View file

@ -47,7 +47,7 @@
*/
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
@ -484,6 +484,11 @@ function findMarkdownFiles(dir: string): string[] {
const files: string[] = [];
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
// Generated API reference markdown (docs/api/, produced by `pnpm docs:api`) is skipped — its
// fences carry source="..." attributes copied from JSDoc whose relative paths don't resolve
// from the generated location.
const generatedApiDir = join(dir, 'api') + sep;
for (const entry of entries) {
if (!entry.isFile()) continue;
@ -491,6 +496,8 @@ function findMarkdownFiles(dir: string): string[] {
if (!entry.name.endsWith('.md')) continue;
const fullPath = join(entry.parentPath, entry.name);
if (fullPath.startsWith(generatedApiDir)) continue;
files.push(fullPath);
}