#!/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();