32 KiB
MCP server guide
Experimental / preview. The MCP server ships behind the optional
mcpextra. Its tool surface (names, parameters, output shapes) is not covered by the library's semver guarantees and may change between releases.pip install notebooklm-pyis unaffected — the server and its dependencies only arrive with themcpextra.
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_TOKENbearer (anAuthorizationheader). - 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=filereturns anupload_requiredlink. Open it in your browser, pick the file, and it's added to the notebook. (Claude can alsoPUTa 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 thePUTfails.) - 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_uploadPOST fails, and no human device has the file —source_upload_bytestakes 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 noNOTEBOOKLM_MCP_PUBLIC_URL; a larger file must use thesource_add type=filesigned-URL flow above. - Download an artifact:
studio_downloadreturns adownload_readylink (a clickableresource_link); open it to stream the podcast/video/PDF to your device. Thedownload_readypayload is self-describing so a client can render a download affordance before opening the URL: alongsideurl/expires_at/artifact_type/artifact_idit carriesfilename(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/dlroute serves with, so the advertised type and the streamedContent-Typecan't drift), andsize_bytes(null— the size isn't known without eagerly fetching the artifact, which the broker won't do; the route sets the realContent-Lengthwhen 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_addwith 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, andsource_add_and_wait, return a JSON text content block plus the samestructured_content. Aresource_linkappears only when a tool explicitly brokers file transfer, such asstudio_download. - Name or ID. Every
notebook/source/note/artifactargument 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*_listtool to discover them. An ambiguous name or prefix returns aVALIDATIONerror 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'sDid 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 viasource_list(label=…)gets the same enrichment on itsVALIDATIONerror). - Canonical IDs come back. Every response echoes the canonical
notebook_id(and, where a tool resolves them, thesource_idsscope /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=1on the server to require a full canonical id for everynotebook/source/note/artifactreference: names, titles, and short id prefixes are rejected with aVALIDATIONerror 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 studioitemandstudio_download'sartifact_id; thesource_list(label=…)name filter is out of scope.) - Destructive tools need confirmation.
notebook_delete,source_delete,studio_delete, andshare_remove_usertakeconfirm(defaultfalse). Called without it, they return aneeds_confirmationpreview (with the resolved title) and delete nothing; call again withconfirm=trueto execute. - Sharing-widening tools need confirmation too.
share_set_user(every grant/regrade) andshare_set_accesswhen it would widen link access (public=trueon a currently-restricted notebook) takeconfirm(defaultfalse) and return aneeds_confirmationpreview instead of mutating; call again withconfirm=trueto apply. Restricting (public=false) andview_level-only changes are not gated. These tools are not flaggeddestructiveHint— the gate is on the widening direction only. - Long-running work is non-blocking.
studio_generatereturns immediately with atask_id; pollstudio_statusuntil it's complete, thenstudio_download. Research is the same shape:research_start→research_status→research_import. - Mutation envelope. Synchronous create/rename/update/delete tools return a top-level
statusstring naming the outcome — one ofcreated,renamed,updated,deleted,removed,added,imported,cancel_requested,configured(plus theneeds_confirmation/upload_required/download_readyflow values) — alongside the affected id(s). An agent can branch onstatusuniformly instead of learning a different success shape per tool. Two carve-outs: the long-running startersstudio_generate/research_startreturn atask_id(the handle is the result — poll it) rather than a mutation status — as does the re-runnerstudio_retry(itstask_idequals the artifact id); and the read toolsstudio_status/research_statuskeystatusto a lifecycle vocabulary (in_progress/completed/ …), a different enum. (Batchsource_addreportsaddedonce ≥1 succeeded,errorif all failed — see theadded/failedtallies + per-itemresults[].statusfor partial outcomes.) - Structured errors. Failures arrive as
CODE: message (retriable=…), whereCODEis one ofAUTH,RATE_LIMITED,NOT_FOUND,VALIDATION,TIMEOUT,NETWORK,SERVER,RPC,CONFIG,NOTEBOOK_LIMIT,ARTIFACT_TIMEOUT,SOURCE_MUTATION,ERROR, orUNEXPECTED. Theretriableflag tells an agent whether a retry could succeed (e.g.RATE_LIMITED,TIMEOUT,NETWORK). Many errors also carry an actionablehint(e.g.AUTH → run notebooklm login); a near-miss name lookup puts itsDid 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_importalso accept the oldtask_idname andresearch_cancelthe oldrun_idname as aliases forpoll_task_id. Passing an alias still works but emits aDeprecationWarningand adds adeprecationnote to the result — switch topoll_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 create — title AND content required; pass a note ref to update — title 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
AUTHerrors / "not authenticated". Runnotebooklm login(ornotebooklm -p <profile> login) in a terminal, then restart the server. Check with theserver_infotool, which reports auth health.uvx/uvnot found. Install uv:curl -LsSf https://astral.sh/uv/install.sh | sh(macOS/Linux) orpowershell -c "irm https://astral.sh/uv/install.ps1 | iex"(Windows). The desktop launcher also searches common install dirs beyondPATH.- 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 setNOTEBOOKLM_PROFILE. See configuration.md. RATE_LIMITED. NotebookLM enforces per-account quotas; the error isretriable=true— back off and retry.
See also
- installation.md — the
mcpextra + run/connect summary desktop-extension/README.md— one-click Claude Desktop.mcpbbundle (prebuilt, attached to each stable release)- configuration.md — profiles, multi-account, storage
- cli-reference.md — the equivalent CLI commands