Skip to main content
Glama
index.ts16.4 kB
import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { NotebookStateManager } from "./state.js"; import type { Cell, CodeLanguage } from "./types.js"; import { randomid } from "./types.js"; import { NOTEBOOK_OPERATIONS, getOperation, getOperationNames, getOperationsCatalog, } from "./operations.js"; import { AVAILABLE_TEMPLATES } from "./templates.generated.js"; /** * Notebook Server - MCP tool handlers for headless Srcbook notebooks */ export class NotebookServer { private stateManager: NotebookStateManager; constructor(tempDir?: string) { this.stateManager = new NotebookStateManager(tempDir); } async init(): Promise<void> { await this.stateManager.init(); } /** * Handle notebook_create tool call */ async handleCreateNotebook(args: any): Promise<any> { const { title, language, template } = args; if (!title || typeof title !== "string") { throw new Error("title is required and must be a string"); } if (!language || (language !== "javascript" && language !== "typescript")) { throw new Error('language must be "javascript" or "typescript"'); } // Validate template parameter if provided if (template !== undefined) { if (typeof template !== "string") { throw new Error("template must be a string"); } if (!AVAILABLE_TEMPLATES.includes(template as any)) { throw new Error( `Invalid template: "${template}". Available templates: ${AVAILABLE_TEMPLATES.join(", ")}` ); } } let notebook: any; // Create from template if provided if (template) { notebook = await this.stateManager.createNotebookFromTemplate( title, language as CodeLanguage, template ); } else { // Create blank notebook notebook = await this.stateManager.createNotebook( title, language as CodeLanguage ); } return { success: true, notebook: { id: notebook.id, title: notebook.cells.find((c: Cell) => c.type === "title")?.text || title, language: notebook.language, cellCount: notebook.cells.length, createdAt: notebook.createdAt, }, }; } /** * Handle notebook_list tool call */ async handleListNotebooks(): Promise<any> { const notebooks = this.stateManager.listNotebooks(); return { success: true, notebooks: notebooks.map((nb) => ({ id: nb.id, title: nb.cells.find((c) => c.type === "title")?.text || "Untitled", language: nb.language, cellCount: nb.cells.length, createdAt: nb.createdAt, updatedAt: nb.updatedAt, })), }; } /** * Handle notebook_load tool call */ async handleLoadNotebook(args: any): Promise<any> { const { path, content } = args; // Validate: exactly one of path or content required const hasPath = path !== undefined && typeof path === "string"; const hasContent = content !== undefined && typeof content === "string"; if (!hasPath && !hasContent) { throw new Error("Either 'path' or 'content' parameter is required"); } if (hasPath && hasContent) { throw new Error( "Cannot provide both 'path' and 'content' parameters. Choose one." ); } // Load notebook from appropriate source const notebook = await this.stateManager.loadNotebook( hasPath ? { path } : { content } ); return { success: true, notebook: { id: notebook.id, title: notebook.cells.find((c) => c.type === "title")?.text || "Untitled", language: notebook.language, cellCount: notebook.cells.length, createdAt: notebook.createdAt, }, }; } /** * Handle notebook_add_cell tool call */ async handleAddCell(args: any): Promise<any> { const { notebookId, cellType, content, filename, position } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } if (!cellType || typeof cellType !== "string") { throw new Error("cellType is required and must be a string"); } const notebook = this.stateManager.getNotebook(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } let cell: Cell; switch (cellType) { case "title": if (!content) throw new Error("content is required for title cells"); cell = { id: randomid(), type: "title", text: content, }; break; case "markdown": if (!content) throw new Error("content is required for markdown cells"); cell = { id: randomid(), type: "markdown", text: content, }; break; case "code": if (!content) throw new Error("content is required for code cells"); if (!filename) throw new Error("filename is required for code cells"); cell = { id: randomid(), type: "code", language: notebook.language, filename, source: content, status: "idle", }; break; default: throw new Error(`Unsupported cell type: ${cellType}`); } await this.stateManager.addCell(notebookId, cell, position); return { success: true, cell: { id: cell.id, type: cell.type, }, }; } /** * Handle notebook_update_cell tool call */ async handleUpdateCell(args: any): Promise<any> { const { notebookId, cellId, content } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } if (!cellId || typeof cellId !== "string") { throw new Error("cellId is required and must be a string"); } if (!content || typeof content !== "string") { throw new Error("content is required and must be a string"); } const cell = this.stateManager.getCell(notebookId, cellId); if (!cell) { throw new Error(`Cell ${cellId} not found in notebook ${notebookId}`); } const updates: any = {}; if (cell.type === "title" || cell.type === "markdown") { updates.text = content; } else if (cell.type === "code" || cell.type === "package.json") { updates.source = content; } await this.stateManager.updateCell(notebookId, cellId, updates); return { success: true, cell: { id: cellId, type: cell.type, }, }; } /** * Handle notebook_run_cell tool call */ async handleRunCell(args: any): Promise<any> { const { notebookId, cellId } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } if (!cellId || typeof cellId !== "string") { throw new Error("cellId is required and must be a string"); } const result = await this.stateManager.executeCell(notebookId, cellId); return { success: result.success, execution: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, success: result.success, }, }; } /** * Handle notebook_install_deps tool call */ async handleInstallDeps(args: any): Promise<any> { const { notebookId } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } const notebook = this.stateManager.getNotebook(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } // Find package.json cell const pkgCell = notebook.cells.find((c) => c.type === "package.json"); if (!pkgCell) { throw new Error("No package.json cell found in notebook"); } const result = await this.stateManager.executeCell(notebookId, pkgCell.id); return { success: result.success, execution: { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, success: result.success, }, }; } /** * Handle notebook_list_cells tool call */ async handleListCells(args: any): Promise<any> { const { notebookId } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } const notebook = this.stateManager.getNotebook(notebookId); if (!notebook) { throw new Error(`Notebook ${notebookId} not found`); } return { success: true, cells: notebook.cells.map((cell) => { const base = { id: cell.id, type: cell.type, }; if (cell.type === "title" || cell.type === "markdown") { return { ...base, text: cell.text }; } else if (cell.type === "code") { return { ...base, filename: cell.filename, language: cell.language, status: cell.status, hasOutput: !!cell.output, hasError: !!cell.error, }; } else if (cell.type === "package.json") { return { ...base, filename: cell.filename, status: cell.status, }; } return base; }), }; } /** * Handle notebook_get_cell tool call */ async handleGetCell(args: any): Promise<any> { const { notebookId, cellId } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } if (!cellId || typeof cellId !== "string") { throw new Error("cellId is required and must be a string"); } const cell = this.stateManager.getCell(notebookId, cellId); if (!cell) { throw new Error(`Cell ${cellId} not found in notebook ${notebookId}`); } return { success: true, cell, }; } /** * Handle notebook_export tool call */ async handleExportNotebook(args: any): Promise<any> { const { notebookId, path } = args; if (!notebookId || typeof notebookId !== "string") { throw new Error("notebookId is required and must be a string"); } if (path !== undefined && typeof path !== "string") { throw new Error("path must be a string if provided"); } // Always get content, optionally write to path const content = await this.stateManager.exportNotebook(notebookId, path); // Build response const response: any = { success: true, content, // Always include content }; // If path was provided, include it in response if (path) { response.path = path; } return response; } /** * Process MCP tool call (Toolhost dispatcher pattern) */ async processTool(operation: string, args: any): Promise<any> { try { let result: any; // Get operation definition for resource embedding const opDef = getOperation(operation); switch (operation) { case "create": result = await this.handleCreateNotebook(args); break; case "list": result = await this.handleListNotebooks(); break; case "load": result = await this.handleLoadNotebook(args); break; case "add_cell": result = await this.handleAddCell(args); break; case "update_cell": result = await this.handleUpdateCell(args); break; case "run_cell": result = await this.handleRunCell(args); break; case "install_deps": result = await this.handleInstallDeps(args); break; case "list_cells": result = await this.handleListCells(args); break; case "get_cell": result = await this.handleGetCell(args); break; case "export": result = await this.handleExportNotebook(args); break; default: throw new Error(`Unknown notebook operation: ${operation}`); } // Build response with embedded operation resource const content: Array<any> = [ { type: "text", text: JSON.stringify(result, null, 2), }, ]; // Embed operation details as resource if (opDef) { content.push({ type: "resource", resource: { uri: `thoughtbox://notebook/operations/${operation}`, title: opDef.title, mimeType: "application/json", text: JSON.stringify(opDef, null, 2), annotations: { audience: ["assistant"], priority: 0.5, }, }, }); } return { content, isError: false, }; } catch (error) { return { content: [ { type: "text", text: JSON.stringify( { success: false, error: error instanceof Error ? error.message : String(error), }, null, 2 ), }, ], isError: true, }; } } /** * Get operations catalog as JSON */ getOperationsCatalog(): string { return getOperationsCatalog(); } /** * Get server status */ getStatus(): any { const notebooks = this.stateManager.listNotebooks(); return { status: "healthy", notebooks: { count: notebooks.length, active: notebooks.filter((nb) => nb.cells.some((c) => c.type === "code" && (c as any).status === "running")).length, }, timestamp: Date.now(), }; } } /** * MCP Tool definition for notebook toolhost dispatcher */ export const NOTEBOOK_TOOL: Tool = { name: "notebook", description: `Notebook toolhost for literate programming with JavaScript/TypeScript. Create, manage, and execute interactive notebooks with markdown documentation and executable code cells. Each notebook runs in an isolated environment with its own package.json and workspace. ✨ NEW: Pre-structured templates for guided workflows - Use template: "sequential-feynman" for deep learning with Feynman Technique - Templates provide scaffolded cells, metacognitive prompts, and progress tracking - Perfect for complex topics requiring validated understanding Available operations: - create: Create a new notebook (optionally from template) - list: List all active notebooks - load: Load notebook from .src.md file - add_cell: Add cell (title/markdown/code) - update_cell: Update cell content - run_cell: Execute code cell - install_deps: Install npm dependencies - list_cells: List all cells in notebook - get_cell: Get cell details - export: Export notebook to .src.md Common operation examples: Create a blank notebook: { operation: "create", args: { title: "My Analysis", language: "typescript" } } Create from Sequential Feynman template: { operation: "create", args: { title: "React Server Components", language: "typescript", template: "sequential-feynman" } } Add a code cell: { operation: "add_cell", args: { notebookId: "abc123", cellType: "code", content: "console.log('hello')", filename: "example.ts" } } Run a cell: { operation: "run_cell", args: { notebookId: "abc123", cellId: "cell_456" } } List notebooks: { operation: "list", args: {} } For detailed schemas of all operations, see the thoughtbox://notebook/operations resource. When to use: - Writing executable documentation - Building reproducible code examples - Creating step-by-step tutorials - Developing and testing code snippets - Prototyping with immediate feedback - Deep learning workflows (with templates)`, inputSchema: { type: "object", properties: { operation: { type: "string", enum: getOperationNames(), description: "The notebook operation to execute", }, args: { type: "object", description: "Arguments for the operation (varies by operation)", }, }, required: ["operation"], }, annotations: {}, _meta: { available_operations: getOperationNames(), docs: "thoughtbox://notebook/operations", quickstart: "prompt://list_mcp_assets", }, };

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/glassBead-tc/Thoughtbox'

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