@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 +2 -1
- package/dist/content/content-manager.d.ts.map +1 -1
- package/dist/content/content-manager.js +66 -17
- package/dist/content/content-manager.js.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/session/browser-session.js +1 -1
- package/dist/session/browser-session.js.map +1 -1
- package/dist/session/shared-context-manager.d.ts.map +1 -1
- package/dist/session/shared-context-manager.js +1 -1
- package/dist/session/shared-context-manager.js.map +1 -1
- package/dist/stdio-http-proxy.js +17 -5
- package/dist/stdio-http-proxy.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +232 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/tool-names.d.ts +19 -0
- package/dist/tools/tool-names.d.ts.map +1 -0
- package/dist/tools/tool-names.js +63 -0
- package/dist/tools/tool-names.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/MCP_DIRECTORIES.md +153 -30
- package/package.json +6 -2
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
|
-
>
|
|
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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,IAAI,EAA0B,MAAM,YAAY,CAAC;AAmC/D,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,sBAAsB,EACtB,uBAAuB,EACvB,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACrB,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAMpB,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAO;gBAEP,IAAI,EAAE,IAAI;IAQtB;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;
|
|
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
|
|
604
|
-
|
|
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
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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);
|