Skip to main content
Glama
resource-handlers.ts8.2 kB
import { ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, CompleteRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { NotebookLibrary } from "../library/notebook-library.js"; import { log } from "../utils/logger.js"; /** * Handlers for MCP Resource-related requests */ export class ResourceHandlers { private library: NotebookLibrary; constructor(library: NotebookLibrary) { this.library = library; } /** * Register all resource handlers to the server */ public registerHandlers(server: Server): void { // List available resources 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 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 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 and resource templates) 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; }); } /** * 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, }, }; } }

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/PleasePrompto/notebooklm-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server