Skip to main content
Glama
stdio-http-proxy.ts16.7 kB
#!/usr/bin/env node /** * Stdio-to-HTTP Proxy for NotebookLM MCP Server * * This proxy enables Claude Desktop (which only supports stdio MCP) * to use the HTTP server backend. It translates MCP stdio protocol * to HTTP REST API calls. * * Architecture: * Claude Desktop → stdio → this proxy → HTTP → NotebookLM MCP Server → Chrome → NotebookLM * * Benefits: * - No Chrome profile conflicts (HTTP server owns the Chrome instance) * - Can run simultaneously with HTTP server * - Lightweight client (no Playwright dependency) * * Usage: * node dist/stdio-http-proxy.js * * Environment Variables: * MCP_HTTP_URL - HTTP server URL (default: http://localhost:3000) */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, Tool, Resource, } from '@modelcontextprotocol/sdk/types.js'; // HTTP server URL (configurable via environment) const HTTP_BASE_URL = process.env.MCP_HTTP_URL || 'http://localhost:3000'; /** * Helper to make HTTP requests to the backend server */ async function httpRequest<T>( method: 'GET' | 'POST' | 'PUT' | 'DELETE', endpoint: string, body?: unknown ): Promise<T> { const url = `${HTTP_BASE_URL}${endpoint}`; const options: RequestInit = { method, headers: { 'Content-Type': 'application/json', }, }; if (body && (method === 'POST' || method === 'PUT')) { options.body = JSON.stringify(body); } const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } return response.json() as Promise<T>; } /** * Log to stderr (stdio uses stdout for MCP protocol) */ function log(message: string): void { console.error(`[stdio-proxy] ${message}`); } /** * Build tool definitions for the proxy * These mirror the HTTP server's tools but are fetched dynamically */ function buildProxyToolDefinitions(): Tool[] { // Static tool definitions that match the HTTP server // These could be fetched dynamically in the future return [ { name: 'ask_question', description: '🔌 [PROXY MODE] Ask NotebookLM via HTTP server.\n\n' + 'This tool proxies requests to the HTTP server at ' + HTTP_BASE_URL + '\n' + 'Ensure the HTTP server is running: npm run start:http\n\n' + 'Parameters:\n' + '- question (required): The question to ask\n' + '- notebook_id: Library notebook ID\n' + '- notebook_url: Direct notebook URL\n' + '- session_id: Reuse existing session\n' + '- show_browser: Show browser window for debugging', inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'The question to ask NotebookLM' }, session_id: { type: 'string', description: 'Optional session ID for context' }, notebook_id: { type: 'string', description: 'Optional notebook ID from library' }, notebook_url: { type: 'string', description: 'Optional direct notebook URL' }, show_browser: { type: 'boolean', description: 'Show browser window for debugging' }, }, required: ['question'], }, }, { name: 'list_notebooks', description: '🔌 [PROXY] List all notebooks from library via HTTP server', inputSchema: { type: 'object', properties: {} }, }, { name: 'get_notebook', description: '🔌 [PROXY] Get notebook details by ID via HTTP server', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Notebook ID' } }, required: ['id'], }, }, { name: 'add_notebook', description: '🔌 [PROXY] Add notebook to library via HTTP server', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'NotebookLM URL' }, name: { type: 'string', description: 'Display name' }, description: { type: 'string', description: 'Description' }, topics: { type: 'array', items: { type: 'string' }, description: 'Topics' }, content_types: { type: 'array', items: { type: 'string' } }, use_cases: { type: 'array', items: { type: 'string' } }, tags: { type: 'array', items: { type: 'string' } }, }, required: ['url', 'name', 'description', 'topics'], }, }, { name: 'auto_discover_notebook', description: '🔌 [PROXY] Auto-discover notebook metadata via HTTP server', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'NotebookLM URL' } }, required: ['url'], }, }, { name: 'select_notebook', description: '🔌 [PROXY] Set active notebook via HTTP server', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Notebook ID' } }, required: ['id'], }, }, { name: 'update_notebook', description: '🔌 [PROXY] Update notebook metadata via HTTP server', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Notebook ID' }, name: { type: 'string' }, description: { type: 'string' }, topics: { type: 'array', items: { type: 'string' } }, content_types: { type: 'array', items: { type: 'string' } }, use_cases: { type: 'array', items: { type: 'string' } }, tags: { type: 'array', items: { type: 'string' } }, url: { type: 'string' }, }, required: ['id'], }, }, { name: 'remove_notebook', description: '🔌 [PROXY] Remove notebook from library via HTTP server', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Notebook ID' } }, required: ['id'], }, }, { name: 'search_notebooks', description: '🔌 [PROXY] Search notebooks via HTTP server', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'], }, }, { name: 'get_library_stats', description: '🔌 [PROXY] Get library statistics via HTTP server', inputSchema: { type: 'object', properties: {} }, }, { name: 'list_sessions', description: '🔌 [PROXY] List active sessions via HTTP server', inputSchema: { type: 'object', properties: {} }, }, { name: 'close_session', description: '🔌 [PROXY] Close session via HTTP server', inputSchema: { type: 'object', properties: { session_id: { type: 'string', description: 'Session ID' } }, required: ['session_id'], }, }, { name: 'reset_session', description: '🔌 [PROXY] Reset session history via HTTP server', inputSchema: { type: 'object', properties: { session_id: { type: 'string', description: 'Session ID' } }, required: ['session_id'], }, }, { name: 'get_health', description: '🔌 [PROXY] Get server health status via HTTP server', inputSchema: { type: 'object', properties: {} }, }, { name: 'setup_auth', description: '🔌 [PROXY] Setup Google authentication via HTTP server.\n' + 'Opens browser on the HTTP server machine for login.', inputSchema: { type: 'object', properties: { show_browser: { type: 'boolean', description: 'Show browser window' } }, }, }, { name: 'de_auth', description: '🔌 [PROXY] Logout (clear credentials) via HTTP server', inputSchema: { type: 'object', properties: {} }, }, { name: 're_auth', description: '🔌 [PROXY] Re-authenticate with different account via HTTP server', inputSchema: { type: 'object', properties: { show_browser: { type: 'boolean', description: 'Show browser window' } }, }, }, { name: 'cleanup_data', description: '🔌 [PROXY] Cleanup server data via HTTP server', inputSchema: { type: 'object', properties: { confirm: { type: 'boolean', description: 'Confirm deletion (false for preview)' }, preserve_library: { type: 'boolean', description: 'Keep library.json' }, }, required: ['confirm'], }, }, ]; } /** * Map tool calls to HTTP endpoints */ async function handleToolCall( name: string, args: Record<string, unknown> ): Promise<{ success: boolean; data?: unknown; error?: string }> { try { switch (name) { // Query endpoints case 'ask_question': return await httpRequest('POST', '/ask', args); case 'get_health': return await httpRequest('GET', '/health'); // Notebook endpoints case 'list_notebooks': return await httpRequest('GET', '/notebooks'); case 'get_notebook': return await httpRequest('GET', `/notebooks/${args.id}`); case 'add_notebook': return await httpRequest('POST', '/notebooks', args); case 'auto_discover_notebook': return await httpRequest('POST', '/notebooks/auto-discover', args); case 'select_notebook': return await httpRequest('PUT', `/notebooks/${args.id}/activate`); case 'update_notebook': { const { id, ...updateData } = args; return await httpRequest('PUT', `/notebooks/${id}`, updateData); } case 'remove_notebook': return await httpRequest('DELETE', `/notebooks/${args.id}`); case 'search_notebooks': return await httpRequest( 'GET', `/notebooks/search?query=${encodeURIComponent(String(args.query))}` ); case 'get_library_stats': return await httpRequest('GET', '/notebooks/stats'); // Session endpoints case 'list_sessions': return await httpRequest('GET', '/sessions'); case 'close_session': return await httpRequest('DELETE', `/sessions/${args.session_id}`); case 'reset_session': return await httpRequest('POST', `/sessions/${args.session_id}/reset`); // Auth endpoints case 'setup_auth': return await httpRequest('POST', '/setup-auth', args); case 'de_auth': return await httpRequest('POST', '/de-auth'); case 're_auth': return await httpRequest('POST', '/re-auth', args); case 'cleanup_data': return await httpRequest('POST', '/cleanup-data', args); default: return { success: false, error: `Unknown tool: ${name}` }; } } catch (error) { const message = error instanceof Error ? error.message : String(error); // Check if HTTP server is not running if (message.includes('ECONNREFUSED') || message.includes('fetch failed')) { return { success: false, error: `Cannot connect to HTTP server at ${HTTP_BASE_URL}\n\n` + 'Please ensure the HTTP server is running:\n' + ' npm run start:http\n\n' + 'Or start it as a daemon:\n' + ' npm run daemon:start\n\n' + `Original error: ${message}`, }; } return { success: false, error: message }; } } /** * Main Proxy Server Class */ class StdioHttpProxyServer { private server: Server; private toolDefinitions: Tool[]; constructor() { this.server = new Server( { name: 'notebooklm-mcp-proxy', version: '1.3.6', }, { capabilities: { tools: {}, resources: {}, resourceTemplates: {}, }, } ); this.toolDefinitions = buildProxyToolDefinitions(); this.setupHandlers(); this.setupShutdownHandlers(); log('🔌 NotebookLM MCP Stdio-HTTP Proxy initialized'); log(` Backend: ${HTTP_BASE_URL}`); } private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { log('📋 list_tools request'); return { tools: this.toolDefinitions }; }); // List resources (proxy to HTTP server or return empty) this.server.setRequestHandler(ListResourcesRequestSchema, async () => { log('📚 list_resources request'); // For now, return a simple resource pointing to the library const resources: Resource[] = [ { uri: 'notebooklm://library', name: 'Notebook Library (via HTTP proxy)', description: 'Access notebook library through HTTP server. Use list_notebooks tool instead.', mimeType: 'application/json', }, ]; return { resources }; }); // List resource templates this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { log('📑 list_resource_templates request'); return { resourceTemplates: [] }; }); // Read resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; log(`📖 read_resource: ${uri}`); // Proxy library resource to HTTP if (uri === 'notebooklm://library') { try { const result = await httpRequest<{ success: boolean; data?: unknown }>( 'GET', '/notebooks' ); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(result.data || result, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to fetch library: ${error}`); } } throw new Error(`Unknown resource: ${uri}. Use tools instead of resources with the proxy.`); }); // Handle tool calls - proxy to HTTP server this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; log(`🔧 Tool call: ${name}`); const result = await handleToolCall(name, (args || {}) as Record<string, unknown>); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }); } private setupShutdownHandlers(): void { let shuttingDown = false; const shutdown = async (signal: string) => { if (shuttingDown) return; shuttingDown = true; log(`🛑 Received ${signal}, shutting down...`); try { await this.server.close(); log('✅ Proxy shutdown complete'); process.exit(0); } catch (error) { log(`❌ Error during shutdown: ${error}`); process.exit(1); } }; process.on('SIGINT', () => void shutdown('SIGINT')); process.on('SIGTERM', () => void shutdown('SIGTERM')); } async start(): Promise<void> { log('🎯 Starting Stdio-HTTP Proxy...'); const transport = new StdioServerTransport(); await this.server.connect(transport); log('✅ Proxy connected via stdio'); log('🎉 Ready to proxy requests to HTTP server!'); } } /** * Main entry point */ async function main() { console.error('╔══════════════════════════════════════════════════════════╗'); console.error('║ ║'); console.error('║ NotebookLM MCP Stdio-HTTP Proxy v1.3.6 ║'); console.error('║ ║'); console.error('║ Proxy stdio MCP requests to HTTP server ║'); console.error('║ ║'); console.error('╚══════════════════════════════════════════════════════════╝'); console.error(''); console.error(`Backend URL: ${HTTP_BASE_URL}`); console.error(''); try { const server = new StdioHttpProxyServer(); await server.start(); } catch (error) { console.error(`💥 Fatal error: ${error}`); process.exit(1); } } 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