notebooklm-mcp-server 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +36 -16
  2. package/build/index.js +334 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,7 +71,7 @@ Before using the server, you must link it to your Google Account. This version u
71
71
 
72
72
  1. Run the authentication command:
73
73
  ```bash
74
- notebooklm-mcp-server auth
74
+ npx notebooklm-mcp-server auth
75
75
  ```
76
76
  2. A browser window will open. Log in with your Google account.
77
77
  3. Close the browser once you see your notebooks. Your session is now securely saved locally.
@@ -124,21 +124,36 @@ Since VS Code does not support MCP natively yet, you must use an extension:
124
124
 
125
125
  ### 🌌 Antigravity
126
126
 
127
- 1. Open your `mcp.json` configuration file.
128
- 2. Add the following entry to the `servers` object:
129
- ```json
130
- "notebooklm": {
131
- "command": "npx",
132
- "args": ["-y", "notebooklm-mcp-server", "start"]
133
- }
134
- ```
127
+ Antigravity supports MCP natively. You can add the server by editing your global configuration file:
128
+
129
+ 1. **Locate your `mcp.json`**:
130
+ - **Windows**: `%APPDATA%\antigravity\mcp.json`
131
+ - **macOS**: `~/Library/Application Support/antigravity/mcp.json`
132
+ - **Linux**: `~/.config/antigravity/mcp.json`
133
+
134
+ 2. **Add the server** to the `mcpServers` object:
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "notebooklm": {
140
+ "command": "npx",
141
+ "args": ["-y", "notebooklm-mcp-server", "start"]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ 3. **Restart Antigravity**: The new tools will appear in your sidebar instantly.
148
+
149
+ ---
135
150
 
136
151
  ### 💎 Gemini CLI
137
152
 
138
153
  Run the following command in your terminal to add the notebooklm skill:
139
154
 
140
155
  ```bash
141
- gemini mcp add notebooklm -- npx -y notebooklm-mcp-server start
156
+ gemini mcp add notebooklm --scope user -- npx -y notebooklm-mcp-server start
142
157
  ```
143
158
 
144
159
  ---
@@ -155,12 +170,17 @@ claude skill add notebooklm -- "npx -y notebooklm-mcp-server start"
155
170
 
156
171
  ## 📖 Documentation
157
172
 
158
- | Tool | Description |
159
- | :---------------- | :------------------------------------------------------ |
160
- | `list_notebooks` | Lists all notebooks available in your account. |
161
- | `create_notebook` | Creates a new notebook with an optional title. |
162
- | `get_notebook` | Retrieves the full content and summaries of a notebook. |
163
- | `query_notebook` | Asks a grounded question to a specific notebook. |
173
+ | Tool | Description |
174
+ | :------------------------ | :---------------------------------------------------- |
175
+ | `list_notebooks` | Lists all notebooks available in your account. |
176
+ | `create_notebook` | Creates a new notebook with an optional title. |
177
+ | `get_notebook` | Retrieves the details and sources of a notebook. |
178
+ | `query_notebook` | Asks a grounded question to a specific notebook. |
179
+ | `add_source_url` | Adds a website or YouTube video as a source. |
180
+ | `add_source_text` | Adds pasted text content as a source. |
181
+ | `generate_audio_overview` | Triggers the generation of an Audio Overview podcast. |
182
+ | `rename_notebook` | Renames an existing notebook. |
183
+ | `delete_notebook` | Deletes a notebook (Warning: Destructive). |
164
184
 
165
185
  ---
166
186
 
package/build/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
+ import { chromium } from "playwright";
7
+ import * as path from "path";
8
+ import * as os from "os";
6
9
  import { Command } from "commander";
7
10
  import { runAuth } from "./auth.js";
8
- // ... (schemas stay the same)
9
- const ListNotebooksSchema = z.object({});
11
+ // Validation Schemas
10
12
  const CreateNotebookSchema = z.object({
11
13
  title: z.string().optional(),
12
14
  });
15
+ const ListNotebooksSchema = z.object({});
13
16
  const GetNotebookSchema = z.object({
14
17
  notebookId: z.string(),
15
18
  });
@@ -17,27 +20,73 @@ const QueryNotebookSchema = z.object({
17
20
  notebookId: z.string(),
18
21
  query: z.string(),
19
22
  });
23
+ const AddSourceUrlSchema = z.object({
24
+ notebookId: z.string(),
25
+ url: z.string().url(),
26
+ });
27
+ const AddSourceTextSchema = z.object({
28
+ notebookId: z.string(),
29
+ title: z.string(),
30
+ text: z.string(),
31
+ });
32
+ const RenameNotebookSchema = z.object({
33
+ notebookId: z.string(),
34
+ newTitle: z.string(),
35
+ });
36
+ const DeleteNotebookSchema = z.object({
37
+ notebookId: z.string(),
38
+ });
39
+ const AudioOverviewSchema = z.object({
40
+ notebookId: z.string(),
41
+ });
20
42
  class NotebookLMServer {
21
43
  server;
22
44
  browser = null;
45
+ userDataDir;
23
46
  constructor() {
47
+ this.userDataDir = path.join(os.homedir(), ".notebooklm-mcp-auth");
24
48
  this.server = new Server({
25
49
  name: "notebooklm-mcp-server",
26
- version: "1.0.0",
50
+ version: "1.0.2",
27
51
  }, {
28
52
  capabilities: {
29
53
  tools: {},
30
54
  },
31
55
  });
32
56
  this.setupTools();
57
+ // Error handling
58
+ this.server.onerror = (error) => console.error("[MCP Error]", error);
59
+ }
60
+ async cleanup() {
61
+ if (this.browser) {
62
+ await this.browser.close();
63
+ this.browser = null;
64
+ }
65
+ }
66
+ async getBrowser() {
67
+ if (!this.browser) {
68
+ this.browser = await chromium.launchPersistentContext(this.userDataDir, {
69
+ headless: true, // Standard for MCP servers
70
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
71
+ });
72
+ }
73
+ return this.browser;
74
+ }
75
+ async getPage(notebookId) {
76
+ const context = await this.getBrowser();
77
+ const page = await context.newPage();
78
+ const url = notebookId
79
+ ? `https://notebooklm.google.com/notebook/${notebookId}`
80
+ : "https://notebooklm.google.com/";
81
+ await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
82
+ return page;
33
83
  }
34
- // ... (implementation same as before but including the setupTools)
35
84
  setupTools() {
36
85
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
86
  tools: [
38
87
  {
39
88
  name: "list_notebooks",
40
- description: "Lists all your NotebookLM notebooks",
89
+ description: "Lists all your Google NotebookLM notebooks",
41
90
  inputSchema: { type: "object", properties: {} },
42
91
  },
43
92
  {
@@ -46,16 +95,288 @@ class NotebookLMServer {
46
95
  inputSchema: {
47
96
  type: "object",
48
97
  properties: {
98
+ title: {
99
+ type: "string",
100
+ description: "Optional title for the new notebook",
101
+ },
102
+ },
103
+ },
104
+ },
105
+ {
106
+ name: "get_notebook",
107
+ description: "Retrieves details, sources, and summaries of a notebook",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ notebookId: { type: "string" },
112
+ },
113
+ required: ["notebookId"],
114
+ },
115
+ },
116
+ {
117
+ name: "query_notebook",
118
+ description: "Asks a grounded question to a specific notebook",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ notebookId: { type: "string" },
123
+ query: { type: "string" },
124
+ },
125
+ required: ["notebookId", "query"],
126
+ },
127
+ },
128
+ {
129
+ name: "add_source_url",
130
+ description: "Adds a website or YouTube video as a source",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ notebookId: { type: "string" },
135
+ url: { type: "string", format: "uri" },
136
+ },
137
+ required: ["notebookId", "url"],
138
+ },
139
+ },
140
+ {
141
+ name: "add_source_text",
142
+ description: "Adds pasted text content as a source",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ notebookId: { type: "string" },
49
147
  title: { type: "string" },
148
+ text: { type: "string" },
149
+ },
150
+ required: ["notebookId", "title", "text"],
151
+ },
152
+ },
153
+ {
154
+ name: "generate_audio_overview",
155
+ description: "Triggers the generation of an Audio Overview (Deep Dive podcast)",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ notebookId: { type: "string" },
160
+ },
161
+ required: ["notebookId"],
162
+ },
163
+ },
164
+ {
165
+ name: "rename_notebook",
166
+ description: "Renames a notebook",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ notebookId: { type: "string" },
171
+ newTitle: { type: "string" },
50
172
  },
