Skip to content

NotebookLM prep integration (Technical)

Status: optional, opt-in, off by default. This document covers letting a user send their own Study & prep materials into their own NotebookLM notebook for audio overviews and grounded Q&A, as a companion to the Claude study project already generated on the Study screen. It records the feasibility research verdict, the guardrails that make this safe to ship, and the two implementations behind one seam.

Plain-language version: ../plain/notebooklm.md

0. Sources

1. Feasibility verdict: no official consumer API, unofficial MCP only

Consumer NotebookLM (the free, personal-Google-account product this feature targets) has no public API of any kind. Google's only official programmatic surface is the NotebookLM Enterprise API — gated behind a Google Cloud project + a paid Gemini Enterprise / Gemini Education Premium license. That's an organizational/compliance product for companies, not something a solo user's personal Google account can call. There is no beta, waitlist, or announced timeline for a consumer-facing API as of this writing.

Every "NotebookLM MCP server" that exists (the ones read for this task: PleasePrompto/notebooklm-mcp, moodRobotics/notebooklm-mcp-server, jacob-bd/notebooklm-mcp-cli, and the setup guides at aimaker.substack and XDA) is community-built browser automation, not an API client. They all follow the same shape:

  1. A Playwright/Chrome-driven session logs into notebooklm.google.com as if a human were typing — the setup guides describe a setup_auth step that opens a visible Chrome window the first time, where the user manually signs into their own Google account by hand, exactly like any other browser session. The tool then persists that browser profile/session so subsequent runs are headless.
  2. Once authenticated, the tool drives the same UI actions a human would: create a notebook, add a source (paste text / upload a file / paste a URL), ask a question, or click "Generate" on an Audio Overview / Study Guide / etc.
  3. Nothing here is a documented, versioned API contract — it is scripted UI automation against whatever DOM NotebookLM currently renders.

Capabilities, as documented by these projects: create/open a notebook, add sources (text, files, URLs), ask grounded questions over the notebook's sources with citations, and trigger studio outputs (Audio Overview, and per some projects' READMEs, video/mind-map/report/flashcard/quiz/ slide-deck generation — NotebookLM has been adding studio formats faster than any one MCP's docs stay current, so treat the exact list as "whatever NotebookLM's UI currently offers," not a fixed API surface).

Auth, concretely: the user's own Google account, via a real interactive browser login on first use, with the session persisted afterward. No MCP surveyed asks for a Google password to be typed into a config file, an environment variable, or any non-Google input field — the login always happens in an actual Google-hosted login page inside the automated browser.

Reliability caveats (real, not hypothetical): these tools scrape a live, unversioned web UI. Google can and does change NotebookLM's DOM/flows without notice, which breaks selector-based automation until the MCP project ships a fix — several of the surveyed repos show recent commits chasing exactly this. Expect occasional breakage, not five-nines reliability. None of this is Google-supported; if NotebookLM's UI changes in a way that breaks a given MCP, there's no support ticket to file with Google about it.

ToS posture: automating a personal Google product's web UI on the user's own logged-in session is a materially different risk profile than the bulk/anonymous scraping this codebase already refuses to do elsewhere (see job-signals.md for the LinkedIn/layoffs.fyi precedent) — there's no bulk data extraction, no anonymous crawling, and no redistribution of anyone else's content. It is still browser automation of a consumer product's UI that Google has not published an API or an automation policy for, which sits in a ToS gray area common to every "MCP for [consumer Google/Microsoft/etc. product]" pattern. WorkWingman's answer to that gray area, consistent with how it already treats LinkedIn scraping and the Google OAuth stubs: never ship this as a silent default. It's optional, off by default, requires the user to install and configure the MCP server themselves (this app doesn't bundle or auto-install one), and requires an explicit confirmation on every single publish.

Conclusion this design is built around: treat any NotebookLM MCP as an optional, unofficial, user-configured power path — never a hard dependency, never silently assumed present, never the only way to get value out of this feature.

2. The INotebookLmPublisher seam

public interface INotebookLmPublisher
{
    bool IsAutomated { get; } // true only for the MCP-backed publisher
    Task<NotebookLmPublishResult> PublishAsync(NotebookPrepPack pack, CancellationToken ct = default);
}

src/WorkWingman.Core/Interfaces/INotebookLmPublisher.cs — one contract, two implementations, exactly the same DI-swap idiom as IPageAdvisor / ClaudePageAdvisor vs CodexPageAdvisor: callers never branch on which implementation they got.

NotebookPrepPack — the source-document contract

