@pan-sec/notebooklm-mcp 2026.4.0 → 2026.4.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.
@@ -4,8 +4,7 @@
4
4
  * Manages audio overview generation in NotebookLM notebooks.
5
5
  * Audio overviews are AI-generated podcast-style summaries of notebook content.
6
6
  */
7
- import { AuthManager } from "../auth/auth-manager.js";
8
- import { SharedContextManager } from "../session/shared-context-manager.js";
7
+ import { StudioManagerBase } from "./studio-manager-base.js";
9
8
  export interface AudioStatus {
10
9
  status: "not_started" | "generating" | "ready" | "failed" | "unknown";
11
10
  progress?: number;
@@ -23,15 +22,12 @@ export interface DownloadAudioResult {
23
22
  size?: number;
24
23
  error?: string;
25
24
  }
26
- export declare class AudioManager {
27
- private authManager;
28
- private contextManager;
29
- private page;
30
- constructor(authManager: AuthManager, contextManager: SharedContextManager);
31
- /**
32
- * Navigate to a notebook and ensure we're on the right page
33
- */
34
- private navigateToNotebook;
25
+ export declare class AudioManager extends StudioManagerBase {
26
+ protected readonly logName = "audio-manager";
27
+ protected readonly navigateDelay: {
28
+ min: number;
29
+ max: number;
30
+ };
35
31
  /**
36
32
  * Generate an audio overview for a notebook
37
33
  */
@@ -48,8 +44,4 @@ export declare class AudioManager {
48
44
  * Download the generated audio file
49
45
  */
50
46
  downloadAudio(notebookUrl: string, outputPath?: string): Promise<DownloadAudioResult>;
51
- /**
52
- * Close the page if open
53
- */
54
- private closePage;
55
47
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { log } from "../utils/logger.js";
8
8
  import { randomDelay } from "../utils/stealth-utils.js";
9
+ import { StudioManagerBase, } from "./studio-manager-base.js";
9
10
  import fs from "fs";
10
11
  import path from "path";
11
12
  import os from "os";
@@ -100,29 +101,9 @@ const AUDIO_SELECTORS = {
100
101
  text: '[class*="progress-text"], [class*="eta"]',
101
102
  },
102
103
  };
103
- export class AudioManager {
104
- authManager;
105
- contextManager;
106
- page = null;
107
- constructor(authManager, contextManager) {
108
- this.authManager = authManager;
109
- this.contextManager = contextManager;
110
- }
111
- /**
112
- * Navigate to a notebook and ensure we're on the right page
113
- */
114
- async navigateToNotebook(notebookUrl) {
115
- const context = await this.contextManager.getOrCreateContext();
116
- const isAuth = await this.authManager.validateWithRetry(context);
117
- if (!isAuth) {
118
- throw new Error("Not authenticated. Run setup_auth first.");
119
- }
120
- this.page = await context.newPage();
121
- await this.page.goto(notebookUrl, { waitUntil: "domcontentloaded" });
122
- await this.page.waitForLoadState("networkidle").catch(() => { });
123
- await randomDelay(1500, 2500);
124
- return this.page;
125
- }
104
+ export class AudioManager extends StudioManagerBase {
105
+ logName = "audio-manager";
106
+ navigateDelay = { min: 1500, max: 2500 };
126
107
  /**
127
108
  * Generate an audio overview for a notebook
128
109
  */
@@ -403,19 +384,4 @@ export class AudioManager {
403
384
  await this.closePage();
404
385
  }
405
386
  }
406
- /**
407
- * Close the page if open
408
- */
409
- async closePage() {
410
- if (this.page) {
411
- try {
412
- await this.page.close();
413
- }
414
- catch (err) {
415
- log.debug(`audio-manager: closing page: ${err instanceof Error ? err.message : String(err)}`);
416
- // Ignore close errors
417
- }
418
- this.page = null;
419
- }
420
- }
421
387
  }
@@ -14,8 +14,7 @@
14
14
  * - Artifact title during generation: "Generating data table…"
15
15
  * - Chat-embedded tables use standard <table><tr><th>/<td> (no <tbody>)
16
16
  */
17
- import { AuthManager } from "../auth/auth-manager.js";
18
- import { SharedContextManager } from "../session/shared-context-manager.js";
17
+ import { StudioManagerBase } from "./studio-manager-base.js";
19
18
  export interface DataTable {
20
19
  headers: string[];
21
20
  rows: string[][];
@@ -36,15 +35,8 @@ export interface GetDataTableResult {
36
35
  table?: DataTable;
37
36
  error?: string;
38
37
  }
39
- export declare class DataTableManager {
40
- private authManager;
41
- private contextManager;
42
- private page;
43
- constructor(authManager: AuthManager, contextManager: SharedContextManager);
44
- /**
45
- * Navigate to a notebook and ensure we're on the right page
46
- */
47
- private navigateToNotebook;
38
+ export declare class DataTableManager extends StudioManagerBase {
39
+ protected readonly logName = "data-table-manager";
48
40
  /**
49
41
  * Ensure the Studio panel is visible (expand if collapsed).
50
42
  *
@@ -94,8 +86,4 @@ export declare class DataTableManager {
94
86
  * to the calling model.
95
87
  */
96
88
  private sanitizeTable;
97
- /**
98
- * Close the page if open
99
- */
100
- private closePage;
101
89
  }
@@ -17,29 +17,9 @@
17
17
  import { log } from "../utils/logger.js";
18
18
  import { randomDelay } from "../utils/stealth-utils.js";
19
19
  import { validateResponse } from "../utils/response-validator.js";
20
- export class DataTableManager {
21
- authManager;
22
- contextManager;
23
- page = null;
24
- constructor(authManager, contextManager) {
25
- this.authManager = authManager;
26
- this.contextManager = contextManager;
27
- }
28
- /**
29
- * Navigate to a notebook and ensure we're on the right page
30
- */
31
- async navigateToNotebook(notebookUrl) {
32
- const context = await this.contextManager.getOrCreateContext();
33
- const isAuth = await this.authManager.validateWithRetry(context);
34
- if (!isAuth) {
35
- throw new Error("Not authenticated. Run setup_auth first.");
36
- }
37
- this.page = await context.newPage();
38
- await this.page.goto(notebookUrl, { waitUntil: "domcontentloaded" });
39
- await this.page.waitForLoadState("networkidle").catch(() => { });
40
- await randomDelay(2000, 3000);
41
- return this.page;
42
- }
20
+ import { StudioManagerBase, } from "./studio-manager-base.js";
21
+ export class DataTableManager extends StudioManagerBase {
22
+ logName = "data-table-manager";
43
23
  /**
44
24
  * Ensure the Studio panel is visible (expand if collapsed).
45
25
  *
@@ -288,6 +268,12 @@ export class DataTableManager {
288
268
  error: "Could not click on data table artifact.",
289
269
  };
290
270
  }
271
+ // Status is already confirmed "ready" above, so a table is expected to
272
+ // render after opening the artifact. Wait for the concrete <table> element
273
+ // that extractTableData() reads, instead of relying on a fixed sleep as the
274
+ // only readiness gate (M30). Bounded + .catch so a miss degrades to the
275
+ // prior behavior (fall through to extractTableData, which returns null).
276
+ await page.waitForSelector("table", { timeout: 10000 }).catch(() => { });
291
277
  await randomDelay(2000, 3000);
292
278
  // Extract table data from the page
293
279
  const table = await this.extractTableData(page);
@@ -392,19 +378,4 @@ export class DataTableManager {
392
378
  totalColumns: table.totalColumns,
393
379
  };
394
380
  }
395
- /**
396
- * Close the page if open
397
- */
398
- async closePage() {
399
- if (this.page) {
400
- try {
401
- await this.page.close();
402
- }
403
- catch (err) {
404
- log.debug(`data-table-manager: closing page: ${err instanceof Error ? err.message : String(err)}`);
405
- // Ignore close errors
406
- }
407
- this.page = null;
408
- }
409
- }
410
381
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Source Manager — shared primitives
3
+ *
4
+ * Helpers genuinely shared between the two source managers in
5
+ * `source-manager.ts`:
6
+ * - SourceManager (legacy: list/add/remove on an existing
7
+ * notebook)
8
+ * - NotebookCreationSourceManager (the creation flow)
9
+ *
10
+ * Only logic that is byte-equivalent between the two classes lives here; the
11
+ * managers' distinct flows (source-type selection, submit/insert clicks, error
12
+ * alert handling, waitForSourceProcessing) intentionally differ and stay in
13
+ * place. These helpers are a pure de-duplication and are behavior-preserving.
14
+ */
15
+ import type { Page } from "patchright";
16
+ export type BrowserDomElement = unknown;
17
+ export type BrowserSourceItem = {
18
+ textContent?: string | null;
19
+ className?: string;
20
+ id?: string;
21
+ getAttribute(name: string): string | null;
22
+ querySelector(selector: string): BrowserDomElement | null;
23
+ click(): void;
24
+ };
25
+ export type BrowserVisibleElement = {
26
+ textContent?: string | null;
27
+ className?: string;
28
+ offsetParent?: unknown;
29
+ parentElement?: BrowserVisibleElement | null;
30
+ getAttribute(name: string): string | null;
31
+ click(): void;
32
+ };
33
+ export type BrowserTextVisibleElement = BrowserVisibleElement & {
34
+ className?: string;
35
+ };
36
+ export type BrowserTextAreaCandidate = BrowserVisibleElement & {
37
+ value?: string;
38
+ };
39
+ export type BrowserInnerTextHandle = {
40
+ innerText(): Promise<string>;
41
+ };
42
+ export type BrowserDocumentContext = {
43
+ document: {
44
+ querySelector(selector: string): BrowserDomElement | null;
45
+ querySelectorAll(selector: string): Iterable<BrowserDomElement>;
46
+ };
47
+ window: {
48
+ innerWidth: number;
49
+ };
50
+ };
51
+ /**
52
+ * Find the first `textInput` selector that resolves to a visible textarea which
53
+ * is NOT the "discover sources" / "search the web" / query-box control.
54
+ *
55
+ * This is the exact, equivalent logic that both SourceManager and
56
+ * NotebookCreationSourceManager used (their two private copies differed only in
57
+ * an unused `rejected` field and the debug-log wording — callers read only the
58
+ * boolean match). The three exclusion substrings are kept byte-identical.
59
+ *
60
+ * @param debugLabel preserves each caller's original debug log line verbatim.
61
+ */
62
+ export declare function findValidTextInputSelector(page: Page, debugLabel: string): Promise<string | null>;
63
+ /**
64
+ * Set the text-source content via the direct DOM-value path only.
65
+ * The value is passed as a serialized page.evaluate argument (never
66
+ * string-interpolated). We deliberately do NOT use the OS copy/paste
67
+ * buffer: writing to it leaks/overwrites the host's copied data and, when
68
+ * the relevant browser permission is denied (common in headless), a paste
69
+ * keystroke would inject STALE buffered contents and corrupt the source.
70
+ *
71
+ * This is the byte-identical evaluate block both classes used. The differing
72
+ * focus/click step that precedes it (textArea.focus() in the legacy manager,
73
+ * textarea.click() in the creation manager) stays with each caller.
74
+ */
75
+ export declare function setTextSourceValue(page: Page, selector: string, value: string): Promise<void>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Source Manager — shared primitives
3
+ *
4
+ * Helpers genuinely shared between the two source managers in
5
+ * `source-manager.ts`:
6
+ * - SourceManager (legacy: list/add/remove on an existing
7
+ * notebook)
8
+ * - NotebookCreationSourceManager (the creation flow)
9
+ *
10
+ * Only logic that is byte-equivalent between the two classes lives here; the
11
+ * managers' distinct flows (source-type selection, submit/insert clicks, error
12
+ * alert handling, waitForSourceProcessing) intentionally differ and stay in
13
+ * place. These helpers are a pure de-duplication and are behavior-preserving.
14
+ */
15
+ import { log } from "../utils/logger.js";
16
+ import { getSelectors } from "./selectors.js";
17
+ /**
18
+ * Find the first `textInput` selector that resolves to a visible textarea which
19
+ * is NOT the "discover sources" / "search the web" / query-box control.
20
+ *
21
+ * This is the exact, equivalent logic that both SourceManager and
22
+ * NotebookCreationSourceManager used (their two private copies differed only in
23
+ * an unused `rejected` field and the debug-log wording — callers read only the
24
+ * boolean match). The three exclusion substrings are kept byte-identical.
25
+ *
26
+ * @param debugLabel preserves each caller's original debug log line verbatim.
27
+ */
28
+ export async function findValidTextInputSelector(page, debugLabel) {
29
+ for (const selector of getSelectors("textInput")) {
30
+ try {
31
+ const state = await page.evaluate((targetSelector) => {
32
+ const browser = globalThis;
33
+ const textarea = browser.document.querySelector(targetSelector);
34
+ if (!textarea || textarea.offsetParent === null) {
35
+ return { matches: false };
36
+ }
37
+ const aria = textarea.getAttribute("aria-label")?.toLowerCase() || "";
38
+ const placeholder = textarea.getAttribute("placeholder")?.toLowerCase() || "";
39
+ const className = textarea.className?.toLowerCase() || "";
40
+ const haystack = `${aria} ${placeholder} ${className}`;
41
+ return {
42
+ matches: !haystack.includes("discover sources") &&
43
+ !haystack.includes("search the web") &&
44
+ !haystack.includes("query-box"),
45
+ };
46
+ }, selector);
47
+ if (state.matches) {
48
+ return selector;
49
+ }
50
+ }
51
+ catch (err) {
52
+ log.debug(`${debugLabel}: ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Set the text-source content via the direct DOM-value path only.
59
+ * The value is passed as a serialized page.evaluate argument (never
60
+ * string-interpolated). We deliberately do NOT use the OS copy/paste
61
+ * buffer: writing to it leaks/overwrites the host's copied data and, when
62
+ * the relevant browser permission is denied (common in headless), a paste
63
+ * keystroke would inject STALE buffered contents and corrupt the source.
64
+ *
65
+ * This is the byte-identical evaluate block both classes used. The differing
66
+ * focus/click step that precedes it (textArea.focus() in the legacy manager,
67
+ * textarea.click() in the creation manager) stays with each caller.
68
+ */
69
+ export async function setTextSourceValue(page, selector, value) {
70
+ await page.evaluate(({ selector, value }) => {
71
+ const browser = globalThis;
72
+ const textarea = browser.document.querySelector(selector);
73
+ if (!textarea)
74
+ return false;
75
+ textarea.focus();
76
+ if (typeof textarea.value === "string") {
77
+ textarea.value = value;
78
+ }
79
+ else {
80
+ textarea.textContent = value;
81
+ }
82
+ textarea.dispatchEvent(new browser.Event("input", { bubbles: true }));
83
+ textarea.dispatchEvent(new browser.Event("change", { bubbles: true }));
84
+ return true;
85
+ }, { selector, value });
86
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Studio Manager Base
3
+ *
4
+ * Shared page-lifecycle plumbing for the NotebookLM "Studio" managers
5
+ * (audio, video, data table). These managers all open a notebook page,
6
+ * validate authentication, drive a Studio-panel artifact flow, then close
7
+ * the page. The navigation and teardown logic is identical across them and
8
+ * lives here; the artifact-specific flows stay in each subclass.
9
+ *
10
+ * Behavior is preserved exactly:
11
+ * - navigateToNotebook: getOrCreateContext → validateWithRetry (throws
12
+ * "Not authenticated. Run setup_auth first." when false) → newPage →
13
+ * goto(domcontentloaded) → waitForLoadState("networkidle") (errors
14
+ * swallowed) → per-subclass randomDelay → return page.
15
+ * - closePage: close the page, swallowing errors, then null the handle.
16
+ *
17
+ * Per-subclass differences are expressed as protected properties so that no
18
+ * call site needs to thread arguments:
19
+ * - navigateDelay: video & data table use 2000–3000ms, audio uses 1500–2500ms.
20
+ * - logName: the log-prefix used in the swallowed close-error debug line.
21
+ */
22
+ import type { Page } from "patchright";
23
+ import { AuthManager } from "../auth/auth-manager.js";
24
+ import { SharedContextManager } from "../session/shared-context-manager.js";
25
+ /** Opaque DOM element handle used only in `as`-casts inside page.evaluate bodies. */
26
+ export type BrowserDomElement = unknown;
27
+ /** Minimal `document` shape used inside page.evaluate bodies. Common to all managers. */
28
+ export interface BrowserDocumentContext {
29
+ document: {
30
+ querySelector(selector: string): BrowserDomElement | null;
31
+ querySelectorAll(selector: string): Iterable<BrowserDomElement>;
32
+ };
33
+ }
34
+ export declare abstract class StudioManagerBase {
35
+ protected authManager: AuthManager;
36
+ protected contextManager: SharedContextManager;
37
+ protected page: Page | null;
38
+ /**
39
+ * Trailing delay applied after navigation settles. Video & data table use the
40
+ * default (2000–3000ms); audio overrides to 1500–2500ms.
41
+ */
42
+ protected readonly navigateDelay: {
43
+ min: number;
44
+ max: number;
45
+ };
46
+ /** Log prefix used in the swallowed close-error debug line (kept per-manager). */
47
+ protected abstract readonly logName: string;
48
+ constructor(authManager: AuthManager, contextManager: SharedContextManager);
49
+ /**
50
+ * Navigate to a notebook and ensure we're on the right page
51
+ */
52
+ protected navigateToNotebook(notebookUrl: string): Promise<Page>;
53
+ /**
54
+ * Close the page if open
55
+ */
56
+ protected closePage(): Promise<void>;
57
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Studio Manager Base
3
+ *
4
+ * Shared page-lifecycle plumbing for the NotebookLM "Studio" managers
5
+ * (audio, video, data table). These managers all open a notebook page,
6
+ * validate authentication, drive a Studio-panel artifact flow, then close
7
+ * the page. The navigation and teardown logic is identical across them and
8
+ * lives here; the artifact-specific flows stay in each subclass.
9
+ *
10
+ * Behavior is preserved exactly:
11
+ * - navigateToNotebook: getOrCreateContext → validateWithRetry (throws
12
+ * "Not authenticated. Run setup_auth first." when false) → newPage →
13
+ * goto(domcontentloaded) → waitForLoadState("networkidle") (errors
14
+ * swallowed) → per-subclass randomDelay → return page.
15
+ * - closePage: close the page, swallowing errors, then null the handle.
16
+ *
17
+ * Per-subclass differences are expressed as protected properties so that no
18
+ * call site needs to thread arguments:
19
+ * - navigateDelay: video & data table use 2000–3000ms, audio uses 1500–2500ms.
20
+ * - logName: the log-prefix used in the swallowed close-error debug line.
21
+ */
22
+ import { log } from "../utils/logger.js";
23
+ import { randomDelay } from "../utils/stealth-utils.js";
24
+ export class StudioManagerBase {
25
+ authManager;
26
+ contextManager;
27
+ page = null;
28
+ /**
29
+ * Trailing delay applied after navigation settles. Video & data table use the
30
+ * default (2000–3000ms); audio overrides to 1500–2500ms.
31
+ */
32
+ navigateDelay = { min: 2000, max: 3000 };
33
+ constructor(authManager, contextManager) {
34
+ this.authManager = authManager;
35
+ this.contextManager = contextManager;
36
+ }
37
+ /**
38
+ * Navigate to a notebook and ensure we're on the right page
39
+ */
40
+ async navigateToNotebook(notebookUrl) {
41
+ const context = await this.contextManager.getOrCreateContext();
42
+ const isAuth = await this.authManager.validateWithRetry(context);
43
+ if (!isAuth) {
44
+ throw new Error("Not authenticated. Run setup_auth first.");
45
+ }
46
+ this.page = await context.newPage();
47
+ await this.page.goto(notebookUrl, { waitUntil: "domcontentloaded" });
48
+ await this.page.waitForLoadState("networkidle").catch(() => { });
49
+ await randomDelay(this.navigateDelay.min, this.navigateDelay.max);
50
+ return this.page;
51
+ }
52
+ /**
53
+ * Close the page if open
54
+ */
55
+ async closePage() {
56
+ if (this.page) {
57
+ try {
58
+ await this.page.close();
59
+ }
60
+ catch (err) {
61
+ log.debug(`${this.logName}: closing page: ${err instanceof Error ? err.message : String(err)}`);
62
+ // Ignore close errors
63
+ }
64
+ this.page = null;
65
+ }
66
+ }
67
+ }
@@ -14,8 +14,7 @@
14
14
  * - Artifacts appear in .artifact-library-container with .artifact-item-button
15
15
  * - Generating state: .shimmer-blue class + .rotate icon + "Generating" title
16
16
  */
17
- import { AuthManager } from "../auth/auth-manager.js";
18
- import { SharedContextManager } from "../session/shared-context-manager.js";
17
+ import { StudioManagerBase } from "./studio-manager-base.js";
19
18
  /**
20
19
  * Visual styles for video overviews (matches actual NotebookLM UI)
21
20
  */
@@ -34,15 +33,8 @@ export interface GenerateVideoResult {
34
33
  status: VideoStatus;
35
34
  error?: string;
36
35
  }
37
- export declare class VideoManager {
38
- private authManager;
39
- private contextManager;
40
- private page;
41
- constructor(authManager: AuthManager, contextManager: SharedContextManager);
42
- /**
43
- * Navigate to a notebook and ensure we're on the right page
44
- */
45
- private navigateToNotebook;
36
+ export declare class VideoManager extends StudioManagerBase {
37
+ protected readonly logName = "video-manager";
46
38
  /**
47
39
  * Ensure the Studio panel is visible (expand if collapsed).
48
40
  *
@@ -93,8 +85,4 @@ export declare class VideoManager {
93
85
  * Check the current video status for a notebook
94
86
  */
95
87
  getVideoStatus(notebookUrl: string): Promise<VideoStatus>;
96
- /**
97
- * Close the page if open
98
- */
99
- private closePage;
100
88
  }
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { log } from "../utils/logger.js";
18
18
  import { randomDelay } from "../utils/stealth-utils.js";
19
+ import { StudioManagerBase, } from "./studio-manager-base.js";
19
20
  /**
20
21
  * Allowlisted style values. Single source of truth used to validate untrusted
21
22
  * input before it is passed into the page context (prevents attribute-selector
@@ -39,29 +40,8 @@ const VALID_VIDEO_STYLES = [
39
40
  * injection in selectFormat).
40
41
  */
41
42
  const VALID_VIDEO_FORMATS = ["explainer", "brief"];
42
- export class VideoManager {
43
- authManager;
44
- contextManager;
45
- page = null;
46
- constructor(authManager, contextManager) {
47
- this.authManager = authManager;
48
- this.contextManager = contextManager;
49
- }
50
- /**
51
- * Navigate to a notebook and ensure we're on the right page
52
- */
53
- async navigateToNotebook(notebookUrl) {
54
- const context = await this.contextManager.getOrCreateContext();
55
- const isAuth = await this.authManager.validateWithRetry(context);
56
- if (!isAuth) {
57
- throw new Error("Not authenticated. Run setup_auth first.");
58
- }
59
- this.page = await context.newPage();
60
- await this.page.goto(notebookUrl, { waitUntil: "domcontentloaded" });
61
- await this.page.waitForLoadState("networkidle").catch(() => { });
62
- await randomDelay(2000, 3000);
63
- return this.page;
64
- }
43
+ export class VideoManager extends StudioManagerBase {
44
+ logName = "video-manager";
65
45
  /**
66
46
  * Ensure the Studio panel is visible (expand if collapsed).
67
47
  *
@@ -449,19 +429,4 @@ export class VideoManager {
449
429
  await this.closePage();
450
430
  }
451
431
  }
452
- /**
453
- * Close the page if open
454
- */
455
- async closePage() {
456
- if (this.page) {
457
- try {
458
- await this.page.close();
459
- }
460
- catch (err) {
461
- log.debug(`video-manager: closing page: ${err instanceof Error ? err.message : String(err)}`);
462
- // Ignore close errors
463
- }
464
- this.page = null;
465
- }
466
- }
467
432
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pan-sec/notebooklm-mcp",
3
- "version": "2026.4.0",
3
+ "version": "2026.4.1",
4
4
  "mcpName": "io.github.Pantheon-Security/notebooklm-mcp-secure",
5
5
  "description": "Security-hardened MCP server for NotebookLM API with compliance-ready architecture (GDPR, SOC2, CSSF controls implemented)",
6
6
  "type": "module",