Skip to main content
Glama
srcbookParser.js•9.19 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 { marked } from "marked"; const SRCBOOK_METADATA_RE = /^<!--\s*srcbook:(.+)\s*-->$/; /** * Parse a .src.md file into a structured Srcbook format */ export function parseSrcbook(contents, filename) { // 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"); const title = titleCell?.text || filename.replace(".src.md", ""); return { metadata, cells, title, }; } /** * Extract Srcbook metadata from tokens */ function extractMetadata(tokens) { 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, language) { const cells = []; 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, notebookName) { 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, notebookName, cellIndex) { 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) { const parts = []; // 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