173
+ required: ["notebookId", "newTitle"],
174
+ },
175
+ },
176
+ {
177
+ name: "delete_notebook",
178
+ description: "Deletes a notebook by its ID (Warning: Destructive)",
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ notebookId: { type: "string" },
183
+ },
184
+ required: ["notebookId"],
51
185
  },
52
186
  },
53
- // ...
54
187
  ],
55
188
  }));
56
189
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
57
- // ...
58
- return { content: [{ type: "text", text: "Tool execution logic here" }] };
190
+ const { name, arguments: args } = request.params;
191
+ try {
192
+ switch (name) {
193
+ case "list_notebooks": {
194
+ const page = await this.getPage();
195
+ await page
196
+ .locator('a[href*="/notebook/"]')
197
+ .first()
198
+ .waitFor({ state: "visible", timeout: 15000 });
199
+ const notebooks = await page.evaluate(() => {
200
+ const cards = Array.from(document.querySelectorAll('a[href*="/notebook/"]'));
201
+ return cards
202
+ .map((c) => {
203
+ const href = c.href;
204
+ const matches = href.match(/notebook\/([a-zA-Z0-9_-]+)/);
205
+ const id = matches ? matches[1] : "";
206
+ const title = c.textContent?.trim().split("\n")[0] || "Untitled";
207
+ return { id, title };
208
+ })
209
+ .filter((n) => n.id && n.id !== "notebook");
210
+ });
211
+ await page.close();
212
+ return {
213
+ content: [
214
+ { type: "text", text: JSON.stringify(notebooks, null, 2) },
215
+ ],
216
+ };
217
+ }
218
+ case "create_notebook": {
219
+ const page = await this.getPage();
220
+ // Look for the "Create new" button or the plus icon
221
+ const createBtn = page
222
+ .locator('text="Create new", [aria-label*="Create"]')
223
+ .first();
224
+ await createBtn.click();
225
+ await page.waitForURL(/notebook\/[a-zA-Z0-9_-]+/);
226
+ const id = page.url().match(/notebook\/([a-zA-Z0-9_-]+)/)?.[1] || "";
227
+ const { title } = CreateNotebookSchema.parse(args);
228
+ if (title) {
229
+ await page.waitForTimeout(2000);
230
+ // Try to find the title element and rename
231
+ const titleElem = page
232
+ .locator('input[aria-label*="title"], [contenteditable="true"]')
233
+ .first();
234
+ await titleElem.click();
235
+ await page.keyboard.press("Control+A");
236
+ await page.keyboard.type(title);
237
+ await page.keyboard.press("Enter");
238
+ }
239
+ await page.close();
240
+ return {
241
+ content: [
242
+ {
243
+ type: "text",
244
+ text: `Success: Notebook created with ID: ${id}`,
245
+ },
246
+ ],
247
+ };
248
+ }
249
+ case "query_notebook": {
250
+ const { notebookId, query } = QueryNotebookSchema.parse(args);
251
+ const page = await this.getPage(notebookId);
252
+ const chatInput = page
253
+ .locator('textarea[placeholder*="Ask"], [role="textbox"][aria-label*="chat"]')
254
+ .first();
255
+ await chatInput.waitFor({ state: "visible" });
256
+ await chatInput.fill(query);
257
+ await page.keyboard.press("Enter");
258
+ // Wait for response bubble (Google usually uses specific indicators)
259
+ await page.waitForTimeout(3000);
260
+ await page.waitForSelector('[role="log"], .chat-bubble, .response-content', { timeout: 60000 });
261
+ const result = await page.evaluate(() => {
262
+ const bubbles = Array.from(document.querySelectorAll('[role="log"], .chat-bubble, .response-content'));
263
+ const last = bubbles[bubbles.length - 1];
264
+ return last?.textContent?.trim() || "No response received.";
265
+ });
266
+ await page.close();
267
+ return { content: [{ type: "text", text: result }] };
268
+ }
269
+ case "get_notebook": {
270
+ const { notebookId } = GetNotebookSchema.parse(args);
271
+ const page = await this.getPage(notebookId);
272
+ const info = await page.evaluate(() => {
273
+ const title = document.title.replace(" - NotebookLM", "");
274
+ const sources = Array.from(document.querySelectorAll('.source-card-title, [aria-label*="Source"]'))
275
+ .map((s) => s.textContent?.trim())
276
+ .filter(Boolean);
277
+ return { title, sources };
278
+ });
279
+ await page.close();
280
+ return {
281
+ content: [{ type: "text", text: JSON.stringify(info, null, 2) }],
282
+ };
283
+ }
284
+ case "add_source_url": {
285
+ const { notebookId, url } = AddSourceUrlSchema.parse(args);
286
+ const page = await this.getPage(notebookId);
287
+ await page.getByText("Add source", { exact: false }).click();
288
+ await page.getByText("Link", { exact: true }).click();
289
+ await page.locator('input[type="url"]').fill(url);
290
+ await page.getByRole("button", { name: /Add|Insert/i }).click();
291
+ await page.waitForTimeout(5000); // Give it time to start processing
292
+ await page.close();
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: `Source ${url} added to notebook ${notebookId}.`,
298
+ },
299
+ ],
300
+ };
301
+ }
302
+ case "add_source_text": {
303
+ const { notebookId, title, text } = AddSourceTextSchema.parse(args);
304
+ const page = await this.getPage(notebookId);
305
+ await page.getByText("Add source", { exact: false }).click();
306
+ await page.getByText("Text", { exact: true }).click();
307
+ await page.locator('input[placeholder*="Title"]').fill(title);
308
+ await page.locator("textarea").fill(text);
309
+ await page.getByRole("button", { name: /Add|Insert/i }).click();
310
+ await page.waitForTimeout(3000);
311
+ await page.close();
312
+ return {
313
+ content: [
314
+ { type: "text", text: `Text source "${title}" added.` },
315
+ ],
316
+ };
317
+ }
318
+ case "rename_notebook": {
319
+ const { notebookId, newTitle } = RenameNotebookSchema.parse(args);
320
+ const page = await this.getPage(notebookId);
321
+ const titleElem = page
322
+ .locator('input[aria-label*="title"], [contenteditable="true"]')
323
+ .first();
324
+ await titleElem.click();
325
+ await page.keyboard.press("Control+A");
326
+ await page.keyboard.type(newTitle);
327
+ await page.keyboard.press("Enter");
328
+ await page.waitForTimeout(1000);
329
+ await page.close();
330
+ return {
331
+ content: [
332
+ { type: "text", text: `Notebook renamed to: ${newTitle}` },
333
+ ],
334
+ };
335
+ }
336
+ case "delete_notebook": {
337
+ const { notebookId } = DeleteNotebookSchema.parse(args);
338
+ const page = await this.getPage(); // Go to home to delete from menu
339
+ // Find the notebook menu option (three dots) for this ID
340
+ const menuBtn = page
341
+ .locator(`a[href*="${notebookId}"]`)
342
+ .locator("..")
343
+ .locator('button[aria-label*="Menu"]')
344
+ .first();
345
+ await menuBtn.click();
346
+ await page.getByText("Delete", { exact: false }).click();
347
+ await page.getByRole("button", { name: /Delete/i }).click(); // Confirm dialog
348
+ await page.waitForTimeout(2000);
349
+ await page.close();
350
+ return {
351
+ content: [
352
+ { type: "text", text: `Notebook ${notebookId} deleted.` },
353
+ ],
354
+ };
355
+ }
356
+ case "generate_audio_overview": {
357
+ const { notebookId } = AudioOverviewSchema.parse(args);
358
+ const page = await this.getPage(notebookId);
359
+ await page.getByText("Notebook guide", { exact: false }).click();
360
+ await page.getByText("Audio Overview", { exact: false }).click();
361
+ await page.getByRole("button", { name: /Generate/i }).click();
362
+ await page.close();
363
+ return {
364
+ content: [
365
+ { type: "text", text: "Audio overview generation triggered." },
366
+ ],
367
+ };
368
+ }
369
+ default:
370
+ throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${name}`);
371
+ }
372
+ }
373
+ catch (error) {
374
+ console.error(`Error in tool ${name}:`, error);
375
+ return {
376
+ content: [{ type: "text", text: `Error: ${error.message}` }],
377
+ isError: true,
378
+ };
379
+ }
59
380
  });
60
381
  }
61
382
  async run() {
@@ -68,24 +389,22 @@ const program = new Command();
68
389
  program
69
390
  .name("notebooklm-mcp")
70
391
  .description("MCP server for Google NotebookLM")
71
- .version("1.0.0");
392
+ .version("1.0.2");
72
393
  program
73
394
  .command("start")
74
- .description("Start the MCP server (stdio mode)")
395
+ .description("Start the MCP server")
75
396
  .action(async () => {
76
397
  const server = new NotebookLMServer();
77
398
  await server.run();
78
399
  });
79
400
  program
80
401
  .command("auth")
81
- .description("Open a browser to log in to Google")
402
+ .description("Open browser for authentication")
82
403
  .action(async () => {
83
404
  await runAuth();
84
405
  });
85
- // Default to start if no command provided (for MCP clients)
86
406
  if (process.argv.length <= 2) {
87
- const server = new NotebookLMServer();
88
- server.run().catch(console.error);
407
+ new NotebookLMServer().run().catch(console.error);
89
408
  }
90
409
  else {
91
410
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notebooklm-mcp-server",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Professional MCP server to connect AI agents with Google NotebookLM",
5
5
  "main": "build/index.js",
6
6
  "type": "module",