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,
},
};
}
}