Skip to main content
Glama
auto-discovery.ts6.92 kB
/** * Auto-Discovery Module * * Automatically generates notebook metadata by querying NotebookLM itself. * This enables autonomous resource discovery for AI orchestrators. * * Progressive Disclosure Pattern (inspired by Claude Skills best practices): * - Level 0: Lightweight metadata loaded at startup * - Level 1: Local matching without NotebookLM queries * - Level 2: Deep query to NotebookLM only when needed */ import { SessionManager } from '../session/session-manager.js'; import { log } from '../utils/logger.js'; /** * Metadata structure auto-generated from NotebookLM */ export interface AutoGeneratedMetadata { name: string; // kebab-case, 3 words max description: string; // 2 sentences, under 150 chars tags: string[]; // 8-10 keywords } /** * AutoDiscovery class * Queries NotebookLM to auto-generate notebook metadata */ export class AutoDiscovery { constructor(private sessionManager: SessionManager) {} /** * Query the notebook to auto-generate its metadata * Includes retry logic (max 2 retries with 2s delay) * * @param notebookUrl NotebookLM notebook URL * @param maxRetries Maximum retry attempts (default: 2) * @returns Auto-generated metadata */ async discoverMetadata(notebookUrl: string, maxRetries = 2): Promise<AutoGeneratedMetadata> { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { log.info(`🔍 Auto-discovering metadata (attempt ${attempt + 1}/${maxRetries + 1})...`); // 1. Create session for this notebook const session = await this.sessionManager.getOrCreateSession(undefined, notebookUrl); // 2. Send discovery prompt const prompt = this.generatePrompt(); log.dim(` Sending discovery prompt to NotebookLM...`); const askResult = await session.ask(prompt); const response = askResult.answer; log.dim(` Received response (${response.length} chars)`); // 3. Parse and validate response const metadata = this.parseResponse(response); this.validateMetadata(metadata); log.success(`✅ Metadata auto-generated successfully`); log.dim(` Name: ${metadata.name}`); log.dim(` Tags: ${metadata.tags.length} keywords`); return metadata; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (attempt === maxRetries) { log.error(`❌ Failed to auto-discover metadata after ${maxRetries + 1} attempts`); throw new Error( `Auto-discovery failed: ${errorMessage}. Please verify the notebook is accessible.` ); } log.warning(`⚠️ Attempt ${attempt + 1} failed: ${errorMessage}. Retrying in 2s...`); await this.delay(2000); } } throw new Error('Auto-discovery failed after all retries'); } /** * Generate the discovery prompt for NotebookLM * * This prompt is CRITICAL - it must be ultra-clear with no ambiguity. * NotebookLM will analyze its own content and return structured metadata. */ private generatePrompt(): string { return `Analyze your complete content and respond ONLY in this JSON format: { "name": "example-notebook-name", "description": "First sentence describes main topic. Second sentence adds key details.", "tags": ["keyword1", "keyword2", "keyword3", "keyword4", "keyword5", "keyword6", "keyword7", "keyword8"] } Rules: - name: 3 words maximum, kebab-case format (e.g., "react-hooks-guide", "n8n-api-docs") - description: exactly 2 sentences, total length under 150 characters - tags: between 8 and 10 keywords covering: * Core concepts (3-4 tags like "react", "hooks", "state") * Actions/verbs (2-3 tags like "debugging", "testing") * Use contexts (2-3 tags like "frontend", "api", "automation") OUTPUT ONLY VALID JSON. NO OTHER TEXT.`; } /** * Parse and validate the NotebookLM response * * Handles common issues: * - Markdown code blocks wrapping JSON * - Extra whitespace * - Invalid JSON syntax */ private parseResponse(response: string): AutoGeneratedMetadata { try { // 1. Clean markdown code blocks if present const cleaned = response .replace(/```json\n?/g, '') .replace(/```\n?/g, '') .trim(); // 2. Parse JSON const parsed = JSON.parse(cleaned); // 3. Ensure required fields exist if (!parsed.name || !parsed.description || !parsed.tags) { throw new Error('Missing required fields (name, description, or tags)'); } return { name: parsed.name, description: parsed.description, tags: parsed.tags, }; } catch (error) { if (error instanceof SyntaxError) { throw new Error( `Invalid JSON response from NotebookLM. Response: ${response.substring(0, 200)}...` ); } throw error; } } /** * Validate that metadata respects all rules * * Throws detailed errors to help debugging */ private validateMetadata(metadata: AutoGeneratedMetadata): void { // Validate name: kebab-case, 3 words max, no spaces const nameRegex = /^[a-z0-9]+(-[a-z0-9]+){0,2}$/; if (!nameRegex.test(metadata.name)) { throw new Error( `Invalid name format: "${metadata.name}". Must be kebab-case with max 3 words (e.g., "my-notebook" or "my-notebook-name")` ); } // Auto-truncate description if too long (NotebookLM doesn't always respect the limit) if (metadata.description.length > 150) { log.warning( `⚠️ Description too long (${metadata.description.length} chars), truncating to 150...` ); // Truncate at last complete sentence before 147 chars (leave room for "...") let truncated = metadata.description.substring(0, 147); const lastPeriod = truncated.lastIndexOf('.'); if (lastPeriod > 50) { // Keep complete sentence if it's not too short truncated = truncated.substring(0, lastPeriod + 1); } else { // Otherwise just truncate and add ... truncated = truncated.trim() + '...'; } metadata.description = truncated; log.dim(` Truncated to: "${truncated}"`); } // Validate tags: 8-10 elements if (metadata.tags.length < 8 || metadata.tags.length > 10) { throw new Error( `Invalid tags count: ${metadata.tags.length} (must be 8-10). Tags: ${metadata.tags.join(', ')}` ); } // Validate tags are non-empty strings for (const tag of metadata.tags) { if (typeof tag !== 'string' || tag.trim().length === 0) { throw new Error(`Invalid tag: "${tag}". Tags must be non-empty strings`); } } } /** * Simple delay helper for retry logic */ private delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } }

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