/**
* HTTP Wrapper for NotebookLM MCP Server
*
* Exposes the MCP server via HTTP REST API
* Allows n8n and other tools to call the server without stdio
*/
import express, { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
import { AuthManager } from './auth/auth-manager.js';
import { SessionManager } from './session/session-manager.js';
import { NotebookLibrary } from './library/notebook-library.js';
import { ToolHandlers } from './tools/index.js';
import { AutoDiscovery } from './auto-discovery/auto-discovery.js';
import { StartupManager } from './startup/startup-manager.js';
import { log } from './utils/logger.js';
// Extend Express Request to include requestId
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
requestId: string;
}
}
}
const app = express();
app.use(express.json({ limit: '10mb' }));
// Request ID middleware for debugging and log correlation
app.use((req: Request, res: Response, next: NextFunction) => {
// Use existing X-Request-ID header or generate a new one
const requestId = (req.headers['x-request-id'] as string) || randomUUID();
req.requestId = requestId;
res.setHeader('X-Request-ID', requestId);
next();
});
// CORS for n8n
app.use((_req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (_req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// Initialize managers
const authManager = new AuthManager();
const sessionManager = new SessionManager(authManager);
const library = new NotebookLibrary(sessionManager);
const toolHandlers = new ToolHandlers(sessionManager, authManager, library);
// Root endpoint - API info
app.get('/', (_req: Request, res: Response) => {
res.json({
name: 'NotebookLM MCP HTTP Server',
version: process.env.npm_package_version || '1.5.2',
endpoints: {
health: 'GET /health',
ask: 'POST /ask',
setup_auth: 'POST /setup-auth',
notebooks: 'GET /notebooks',
sessions: 'GET /sessions',
},
docs: 'https://github.com/carterlasalle/notebooklm-mcp',
});
});
// Health check
app.get('/health', async (_req: Request, res: Response) => {
try {
const result = await toolHandlers.handleGetHealth();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Ask question
app.post('/ask', async (req: Request, res: Response) => {
const reqId = req.requestId.substring(0, 8); // Short ID for logs
try {
const { question, session_id, notebook_id, notebook_url, show_browser, source_format } =
req.body;
if (!question) {
log.warning(`[${reqId}] /ask - Missing question`);
return res.status(400).json({
success: false,
error: 'Missing required field: question',
});
}
log.info(`[${reqId}] /ask - "${question.substring(0, 50)}..."`);
const result = await toolHandlers.handleAskQuestion(
{ question, session_id, notebook_id, notebook_url, show_browser, source_format },
async (message, progress, total) => {
log.info(`[${reqId}] Progress: ${message} (${progress}/${total})`);
}
);
log.success(`[${reqId}] /ask - Completed`);
res.json(result);
} catch (error) {
log.error(`[${reqId}] /ask - Error: ${error instanceof Error ? error.message : String(error)}`);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Setup auth
app.post('/setup-auth', async (req: Request, res: Response) => {
try {
const { show_browser } = req.body;
const result = await toolHandlers.handleSetupAuth(
{ show_browser },
async (message, progress, total) => {
log.info(`Progress: ${message} (${progress}/${total})`);
}
);
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// De-authenticate (logout)
app.post('/de-auth', async (_req: Request, res: Response) => {
try {
const result = await toolHandlers.handleDeAuth();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Re-authenticate
app.post('/re-auth', async (req: Request, res: Response) => {
try {
const { show_browser } = req.body;
const result = await toolHandlers.handleReAuth(
{ show_browser },
async (message, progress, total) => {
log.info(`Progress: ${message} (${progress}/${total})`);
}
);
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Cleanup data
app.post('/cleanup-data', async (req: Request, res: Response) => {
try {
const { confirm, preserve_library } = req.body;
const result = await toolHandlers.handleCleanupData({ confirm, preserve_library });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// List notebooks
app.get('/notebooks', async (_req: Request, res: Response) => {
try {
const result = await toolHandlers.handleListNotebooks();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Add notebook
app.post('/notebooks', async (req: Request, res: Response) => {
try {
const { url, name, description, topics, content_types, use_cases, tags } = req.body;
if (!url || !name || !description || !topics) {
return res.status(400).json({
success: false,
error: 'Missing required fields: url, name, description, topics',
});
}
const result = await toolHandlers.handleAddNotebook({
url,
name,
description,
topics,
content_types,
use_cases,
tags,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Search notebooks (MUST be before /notebooks/:id to avoid being shadowed)
app.get('/notebooks/search', async (req: Request, res: Response) => {
try {
const { query } = req.query;
if (typeof query !== 'string' || !query.trim()) {
return res.status(400).json({
success: false,
error: 'Missing or invalid query parameter',
});
}
const result = await toolHandlers.handleSearchNotebooks({ query });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Get library stats (MUST be before /notebooks/:id to avoid being shadowed)
app.get('/notebooks/stats', async (_req: Request, res: Response) => {
try {
const result = await toolHandlers.handleGetLibraryStats();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Scrape notebooks from NotebookLM homepage (MUST be before /notebooks/:id to avoid being shadowed)
app.get('/notebooks/scrape', async (req: Request, res: Response) => {
try {
const showBrowser = req.query.show_browser === 'true';
const result = await toolHandlers.handleListNotebooksFromNblm(
{ show_browser: showBrowser },
async (message, progress, total) => {
log.info(`Progress: ${message} (${progress}/${total})`);
}
);
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Import scraped notebooks into library (MUST be before /notebooks/:id)
app.post('/notebooks/import-from-scrape', async (req: Request, res: Response) => {
try {
const { notebook_ids, auto_discover, show_browser } = req.body;
// Step 1: Scrape notebooks from NotebookLM
log.info('đĨ [IMPORT] Starting import from scrape...');
const scrapeResult = await toolHandlers.handleListNotebooksFromNblm(
{ show_browser: show_browser === true },
async (message, progress, total) => {
log.info(`Progress: ${message} (${progress}/${total})`);
}
);
if (!scrapeResult.success || !scrapeResult.data) {
res.status(500).json({
success: false,
error: `Scrape failed: ${scrapeResult.error || 'Unknown error'}`,
});
return;
}
const scrapedNotebooks = scrapeResult.data.notebooks;
log.info(` đ Found ${scrapedNotebooks.length} notebooks from scrape`);
// Step 2: Filter notebooks if notebook_ids provided
let notebooksToImport = scrapedNotebooks;
if (notebook_ids && Array.isArray(notebook_ids) && notebook_ids.length > 0) {
notebooksToImport = scrapedNotebooks.filter((nb) => notebook_ids.includes(nb.id));
log.info(` đ Filtered to ${notebooksToImport.length} notebooks`);
}
// Step 3: Import each notebook
const imported: Array<{ id: string; name: string; status: string }> = [];
const errors: Array<{ id: string; name: string; error: string }> = [];
for (const notebook of notebooksToImport) {
try {
if (auto_discover === true) {
// Use auto-discovery to generate metadata
log.info(` đ¤ Auto-discovering: ${notebook.name}`);
const discoverResult = await toolHandlers.handleAutoDiscoverNotebook({
url: notebook.url,
});
if (discoverResult.success) {
imported.push({ id: notebook.id, name: notebook.name, status: 'auto-discovered' });
} else {
errors.push({
id: notebook.id,
name: notebook.name,
error: discoverResult.error || 'Auto-discovery failed',
});
}
} else {
// Add with minimal metadata
log.info(` đ Adding: ${notebook.name}`);
const addResult = await toolHandlers.handleAddNotebook({
url: notebook.url,
name: notebook.name,
description: `Imported from NotebookLM scrape`,
topics: [notebook.name.toLowerCase().replace(/\s+/g, '-')],
});
if (addResult.success) {
imported.push({ id: notebook.id, name: notebook.name, status: 'imported' });
} else {
errors.push({
id: notebook.id,
name: notebook.name,
error: addResult.error || 'Add failed',
});
}
}
} catch (error) {
errors.push({
id: notebook.id,
name: notebook.name,
error: error instanceof Error ? error.message : String(error),
});
}
}
log.success(`â
[IMPORT] Completed: ${imported.length} imported, ${errors.length} errors`);
res.json({
success: true,
data: {
imported,
errors,
total_scraped: scrapedNotebooks.length,
total_imported: imported.length,
total_errors: errors.length,
},
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Bulk delete notebooks from NotebookLM (MUST be before /notebooks/:id)
app.delete('/notebooks/bulk-delete', async (req: Request, res: Response) => {
try {
const { notebook_ids, show_browser } = req.body;
if (!notebook_ids || !Array.isArray(notebook_ids) || notebook_ids.length === 0) {
res.status(400).json({
success: false,
error: 'notebook_ids array is required',
});
return;
}
const result = await toolHandlers.handleDeleteNotebooksFromNblm(
{ notebook_ids, show_browser: show_browser === true },
async (message, progress, total) => {
log.info(`Progress: ${message} (${progress}/${total})`);
}
);
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Get notebook
app.get('/notebooks/:id', async (req: Request, res: Response) => {
try {
const result = await toolHandlers.handleGetNotebook({ id: req.params.id });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Update notebook
app.put('/notebooks/:id', async (req: Request, res: Response) => {
try {
const result = await toolHandlers.handleUpdateNotebook({
id: req.params.id,
...req.body,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Delete notebook
app.delete('/notebooks/:id', async (req: Request, res: Response) => {
try {
const result = await toolHandlers.handleRemoveNotebook({ id: req.params.id });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Auto-discover notebook metadata
app.post('/notebooks/auto-discover', async (req: Request, res: Response) => {
try {
const { url } = req.body;
// Validate URL is provided
if (!url) {
return res.status(400).json({
success: false,
error: 'Missing required field: url',
});
}
// Validate it's a NotebookLM URL (proper URL parsing to prevent bypass)
try {
const parsedUrl = new URL(url);
if (parsedUrl.hostname !== 'notebooklm.google.com') {
return res.status(400).json({
success: false,
error: 'Invalid URL: must be a NotebookLM URL (notebooklm.google.com)',
});
}
} catch {
return res.status(400).json({
success: false,
error: 'Invalid URL format',
});
}
// Create AutoDiscovery instance and discover metadata
const autoDiscovery = new AutoDiscovery(sessionManager);
let metadata;
try {
metadata = await autoDiscovery.discoverMetadata(url);
} catch (error) {
return res.status(500).json({
success: false,
error: `Failed to discover metadata: ${error instanceof Error ? error.message : String(error)}`,
});
}
// Transform metadata to NotebookLibrary format
// - tags â topics (rename field)
// - Add default content_types
// - Add default use_cases based on first few tags
const notebookInput = {
url,
name: metadata.name,
description: metadata.description,
topics: metadata.tags, // tags â topics
content_types: ['documentation'],
use_cases: metadata.tags.slice(0, 3), // Use first 3 tags as use cases
auto_generated: true,
};
// Add notebook to library
let notebook;
try {
notebook = await library.addNotebook(notebookInput);
} catch (error) {
return res.status(500).json({
success: false,
error: `Failed to add notebook to library: ${error instanceof Error ? error.message : String(error)}`,
});
}
// Return success with created notebook
res.json({
success: true,
notebook,
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Create a new notebook in NotebookLM (via browser automation)
app.post('/notebooks/create', async (req: Request, res: Response) => {
try {
const { name, show_browser } = req.body;
const result = await toolHandlers.handleCreateNotebook(
{ name, show_browser },
async (message, progress, total) => {
log.info(`Progress: ${message} (${progress}/${total})`);
}
);
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Activate notebook (set as active)
app.put('/notebooks/:id/activate', async (req: Request, res: Response) => {
try {
const result = await toolHandlers.handleSelectNotebook({ id: req.params.id });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// List sessions
app.get('/sessions', async (_req: Request, res: Response) => {
try {
const result = await toolHandlers.handleListSessions();
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Close session
app.delete('/sessions/:id', async (req: Request, res: Response) => {
try {
const result = await toolHandlers.handleCloseSession({ session_id: req.params.id });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Reset session
app.post('/sessions/:id/reset', async (req: Request, res: Response) => {
try {
const result = await toolHandlers.handleResetSession({ session_id: req.params.id });
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// ========================================
// Content Management Routes
// ========================================
// Add source to notebook
app.post('/content/sources', async (req: Request, res: Response) => {
try {
const { source_type, file_path, url, text, title, notebook_url, session_id, show_browser } =
req.body;
if (!source_type) {
return res.status(400).json({
success: false,
error: 'Missing required field: source_type',
});
}
const result = await toolHandlers.handleAddSource({
source_type,
file_path,
url,
text,
title,
notebook_url,
session_id,
show_browser,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Delete source from notebook
app.delete('/content/sources/:id', async (req: Request, res: Response) => {
try {
const { notebook_url, session_id } = req.query;
const sourceId = req.params.id;
if (!sourceId) {
return res.status(400).json({
success: false,
error: 'Missing source ID in URL path',
});
}
const result = await toolHandlers.handleDeleteSource({
source_id: sourceId,
notebook_url: typeof notebook_url === 'string' ? notebook_url : undefined,
session_id: typeof session_id === 'string' ? session_id : undefined,
});
if (!result.success) {
// Return 404 if source not found
if (result.error?.includes('not found')) {
return res.status(404).json(result);
}
return res.status(500).json(result);
}
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Delete source by name (alternative endpoint)
app.delete('/content/sources', async (req: Request, res: Response) => {
try {
const { source_name, source_id, notebook_url, session_id } = req.query;
if (!source_name && !source_id) {
return res.status(400).json({
success: false,
error: 'Missing required query parameter: source_name or source_id',
});
}
const result = await toolHandlers.handleDeleteSource({
source_id: typeof source_id === 'string' ? source_id : undefined,
source_name: typeof source_name === 'string' ? source_name : undefined,
notebook_url: typeof notebook_url === 'string' ? notebook_url : undefined,
session_id: typeof session_id === 'string' ? session_id : undefined,
});
if (!result.success) {
// Return 404 if source not found
if (result.error?.includes('not found')) {
return res.status(404).json(result);
}
return res.status(500).json(result);
}
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Generate content (audio_overview, presentation, report, data_table, infographic, and video are supported)
app.post('/content/generate', async (req: Request, res: Response) => {
try {
const {
content_type,
custom_instructions,
notebook_url,
session_id,
language,
video_style,
video_format,
infographic_format,
report_format,
presentation_style,
presentation_length,
} = req.body;
if (!content_type) {
return res.status(400).json({
success: false,
error: 'Missing required field: content_type',
});
}
// Validate content_type is supported
const supportedTypes = [
'audio_overview',
'presentation',
'report',
'infographic',
'data_table',
'video',
];
if (!supportedTypes.includes(content_type)) {
return res.status(400).json({
success: false,
error:
`Content type '${content_type}' is not supported. Supported types: ${supportedTypes.join(', ')}. ` +
'These use real NotebookLM Studio UI buttons or the generic ContentGenerator.',
});
}
// Warn if custom_instructions provided for content types that don't support it
const noCustomInstructionsTypes = ['report']; // report and mindmap (when implemented) don't support prompts
if (custom_instructions && noCustomInstructionsTypes.includes(content_type)) {
return res.status(400).json({
success: false,
error:
`Content type '${content_type}' does not support custom_instructions. ` +
`Only format/language options are available for this type.`,
});
}
// Validate video_style if provided (only valid for video content type)
const validVideoStyles = [
'classroom',
'documentary',
'animated',
'corporate',
'cinematic',
'minimalist',
];
if (video_style && !validVideoStyles.includes(video_style)) {
return res.status(400).json({
success: false,
error: `Video style '${video_style}' is not supported. Supported styles: ${validVideoStyles.join(', ')}.`,
});
}
if (video_style && content_type !== 'video') {
return res.status(400).json({
success: false,
error: `video_style is only valid for content_type 'video', not '${content_type}'.`,
});
}
// Validate video_format if provided
if (video_format && !['brief', 'explainer'].includes(video_format)) {
return res.status(400).json({
success: false,
error: `Video format '${video_format}' is not supported. Supported formats: brief, explainer.`,
});
}
// Validate infographic_format if provided
if (infographic_format && !['horizontal', 'vertical'].includes(infographic_format)) {
return res.status(400).json({
success: false,
error: `Infographic format '${infographic_format}' is not supported. Supported formats: horizontal, vertical.`,
});
}
// Validate report_format if provided
if (report_format && !['summary', 'detailed'].includes(report_format)) {
return res.status(400).json({
success: false,
error: `Report format '${report_format}' is not supported. Supported formats: summary, detailed.`,
});
}
// Validate presentation_style if provided
if (
presentation_style &&
!['detailed_slideshow', 'presenter_notes'].includes(presentation_style)
) {
return res.status(400).json({
success: false,
error: `Presentation style '${presentation_style}' is not supported. Supported styles: detailed_slideshow, presenter_notes.`,
});
}
// Validate presentation_length if provided
if (presentation_length && !['short', 'default'].includes(presentation_length)) {
return res.status(400).json({
success: false,
error: `Presentation length '${presentation_length}' is not supported. Supported lengths: short, default.`,
});
}
// Note: data_table has no format options - it exports to Google Sheets
const result = await toolHandlers.handleGenerateContent({
content_type,
custom_instructions,
notebook_url,
session_id,
language,
video_style,
video_format,
infographic_format,
report_format,
presentation_style,
presentation_length,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// List sources and generated content
app.get('/content', async (req: Request, res: Response) => {
try {
const { notebook_url, session_id } = req.query;
const result = await toolHandlers.handleListContent({
notebook_url: typeof notebook_url === 'string' ? notebook_url : undefined,
session_id: typeof session_id === 'string' ? session_id : undefined,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Download/export content (audio, video, infographic, presentation, data_table)
app.get('/content/download', async (req: Request, res: Response) => {
try {
const { content_type, output_path, notebook_url, session_id } = req.query;
if (!content_type) {
return res.status(400).json({
success: false,
error: 'Missing required field: content_type',
});
}
// Validate content_type is downloadable/exportable
// - audio_overview, video, infographic: downloadable as files
// - presentation: exports to Google Slides
// - data_table: exports to Google Sheets
// - report: text-based only (no export)
const exportableTypes = [
'audio_overview',
'video',
'infographic',
'presentation',
'data_table',
];
if (!exportableTypes.includes(content_type as string)) {
return res.status(400).json({
success: false,
error:
`Content type '${content_type}' is not exportable. Exportable types: ${exportableTypes.join(', ')}. ` +
'Report content is text-based and returned in the generation response.',
});
}
const result = await toolHandlers.handleDownloadContent({
content_type: content_type as
| 'audio_overview'
| 'video'
| 'infographic'
| 'presentation'
| 'data_table',
output_path: typeof output_path === 'string' ? output_path : undefined,
notebook_url: typeof notebook_url === 'string' ? notebook_url : undefined,
session_id: typeof session_id === 'string' ? session_id : undefined,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Create a note in the notebook
app.post('/content/notes', async (req: Request, res: Response) => {
try {
const { title, content, notebook_url, session_id } = req.body;
if (!title) {
return res.status(400).json({
success: false,
error: 'Missing required field: title',
});
}
if (!content) {
return res.status(400).json({
success: false,
error: 'Missing required field: content',
});
}
const result = await toolHandlers.handleCreateNote({
title,
content,
notebook_url,
session_id,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Save chat/discussion to a note
app.post('/content/chat-to-note', async (req: Request, res: Response) => {
try {
const { title, notebook_url, session_id } = req.body;
const result = await toolHandlers.handleSaveChatToNote({
title,
notebook_url,
session_id,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Convert a note to a source
app.post('/content/notes/:noteTitle/to-source', async (req: Request, res: Response) => {
try {
const { noteTitle } = req.params;
const { notebook_url, session_id } = req.body;
if (!noteTitle) {
return res.status(400).json({
success: false,
error: 'Missing required parameter: noteTitle',
});
}
const result = await toolHandlers.handleConvertNoteToSource({
note_title: decodeURIComponent(noteTitle),
notebook_url,
session_id,
});
res.json(result);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error),
});
}
});
// Global error handler - catches any unhandled errors in async routes
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
const reqId = req.requestId?.substring(0, 8) || 'unknown';
log.error(`[${reqId}] Unhandled error: ${err.message}`);
res.status(500).json({
success: false,
error: 'Internal server error',
requestId: req.requestId,
});
});
// Start server with startup sequence
const PORT = Number(process.env.HTTP_PORT) || 3000;
const HOST = process.env.HTTP_HOST || '0.0.0.0';
const VERSION = '1.5.3';
const startupManager = new StartupManager(authManager);
app.listen(PORT, HOST, async () => {
log.success(`đ NotebookLM MCP HTTP Server v${VERSION}`);
log.success(` Listening on ${HOST}:${PORT}`);
// Run startup sequence (account connection, auth verification)
const startupResult = await startupManager.startup();
// Show quick links and endpoints after startup
log.info('');
log.info('đ Quick Links:');
log.info(` Health check: http://localhost:${PORT}/health`);
log.info(` API endpoint: http://localhost:${PORT}/ask`);
log.info('');
log.info('đ Available Endpoints:');
log.info(' Authentication:');
log.info(' POST /setup-auth First-time authentication');
log.info(' POST /de-auth Logout (clear credentials)');
log.info(' POST /re-auth Re-authenticate / switch account');
log.info(' POST /cleanup-data Clean all data (requires confirm)');
log.info('');
log.info(' Queries:');
log.info(' POST /ask Ask a question to NotebookLM');
log.info(' GET /health Server health check');
log.info('');
log.info(' Notebooks:');
log.info(' GET /notebooks List all notebooks');
log.info(' POST /notebooks Add a new notebook');
log.info(' POST /notebooks/auto-discover Auto-discover notebook metadata');
log.info(' GET /notebooks/search Search notebooks by query');
log.info(' GET /notebooks/stats Get library statistics');
log.info(' GET /notebooks/scrape Scrape real notebooks from NotebookLM');
log.info(' GET /notebooks/:id Get notebook details');
log.info(' PUT /notebooks/:id Update notebook metadata');
log.info(' DELETE /notebooks/:id Delete a notebook');
log.info(' PUT /notebooks/:id/activate Activate a notebook (set as default)');
log.info('');
log.info(' Sessions:');
log.info(' GET /sessions List active sessions');
log.info(' POST /sessions/:id/reset Reset session history');
log.info(' DELETE /sessions/:id Close a session');
log.info('');
log.info(' Content Management:');
log.info(' POST /content/sources Add source to notebook');
log.info(' DELETE /content/sources/:id Delete source by ID');
log.info(' DELETE /content/sources Delete source by name (query param)');
log.info(' POST /content/generate Generate content (audio, video, etc.)');
log.info(' GET /content/download Download/export generated content');
log.info(' POST /content/notes Create a note in the notebook');
log.info(' POST /content/chat-to-note Save chat/discussion to a note');
log.info(' POST /content/notes/:title/to-source Convert note to source');
log.info(' GET /content List sources and content');
log.info('');
log.info('đĄ Configuration:');
log.info(
` Host: ${HOST} ${HOST === '0.0.0.0' ? '(accessible from network)' : '(localhost only)'}`
);
log.info(` Port: ${PORT}`);
// Show startup result summary
log.info('');
if (startupResult.authenticated) {
log.success(
`đ Status: Authenticated${startupResult.accountEmail ? ` as ${startupResult.accountEmail}` : ''}`
);
} else {
log.warning(`đ Status: Not authenticated - ${startupResult.message}`);
}
log.info('');
log.dim('đ Documentation: ./deployment/docs/');
log.dim('âšī¸ Press Ctrl+C to stop');
});
// Graceful shutdown with error handling
process.on('SIGTERM', async () => {
log.info('SIGTERM received, shutting down gracefully...');
try {
await toolHandlers.cleanup();
} catch (error) {
log.error(`Cleanup failed: ${error}`);
}
process.exit(0);
});
process.on('SIGINT', async () => {
log.info('SIGINT received, shutting down gracefully...');
try {
await toolHandlers.cleanup();
} catch (error) {
log.error(`Cleanup failed: ${error}`);
}
process.exit(0);
});