Files
wehub-resource-sync 09e9f3545f
CodeQL / Analyze (push) Waiting to run
dependency-audit / pip-audit (push) Waiting to run
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

32 KiB

MCP server guide

Experimental / preview. The MCP server ships behind the optional mcp extra. Its tool surface (names, parameters, output shapes) is not covered by the library's semver guarantees and may change between releases. pip install notebooklm-py is unaffected — the server and its dependencies only arrive with the mcp extra.

The MCP server exposes NotebookLM to any Model Context Protocol client (Claude Desktop, Claude Code, Cursor, Windsurf, …) as a set of 32 tools — manage notebooks and sources, chat over a notebook's sources, generate and download studio artifacts, and run deep research. It is a thin adapter over the same business logic the CLI uses, so it behaves identically to notebooklm <command>.

Install

The server is behind the mcp extra (pulls in fastmcp):

pip install "notebooklm-py[mcp]"
# or run with no install, straight from PyPI:
uvx --from "notebooklm-py[mcp]" notebooklm-mcp --help

Authenticate (once)

The server reuses the CLI's stored credentials — it does not log in on its own. Authenticate once before starting it:

notebooklm login
# or, if you didn't install the package:
uvx --from "notebooklm-py[mcp]" notebooklm login

Credentials are stored per profile under ~/.notebooklm/. The server binds the active profile at startup (override with --profile, below). See configuration.md for profiles and multi-account setup.

Connect a client

The fastest path is the auto-config command, which writes the server block into a client's MCP config (idempotent, never clobbers other servers):

notebooklm mcp install claude-desktop   # or: claude-code | cursor | windsurf
Client Config written
claude-desktop claude_desktop_config.json (per-OS location)
claude-code ~/.claude.json (user scope)
cursor ~/.cursor/mcp.json
windsurf ~/.codeium/windsurf/mcp_config.json

It writes a block that launches the server via uvx (so only uv needs to be on the host):

{
  "mcpServers": {
    "notebooklm": {
      "command": "uvx",
      "args": ["--from", "notebooklm-py[mcp]", "notebooklm-mcp"]
    }
  }
}

Restart the client after installing. For a one-click Claude Desktop bundle, download notebooklm-mcp.mcpb from the latest release (Assets) and use "Install Extension"; see desktop-extension/README.md for details.

Run it directly

The console script is notebooklm-mcp:

notebooklm-mcp                         # stdio transport (default — for desktop hosts)
notebooklm-mcp --profile work          # bind a specific auth profile
notebooklm-mcp --transport http        # loopback streamable-HTTP on 127.0.0.1:9420
notebooklm-mcp --transport http --port 9000
Flag Default Notes
--profile active profile which stored auth profile the process binds
--transport stdio stdio (subprocess hosts) or http (loopback)
--host 127.0.0.1 http only; non-loopback is refused unless NOTEBOOKLM_MCP_ALLOW_EXTERNAL_BIND=1
--port 9420 http only
--log-level INFO logs go to stderr; stdout stays pure JSON-RPC

There is no --token flag — the HTTP bearer token is env-only (NOTEBOOKLM_MCP_TOKEN) so it cannot leak via ps aux.

stdio is right for Claude Desktop/Code, Cursor, and Windsurf (they launch the server as a subprocess). Use http for a local web client or to share one running server across clients on the same machine. The HTTP transport is loopback-only by default; binding to a non-loopback address requires both the explicit NOTEBOOKLM_MCP_ALLOW_EXTERNAL_BIND=1 override and a NOTEBOOKLM_MCP_TOKEN — the server fails closed (refuses to start) on a network bind without a token, since it fronts a full Google account.

Remote deployment (Docker + a tunnel)

Because master-token auth keeps the session alive unattended (no browser), the HTTP transport can run as a remote connector reachable from Claude Code, Claude Desktop, claude.ai, mobile, and ChatGPT. The deploy/ directory ships a turn-key Docker + Compose stack with a tunnel sidecar — pick one via a Compose profile — so you get HTTPS with no public IP, no open ports, and no TLS certificate to manage (the tunnel terminates TLS at its edge).

→ The full step-by-step lives in deploy/README.md — run it from inside deploy/ (make setup → finish the one manual tunnel step → make up), where the Compose stack, Makefile, and .env.example it references sit. It walks both tunnels end to end: Cloudflare (needs a domain in your Cloudflare account) and Tailscale Funnel (no domain — a free, stable *.ts.net HTTPS hostname). The rest of this section is the two things worth knowing before you start: the auth model and remote file transfer.

