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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
606 changes: 606 additions & 0 deletions bench/results.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs-v2/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.mdx linguist-vendored
13 changes: 13 additions & 0 deletions docs-v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
dist/
.astro/
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.env
.env.production
.DS_Store
.idea/
teaser.pptx
~$teaser.pptx
12 changes: 12 additions & 0 deletions docs-v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Anchor docs

Astro site that builds the v1 and v2 Anchor documentation.

### Develop

```bash
bun install
bun run dev
```

The dev server is on `http://localhost:4321/docs/`.
118 changes: 118 additions & 0 deletions docs-v2/api/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// docs using vercel's justbash
import { Bash } from 'just-bash'

type Doc = { id: string; title: string; description: string; url: string; body: string }

const PROTOCOL_VERSION = '2025-06-18'
const cache = new Map<string, Doc[]>()

async function getDocs(origin: string): Promise<Doc[]> {
if (!cache.has(origin)) {
const url = process.env.MCP_INDEX_URL ?? `${origin}/docs/search-index.json`
cache.set(origin, await fetch(url).then((r) => r.json()))
}
return cache.get(origin)!
}

const TOOLS = [
{
name: 'search_anchor_docs',
description: 'Search the Anchor (anchor-lang) docs. Returns matching pages with title, path, url, snippet.',
inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
},
{
name: 'read_anchor_doc',
description: 'Read the full Markdown of an Anchor docs page by its path (from search results), e.g. "v2/fundamentals/pda".',
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
},
{
name: 'query_docs_filesystem_anchor',
description:
'Run a read-only shell command (rg, grep, find, tree, ls, cat, head, sed, awk, jq, pipes, &&) against an in-memory filesystem of the Anchor docs rooted at /. Pages are at /<path>.mdx. No network, no persisted writes.',
inputSchema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
},
]

