Skip to main content
Glama
index.tsβ€’20.6 kB
#!/usr/bin/env node /** * NotebookLM MCP Server * * MCP Server for Google NotebookLM - Chat with Gemini 2.5 through NotebookLM * with session support and human-like behavior! * * Features: * - Session-based contextual conversations * - Auto re-login on session expiry * - Human-like typing and mouse movements * - Persistent browser fingerprint * - Stealth mode with Patchright * - Claude Code integration via npx * * Usage: * npx notebooklm-mcp * node dist/index.js * * Environment Variables: * NOTEBOOK_URL - Default NotebookLM notebook URL * AUTO_LOGIN_ENABLED - Enable automatic login (true/false) * LOGIN_EMAIL - Google email for auto-login * LOGIN_PASSWORD - Google password for auto-login * HEADLESS - Run browser in headless mode (true/false) * MAX_SESSIONS - Maximum concurrent sessions (default: 10) * SESSION_TIMEOUT - Session timeout in seconds (default: 900) * * Based on the Python NotebookLM API implementation */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, // CompleteRequestSchema, // Disabled for Claude Desktop compatibility Tool, Resource, } from '@modelcontextprotocol/sdk/types.js'; import { AuthManager } from './auth/auth-manager.js'; import { SessionManager } from './session/session-manager.js'; import { NotebookLibrary } from './library/notebook-library.js'; import { ToolHandlers, buildToolDefinitions } from './tools/index.js'; import { CONFIG } from './config.js'; import { log } from './utils/logger.js'; /** * Main MCP Server Class */ class NotebookLMMCPServer { private server: Server; private authManager: AuthManager; private sessionManager: SessionManager; private library: NotebookLibrary; private toolHandlers: ToolHandlers; private toolDefinitions: Tool[]; constructor() { // Initialize MCP Server this.server = new Server( { name: 'notebooklm-mcp', version: '1.3.5', }, { capabilities: { tools: {}, resources: {}, resourceTemplates: {}, }, } ); // Initialize managers this.authManager = new AuthManager(); this.sessionManager = new SessionManager(this.authManager); this.library = new NotebookLibrary(); this.toolHandlers = new ToolHandlers(this.sessionManager, this.authManager, this.library); // Build tool definitions with library context this.toolDefinitions = buildToolDefinitions(this.library) as Tool[]; // Setup handlers this.setupHandlers(); this.setupShutdownHandlers(); log.info('πŸš€ NotebookLM MCP Server initialized'); log.info(` Version: 1.3.5`); log.info(` Node: ${process.version}`); log.info(` Platform: ${process.platform}`); } /** * Return notebook IDs matching the provided input (case-insensitive contains) * Note: Disabled for Claude Desktop compatibility */ // private completeNotebookIds(input: unknown): string[] { // const query = String(input ?? "").toLowerCase(); // return this.library // .listNotebooks() // .map((nb) => nb.id) // .filter((id) => id.toLowerCase().includes(query)) // .slice(0, 50); // } /** * Build a completion payload for MCP responses * Note: Disabled for Claude Desktop compatibility */ // private buildCompletion(values: string[]) { // return { // completion: { // values, // total: values.length, // }, // }; // } /** * Setup MCP request handlers */ private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { log.info('πŸ“‹ [MCP] list_tools request received'); return { tools: this.toolDefinitions, }; }); // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { log.info('πŸ“š [MCP] list_resources request received'); const notebooks = this.library.listNotebooks(); const resources: Resource[] = [ { uri: 'notebooklm://library', name: 'Notebook Library', description: 'Complete notebook library with all available knowledge sources. ' + 'Read this to discover what notebooks are available. ' + "⚠️ If you think a notebook might help with the user's task, " + 'ASK THE USER FOR PERMISSION before consulting it: ' + "'Should I consult the [notebook] for this task?'", mimeType: 'application/json', }, ]; // Add individual notebook resources for (const notebook of notebooks) { resources.push({ uri: `notebooklm://library/${notebook.id}`, name: notebook.name, description: `${notebook.description} | Topics: ${notebook.topics.join(', ')} | ` + `πŸ’‘ Use ask_question to query this notebook (ask user permission first if task isn't explicitly about these topics)`, mimeType: 'application/json', }); } // Add legacy metadata resource for backwards compatibility const active = this.library.getActiveNotebook(); if (active) { resources.push({ uri: 'notebooklm://metadata', name: 'Active Notebook Metadata (Legacy)', description: 'Information about the currently active notebook. ' + 'DEPRECATED: Use notebooklm://library instead for multi-notebook support. ' + "⚠️ Always ask user permission before using notebooks for tasks they didn't explicitly mention.", mimeType: 'application/json', }); } return { resources }; }); // List resource templates this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { log.info('πŸ“‘ [MCP] list_resource_templates request received'); return { resourceTemplates: [ { uriTemplate: 'notebooklm://library/{id}', name: 'Notebook by ID', description: 'Access a specific notebook from your library by ID. ' + 'Provides detailed metadata about the notebook including topics, use cases, and usage statistics. ' + "πŸ’‘ Use the 'id' parameter from list_notebooks to access specific notebooks.", mimeType: 'application/json', }, ], }; }); // Read resource content this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; log.info(`πŸ“– [MCP] read_resource request: ${uri}`); // Handle library resource if (uri === 'notebooklm://library') { const notebooks = this.library.listNotebooks(); const stats = this.library.getStats(); const active = this.library.getActiveNotebook(); const libraryData = { active_notebook: active ? { id: active.id, name: active.name, description: active.description, topics: active.topics, } : null, notebooks: notebooks.map((nb) => ({ id: nb.id, name: nb.name, description: nb.description, topics: nb.topics, content_types: nb.content_types, use_cases: nb.use_cases, url: nb.url, use_count: nb.use_count, last_used: nb.last_used, tags: nb.tags, })), stats, }; return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(libraryData, null, 2), }, ], }; } // Handle individual notebook resource if (uri.startsWith('notebooklm://library/')) { const prefix = 'notebooklm://library/'; const encodedId = uri.slice(prefix.length); if (!encodedId) { throw new Error('Notebook resource requires an ID (e.g. notebooklm://library/{id})'); } let id: string; try { id = decodeURIComponent(encodedId); } catch { throw new Error(`Invalid notebook identifier encoding: ${encodedId}`); } if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(id)) { throw new Error( `Invalid notebook identifier: ${encodedId}. Notebook IDs may only contain letters, numbers, and hyphens.` ); } const notebook = this.library.getNotebook(id); if (!notebook) { throw new Error(`Notebook not found: ${id}`); } return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(notebook, null, 2), }, ], }; } // Legacy metadata resource (backwards compatibility) if (uri === 'notebooklm://metadata') { const active = this.library.getActiveNotebook(); if (!active) { throw new Error('No active notebook. Use notebooklm://library to see all notebooks.'); } const metadata = { description: active.description, topics: active.topics, content_types: active.content_types, use_cases: active.use_cases, notebook_url: active.url, notebook_id: active.id, last_used: active.last_used, use_count: active.use_count, note: 'DEPRECATED: Use notebooklm://library or notebooklm://library/{id} instead', }; return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(metadata, null, 2), }, ], }; } throw new Error(`Unknown resource: ${uri}`); }); // Argument completions (for prompt arguments) // Note: Completions are optional and not supported by all MCP clients (e.g., Claude Desktop) // We skip registering this handler to maintain compatibility // this.server.setRequestHandler(CompleteRequestSchema, async (request) => { // const { ref, argument } = request.params as any; // try { // if (ref?.type === "ref/resource") { // // Complete variables for resource templates // const uri = String(ref.uri || ""); // // Notebook by ID template // if (uri === "notebooklm://library/{id}" && argument?.name === "id") { // const values = this.completeNotebookIds(argument?.value); // return this.buildCompletion(values) as any; // } // } // } catch (e) { // log.warning(`⚠️ [MCP] completion error: ${e}`); // } // return { completion: { values: [], total: 0 } } as any; // }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const progressToken = (args as Record<string, unknown> | undefined)?._meta as | { progressToken?: string } | undefined; const token = progressToken?.progressToken; log.info(`πŸ”§ [MCP] Tool call: ${name}`); if (token) { log.info(` πŸ“Š Progress token: ${token}`); } // Create progress callback function const sendProgress = async (message: string, progress?: number, total?: number) => { if (token) { await this.server.notification({ method: 'notifications/progress', params: { progressToken: token, message, ...(progress !== undefined && { progress }), ...(total !== undefined && { total }), }, }); log.dim(` πŸ“Š Progress: ${message}`); } }; try { let result; switch (name) { case 'ask_question': result = await this.toolHandlers.handleAskQuestion( args as { question: string; session_id?: string; notebook_id?: string; notebook_url?: string; show_browser?: boolean; }, sendProgress ); break; case 'auto_discover_notebook': result = await this.toolHandlers.handleAutoDiscoverNotebook(args as { url: string }); break; case 'add_notebook': result = await this.toolHandlers.handleAddNotebook( args as { url: string; name: string; description: string; topics: string[]; content_types?: string[]; use_cases?: string[]; tags?: string[]; } ); break; case 'list_notebooks': result = await this.toolHandlers.handleListNotebooks(); break; case 'get_notebook': result = await this.toolHandlers.handleGetNotebook(args as { id: string }); break; case 'select_notebook': result = await this.toolHandlers.handleSelectNotebook(args as { id: string }); break; case 'update_notebook': result = await this.toolHandlers.handleUpdateNotebook( args as { id: string; name?: string; description?: string; topics?: string[]; content_types?: string[]; use_cases?: string[]; tags?: string[]; url?: string; } ); break; case 'remove_notebook': result = await this.toolHandlers.handleRemoveNotebook(args as { id: string }); break; case 'search_notebooks': result = await this.toolHandlers.handleSearchNotebooks(args as { query: string }); break; case 'get_library_stats': result = await this.toolHandlers.handleGetLibraryStats(); break; case 'list_sessions': result = await this.toolHandlers.handleListSessions(); break; case 'close_session': result = await this.toolHandlers.handleCloseSession(args as { session_id: string }); break; case 'reset_session': result = await this.toolHandlers.handleResetSession(args as { session_id: string }); break; case 'get_health': result = await this.toolHandlers.handleGetHealth(); break; case 'setup_auth': result = await this.toolHandlers.handleSetupAuth( args as { show_browser?: boolean }, sendProgress ); break; case 'de_auth': result = await this.toolHandlers.handleDeAuth(); break; case 're_auth': result = await this.toolHandlers.handleReAuth( args as { show_browser?: boolean }, sendProgress ); break; case 'cleanup_data': result = await this.toolHandlers.handleCleanupData(args as { confirm: boolean }); break; default: log.error(`❌ [MCP] Unknown tool: ${name}`); return { content: [ { type: 'text', text: JSON.stringify( { success: false, error: `Unknown tool: ${name}`, }, null, 2 ), }, ], }; } // Return result return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); log.error(`❌ [MCP] Tool execution error: ${errorMessage}`); return { content: [ { type: 'text', text: JSON.stringify( { success: false, error: errorMessage, }, null, 2 ), }, ], }; } }); } /** * Setup graceful shutdown handlers */ private setupShutdownHandlers(): void { let shuttingDown = false; const shutdown = async (signal: string) => { if (shuttingDown) { return; } shuttingDown = true; log.info(`\nπŸ›‘ Received ${signal}, shutting down gracefully...`); try { // Cleanup tool handlers (closes all sessions) await this.toolHandlers.cleanup(); // Close server await this.server.close(); log.success('βœ… Shutdown complete'); process.exit(0); } catch (error) { log.error(`❌ Error during shutdown: ${error}`); process.exit(1); } }; const requestShutdown = (signal: string) => { void shutdown(signal); }; process.on('SIGINT', () => requestShutdown('SIGINT')); process.on('SIGTERM', () => requestShutdown('SIGTERM')); process.on('uncaughtException', (error) => { log.error(`πŸ’₯ Uncaught exception: ${error}`); log.error(error.stack || ''); requestShutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { log.error(`πŸ’₯ Unhandled rejection at: ${promise}`); log.error(`Reason: ${reason}`); requestShutdown('unhandledRejection'); }); } /** * Start the MCP server */ async start(): Promise<void> { log.info('🎯 Starting NotebookLM MCP Server...'); log.info(''); log.info('πŸ“ Configuration:'); log.info(` Config Dir: ${CONFIG.configDir}`); log.info(` Data Dir: ${CONFIG.dataDir}`); log.info(` Headless: ${CONFIG.headless}`); log.info(` Max Sessions: ${CONFIG.maxSessions}`); log.info(` Session Timeout: ${CONFIG.sessionTimeout}s`); log.info(` Stealth: ${CONFIG.stealthEnabled}`); log.info(''); // Create stdio transport const transport = new StdioServerTransport(); // Connect server to transport await this.server.connect(transport); log.success('βœ… MCP Server connected via stdio'); log.success('πŸŽ‰ Ready to receive requests from Claude Code!'); log.info(''); log.info('πŸ’‘ Available tools:'); for (const tool of this.toolDefinitions) { const desc = tool.description ? tool.description.split('\n')[0] : 'No description'; // First line only log.info(` - ${tool.name}: ${desc.substring(0, 80)}...`); } log.info(''); log.info('πŸ“– For documentation, see: README.md'); log.info('πŸ“– For MCP details, see: MCP_INFOS.md'); log.info(''); } } /** * Main entry point */ async function main() { // Print banner console.error('╔══════════════════════════════════════════════════════════╗'); console.error('β•‘ β•‘'); console.error('β•‘ NotebookLM MCP Server v1.3.5 β•‘'); console.error('β•‘ β•‘'); console.error('β•‘ Chat with Gemini 2.5 through NotebookLM via MCP β•‘'); console.error('β•‘ β•‘'); console.error('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•'); console.error(''); try { const server = new NotebookLMMCPServer(); await server.start(); } catch (error) { log.error(`πŸ’₯ Fatal error starting server: ${error}`); if (error instanceof Error) { log.error(error.stack || ''); } process.exit(1); } } // Run the server main();

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/roomi-fields/notebooklm-mcp'

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