Two auth methods coexist on one /mcp (FastMCP MultiAuth):

  • Claude Code / Desktop → the static NOTEBOOKLM_MCP_TOKEN bearer (an Authorization header).
  • claude.ai (web/mobile) and ChatGPT (Developer Mode) → optional self-hosted OAuth (one password, no external IdP): set NOTEBOOKLM_MCP_OAUTH_PASSWORD (≥16 random chars) + NOTEBOOKLM_MCP_OAUTH_BASE_URL (the bare public origin, no /mcp). Both connector UIs are OAuth-only (no bearer field). Unset → bearer-only (Claude Code/Desktop still work).

Use a dedicated/throwaway Google account — the mounted master_token.json is a durable full-account credential. Multi-tenant hosting is out of scope for this single-tenant setup.

File upload & download (remote)

The MCP/JSON-RPC channel can't carry large binaries, so over a remote connector source_add type=file and studio_download broker a short-lived signed URL served by the same container; your browser does the byte transfer (see ADR-0024). This is the standard pattern for remote MCP file transfer — MCP has no native file-upload primitive, and its native download (binary Resources) is capped far below a podcast/video. (A small file can skip the signed URL entirely — see source_upload_bytes below.)

Enable it: set NOTEBOOKLM_MCP_PUBLIC_URL to your bare public origin (the same host as the tunnel, no /mcp). It falls back to NOTEBOOKLM_MCP_OAUTH_BASE_URL, so if you configured claude.ai OAuth above, file transfer is already on. Unset on a bearer-only deploy → the two file tools return a clear "not configured" error (everything else still works; the server does not refuse to start).

  • Upload a local file: source_add type=file returns an upload_required link. Open it in your browser, pick the file, and it's added to the notebook. (Claude can also PUT a file it already holds to that link from its code-execution sandbox — but that requires Code Execution enabled and your server domain whitelisted in claude.ai Settings → Capabilities → additional allowed domains, or the PUT fails.)
  • Hand a small file's bytes in-channel (no signed URL): when an agent holds the bytes but can complete neither upload path — e.g. its egress is blocked, so the agent_upload POST fails, and no human device has the file — source_upload_bytes takes the file as base64 (≤ 10,000 chars, ≈ 7 KB) and the connector adds it server-side, returning the source directly. It works on any transport and needs no NOTEBOOKLM_MCP_PUBLIC_URL; a larger file must use the source_add type=file signed-URL flow above.
  • Download an artifact: studio_download returns a download_ready link (a clickable resource_link); open it to stream the podcast/video/PDF to your device. The download_ready payload is self-describing so a client can render a download affordance before opening the URL: alongside url / expires_at / artifact_type / artifact_id it carries filename (the artifact title — or the type name on a latest-by-type download — plus the format-resolved extension), mime_type (from a central per-type/format table the /files/dl route serves with, so the advertised type and the streamed Content-Type can't drift), and size_bytes (null — the size isn't known without eagerly fetching the artifact, which the broker won't do; the route sets the real Content-Length when the link is opened).
  • Links are HMAC-signed and short-lived (upload 15 min, download 30 min) and expire on a server restart. Google Drive (source_add with a Drive id) remains a no-browser alternative for adding files. stdio (local) installs are unchanged — they still read and write real local paths directly.

Core concepts

These conventions hold across every tool:

  • JSON by default. Read/wait tools, including source_read, source_wait, and source_add_and_wait, return a JSON text content block plus the same structured_content. A resource_link appears only when a tool explicitly brokers file transfer, such as studio_download.
  • Name or ID. Every notebook/source/note/artifact argument accepts a human title or an ID. Both resolve by prefix: an exact title wins, otherwise a unique title prefix matches (so "Scientific" finds "Scientific PDF Parsing — …"), and likewise a full ID or a unique ID prefix. Use the matching *_list tool to discover them. An ambiguous name or prefix returns a VALIDATION error listing the candidates so you can retry with an exact title or ID. When a name lookup fails but is close to a real title — a punctuation-only slip such as a hyphen typed for an em-dash () or a normal space for a non-breaking one — the error's Did you mean: … hint names up to three near-miss candidates, each with its title and id inline, so you can retry with the full title or id instead of guessing (a label near-miss reached via source_list(label=…) gets the same enrichment on its VALIDATION error).
  • Canonical IDs come back. Every response echoes the canonical notebook_id (and, where a tool resolves them, the source_ids scope / artifact_id) — so a call made by name hands you the id to chain the next call on.
  • Strict IDs-only mode (opt-in). Set NOTEBOOKLM_MCP_STRICT_IDS=1 on the server to require a full canonical id for every notebook/source/note/artifact reference: names, titles, and short id prefixes are rejected with a VALIDATION error before any list call. This trades the convenience above for deterministic, fail-fast behavior in long-lived automation, where a prefix or title that is unique today can quietly resolve to a different (or ambiguous) entity tomorrow. Off by default. (Governs every notebook/source/note/artifact reference — including studio item and studio_download's artifact_id; the source_list(label=…) name filter is out of scope.)
  • Destructive tools need confirmation. notebook_delete, source_delete, studio_delete, and share_remove_user take confirm (default false). Called without it, they return a needs_confirmation preview (with the resolved title) and delete nothing; call again with confirm=true to execute.
  • Sharing-widening tools need confirmation too. share_set_user (every grant/regrade) and share_set_access when it would widen link access (public=true on a currently-restricted notebook) take confirm (default false) and return a needs_confirmation preview instead of mutating; call again with confirm=true to apply. Restricting (public=false) and view_level-only changes are not gated. These tools are not flagged destructiveHint — the gate is on the widening direction only.
  • Long-running work is non-blocking. studio_generate returns immediately with a task_id; poll studio_status until it's complete, then studio_download. Research is the same shape: research_startresearch_statusresearch_import.
  • Mutation envelope. Synchronous create/rename/update/delete tools return a top-level status string naming the outcome — one of created, renamed, updated, deleted, removed, added, imported, cancel_requested, configured (plus the needs_confirmation / upload_required / download_ready flow values) — alongside the affected id(s). An agent can branch on status uniformly instead of learning a different success shape per tool. Two carve-outs: the long-running starters studio_generate / research_start return a task_id (the handle is the result — poll it) rather than a mutation status — as does the re-runner studio_retry (its task_id equals the artifact id); and the read tools studio_status / research_status key status to a lifecycle vocabulary (in_progress / completed / …), a different enum. (Batch source_add reports added once ≥1 succeeded, error if all failed — see the added / failed tallies + per-item results[].status for partial outcomes.)
  • Structured errors. Failures arrive as CODE: message (retriable=…), where CODE is one of AUTH, RATE_LIMITED, NOT_FOUND, VALIDATION, TIMEOUT, NETWORK, SERVER, RPC, CONFIG, NOTEBOOK_LIMIT, ARTIFACT_TIMEOUT, SOURCE_MUTATION, ERROR, or UNEXPECTED. The retriable flag tells an agent whether a retry could succeed (e.g. RATE_LIMITED, TIMEOUT, NETWORK). Many errors also carry an actionable hint (e.g. AUTH → run notebooklm login); a near-miss name lookup puts its Did you mean: … candidates (title + id) in that hint (see Name or ID above).

Workflows

The examples below are MCP tool calls an agent makes (not shell commands).

Add sources and ask a question

nb = notebook_create(title="Quantum Computing")
source_add(notebook="Quantum Computing", source_type="url", url="https://arxiv.org/abs/...")
source_add(notebook="Quantum Computing", source_type="text", title="Notes", text="...")
source_wait(notebook="Quantum Computing")                 # block until sources finish processing
chat_ask(notebook="Quantum Computing", question="What are the open problems?")

source_wait returns a structured aggregate: the four buckets (ready carries source_view rows; timed_out/failed/not_found carry {source_id, error}) plus explicit *_count scalars and a total_count for at-a-glance triage — so a client reads the counts without folding len() over every array. The counts are additive; the arrays stay for backward compatibility. ok is true iff every error bucket is empty, and total_count = ready_count + timed_out_count + failed_count + not_found_count:

source_wait(notebook="Quantum Computing")
# → {"notebook_id": ..., "ok": false,
#    "ready":     [{"id": ..., "title": "Notes", "kind": "pasted_text", "status_label": "ready"}],
#    "timed_out": [{"source_id": ..., "error": "..."}],
#    "failed":    [],
#    "not_found": [],
#    "ready_count": 1, "timed_out_count": 1, "failed_count": 0,
#    "not_found_count": 0, "total_count": 2}

source_type is one of url, text, file (local path), drive (a document_id + a required mime_type, one of google-doc/google-slides/google-sheets/pdf — there is no default, since defaulting a non-Doc Drive file to google-doc fails the import), or youtube. URL and YouTube adds reject internal/loopback hosts by default; pass allow_internal=true only for deliberate local NotebookLM tests. chat_ask continues the most-recent conversation unless you pass a conversation_id.

To add ONE source and block until it finishes processing in a single call, use source_add_and_wait — it composes single-mode source_add + source_wait, so you skip the add→wait round-trip. It takes the same single-mode add inputs plus the timeout/interval wait knobs, and returns the source_wait aggregate plus a top-level source_id (always present — the source persists even if the wait times out or the import fails, so you can retry or delete it):

source_add_and_wait(notebook="Quantum Computing", source_type="url",
                    url="https://arxiv.org/abs/...")
# → {"notebook_id": ..., "ok": true,
#    "ready":     [{"id": ..., "title": "...", "kind": "web_page", "status_label": "ready"}],
#    "timed_out": [], "failed": [], "not_found": [],
#    "ready_count": 1, "timed_out_count": 0, "failed_count": 0,
#    "not_found_count": 0, "total_count": 1, "source_id": ...}

It is single-source only (no urls batch) and cannot one-shot a remote file upload (that upload is a separate step — use source_add(source_type="file") then source_wait, or source_upload_bytes for a tiny file).

To ingest many URLs at once, pass urls (batch mode) instead of source_type — one call instead of one round-trip each. The response is an explicit per-item list so a partial failure is never hidden behind a single success flag:

source_add(notebook="Quantum Computing", urls=[
    "https://arxiv.org/abs/2401.00001",
    "https://www.youtube.com/watch?v=...",
])
# → {"notebook_id": ..., "added": 2, "failed": 0,
#    "results": [{"input": "https://arxiv.org/abs/2401.00001", "status": "added", "source_id": ..., "title": ...},
#                {"input": "https://www.youtube.com/watch?v=...", "status": "added", "source_id": ..., "title": ...}]}

Batch mode is URL-only (a non-URL entry is reported as a per-item VALIDATION error, never added as text); source_type/url/text/title/path/ document_id/mime_type are not valid with urls, but allow_internal applies to every entry.

Content-sanity warnings on ready web pages

A dead link, soft-404, or paywalled page frequently ingests as a READY source with little-to-no extractable text — a "ghost source" that add-time status can't catch because a soft-404 serves HTTP 200. source_wait — and batch source_add(urls=[...]) for an item that is already READY the moment it returns (single-mode source_add adds asynchronously, so it never runs this check) — attaches a non-blocking, advisory warning to such a source. The check is best-effort and never rejects: the source stays READY, ok stays true, and any fetch failure (including a >5s slow source_read) degrades to no warning rather than breaking the wait.

It fires on a web-page source only (kind == "web_page") via two body-only signals — the title is never scanned:

Signal Threshold Warning contains
char-thin indexed text shorter than 100 characters "little/no text extracted (N chars) …"
dead-link boilerplate indexed text shorter than 2000 characters that (casefolded) contains any of the phrases below "ingested as ready (N chars) but the body matches a dead-link / error-page pattern …"

The full dead-link phrase set (the complete list, so you can build a fixture that trips it): broken link, page not found, page isn't available, page does not exist, page no longer available, no longer available, error 404, 404 not found, whoops!.

Both gates measure the source's indexed text length (char_count from a source_read with detail="full"), not the raw HTTP response — a large HTML page that indexes to little text is still caught. The 2000-char gate is what keeps the weaker phrases safe: a page whose indexed text is 2000 chars or longer is never phrase-scanned (so broken link in a real article about broken links, or a shop's no longer available, does not false-positive), and the phrases are all multi-word / anchored — no bare 404 or not found. Every warning ends with verify with source_read (detail="full"). (trailing period included).

To exercise the warning branch (the reason this is documented): note that a text source — even an empty one — is never flagged; only a web_page under the thresholds above is. So the reliable trigger is a URL that resolves to a near-empty or soft-404 page. To unit-test your own handling of the branch without a live URL, mock the source's fetched body under the threshold and assert the warning shape — copy the pattern from tests/unit/mcp/test_sources.py (see test_source_wait_thin_web_page_warns, test_source_wait_soft_404_body_phrase_warns, and the _THIN_SOURCE_CHAR_THRESHOLD boundary test).

Generate and download a studio artifact

task = studio_generate(notebook="Quantum Computing", artifact_type="audio")
studio_status(notebook="Quantum Computing", task_id="<task_id from above>")   # poll until complete
studio_download(notebook="Quantum Computing", artifact_type="audio", path="podcast.mp3")

# Target a specific/older artifact instead of the latest-by-type (full ID or unique prefix):
studio_download(notebook="Quantum Computing", artifact_type="audio", path="old_podcast.mp3", artifact_id="aaaaaaaa-aaaa")

# Per-kind styling options are agent-settable, e.g. a custom-styled video:
studio_generate(notebook="Quantum Computing", artifact_type="video",
                  style="custom", style_prompt="hand-drawn diagrams")

studio_download's output_format overrides the download file format, but only some artifact types have a format axis:

artifact_type Supported output_format
slide-deck pdf (default), pptx
quiz, flashcards json (default), markdown, html
audio, video, infographic, report, mind-map, data-table none — omit output_format

Passing output_format to a type with no format axis (e.g. report + markdown) is a validation error that says supported formats: default only rather than silently ignoring it, and the message is identical whether the download runs over stdio (path) or the remote signed-URL connector.

artifact_type is one of audio, video, cinematic-video, slide-deck, quiz, flashcards, infographic, data-table, mind-map, report. Each kind's styling options are agent-settable (matching the CLI flags): audio_format / audio_length (audio); video_format / style / style_prompt (video — style / style_prompt are rejected for video_format cinematic and short, which use a fixed visual style); deck_format / deck_length (slide-deck); quantity / difficulty (quiz, flashcards); orientation / detail / style (infographic); map_kind (mind-map); and report_format (report). cinematic-video and data-table take no per-kind options. An option is valid only for its own kind — passing one to a different artifact_type is a validation error, not a silent no-op.

Run deep research and import the findings

task = research_start(notebook="Quantum Computing", query="post-quantum cryptography", source="web", mode="deep")
research_status(notebook="Quantum Computing", poll_task_id=task["poll_task_id"])
research_import(notebook="Quantum Computing", poll_task_id=task["poll_task_id"])

source is web or drive; mode is fast or deep. Pass the poll_task_id returned by research_start — under the same parameter name, poll_task_id — when polling, importing, or cancelling, so the value copies verbatim from one tool's output into the next and the request is pinned to the intended research task (for a deep run it is the report_id; the raw task_id is an unpollable sessionId). Omitting the pin on research_status is allowed only when the notebook has a single in-flight task. research_status omits the large report by default — pass include_report=true to fetch it once completed.

Deprecated (removed in v0.9.0): research_status/research_import also accept the old task_id name and research_cancel the old run_id name as aliases for poll_task_id. Passing an alias still works but emits a DeprecationWarning and adds a deprecation note to the result — switch to poll_task_id. See docs/deprecations.md.

Tool reference

Domain Tools
Notebooks notebook_list(limit?, offset?) · notebook_create(title) · notebook_describe(notebook, include_metadata?) (AI description; include_metadata=true adds a metadata block with notebook details + source list) · notebook_rename(notebook, new_title) · notebook_delete(notebook, confirm)
Sources source_list(notebook, status?, label?, detail?, limit?, offset?) (each source has string kind/status_label; status filters to one of ready|processing|error|preparing — e.g. status="error" finds failed imports; detail=compact returns a low-token roster of just id/title/kind/status_label/created_at) · source_read(notebook, source, detail?, output_format?, max_chars?, offset?) (detail=full (default) → metadata + a bounded slice of the indexed text: max_chars caps content (default 10k), offset pages, plus a truncated flag and the full char_count; detail=summary → low-token triage: AI summary + keywords, not the body; output_format: text|markdown) · source_rename(notebook, source, new_title) · source_delete(notebook, source, confirm) · source_wait(notebook, source?, timeout, interval) (a READY web page with thin/empty text, or a short body matching a dead-link / soft-404 boilerplate pattern, carries a non-blocking warning) · source_add(notebook, source_type, ..., allow_internal?) (single; echoes kind/status_label, flags a failed import inline with a warning) / source_add(notebook, urls=[...], allow_internal?) (batch → per-item results; a synchronously-ready web-page item may also carry the same content-sanity warning) · source_add_and_wait(notebook, source_type, ..., timeout?, interval?) (single-mode source_add + source_wait in one call → the source_wait aggregate plus a top-level source_id; not for batch or a remote file upload)
Chat chat_ask(notebook, question?, conversation_id?, references?, source_ids?, history?, suggest_followups?) (references: lite|full; never returns the raw debug blob; source_ids scopes to specific sources — list, JSON-array string, or comma string; omit for all; history>0 also returns up to N prior {question, answer} pairs — omit question to recall only; suggest_followups=true also returns suggested_prompts (3 questions to ask — works question-less too)) · chat_configure(notebook, chat_mode?, goal?, response_length?) (chat_mode: default|learning-guide|concise|detailed — a preset, mutually exclusive with goal/response_length; a partial custom call sets just goal or just response_length and merges with the current settings — the omitted field is preserved, not reset; only a bare call, no preset and neither field, is rejected) · suggest_prompts(notebook, surface?, source_ids?, query?) (READ_ONLY; surface: ask|audio-deep-dive|audio-brief|audio-critique|audio-debate|video-explainer|video-short|quiz|flashcards — returns {title, prompt} suggestions to steer that studio surface; ask (default) = chat questions)
Notes note_save(notebook, note?, title?, content?) (upsert: omit note to createtitle AND content required; pass a note ref to updatetitle and/or content, title-only = rename). Reading and deleting notes fold into the Studio row below.
Studio studio_list(notebook, item?, kind?, detail?, limit?, offset?) (the unified Studio panel — notes AND artifacts merged into one items list; each item has id/title/type where type is note or a hyphenated artifact kind; artifacts add status_label/url; detail=summary (default) gives each note a bounded content_preview + full-body char_count to keep a discovery listing low-token, detail=full returns the whole note content, detail=compact collapses every item to id/title/type/status_label/created_at; kind filters to one type; item fetches one note-or-artifact by ref as a 1-element list, always with the note's full content) · studio_generate(notebook, artifact_type, …) · studio_status(notebook, task_id) · studio_get_prompt(notebook, artifact) (the free-text prompt an artifact was generated from; null if none) · studio_download(notebook, artifact? | artifact_type?, path?, output_format?, artifact_id?) (target by artifact name-or-id ref or by artifact_type [+ artifact_id for a specific one, else latest]) · studio_rename(notebook, item, new_title) (cross-type: renames a note OR an artifact resolved from the merged list) · studio_retry(notebook, artifact) (re-run a failed artifact in place; task_id == artifact_id) · studio_delete(notebook, item, confirm) (cross-type: deletes a note OR an artifact resolved from the merged list)
Research research_start(notebook, query, source, mode) (returns poll_task_id — the one id status/import/cancel drive off) · research_status(notebook, poll_task_id?, include_report?, report_max_chars?, source_limit?, source_offset?) (report + per-source report_markdown omitted unless include_report) · research_import(notebook, poll_task_id) · research_cancel(notebook, poll_task_id) (sends the cancel unless the run is already terminal → cancel_requested). The old task_id / run_id param names are deprecated aliases for poll_task_id, removed in v0.9.0
Sharing share_status(notebook) (is_public/access/share_url/shared_users; enums as string labels; view_level omitted — the read API can't report it) · share_set_access(notebook, public?, view_level?, confirm) (link settings; view_level: full|chat, echoed back only when set; confirm gates public widening restricted→public) · share_set_user(notebook, email, permission?, notify?, message?, confirm) (upsert grant; permission: editor|viewer; notify defaults false; confirm gates every grant) · share_remove_user(notebook, email, confirm)
Server server_info(include_account?) — version + local auth health; include_account=true adds an account block: signed-in identity (email, authuser) plus notebook/source limits, the subscription tier (opaque enum, e.g. 1=Free/2=Pro, null on legacy responses; per-tier limits in quota-limits.md), and global output_language for quota pacing + language context (best-effort; identity is network-free from the profile, the quota fields need a live session). email is real account PII, returned only under this opt-in flag

Tools that only read are annotated read-only; the destructive tools (the three *_delete tools plus share_remove_user) are annotated destructive and require confirm. A host that honors MCP annotations can auto-allow the read-only calls and gate the destructive ones.

Troubleshooting

  • AUTH errors / "not authenticated". Run notebooklm login (or notebooklm -p <profile> login) in a terminal, then restart the server. Check with the server_info tool, which reports auth health.
  • uvx / uv not found. Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh (macOS/Linux) or powershell -c "irm https://astral.sh/uv/install.ps1 | iex" (Windows). The desktop launcher also searches common install dirs beyond PATH.
  • Client doesn't see the tools. Confirm the config was written (notebooklm mcp install <client>) and restart the client — most hosts only read MCP config at startup.
  • Wrong account. The server binds one profile per process. Start it with --profile <name>, or set NOTEBOOKLM_PROFILE. See configuration.md.
  • RATE_LIMITED. NotebookLM enforces per-account quotas; the error is retriable=true — back off and retry.

See also