function search(docs: Doc[], query: string, limit = 8) {
const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
return docs
.map((d) => {
const hay = `${d.title}\n${d.description}\n${d.body}`.toLowerCase()
let score = 0
for (const t of terms) {
if (d.title.toLowerCase().includes(t)) score += 10
if (d.description.toLowerCase().includes(t)) score += 4
score += Math.min(hay.split(t).length - 1, 5)
}
return { d, score }
})
.filter((r) => r.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map((r) => r.d)
}

async function shell(docs: Doc[], command: string): Promise<string> {
const files = Object.fromEntries(docs.map((d) => [`/${d.id}.mdx`, `Title: ${d.title}\n${d.description}\n\n${d.body}`]))
//this is a fake shell to make it easier for the agent to interact, nothing too fancy
const { stdout, stderr, exitCode } = await new Bash({
files,
cwd: '/',
executionLimits: { maxCommandCount: 200, maxLoopIterations: 10_000 },
}).exec(command)
return `exit: ${exitCode}` + (stdout ? `\n--- stdout ---\n${stdout}` : '') + (stderr ? `\n--- stderr ---\n${stderr}` : '')
}

const ok = (id: unknown, result: unknown) =>
Response.json({ jsonrpc: '2.0', id, result }, { headers: { 'Cache-Control': 'no-store' } })
const fail = (id: unknown, code: number, message: string) =>
Response.json({ jsonrpc: '2.0', id, error: { code, message } })

export async function POST(request: Request) {
let m: any
try {
m = await request.json()
} catch {
return fail(null, -32700, 'Parse error')
}
if (m.id === undefined || m.id === null) return new Response(null, { status: 202 }) // notification

if (m.method === 'initialize')
return ok(m.id, {
protocolVersion: PROTOCOL_VERSION,
capabilities: { tools: { listChanged: false } },
serverInfo: { name: 'Anchor Docs', version: '1.0.0' },
})
if (m.method === 'ping') return ok(m.id, {})
if (m.method === 'tools/list') return ok(m.id, { tools: TOOLS })
if (m.method === 'tools/call') {
const { name, arguments: args = {} } = m.params ?? {}
const docs = await getDocs(new URL(request.url).origin)

if (name === 'search_anchor_docs') {
const q = String(args.query ?? '')
const hits = search(docs, q)
const text = hits.length
? hits.map((d) => `## ${d.title}\npath: ${d.id}\nurl: ${d.url}\n${d.description || d.body.slice(0, 200)}`).join('\n\n')
: `No results for "${q}".`
return ok(m.id, { content: [{ type: 'text', text }] })
}
if (name === 'read_anchor_doc') {
const path = String(args.path ?? '').replace(/^\/+|\/+$|\.mdx?$/g, '')
const d = docs.find((x) => x.id === path || x.id === `${path}/index`)
return ok(m.id, {
content: [{ type: 'text', text: d ? `# ${d.title}\n${d.url}\n\n${d.body}` : `Not found: ${path}` }],
isError: !d,
})
}
if (name === 'query_docs_filesystem_anchor')
return ok(m.id, { content: [{ type: 'text', text: await shell(docs, String(args.command ?? '')) }] })

return fail(m.id, -32602, `Unknown tool: ${name}`)
}
return fail(m.id, -32601, `Method not found: ${m.method}`)
}

export function GET() {
return new Response('Method Not Allowed', { status: 405, headers: { Allow: 'POST' } })
}
152 changes: 152 additions & 0 deletions docs-v2/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { rehypeHeadingIds } from '@astrojs/markdown-remark'
import mdx from '@astrojs/mdx'
import react from '@astrojs/react'
import sitemap from '@astrojs/sitemap'
import rehypeShiki from '@shikijs/rehype'
import tailwindcss from '@tailwindcss/vite'
import pagefind from 'astro-pagefind'
import { defineConfig } from 'astro/config'
import { readFile } from 'node:fs/promises'
import { extname, isAbsolute, relative, resolve } from 'node:path'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeExpressiveCode from 'rehype-expressive-code'
import rehypeExternalLinks from 'rehype-external-links'
import rehypeKatex from 'rehype-katex'
import remarkEmoji from 'remark-emoji'
import remarkMath from 'remark-math'
import { expressiveCodeOptions } from './src/lib/expressive-code-config'
import { rehypeCodeAnnotations } from './src/lib/rehype-code-annotations'
import { rehypeCodePathHints, rehypeCodePathIcons } from './src/lib/rehype-code-paths'
import { rehypeCopyableShellCommands } from './src/lib/rehype-copyable-shell-commands'
import { rehypeLinkIcons } from './src/lib/rehype-link-icons'
import { rehypeTableWrappers } from './src/lib/rehype-table-wrappers'
import { darkTheme, lightTheme } from './src/lib/shiki-themes'

type DevMiddleware = (
req: { url?: string },
res: { setHeader(name: string, value: string): void; end(data: Uint8Array): void },
next: () => void,
) => void | Promise<void>

type DevServer = {
middlewares: {
use(path: string, handler: DevMiddleware): void
}
}

const DOCS_BASE = '/docs'
const PAGEFIND_DIST_DIR = resolve(process.cwd(), 'dist', 'docs', 'pagefind')

function pagefindDevServer() {
const mime: Record<string, string> = {
js: 'application/javascript',
mjs: 'application/javascript',
css: 'text/css',
json: 'application/json',
wasm: 'application/wasm',
}

return {
name: 'pagefind-dev-server',
enforce: 'pre' as const,
apply: 'serve' as const,
configureServer(server: DevServer) {
server.middlewares.use('/pagefind', async (req, res, next) => {
const filePath = resolvePagefindAsset(req.url ?? '/')
if (!filePath) return next()

try {
const data = await readFile(filePath)
const ext = extname(filePath).slice(1)
if (mime[ext]) res.setHeader('Content-Type', mime[ext])
res.end(data)
} catch {
next()
}
})
},
}
}

function resolvePagefindAsset(url: string): string | null {
let pathname: string
try {
pathname = decodeURIComponent(url.split('?')[0] ?? '/')
} catch {
return null
}

if (!pathname || pathname === '/') return null

const filePath = resolve(PAGEFIND_DIST_DIR, `.${pathname}`)
const relativePath = relative(PAGEFIND_DIST_DIR, filePath)

if (relativePath.startsWith('..') || isAbsolute(relativePath)) return null

return filePath
}

export default defineConfig({
site: 'https://www.anchor-lang.com',
base: DOCS_BASE,
trailingSlash: 'always',
outDir: './dist/docs',
integrations: [mdx(), react(), sitemap(), pagefind()],
vite: {
plugins: [tailwindcss(), pagefindDevServer()],
},
server: {
port: 4321,
host: true,
},
devToolbar: {
enabled: false,
},
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypeExternalLinks,
{
target: '_blank',
rel: ['nofollow', 'noreferrer', 'noopener'],
},
],
rehypeTableWrappers,
rehypeKatex,
[rehypeExpressiveCode, { themes: [lightTheme, darkTheme], ...expressiveCodeOptions }],
rehypeCodePathHints,
[
rehypeShiki,
{
themes: { light: lightTheme, dark: darkTheme },
inline: 'tailing-curly-colon',
},
],
rehypeCopyableShellCommands,
rehypeCodeAnnotations,
rehypeCodePathIcons,
rehypeLinkIcons,
rehypeHeadingIds,
[
rehypeAutolinkHeadings,
{
behavior: 'append',
properties: {
className: ['heading-anchor'],
'aria-label': 'Link to section',
tabindex: -1,
'data-pagefind-ignore': '',
},
content: {
type: 'text',
value: '#',
},
test: (node: { tagName: string }) =>
['h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName),
},
],
],
remarkPlugins: [remarkMath, remarkEmoji],
},
})
Loading
Loading