#!/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,
Tool,
} 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.1.0",
},
{
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.1.0`);
log.info(` Node: ${process.version}`);
log.info(` Platform: ${process.platform}`);
}
/**
* Return notebook IDs matching the provided input (case-insensitive contains)
*/
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
*/
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: any[] = [
{
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)
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 any)?._meta?.progressToken;
log.info(`π§ [MCP] Tool call: ${name}`);
if (progressToken) {
log.info(` π Progress token: ${progressToken}`);
}
// Create progress callback function
const sendProgress = async (message: string, progress?: number, total?: number) => {
if (progressToken) {
await this.server.notification({
method: "notifications/progress",
params: {
progressToken,
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 "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 "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.0.0 β");
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();