@roomi-fields/notebooklm-mcp 1.7.9 → 2.0.1

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Automate Google NotebookLM at scale. 33-endpoint HTTP REST API for n8n / Zapier / Make / curl, plus an MCP server for Claude Code / Cursor / Codex. Citation-backed Q&A, full Studio generation (audio · video · infographic · report · presentation · data table), multi-account rotation with auto-reauth.**
6
6
 
7
- > v1.7.9 — production-grade, batch-tested on overnight runs of 1 000+ questions. New: `batch_to_vault` is now a first-class MCP tool (no HTTP server required) on top of the existing `POST /batch-to-vault` endpoint. See [RTFM integration](./deployment/docs/14-RTFM-INTEGRATION.md) for the full pattern. [Compare with `PleasePrompto/notebooklm-mcp` v2.0.0](https://roomi-fields.github.io/notebooklm-mcp/compare) to see when this project is the right pick (REST API, full Studio, auto-reauth) and when the MCP-only upstream is.
7
+ > v2.0.1 — production-grade, batch-tested on overnight runs of 1 000+ questions. Tools are now a dot-notation tree (`notebook.ask`, `source.add`, `session.list`…) with MCP `annotations` + `outputSchema` on every tool — **the old flat names still work as aliases, nothing breaks**. See the [changelog](./CHANGELOG.md) for the full mapping. [Compare with `PleasePrompto/notebooklm-mcp` v2.0.0](https://roomi-fields.github.io/notebooklm-mcp/compare) to see when this project is the right pick (REST API, full Studio, auto-reauth) and when the MCP-only upstream is.
8
8
 
9
9
  <!-- Badges -->
10
10
 
@@ -174,6 +174,7 @@ See [ROADMAP.md](./ROADMAP.md) for planned features and version history.
174
174
 
175
175
  **Latest releases:**
176
176
 
177
+ - **v2.0.0** — Tools renamed to a dot-notation tree (`notebook.ask`, `source.add`, `session.list`, `server.health`, `vault.batch`…) across 9 namespaces; `tools/list` advertises only the canonical names. **Backward compatible — the legacy flat names still work as aliases**, so existing scripts and configs keep running. Also adds MCP `annotations` (read-only / destructive / idempotent / open-world hints) and `outputSchema` + `structuredContent` on every tool. Published on the [Smithery registry](https://smithery.ai/servers/roomifields/notebooklm-mcp).
177
178
  - **v1.7.9** — Security: resolve moderate XSS advisory GHSA-v2v4-37r5-5v8g in transitive `ip-address ≤10.1.0` (pulled in via `@modelcontextprotocol/sdk` → `express-rate-limit`) by pinning `ip-address ^10.2.0` in `overrides`. `npm audit` clean. Unblocks the CI security gate that 1.7.8 had been failing.
178
179
  - **v1.7.8** — `add_source` false-negative fix (verified at runtime against a live MCP session this time): the count-based success detection now runs on every poll cycle instead of only after the upload dialog closes, since NotebookLM 2026 keeps the dialog open to allow chained uploads. Also fixes a long-standing packaging bug where `dist/index.js` was published in mode 644 (no `+x`), causing silent `Permission denied` failures in sandbox shells
179
180
  - **v1.7.7** — `add_source` defensive patch: broaden selectors to cover the empty/fresh-notebook "Upload sources" CTA (EN+FR), and replace the bare `Could not find "Add source" button` error with a structured DOM dump (URL, title, top 25 buttons + their aria-label/text/class) so the next iteration can be precise. Not validated runtime — the enriched diagnostic is the deliverable
@@ -1 +1 @@
1
- {"version":3,"file":"content-manager.d.ts","sourceRoot":"","sources":["../../src/content/content-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"content-manager.d.ts","sourceRoot":"","sources":["../../src/content/content-manager.ts"],"names":[],"mappings":"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"}
@@ -51,6 +51,18 @@ export class ContentManager {
51
51
  log.info(` 🎯 Target notebook UUID: ${expectedNotebookUuid || 'NOT FOUND'}`);
52
52
  try {
53
53
  const existingSourceNames = await this.getAllSourceLabels();
54
+ // Capture the source-row count BEFORE opening the upload dialog. The
55
+ // 2026 UI adds the new row to the DOM almost instantly after the user
56
+ // clicks Insert/Upload, so a snapshot taken later by
57
+ // waitForSourceProcessing would already read N+1 and never observe a
58
+ // count increase. We thread this baseline through each upload method.
59
+ let initialSourceCount = -1;
60
+ try {
61
+ initialSourceCount = await this.page.locator('.single-source-container').count();
62
+ }
63
+ catch {
64
+ /* ignore — waitForSourceProcessing falls back to its own snapshot */
65
+ }
54
66
  // Click "Add source" button
55
67
  await this.clickAddSource();
56
68
  // DEBUG: Screenshot after clicking add source to see what UI appeared
@@ -70,7 +82,7 @@ export class ContentManager {
70
82
  case 'file':
71
83
  return await this.uploadFile(input, expectedNotebookUuid, existingSourceNames);
72
84
  case 'url':
73
- return await this.uploadUrl(input, expectedNotebookUuid, existingSourceNames);
85
+ return await this.uploadUrl(input, expectedNotebookUuid, existingSourceNames, initialSourceCount);
74
86
  case 'text':
75
87
  return await this.uploadText(input, expectedNotebookUuid, existingSourceNames);
76
88
  case 'google_drive':
@@ -388,7 +400,7 @@ export class ContentManager {
388
400
  /**
389
401
  * Upload from URL
390
402
  */
391
- async uploadUrl(input, expectedNotebookUuid, previousSourceNames = []) {
403
+ async uploadUrl(input, expectedNotebookUuid, previousSourceNames = [], knownInitialCount) {
392
404
  if (!input.url) {
393
405
  return { success: false, error: 'URL is required' };
394
406
  }
@@ -401,6 +413,11 @@ export class ContentManager {
401
413
  ...i18nSelectors('button:has-text("{text}")', 'sourceTypes', 'website'),
402
414
  ...i18nSelectors('button:has-text("{text}")', 'sourceTypes', 'link'),
403
415
  ...i18nSelectors('button:has-text("{text}")', 'sourceTypes', 'url'),
416
+ // 2026 redesign: source types are Angular Material chips
417
+ ...i18nSelectors('mat-chip:has-text("{text}")', 'sourceTypes', 'website'),
418
+ ...i18nSelectors('mat-mdc-chip:has-text("{text}")', 'sourceTypes', 'website'),
419
+ ...i18nSelectors('[role="button"]:has-text("{text}")', 'sourceTypes', 'website'),
420
+ ...i18nSelectors('[role="button"]:has-text("{text}")', 'sourceTypes', 'link'),
404
421
  // New UI (2025+): source types may be spans, divs, or clickable list items
405
422
  ...i18nSelectors('span:has-text("{text}")', 'sourceTypes', 'website'),
406
423
  ...i18nSelectors('div:has-text("{text}")', 'sourceTypes', 'website'),
@@ -457,6 +474,16 @@ export class ContentManager {
457
474
  // IMPORTANT: Exclude the "Search the web" search bar which is always visible
458
475
  log.info(` 🔍 Looking for URL input...`);
459
476
  const urlInputSelectors = [
477
+ // 2026 redesign: the "Websites" sub-view exposes a textarea with
478
+ // aria-label="Enter URLs" / placeholder="Paste any links". These
479
+ // aria-label selectors must come FIRST so they win against the
480
+ // generic [role="dialog"] textarea fallback below, which would
481
+ // otherwise grab the always-present "Search the web" box.
482
+ 'textarea[aria-label*="Enter URL" i]',
483
+ 'textarea[aria-label*="URL" i]',
484
+ 'input[aria-label*="Enter URL" i]',
485
+ 'input[aria-label*="URL" i]',
486
+ 'textarea[placeholder*="Paste any link" i]',
460
487
  // i18n placeholder selectors
461
488
  ...i18nSelectors('input[placeholder*="{text}"]', 'placeholders', 'pasteUrl'),
462
489
  ...i18nSelectors('textarea[placeholder*="{text}"]', 'placeholders', 'pasteUrl'),
@@ -600,8 +627,21 @@ export class ContentManager {
600
627
  // Click add/upload button
601
628
  log.info(` 🔍 Looking for upload button...`);
602
629
  await this.clickUploadButton();
603
- // Wait for processing
604
- const result = await this.waitForSourceProcessing(input.title || input.url, undefined, expectedNotebookUuid, previousSourceNames);
630
+ // Wait for the upload dialog to close on its own before any verification
631
+ // step touches the page. waitForSourceProcessing getVisibleSourceRows
632
+ // ensureSourcesPanel will press Escape on any visible [role="dialog"],
633
+ // and on the 2026 UI that Escape lands DURING ingest and cancels the
634
+ // add. Letting the dialog close naturally first avoids the race.
635
+ await this.page
636
+ .locator('[role="dialog"]')
637
+ .first()
638
+ .waitFor({ state: 'hidden', timeout: 30000 })
639
+ .catch(() => {
640
+ /* dialog may stay open on success (multi-upload UX) — handled later */
641
+ });
642
+ // Wait for processing — pass the pre-baseline count captured by
643
+ // addSource() so detection works on the 2026 UI (see waitForSourceProcessing).
644
+ const result = await this.waitForSourceProcessing(input.title || input.url, undefined, expectedNotebookUuid, previousSourceNames, knownInitialCount);
605
645
  return result;
606
646
  }
607
647
  catch (error) {
@@ -1000,22 +1040,31 @@ export class ContentManager {
1000
1040
  * @param _textPreview Optional first words of text (for text sources - NotebookLM may use this as name)
1001
1041
  * @param expectedNotebookUuid Optional UUID of the notebook we expect to be on (to detect redirects)
1002
1042
  */
1003
- async waitForSourceProcessing(sourceName, _textPreview, expectedNotebookUuid, previousSourceNames = []) {
1043
+ async waitForSourceProcessing(sourceName, _textPreview, expectedNotebookUuid, previousSourceNames = [], knownInitialCount) {
1004
1044
  log.info(` ⏳ Waiting for source processing: ${sourceName}`);
1005
1045
  const timeout = 90000; // 1.5 minutes (sources can take time)
1006
1046
  const startTime = Date.now();
1007
- // COUNT-BASED DETECTION (primary method for 2025 UI):
1008
- // Capture source count NOW dialog is still open, source not yet added to DOM.
1009
- // Later, if count increases, we know a source was successfully added.
1010
- let initialSourceCount = -1;
1011
- let initialSourceLabels = [];
1012
- try {
1013
- initialSourceCount = await this.page.locator('.single-source-container').count();
1014
- log.info(` 📊 Source count before processing: ${initialSourceCount}`);
1015
- initialSourceLabels = await this.getAllSourceLabels();
1016
- }
1017
- catch {
1018
- /* ignore */
1047
+ // COUNT-BASED DETECTION (primary method for 2025+ UI):
1048
+ // The 2026 UI adds the new source row to the DOM *before* this function
1049
+ // runs, so capturing the count here would already read N+1 and the
1050
+ // `currentCount > initialSourceCount` check would never trip. Callers
1051
+ // therefore capture the baseline BEFORE opening the dialog and pass it
1052
+ // through as `knownInitialCount`. We only fall back to a fresh snapshot
1053
+ // for internal callers that don't know the pre-baseline (e.g. notes).
1054
+ let initialSourceCount = knownInitialCount ?? -1;
1055
+ let initialSourceLabels = previousSourceNames;
1056
+ if (knownInitialCount !== undefined) {
1057
+ log.info(` 📊 Source count before processing (from caller): ${initialSourceCount}`);
1058
+ }
1059
+ else {
1060
+ try {
1061
+ initialSourceCount = await this.page.locator('.single-source-container').count();
1062
+ log.info(` 📊 Source count before processing (snapshot): ${initialSourceCount}`);
1063
+ initialSourceLabels = await this.getAllSourceLabels();
1064
+ }
1065
+ catch {
1066
+ /* ignore */
1067
+ }
1019
1068
  }
1020
1069
  // First, wait a bit for the dialog to close (indicates upload started)
1021
1070
  await randomDelay(2000, 3000);