src/WorkWingman.Core/Models/NotebookPrepPack.cs is a small local input contract: a notebook title plus a list of titled Markdown NotebookSourceDocuments. Prep-studio's richer domain types (feature-prep-studio, not merged to this branch) will eventually supply the real company-problems/assessment-platform/behavioral content; until then, NotebookPrepPackBuilder builds the same four-document shape from what's on this branch today (JobPosting + StudyPlan):

Source document Built from
Job description & gap analysis JobPosting.Description, Fit, StudyPlan.GapAnalysis
Study plan & resources StudyPlan.Resources, grouped by kind
ATS & assessment platform JobPosting.Ats (placeholder until prep-studio's assessment-platform detail merges)
Behavioral prep Static prompt pointing at the Story/voice profile

When feature-prep-studio merges, only the private Build* helpers in NotebookPrepPackBuilder change — the public contract, the publisher seam, and every caller stay identical.

Implementation 1 — ExportNotebookLmPack (default, always available)

src/WorkWingman.Infrastructure/NotebookLm/ExportNotebookLmPack.cs. Writes each NotebookSourceDocument as a .md file under %USERPROFILE%\Wingman\data\notebooklm-exports\{jobId}\ and returns a NotebookLmPublishResult with the file paths and a deep-link to https://notebooklm.google.com. IsAutomated is false. No network call, no browser automation, no account of any kind touched by this code — NotebookLM accepts pasted text and uploaded .md/.txt files as sources natively, so the user drags the exported files in themselves. This is the fully-compliant path: the only side effect is local files, the same local-first posture as LocalJsonStore.

Filename sanitization uses a fixed, platform-neutral forbidden-character set (< > : " / \ | ? * plus control characters), not Path.GetInvalidFileNameChars() — that API is OS-specific (Linux only forbids / and NUL), and this app's backend test suite runs on both Windows (dev) and Ubuntu (CI's determinism/"churn" job), so the export contract — which filename a given source title produces — must not vary by OS.

Implementation 2 — McpNotebookLmPublisher (optional, config-gated)

src/WorkWingman.Infrastructure/NotebookLm/McpNotebookLmPublisher.cs. IsAutomated is true. Delegates to INotebookLmMcpTransport:

public interface INotebookLmMcpTransport
{
    bool IsConfigured { get; }
    Task<string> CreateOrUpdateNotebookAsync(NotebookPrepPack pack, CancellationToken ct = default);
}

The default DI registration is UnconfiguredNotebookLmMcpTransport (IsConfigured => false), mirroring UnconfiguredGoogleTokenProvider's convention exactly: a config-gated stub that reports "not configured" as a normal, silent, successful-shaped outcome rather than throwing. McpNotebookLmPublisher.PublishAsync checks IsConfigured before ever calling the transport — when false (the default, no-MCP-installed state), it returns Success = false with a message pointing the user at the export path, and never invokes the transport at all. An absent optional power tool can never break this feature.

Wiring a real MCP client in later means writing one class that implements INotebookLmMcpTransport (talking to whatever MCP the user configured — e.g. spawning npx notebooklm-mcp@latest as a subprocess and speaking MCP over stdio) and swapping the DI registration in Program.cs; McpNotebookLmPublisher itself does not change, exactly like CodexPageAdvisor awaits its own real transport today.

NotebookLmPublishResult

src/WorkWingman.Core/Models/NotebookLmPublishResult.csSuccess, ExportedFilePaths, NotebookLmUrl (always populated — the deep-link), and CreatedNotebookUrl (populated only when the MCP path actually drove a real notebook creation — this is how callers/tests distinguish "files were written" from "automation ran").

3. Guardrails, and where each is enforced

Guardrail Enforcement
Opt-in only, off by default StudyPlan.NotebookLmEnabled defaults to false (never set true by BuildPlan); IStudyPlanService.PublishToNotebookLmAsync refuses to build a pack or call any publisher unless the plan's flag is true — see StudyPlanService.PublishToNotebookLmAsync returning early with Success = false when disabled.
Per-publish confirmation Both frontend "Export" and "Send to my NotebookLM" buttons call window.confirm(...) with wording naming exactly what will happen, before ever calling the API — see study.ts exportForNotebookLm() / publishToNotebookLmViaMcp().
Confirmation can't be bypassed by a direct API call This API's CORS policy allows any origin (Program.cs), so a confirm() in the renderer alone is not a security boundary — any page loaded in the user's browser could otherwise POST straight to the publish endpoint. POST /api/study/{jobId}/notebooklm/publish-mcp is therefore also gated on the same per-launch ApiToken the app already uses for DependenciesController.Install (see StudyController.PublishViaMcp); only the trusted Electron renderer (or the trusted local dev server origin) can reach it. The frontend's apiTokenInterceptor was extended to attach this token to .../notebooklm/publish-mcp requests automatically. The default export endpoint is intentionally NOT gated this way — it only ever writes local files, so there is nothing external for a hostile page to trigger.
User's own Google account, BYO ExportNotebookLmPack never touches any account. McpNotebookLmPublisher/INotebookLmMcpTransport never store or transmit a Google password — the MCP's own setup_auth-style interactive browser login is the only path in, always under the user's own hands, exactly as documented for the community MCPs. WorkWingman's code has no field, config key, or vault entry for a Google password anywhere in this feature.
Never creates the account / never bypasses login or CAPTCHA Neither implementation contains any account-creation, credential-injection, or CAPTCHA-bypass logic. ExportNotebookLmPackTests and McpNotebookLmPublisherTests pin this by asserting the only side effects are local files (export) or a single mocked transport call gated by IsConfigured (MCP) — there is no code path that could do more even if a test didn't exist to say so.
Graceful degradation when the MCP is absent GET /api/study/notebooklm/mcp-configured lets the frontend hide/relabel the MCP button; McpNotebookLmPublisher no-ops cleanly regardless of whether the frontend checked first.

4. Claude Project cross-reference

When NotebookLM is enabled and has a notebook URL for a job, StudyPlanService.GenerateClaudeStudyProjectAsync (unchanged call signature) treats the NotebookLM notebook as another loaded source alongside the JD, resume, and study links — factual cross-referencing, not promotional copy. The two surfaces are complementary, not redundant: the Claude Project is the interactive coach the rest of this app already builds (JD + resume + gap analysis + daily prompts); NotebookLM adds an audio-overview/podcast-style pass and its own grounded Q&A over the identical source set, useful for passive review (e.g. listening on a commute) that a Claude Project chat doesn't offer.

5. API surface

Endpoint Purpose
POST /api/study/{jobId}/notebooklm/enabled Opt-in toggle. Body: { "enabled": bool }. Never publishes anything.
GET /api/study/notebooklm/mcp-configured Whether an MCP transport is wired in — drives whether the frontend shows the power-path button.
POST /api/study/{jobId}/notebooklm/export Default path: writes files, returns NotebookLmPublishResult.
POST /api/study/{jobId}/notebooklm/publish-mcp Optional path: calls the MCP-backed publisher; no-ops with a clear message if unconfigured. Gated on the per-launch ApiToken (see §3) — returns 401 for an untrusted caller.

See StudyController.

6. Testing

  • NotebookPrepPackBuilderTests — pack shape, per-source content, empty-description handling.
  • ExportNotebookLmPackTests — files are written with exact content, CreatedNotebookUrl stays null (proving no automation happened), sanitizes unsafe titles, keeps jobs in separate directories, and reports IsAutomated = false.
  • McpNotebookLmPublisherTests — no-ops and never calls the transport when unconfigured; calls the transport and surfaces its URL when configured; the real UnconfiguredNotebookLmMcpTransport (the actual DI default) end-to-end no-ops without throwing.
  • StudyPlanServiceTests (NotebookLM section) — opt-in defaults false, toggling persists, disabling clears the stored notebook URL, publishing while disabled never calls the publisher, a successful publish persists the notebook URL, a failed publish does not, and the real ExportNotebookLmPack produces on-disk files through the full service path.
  • ApiTokenTests — pins the trust decision behind the publish-mcp gate: matching token trusted, wrong/missing token + untrusted origin rejected, the trusted dev origin still works without a token, and each ApiToken instance is freshly generated (no replay across launches).
  • Frontend study.spec.ts (NotebookLM section) — MCP-configured check on init, toggle round-trips through the API, both publish actions call window.confirm first and never call the API when declined, the export button only opens the NotebookLM tab on success, and the MCP publish updates the stored notebook URL from createdNotebookUrl.
  • Frontend api-token.interceptor.spec.ts — the token header is attached to .../notebooklm/publish-mcp and /api/dependencies/install requests when a token is present, NOT attached to the (non-gated) export request or unrelated requests, and omitted entirely when no global token exists (dev server).

A scoped Stryker.NET run against just **/NotebookLm/*.cs (mutation-level Standard) killed 114/114 mutants — 100%, well above the repo's 80% high / 60% low thresholds — after strengthening a handful of message/body assertions from Contains to exact-match. Line-ending assertions build on Environment.NewLine rather than a hardcoded "\r\n", since the backend suite's CI "churn" job runs the same tests on Ubuntu where StringBuilder.AppendLine() emits "\n".