Skip to main content
Glama
srcbookParser.ts•8.31 kB
/** * Srcbook Parser for ClearThought MCP Server * * Parses .src.md files following Srcbook format to extract cells * and make them available as MCP resources with embedded instructions. */ import { randomUUID } from "node:crypto"; import type { Token } from "marked"; import { marked } from "marked"; // Cell types matching Srcbook schema export interface TitleCell { id: string; type: "title"; text: string; } export interface MarkdownCell { id: string; type: "markdown"; text: string; } export interface PackageJsonCell { id: string; type: "package.json"; source: string; filename: "package.json"; } export interface CodeCell { id: string; type: "code"; source: string; language: "javascript" | "typescript"; filename: string; } export type Cell = TitleCell | MarkdownCell | PackageJsonCell | CodeCell; export interface SrcbookMetadata { language: "javascript" | "typescript"; "tsconfig.json"?: string; } export interface ParsedSrcbook { metadata: SrcbookMetadata; cells: Cell[]; title: string; } export interface SrcbookResource { uri: string; name: string; title?: string; description?: string; mimeType: string; annotations?: { audience?: ("user" | "assistant")[]; priority?: number; capabilities?: string[]; instructions?: any; metadata?: any; }; } const SRCBOOK_METADATA_RE = /^<!--\s*srcbook:(.+)\s*-->$/; /** * Parse a .src.md file into a structured Srcbook format */ export function parseSrcbook( contents: string, filename: string, ): ParsedSrcbook { // Parse markdown tokens const tokens = marked.lexer(contents); // Extract metadata const metadata = extractMetadata(tokens); // Extract cells const cells = extractCells(tokens, metadata.language); // Get title from first cell const titleCell = cells.find((c) => c.type === "title") as TitleCell; const title = titleCell?.text || filename.replace(".src.md", ""); return { metadata, cells, title, }; } /** * Extract Srcbook metadata from tokens */ function extractMetadata(tokens: Token[]): SrcbookMetadata { for (const token of tokens) { if (token.type !== "html") continue; const match = token.raw.trim().match(SRCBOOK_METADATA_RE); if (match) { try { return JSON.parse(match[1]); } catch (e) { console.error("Failed to parse srcbook metadata:", e); } } } // Default to JavaScript if no metadata found return { language: "javascript" }; } /** * Extract cells from markdown tokens */ function extractCells( tokens: Token[], language: "javascript" | "typescript", ): Cell[] { const cells: Cell[] = []; let currentMarkdown = ""; for (const token of tokens) { // Skip metadata comments if (token.type === "html" && token.raw.match(SRCBOOK_METADATA_RE)) { continue; } // Title cell (H1) if (token.type === "heading" && token.depth === 1) { if (currentMarkdown.trim()) { cells.push({ id: randomUUID(), type: "markdown", text: currentMarkdown.trim(), }); currentMarkdown = ""; } cells.push({ id: randomUUID(), type: "title", text: token.text, }); continue; } // Code cell with filename (H6 followed by code block) if (token.type === "heading" && token.depth === 6) { if (currentMarkdown.trim()) { cells.push({ id: randomUUID(), type: "markdown", text: currentMarkdown.trim(), }); currentMarkdown = ""; } // Look for the next code block const nextIndex = tokens.indexOf(token) + 1; if (nextIndex < tokens.length) { const nextToken = tokens[nextIndex]; if (nextToken && nextToken.type === "code") { const filename = token.text; if (filename === "package.json") { cells.push({ id: randomUUID(), type: "package.json", source: nextToken.text, filename: "package.json", }); } else { cells.push({ id: randomUUID(), type: "code", source: nextToken.text, language: filename.endsWith(".ts") ? "typescript" : language, filename, }); } // Skip the code block token since we've processed it tokens.splice(nextIndex, 1); } } continue; } // Regular code block without filename if (token.type === "code" && !token.lang?.startsWith("```")) { if (currentMarkdown.trim()) { cells.push({ id: randomUUID(), type: "markdown", text: currentMarkdown.trim(), }); currentMarkdown = ""; } // Only create a code cell if it's JS/TS code if ( !token.lang || ["javascript", "typescript", "js", "ts"].includes(token.lang) ) { cells.push({ id: randomUUID(), type: "code", source: token.text, language: token.lang === "typescript" || token.lang === "ts" ? "typescript" : language, filename: `cell-${cells.length}.${language === "typescript" ? "ts" : "js"}`, }); } else { // Non-JS/TS code blocks become part of markdown currentMarkdown += token.raw; } continue; } // Accumulate markdown content currentMarkdown += token.raw; } // Add any remaining markdown if (currentMarkdown.trim()) { cells.push({ id: randomUUID(), type: "markdown", text: currentMarkdown.trim(), }); } return cells; } /** * Convert a parsed Srcbook into MCP resource format */ export function srcbookToResource( srcbook: ParsedSrcbook, notebookName: string, ): SrcbookResource { const codeLanguage = srcbook.metadata.language; const cellCount = srcbook.cells.length; const codeCellCount = srcbook.cells.filter((c) => c.type === "code").length; return { uri: `notebook:///${notebookName}`, name: srcbook.title, title: `đź““ ${srcbook.title}`, description: `Interactive ${codeLanguage === "typescript" ? "TypeScript" : "JavaScript"} notebook with ${codeCellCount} code cells`, mimeType: "text/markdown", annotations: { audience: ["assistant"], priority: 0.8, capabilities: [ codeLanguage, "interactive-execution", "literate-programming", ], instructions: { execution: "Use mcp__ide__executeCode to run code cells", navigation: "Process cells sequentially for learning flow", cellExtraction: "Code cells are marked with ###### filename or are standalone code blocks", interaction: `Extract ${codeLanguage} code and execute with appropriate tool`, }, metadata: { language: codeLanguage, cellCount, codeCellCount, hasPackageJson: srcbook.cells.some((c) => c.type === "package.json"), hasTsConfig: !!srcbook.metadata["tsconfig.json"], }, }, }; } /** * Generate embedded resource for a specific cell */ export function cellToEmbeddedResource( cell: Cell, notebookName: string, cellIndex: number, ): any { if (cell.type === "code") { return { type: "resource", resource: { uri: `notebook:///${notebookName}#cell-${cellIndex}`, title: cell.filename, mimeType: "text/plain", text: cell.source, annotations: { audience: ["assistant"], instructions: `Execute this ${cell.language} code using mcp__ide__executeCode`, cellType: "code", language: cell.language, filename: cell.filename, }, }, }; } else if (cell.type === "markdown") { return { type: "resource", resource: { uri: `notebook:///${notebookName}#cell-${cellIndex}`, title: `Cell ${cellIndex}`, mimeType: "text/markdown", text: cell.text, annotations: { audience: ["assistant"], cellType: "markdown", }, }, }; } return null; } /** * Encode a Srcbook back to .src.md format */ export function encodeSrcbook(srcbook: ParsedSrcbook): string { const parts: string[] = []; // Add metadata parts.push(`<!-- srcbook:${JSON.stringify(srcbook.metadata)} -->`); // Add cells for (const cell of srcbook.cells) { switch (cell.type) { case "title": parts.push(`# ${cell.text}`); break; case "markdown": parts.push(cell.text); break; case "package.json": parts.push("###### package.json"); parts.push("```json"); parts.push(cell.source); parts.push("```"); break; case "code": parts.push(`###### ${cell.filename}`); parts.push(`\`\`\`${cell.language}`); parts.push(cell.source); parts.push("```"); break; } } return `${parts.join("\n\n")}\n`; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/waldzellai/clearthought-onepointfive'

If you have feedback or need assistance with the MCP directory API, please join our Discord server