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¶
- Product: notebooklm.google.com
- Enterprise API (not what this feature uses — see §1): Google Cloud NotebookLM Enterprise API docs
- Community MCP servers surveyed: PleasePrompto/notebooklm-mcp, moodRobotics/notebooklm-mcp-server, jacob-bd/notebooklm-mcp-cli
- Setup guides referenced: aimaker.substack.com — NotebookLM MCP + Claude setup, XDA — "NotebookLM now connects to Claude through MCP"
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:
- A Playwright/Chrome-driven session logs into
notebooklm.google.comas if a human were typing — the setup guides describe asetup_authstep 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. - 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.
- 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.cs —
Success, 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,CreatedNotebookUrlstays null (proving no automation happened), sanitizes unsafe titles, keeps jobs in separate directories, and reportsIsAutomated = false.McpNotebookLmPublisherTests— no-ops and never calls the transport when unconfigured; calls the transport and surfaces its URL when configured; the realUnconfiguredNotebookLmMcpTransport(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 realExportNotebookLmPackproduces 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 eachApiTokeninstance 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 callwindow.confirmfirst 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 fromcreatedNotebookUrl. - Frontend
api-token.interceptor.spec.ts— the token header is attached to.../notebooklm/publish-mcpand/api/dependencies/installrequests 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".