File: README.md
Gemini Notebook (formerly Google NotebookLM) CLI & MCP Server
Programmatic access to Gemini Notebook — via command-line interface (CLI) or Model Context Protocol (MCP) server.
🚨 New: Protected login storage (recommended)
Your saved Google login can now be encrypted, with its key kept in your computer's keychain instead of a plain file. We highly recommend everyone switch on a personal computer:textnlm auth storage set protected💡 You can also use the 'nlm setup' wizard and select the 'Credentials Protection' option in the wizard
Optional: nothing changes unless you turn it on. Servers, cron, Docker and SSH setups can keep the plain file.
📖 How it works · 📺 Watch this short video🤖 Fun fact: this video was created by an agent using the gemini-notebook-mcp.
Note: Personal/consumer accounts are tested regularly. Gemini Notebook Enterprise support is experimental. The documented
notebook.cloud.google.comhost has been live-verified with a project-qualifiedglobaldeployment; other Enterprise host variants may require additional validation.
☕ If you find notebooklm-mcp-cli useful, consider buying me a coffee.
It's free and built in my spare time — but testing every Gemini Notebook feature takes real time and resources. A coffee helps me cover it and keep shipping. Thank you! 🙏
📺 Watch the Demos
Latest
| Claude Cowork & Interactive Reports | Codex Setup + Cinematic Video & Slides |
|---|---|
MCP Demos
| General Overview | Claude Desktop | Perplexity Desktop | MCP Super Assistant |
|---|---|---|---|
CLI Demos
| CLI Overview | CLI, MCP & Skills | Setup, Doctor & mcpb | Infographics Support |
|---|---|---|---|
⚡ Quick Start
Three commands take you from nothing to a connected AI assistant:
- Install (gives you
nlmand the MCP server):
uv tool install notebooklm-mcp-cli- Sign in to your Google account:
nlm login- Connect your AI tools:
nlm setupThe setup wizard (`nlm setup`)
nlm setup finds the AI tools installed on your machine and connects them to
Gemini Notebook — no config files to edit. Pick an option with the arrow keys:
| Menu option | What it does |
|---|---|
| Show my tools' status | One table for every detected tool: MCP connection (✓ set up · ✗ not yet · ⚠ old name) and skill version (⬆ when an upgrade is available). |
| Add the MCP to my tools/agents | Tick the tools to connect (Space), then Enter. Nothing is pre-selected. Connections still using the old notebooklm-mcp name appear under Needs a fix and are renamed to gemini-notebook-mcp, keeping their settings. Then it offers the skill. |
| Add the skill to my tools/agents | Installs the skill that teaches your AI how to use Gemini Notebook well — for All my projects (default) or Just this folder. Upgrades are pre-ticked; older versions are only replaced after you confirm. |
| Remove an MCP or skill | Lists what's installed, grouped into MCP connections and skills. Nothing is pre-selected, and each group needs its own confirmation. |
| Credential protection | Protect your saved logins in your OS keystore, or restore them to plain files. It only offers what applies: Protect when a login is plain, Restore when one is protected. With several logins you get a checkbox picker. |
| Copy MCP setup for a tool not listed | Copies a ready-to-paste JSON snippet to your clipboard. Advanced options switch to uvx, the bare command, or entry-only JSON. |
- Esc goes back from any screen (and quits from the main menu).
- Every file is backed up to
~/.notebooklm-mcp-cli/backups/before it is changed. - Connects to: Claude Code, Claude Desktop (regular and Relay AI / 3P), Codex CLI / ChatGPT desktop app, Gemini CLI, Cursor, Windsurf, Cline, Antigravity, GitHub Copilot (VS Code), and OpenCode. The skill also installs for Hermes and OpenClaw.
- Claude Desktop Chat & Cowork, and claude.ai: these only use skills uploaded to your Claude account. In Add the skill, tick Claude Desktop / claude.ai — the wizard saves
~/Downloads/nlm-skill.zipand shows where to upload it (Customize → Skills → Add). Or runnlm skill package. - Claude Desktop: fully quit it (Cmd+Q) before connecting — it can overwrite its settings while open, so the wizard refuses to write until it's closed.
- Scripts and CI: every option has a direct command —
nlm setup add <tool>,nlm setup remove <tool>,nlm setup list,nlm skill install <tool>. See MCP Configuration.
Two Ways to Use
🖥️ Command-Line Interface (CLI)
Use nlm directly in your terminal for scripting, automation, or interactive use:
nlm notebook list # List all notebooks
nlm notebook create "Research Project" # Create a notebook
nlm source add <notebook> --url "https://..." # Add sources
nlm audio create <notebook> --confirm # Generate podcast
nlm download audio <notebook> --id <artifact-id> # Download audio file
nlm download all <notebook> -d ./exports # Download every artifact
nlm share public <notebook> # Enable public linkRun nlm --ai for comprehensive AI-assistant documentation.
🤖 MCP Server (for AI Agents)
Connect AI assistants (Claude, Gemini, Cursor, etc.) to Gemini Notebook:
nlm setup # Guided wizard — see Quick Start above
nlm setup add codex # Or connect one client directlyThen use natural language: "Create a notebook about quantum computing and generate a podcast"
Features
| Capability | CLI Command | MCP Tool |
|---|---|---|
| List notebooks | nlm notebook list |
notebook_list |
| Create notebook | nlm notebook create |
notebook_create |
| Add Sources (URL, Text, Drive, File) | nlm source add |
source_add |
| Query notebook (persists to web UI) | nlm notebook query |
notebook_query |
| List/view/export chat sessions | nlm chats list/get/export |
chat_list/chat_get/chat_export |
| Create Studio Content (Audio, Video, etc.) | nlm studio create |
studio_create |
| Interactive reports (read + embedded elements) | nlm report get/elements/element |
report (action=get/elements/generate) |
| Revise slide decks | nlm slides revise |
studio_revise |
| Download artifacts | nlm download <type> |
download_artifact |
| Download all artifacts (one or all notebooks) | nlm download all |
download_all_artifacts |
| Web/Drive research | nlm research start |
research_start |
| Share notebook | nlm share public/invite |
notebook_share_* |
| Sync Drive sources | nlm source sync |
source_sync_drive |
| Batch operations | nlm batch query/create/delete |
batch |
| Cross-notebook query | nlm cross query |
cross_notebook_query |
| Pipelines (multi-step workflows) | nlm pipeline run/list |
pipeline |
| Tag & smart select | nlm tag add/list/select |
tag |
| Configure AI tools | nlm setup (wizard), nlm setup add/remove/list |
— |
| Install AI Skills | nlm skill install/update |
— |
| Diagnose issues | nlm doctor |
— |
| Check remaining plan usage | nlm usage [--profile <name>] |
usage_get |
| Manage login encryption (Protected mode) | nlm auth storage status/set |
— |
📚 More Documentation:
- Getting Started — Install, login, agent setup, and migration from another Gemini Notebook MCP
- CLI Guide — Complete command reference
- MCP Guide — All 53 MCP tools with examples
- Authentication — Setup and troubleshooting
- Remote MCP — Web/mobile connector feasibility, security, and authentication limitations
- API Reference — Internal API docs for contributors
Important Disclaimer
This MCP and CLI use internal APIs that:
- Are undocumented and may change without notice
- Require cookie extraction from your browser (I have a tool for that!)
- May not be aligned with Google's Terms of Service
Use at your own risk for personal/experimental purposes.
Installation
🆕 Claude Desktop users: Download the extension (
.mcpbfile) → double-click → done! One-click install, no config needed.
Install from PyPI. This single package includes both the CLI and MCP server:
Using uv (Recommended)
uv tool install notebooklm-mcp-cliUsing uvx (Run Without Install)
uvx --from notebooklm-mcp-cli nlm --help
uvx --from notebooklm-mcp-cli notebooklm-mcpUsing pip
pip install notebooklm-mcp-cliUsing pipx
pipx install notebooklm-mcp-cliAfter installation, you get:
nlm— Command-line interfacenotebooklm-mcp— Gemini Notebook MCP server for AI assistants
Alternative: Install from Source
# Clone the repository
git clone https://github.com/jacob-bd/gemini-notebook-mcp-cli.git
cd notebooklm-mcp
# Install with uv
uv tool install .Upgrading
# Using uv
uv tool upgrade notebooklm-mcp-cli
# Using pip
pip install --upgrade notebooklm-mcp-cli
# Using pipx
pipx upgrade notebooklm-mcp-cliAfter upgrading, restart your AI tool to reconnect to the updated MCP server:
- Claude Code: Restart the application, or use
/mcpto reconnect - Cursor: Restart the application
- Gemini CLI: Restart the CLI session
Upgrading from Legacy Versions
If you previously installed the separate CLI and MCP packages, you need to migrate to the unified package.
Step 1: Check What You Have Installed
uv tool list | grep notebooklmLegacy packages to remove:
| Package | What it was |
|---|---|
notebooklm-cli |
Old CLI-only package |
notebooklm-mcp-server |
Old MCP-only package |
Step 2: Uninstall Legacy Packages
# Remove old CLI package (if installed)
uv tool uninstall notebooklm-cli
# Remove old MCP package (if installed)
uv tool uninstall notebooklm-mcp-serverStep 3: Reinstall the Unified Package
After removing legacy packages, reinstall to fix symlinks:
uv tool install --force notebooklm-mcp-cliWhy
--force? When multiple packages provide the same executable,uvcan leave broken symlinks after uninstalling. The--forceflag ensures clean symlinks.
Step 4: Verify Installation
uv tool list | grep notebooklmYou should see only:
notebooklm-mcp-cli v0.2.0
- nlm
- notebooklm-mcpStep 5: Re-authenticate
Your existing cookies should still work, but if you encounter auth issues:
nlm loginNote: The configured MCP server name is now
gemini-notebook-mcp. The executable remainsnotebooklm-mcpfor compatibility with existing installations.
Getting Started
If you are setting up the tool for the first time — or migrating from a
browser-based Gemini Notebook MCP — see the
Getting Started Guide. It covers install,
login, agent registration, and a step-by-step migration path that avoids
the "two Gemini Notebook servers registered" trap.
Uninstalling
To completely remove the MCP:
# Using uv
uv tool uninstall notebooklm-mcp-cli
# Using pip
pip uninstall notebooklm-mcp-cli
# Using pipx
pipx uninstall notebooklm-mcp-cli
# Remove cached auth tokens and data (optional)
rm -rf ~/.notebooklm-mcp-cliAlso remove from your AI tools:
nlm setup remove claude-code
nlm setup remove cursor
# ... or any configured toolAuthentication
Before using the CLI or MCP, you need to authenticate with Gemini Notebook:
CLI Authentication (Recommended)
# Auto mode: launches your browser, you log in, cookies extracted automatically
nlm login
# Check if already authenticated
nlm login --check
# Use a named profile (for multiple Google accounts)
nlm login --profile work
nlm login --profile personal
# Manual mode: import cookies from a file
nlm login --manual --file cookies.txt
# External CDP provider (e.g., OpenClaw-managed browser)
nlm login --provider openclaw --cdp-url http://127.0.0.1:18800New profiles ask where to keep the saved login before the browser opens: Protected (encrypted, key in your OS keystore, recommended) or a plain file. Scripts can skip the question with nlm login --storage protected (or --storage file). Existing profiles keep their mode; change it any time in nlm setup → Credential protection.
Profile management:
nlm login --check # Show current auth status
nlm login switch # Switch the default profile
nlm login profile list # List all profiles with email addresses
nlm login profile delete # Delete a profile
nlm login profile rename <old> <new> # Rename a profileFrom an MCP-only app (for example Claude CoWork): ask the AI to use the profile tool. It can list your accounts, switch for the session, and, only if you ask, make one your saved default.
Unattended / scheduled refresh:
A live session self-heals — when Google's short-lived cookies age out, the
client automatically runs a headless refresh. For unattended machines you can
also refresh proactively from a scheduler so a session never lapses:
nlm auth refresh # Headless, no interaction; exits non-zero on failure
nlm auth refresh --profile work # Refresh a named profileRun it on a timer (cron/launchd). It needs a saved Chrome profile from a priornlm login, and does not apply when NOTEBOOKLM_COOKIES is set as an
environment variable (that value overrides saved credentials).
Enterprise Authentication (Gemini Notebook Enterprise)
For organizations using Gemini Notebook Enterprise via Google Cloud, ask your Enterprise administrator for the project ID or number, the deployment location/multi-region, and confirmation that your account has access. The current documented host is notebook.cloud.google.com; older deployments may use notebooklm.cloud.google.com or vertexaisearch.cloud.google.com.
Set Base URL, GCP Project ID & Location:
bashexport NOTEBOOKLM_BASE_URL="https://notebook.cloud.google.com" # Required: your GCP project ID or project number from your administrator export NOTEBOOKLM_PROJECT_ID="your-gcp-project-id-or-number" # Optional: GCP location/multi-region from your administrator: global (default), us, or eu export NOTEBOOKLM_LOCATION="global"Save an Enterprise Auth Profile:
This opens the configured Enterprise URL in an isolated browser profile. Sign in with your organization account:bashnlm login --profile enterprise nlm login switch enterpriseYou can switch back to a personal profile at any time:
bashunset NOTEBOOKLM_BASE_URL NOTEBOOKLM_PROJECT_ID NOTEBOOKLM_LOCATION nlm login switch personalConfigure MCP Server with Enterprise Environment Variables:
When adding the MCP server to AI assistants (Claude, Cursor, etc.), passNOTEBOOKLM_BASE_URL,NOTEBOOKLM_PROJECT_ID, and optionallyNOTEBOOKLM_LOCATION:json{ "mcpServers": { "gemini-notebook-mcp": { "command": "notebooklm-mcp", "env": { "NOTEBOOKLM_BASE_URL": "https://notebook.cloud.google.com", "NOTEBOOKLM_PROJECT_ID": "your-gcp-project-id-or-number", "NOTEBOOKLM_LOCATION": "global" } } } }
Each profile gets its own isolated browser session, so you can be logged into multiple Google accounts simultaneously. Enterprise environment variables apply to the current process, so keep them in an Enterprise-only shell or MCP configuration when you also use a personal account.
Standalone Auth Tool
If you only need the MCP server (not the CLI):
nlm login # Auto mode (launches browser)
nlm login --manual # Manual file modeHow it works: Auto mode launches a dedicated browser profile (supports Chrome, Arc, Dia, Comet, Brave, Edge, Chromium, Firefox, and more), you log in to Google, and cookies are extracted automatically. Your login persists for future auth refreshes.
Prefer a specific browser? Set it with nlm config set auth.browser chromium (or comet, dia, brave, arc, edge, chrome, firefox, etc.). Falls back to auto-detection if the preferred named browser is not found. For any other Chromium-compatible browser, set its executable with nlm config set auth.browser_path /path/to/browser or NLM_BROWSER_PATH; an invalid explicit path fails closed instead of launching a different browser. When Chromium/CDP is unavailable, auto can fall back to Firefox.
For detailed instructions and troubleshooting, see docs/AUTHENTICATION.md.
MCP Configuration
⚠️ Context Window Warning: This MCP provides 53 tools. Disable it when not using Gemini Notebook to preserve context. In Claude Code:
@gemini-notebook-mcpto toggle. To keep it on but expose only a subset, see Selective tool exposure.
Automatic Setup (Recommended)
The setup wizard is the recommended way to connect tools — see
Quick Start for what each menu option does:
nlm setupMCP configuration goes to each app's user-level settings. Codex CLI and the
ChatGPT desktop app share one MCP configuration on the same host. GitHub
Copilot uses the VS Code user profile in the wizard; the direct command without--scope user targets the current workspace.
For direct setup or scripts, keep using the existing commands:
nlm setup add codex # Codex CLI / ChatGPT desktop
nlm setup add github-copilot --scope user # VS Code user profile
nlm setup add github-copilot # Current workspace
nlm setup add claude-desktop --profile 3p # Relay AI / Claude 3P
nlm setup list # Show MCP configuration statusClaude Desktop setup only writes to profiles that are detected as present. If
both regular and Relay AI/3P profiles exist, the CLI asks whether to configure
regular, 3P, or both. For scripts, use --profile regular|3p|both. If no
Claude Desktop profile is detected, nothing is created or changed.
Removal uses the same profile selection, for examplenlm setup remove claude-desktop --profile regular.
Removal only offers detected profiles containing this MCP or a recognized
legacy entry; unrelated MCP servers are left untouched.
Before adding or removing the MCP, fully quit the selected Claude Desktop
profile. The CLI detects running regular and Relay AI/3P instances and refuses
to write while they are open, because Claude may rewrite the config and discard
the change. Reopen Claude Desktop after setup completes.
Install AI Skills (Optional)
The wizard can install the prompting and workflow skill after MCP setup. It
defaults to All projects (user level) and can instead install into the
current project. Existing current or newer versions are preserved; older or
unversioned skills require confirmation before replacement, with a backup made
first.
You can also manage skills directly. Supported local skill targets include
Cline, Antigravity, OpenClaw, Hermes, Codex, OpenCode,
Claude Code, and Gemini CLI.
# Install skill files
nlm skill install cline
nlm skill install openclaw
nlm skill install codex
nlm skill install antigravity
# Update skills
nlm skill updateUser-level skill installation requires the target tool to be detected first;
the CLI will not create a missing tool directory or install anyway. Use--level project when you intentionally want a project-local skill.
Claude Desktop (Chat and Cowork) and claude.ai
Claude Desktop's Code tab is Claude Code, so it uses the skill installed in~/.claude/skills/. The Chat and Cowork tabs, and claude.ai, only use
skills uploaded to your Claude account. Create the upload file with the wizard
(tick Claude Desktop / claude.ai in Add the skill) or directly:
nlm skill package # saves ~/Downloads/nlm-skill.zip
nlm skill package --output ~/DesktopThen in Claude Desktop (or claude.ai) open Customize → Skills → Add, picknlm-skill.zip, and turn the skill on. Re-upload after updating nlm to get
the latest skill. The MCP for Chat and Cowork comes from the normal Claude
Desktop connection (nlm setup → Add the MCP).
Remove from a tool
nlm setup remove claude-codeUsing uvx (No Install Required)
If you don't want to install the package, you can use uvx to run on-the-fly:
# Run CLI commands directly
uvx --from notebooklm-mcp-cli nlm setup add cursor
uvx --from notebooklm-mcp-cli nlm loginFor tools that use JSON config, point them to uvx:
{
"mcpServers": {
"gemini-notebook-mcp": {
"command": "uvx",
"args": ["--from", "notebooklm-mcp-cli", "notebooklm-mcp"]
}
}
}Manual Setup (if you prefer)
Tip: Run
nlm setup add jsonfor an interactive wizard that generates the right JSON snippet for your tool.
Claude Code / Gemini CLI support adding MCP servers via their own CLI:
claude mcp add --scope user gemini-notebook-mcp notebooklm-mcp
gemini mcp add --scope user gemini-notebook-mcp notebooklm-mcpCursor / Windsurf resolve commands from your PATH, so the command name is enough:
{
"mcpServers": {
"gemini-notebook-mcp": {
"command": "notebooklm-mcp"
}
}
}| Tool | Config Location |
|---|---|
| Cursor | ~/.cursor/mcp.json |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
GitHub Copilot (VS Code workspace) uses .vscode/mcp.json with a top-level servers key:
{
"servers": {
"gemini-notebook-mcp": {
"command": "notebooklm-mcp",
"args": []
}
}
}Claude Desktop may not resolve PATH — use the full path to the binary:
{
"mcpServers": {
"gemini-notebook-mcp": {
"command": "/full/path/to/notebooklm-mcp"
}
}
}Find your path with: which notebooklm-mcp
| Tool | Config Location |
|---|---|
| Claude Desktop (macOS current/3P) | ~/Library/Application Support/Claude-3p/claude_desktop_config.json |
| Claude Desktop (macOS legacy) | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json (an unambiguous MSIX path is detected automatically) |
| Claude Desktop (Windows 3P) | %LOCALAPPDATA%\Claude-3p\claude_desktop_config.json |
| Claude Desktop (Linux) | ~/.config/Claude/claude_desktop_config.json |
| Claude Desktop (Linux 3P) | ${XDG_CONFIG_HOME:-~/.config}/Claude-3p/claude_desktop_config.json |
| GitHub Copilot | .vscode/mcp.json |
📚 Full configuration details: MCP Guide — Server options, environment variables, HTTP transport, and context window management. For Claude web/mobile and public deployment, read Remote MCP Deployment first.
What You Can Do
Simply chat with your AI tool (Claude Code, Cursor, Gemini CLI) using natural language. Here are some examples:
Research & Discovery
- "List all my Gemini Notebook notebooks"
- "Create a new notebook called 'AI Strategy Research'"
- "Start web research on 'enterprise AI ROI metrics' and show me what sources it finds"
- "Do a deep research on 'cloud marketplace trends' and import the top 10 sources"
- "Search my Google Drive for documents about 'product roadmap' and create a notebook"
Adding Content
- "Add this URL to my notebook: https://example.com/article"
- "Add this YouTube video about Kubernetes to the notebook"
- "Add my meeting notes as a text source to this notebook"
- "Import this Google Doc into my research notebook"
AI-Powered Analysis
- "What are the key findings in this notebook?"
- "Summarize the main arguments across all these sources"
- "What does this source say about security best practices?"
- "Get an AI summary of what this notebook is about"
- "Configure the chat to use a learning guide style with longer responses"
(All queries sent from CLI or MCP automatically persist in your Gemini Notebook web UI chat history!)
Content Generation
- "Create an audio podcast overview of this notebook in deep dive format"
- "Generate a video explainer with classic visual style"
- "Make a short vertical video overview of the key idea"
- "Make a briefing doc from these sources"
- "Create flashcards for studying, medium difficulty"
- "Generate an infographic in landscape orientation with professional style"
- "Build a mind map from my research sources"
- "Create a slide deck presentation from this notebook"
Smart Management
- "Check which Google Drive sources are out of date and sync them"
- "Show me all the sources in this notebook with their freshness status"
- "Delete this source from the notebook"
- "Check the status of my audio overview generation"
- "Check this specific artifact without listing every Studio item"
- "List only the generated videos in this notebook"
Sharing & Collaboration
- "Show me the sharing settings for this notebook"
- "Make this notebook public so anyone with the link can view it"
- "Disable public access to this notebook"
- "Invite [email protected] as an editor to this notebook"
- "Add a viewer to my research notebook"
Pro tip: After creating studio content (audio, video, reports, etc.), poll the status to get download URLs when generation completes.
Authentication Lifecycle
| Component | Duration | Refresh |
|---|---|---|
| Cookies | ~2-4 weeks | Auto-refresh via headless browser (if profile saved) |
| CSRF Token | ~minutes | Auto-refreshed on every request failure |
| Session ID | Per MCP session | Auto-extracted on MCP start |
v0.1.9+: The server now automatically handles token expiration:
- Refreshes CSRF tokens immediately when expired
- Reloads cookies from disk if updated externally
- Runs headless browser auth if profile has saved login
You can also call refresh_auth() to explicitly reload tokens.
If automatic refresh fails (Google login fully expired), run nlm login again.
For suspected browser-bound auth replay failures, run nlm doctor auth-replay.
If the cdp_in_page probe succeeds while normal replay fails, see
Experimental browser-backed RPC transport.
Troubleshooting
`uv tool upgrade` Not Installing Latest Version
Symptoms:
- Running
uv tool upgrade notebooklm-mcp-cliinstalls an older version (e.g., 0.1.5 instead of 0.1.9) uv cache cleandoesn't fix the issue
Why this happens: uv tool upgrade respects version constraints from your original installation. If you initially installed an older version or with a constraint, upgrade stays within those bounds by design.
Fix — Force reinstall:
uv tool install --force notebooklm-mcp-cliThis bypasses any cached constraints and installs the absolute latest version from PyPI.
Verify:
uv tool list | grep notebooklm
# Should show: notebooklm-mcp-cli v0.1.9 (or latest)Limitations
- Rate limits: Chat and Studio usage is metered as compute against a rolling (~5h) window and a weekly cap, scaled by plan tier. Run
nlm usageto see what is left and when it resets - No official support: API may change without notice
- Cookie expiration: Need to re-extract cookies every few weeks
Contributing
See CLAUDE.md for detailed API documentation and how to add new features.
Vibe Coding Alert
Full transparency: this project was built by a non-developer using AI coding assistants. If you're an experienced Python developer, you might look at this codebase and wince. That's okay.
The goal here was to scratch an itch - programmatic access to Gemini Notebook - and learn along the way. The code works, but it's likely missing patterns, optimizations, or elegance that only years of experience can provide.
This is where you come in. If you see something that makes you cringe, please consider contributing rather than just closing the tab. This is open source specifically because human expertise is irreplaceable. Whether it's refactoring, better error handling, type hints, or architectural guidance - PRs and issues are welcome.
Think of it as a chance to mentor an AI-assisted developer through code review. We all benefit when experienced developers share their knowledge.
Credits
Special thanks to:
- Le Anh Tuan (@latuannetnam) for contributing the HTTP transport, debug logging system, and performance optimizations.
- David Szabo-Pele (@davidszp) for the
source_get_contenttool and Linux auth fixes. - saitrogen (@saitrogen) for the research polling query fallback fix.
- devnull03 (@devnull03) for multi-browser CDP authentication support (Arc, Brave, Edge, Chromium, Vivaldi, Opera).
- VooDisss (@VooDisss) for multi-browser authentication improvements.
- codepiano (@codepiano) for the configurable DevTools timeout for the auth CLI.
- Tony Hansmann (@997unix) for contributing the
nlm setupandnlm doctorcommands and CLI Guide documentation. - Fabiana Furtado (@fabianafurtadoff) for batch operations, cross-notebook query, pipelines, and smart select/tagging (PR #90).
- Amy-Ra-lph (@Amy-Ra-lph) for security hardening: TOCTOU-safe credential storage, sensitive cookie redaction from debug logs, and pinning all CI actions to full commit SHAs (PRs #205–207).
- Kyle Brodeur (@kylebrodeur) for WSL2 authentication support with Windows Chrome integration (PR #138).
- Robiton (@Robiton) for enterprise Gemini Notebook support via configurable base URL (PR #114).
- pjeby (@pjeby) for connection pooling and fast startup improvements (PR #54).
- beausea (@beausea) for making the interface language configurable via the
NOTEBOOKLM_HLenvironment variable (PR #59). - JumpLao (@JumpLao) for extended audio, video, and image format support (PR #82).
- cbruyndoncx (@cbruyndoncx) for including
cited_textpassages in query output (PR #81). - zxyasfas (@zxyasfas) for cited-only research import (PR #188).
- Serdar Akın (@SERDAR-AKIN) for the multi-probe
AuthHealthCheckerthat fixes false"stale"reports for semi-stale cookies (PR #219).
Star History
License
File: docs/releases/v0.8.2.md
v0.8.2 Maintenance Release
This maintenance release adds diagnostics and a low-risk recovery path for emerging Gemini Notebook (formerly Google NotebookLM) auth replay failures reported in issue #248.
Added
- Added
nlm doctor auth-replayto compare:- saved-cookie replay through normal
httpx httpxreplay after a forced GoogleRotateCookiescall- optional in-page CDP fetch from the saved Chrome profile
- saved-cookie replay through normal
- Added verdicts that distinguish ordinary cookie freshness failures from likely browser-bound replay behavior.
- Added best-effort Google
RotateCookiessupport during auth token refresh.
Fixed
- Matched the auth replay diagnostic HTTP probes to the normal Gemini Notebook RPC headers to avoid false
browser_bound_replayverdicts from barehttpxrequests. - Confirmed
nlm login --checkhomepage login redirects with a real Gemini Notebook RPC before reporting cookies as expired. Thanks tolaofunfor reporting and contributing this fix in PR #250. - Preserved raw Chrome cookie-list handling across auth checks instead of flattening too early.
- Preferred exact
.google.comcookie values when flattening duplicate browser cookies. Thanks tolaofunfor reporting and contributing this fix in PR #249. - Persisted the Gemini Notebook build label from profile metadata so diagnostics and replay checks use the current saved value.
Verification
uv run ruff check .uv run pytest tests/services/test_auth_service.py tests/core/test_cookie_rotation.py tests/services/test_auth_replay_diagnostic.py -quv run pytest -quv cache clean && uv tool install --force .- Live smoke test:
nlm login --checknlm notebook list --titlenlm query notebook <notebook-id> "In one sentence, what is this notebook about?" --json
Notes
This release does not add a full CDP transport. The new doctor command is
intended to collect evidence first, so we can confirm whether a user is seeing
stale cookies, normal auth expiration, or the stronger browser-bound behavior
described in issue #248.
File: docs/releases/v0.8.3.md
v0.8.3 Maintenance Release
This maintenance release adds an experimental opt-in browser-backed transport
for Gemini Notebook (formerly Google NotebookLM) auth replay failures reported in issue #248.
Added
- Added
NOTEBOOKLM_RPC_TRANSPORT=cdpto route normal batchexecute RPCs through
an in-pagefetchinside the saved Gemini Notebook browser profile. - Added CDP transport support for notebook chat via
GenerateFreeFormStreamed,
including longer CDP response waits for long answers. - Added focused tests for CDP transport routing, streamed query routing, long
CDP timeouts, and cleanup of browser instances launched by the client. - Documented when and how to use the experimental transport.
Changed
- Reused the new CDP fetch helper inside
nlm doctor auth-replay, so the
diagnostic path matches the real experimental transport more closely. - Kept the default
httpxtransport unchanged. The CDP path is off by default.
Notes
Use nlm doctor auth-replay first. Only tryNOTEBOOKLM_RPC_TRANSPORT=cdp when normal replay fails but cdp_in_page
passes. Uploads, downloads, and artifact file transfers still use the existing
HTTP paths.
Verification
uv run ruff check .uv run ruff format --check .uv run --dev --with pytest-github-actions-annotate-failures --with pytest-asyncio pytest -v --tb=short -m "not e2e"uv run pytest -quv cache clean && uv tool install --force .- Live smoke test:
nlm login --checknlm notebook list --titlenlm doctor auth-replay --timeout 20NOTEBOOKLM_RPC_TRANSPORT=cdp nlm notebook list --titleNOTEBOOKLM_RPC_TRANSPORT=cdp nlm query notebook <notebook-id> "Answer in one short sentence: what is this notebook about?" --json- MCP stdio
notebook_listwithNOTEBOOKLM_RPC_TRANSPORT=cdp
File: docs/API_REFERENCE.md
Gemini Notebook (formerly Google NotebookLM) MCP API Reference
This document contains detailed API documentation for the internal Gemini Notebook APIs. Only read this file when debugging API issues or adding new features.
For general project info, see CLAUDE.md
Python Usage Examples
These examples show how to use the MCP tools programmatically via Python (for developers building with the API). For end users: see the main README for natural language examples.
List Notebooks
notebooks = notebook_list()Create and Query
# Create a notebook
notebook = notebook_create(title="Research Project")
# Add sources
notebook_add_url(notebook_id, url="https://example.com/article")
notebook_add_text(notebook_id, text="My research notes...", title="Notes")
# Ask questions
result = notebook_query(notebook_id, query="What are the key points?")
print(result["answer"])Queries use a 120-second wall-clock timeout by default. Source-heavy notebooks
may need a larger budget, for example notebook_query(..., timeout=180). If a
query may take longer, use notebook_query_start(..., timeout=180) and pollnotebook_query_status until it completes. A deadline error includes a retry
hint and the query_deadline_exceeded debug code.
Configure Chat Settings
# Set a custom chat persona with longer responses
chat_configure(
notebook_id=notebook_id,
goal="custom",
custom_prompt="You are an expert data analyst. Provide detailed statistical insights.",
response_length="longer"
)
# Use learning guide mode with default length
chat_configure(
notebook_id=notebook_id,
goal="learning_guide",
response_length="default"
)
# Reset to defaults with concise responses
chat_configure(
notebook_id=notebook_id,
goal="default",
response_length="shorter"
)Goal Options: default, custom (requires custom_prompt), learning_guide
Response Lengths: default, longer, shorter
Get AI Summaries
# Get AI-generated summary of what a notebook is about
summary = notebook_describe(notebook_id)
print(summary["summary"]) # Markdown with **bold** keywords
print(summary["suggested_topics"]) # Suggested report topics
# Get AI-generated summary of a specific source
source_info = source_describe(source_id)
print(source_info["summary"]) # AI summary with **bold** keywords
print(source_info["keywords"]) # Topic chips: ["Medical education", "AI tools", ...]Get Raw Source Content
# Get raw text content from a source (no AI processing)
# Much faster than notebook_query for bulk content export
content = source_get_content(source_id)
print(content["title"]) # Source title
print(content["source_type"]) # pdf, web_page, youtube, pasted_text, google_docs, etc.
print(content["url"]) # Source URL (if available)
print(content["char_count"]) # Character count
print(content["content"]) # Full raw text
# Example: Export all sources to markdown files
sources = notebook_get(notebook_id)["sources"]
for source in sources:
content = source_get_content(source["id"])
with open(f"{content['title']}.md", 'w') as f:
f.write(content["content"])Supported source types: google_docs, google_slides_sheets, pdf, pasted_text, web_page, youtube
Batch, Cross-Notebook, Pipelines & Tags
# Tag notebooks for organization
tag(action="add", notebook_id="...", tags="ai,research")
# Batch query across tagged notebooks
batch(action="query", query="What are the key findings?", tags="ai")
# Cross-notebook query with aggregated answers
cross_notebook_query(query="Compare approaches", notebook_names="Project A, Project B")
# Batch generate podcasts for all tagged notebooks
batch(action="studio", artifact_type="audio", tags="research", confirm=True)
# Run a multi-step pipeline
pipeline(action="run", notebook_id="...", pipeline_name="ingest-and-podcast", input_url="https://...")
# Find relevant notebooks by tag match
tag(action="select", query="ai research")Built-in pipelines: ingest-and-podcast, research-and-report, multi-format
Sync Stale Drive Sources
# Check which sources need syncing
sources = source_list_drive(notebook_id)
# For very large notebooks, skip freshness checks when you only need the source list
sources = source_list_drive(notebook_id, skip_freshness=True)
# Sync stale sources (after user confirmation)
source_sync_drive(source_ids=["id1", "id2"], confirm=True)Delete Sources
# Delete a source from notebook (after user confirmation)
source_delete(source_id="source-uuid", confirm=True)Research and Import Sources
# Start web research (fast mode, ~30 seconds)
result = research_start(
query="value of ISVs on cloud marketplaces",
source="web", # or "drive" for Google Drive
mode="fast", # or "deep" for extended research (web only)
title="ISV Research"
)
notebook_id = result["notebook_id"]
# Poll until complete (built-in wait, polls every 30s for up to 5 min)
# By default, report is truncated to 500 chars to save tokens
# Use compact=False to get full 10,000+ char report and all sources
status = research_status(notebook_id)
# Import all discovered sources
research_import(
notebook_id=notebook_id,
task_id=status["research"]["task_id"],
timeout=600 # Optional: increase for large notebooks (default: 300s)
)
# Or import specific sources by index
research_import(
notebook_id=notebook_id,
task_id=status["research"]["task_id"],
source_indices=[0, 2, 5], # Import only sources at indices 0, 2, and 5
timeout=600 # Optional: increase for large notebooks (default: 300s)
)
# Or import only sources cited by the deep research report
research_import(
notebook_id=notebook_id,
task_id=status["research"]["task_id"],
cited_only=True, # Overrides source_indices when enabled
timeout=600
)Research Modes:
fast+web: Quick web search, ~10 sources in ~30 secondsdeep+web: Extended research with AI report, ~40 sources in 3-5 minutesfast+drive: Quick Google Drive search, ~10 sources in ~30 seconds
Generate Audio/Video Overviews
# Create an audio overview (podcast)
result = audio_overview_create(
notebook_id=notebook_id,
format="deep_dive", # deep_dive, brief, critique, debate
length="default", # short, default, long
language="en",
confirm=True # Required - show settings first, then confirm
)
# Create a video overview
result = video_overview_create(
notebook_id=notebook_id,
format="explainer", # explainer, brief, cinematic, short
visual_style="classic", # auto_select, custom, classic, whiteboard, kawaii, anime, etc.
focus_prompt="", # Optional host/content focus text
visual_style_prompt="", # Optional custom style text when visual_style="custom"
language="en",
confirm=True
)
# Check generation status (takes several minutes)
status = studio_status(notebook_id, artifact_id=result["artifact_id"])
for artifact in status["artifacts"]:
print(f"{artifact['title']}: {artifact['status']}")
# Rich fields are opt-in and responses are paginated.
detailed = studio_status(notebook_id, include_details=True, limit=20, offset=0)
# Delete an artifact (after user confirmation)
studio_delete(
notebook_id=notebook_id,
artifact_id="artifact-uuid",
confirm=True
)Audio Formats: deep_dive (conversation), brief, critique, debate
Audio Lengths: short, default, long
Video Formats: explainer, brief, cinematic, short (vertical, ~60s, no visual style)
Video Styles: auto_select, custom, classic, whiteboard, kawaii, anime, watercolor, retro_print, heritage, paper_craft
For Short videos, language selection is best-effort. The captured RPC uses a
null language slot, so non-English requests are reinforced through the focus
prompt rather than an undocumented payload change.
Base Endpoint
POST https://notebook.google.com/_/LabsTailwindUi/data/batchexecuteGemini Notebook Enterprise
Enterprise uses a project- and location-scoped Cloud NotebookLM endpoint. The
configured base host may be notebook.cloud.google.com (current documented
host), notebooklm.cloud.google.com, or vertexaisearch.cloud.google.com for
older deployments. Set NOTEBOOKLM_PROJECT_ID and NOTEBOOKLM_LOCATION before
starting the client. NOTEBOOKLM_PROJECT_ID is required for Enterprise; the
location defaults to global.
For the documented notebook.cloud.google.com host, the routes are:
POST https://notebook.cloud.google.com/{location}/_/CloudNotebookLmUi/data/batchexecute
POST https://notebook.cloud.google.com/{location}/_/CloudNotebookLmUi/data/google.cloud.notebooklm.v1main.NotebookService/GenerateFreeFormStreamed
POST https://notebook.cloud.google.com/{location}/upload/_/The Enterprise list-notebooks RPC is rG2vCb with a project-qualified parent:
["projects/{project}/locations/{location}", null, null, 1]Enterprise streamed-query requests identify the notebook with the resource
name projects/{project}/locations/{location}/notebooks/{notebook_id}:
[
[[["source-id"]]],
"question",
{"70000": "projects/{project}/locations/{location}/notebooks/{notebook_id}"}
]These structures are based on a contributor-provided Enterprise web-client
capture and are covered by the Enterprise routing tests. Live validation still
requires access to an Enterprise deployment; record a redacted network capture
when updating them because Google may rotate the internal RPC IDs and paths.
Request Format
Content-Type: application/x-www-form-urlencoded
f.req=&at=<CSRF token>The f.req structure:
[[["<RPC_ID>", "", null, "generic"]]]The web client additionally sends browser-style headers on every call
(X-Same-Domain: 1, Chrome UA + sec-ch-ua* hints, Referer, and — on
studio calls such as rc3d8d, Rytqqe and gArtLc — anx-goog-ext-525006520-jspb header whose value is the request's config block,
e.g. [2,null,[1],[1,null,...,[1,3]]]). None of these headers are required:
the CLI omits them for every RPC, and adding them from a non-browser client
does not change the Rytqqe outcome (see the Interactive Report section).
URL Query Parameters
| Param | Description |
|---|---|
rpcids |
The RPC ID being called |
source-path |
Current page path (e.g., /notebook/) |
bl |
Build label, auto-extracted from page HTML (cfb2h key). Override via NOTEBOOKLM_BL env var. |
f.sid |
Session ID |
hl |
Language code (e.g., en) |
_reqid |
Request counter |
rt |
Response type (c) |
Response Format
)]}'
<json_array>- Starts with
)]}'(anti-XSSI prefix) - MUST be stripped - Followed by byte count, then JSON
- Multiple chunks may be present
Known RPC IDs
| RPC ID | Purpose | Params Structure |
|---|---|---|
wXbhsf |
List notebooks | [null, 1, null, [2]] |
rLM1Ne |
Get notebook details | [notebook_id, null, [2], null, 0] |
CCqFvf |
Create notebook | [title, null, null, [2], [1,null,null,null,null,null,null,null,null,null,[1]]] |
s0tc2d |
Rename notebook / Configure chat | See s0tc2d section below |
WWINqb |
Delete notebook | [[notebook_id], [2]] |
izAoDd |
Add source (unified) | See source types below |
hizoJc |
Get source details | [["source_id"], [2], [2]] |
yR9Yof |
Check source freshness | [null, ["source_id"], [2]] - returns false if stale |
FLmJqe |
Sync Drive source | [null, ["source_id"], [2]] |
b7Wfje |
Rename source | [null, ["source_id"], [[["new_title"]]]] - path: /notebook/<notebook_id> |
tGMBJ |
Delete source | [[["source_id"]], [2]] - deletion is IRREVERSIBLE |
hPTbtc |
Get conversation IDs | [notebook_id] |
khqZz |
Get conversation turns (full Q&A history) | See "Conversation Turns (khqZz)" below |
hT54vc |
User preferences | - |
ZwVcOc |
Settings | - |
ozz5Z |
Add source v2 (Unified) | See source types below |
Ljjv0c |
Start Fast Research | [["query", source_type], null, 1, "notebook_id"] |
QA9ei |
Start Deep Research | [null, [1], ["query", source_type], 5, "notebook_id"] |
e3bVqc |
Poll Research Results | [null, null, "notebook_id"] |
LBwxtb |
Import Research Sources | [null, [1], "task_id", "notebook_id", [sources]] |
R7cb6c |
Create Studio Content | See Studio RPCs section |
gArtLc |
Poll Studio Status | [[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"'] |
V5N4be |
Delete Studio Content | [[2], "artifact_id"] |
rc3d8d |
Rename Studio Artifact / Update artifact fields (field mask) | [["artifact_id", "new_title"], [["title"]]] or [sparse_artifact, [["app.generation_options.free_text_steering_prompt", ...]], null, config] |
v9rmvd |
Get one artifact by id (metadata, quiz HTML, report elements) | [artifact_id, [2, null, null, [1, null*9, [1]], [[1, 4, 8, 10, 14, 2, 3, 6, 7]]]] (simple [artifact_id] also works) |
Rytqqe |
Kick off generation of a (suggested) artifact | [[2, null, null, [1, null*9, [1]], [[1, 4, 8, 10, 14, 2, 3, 6]]], "<artifact_id>"] (plain-string id) |
Fxmvse |
Set artifact read state | [config, artifact_id, [null, null, null, null, 0], [["is_unread"]]] |
HpN0Ub |
Mark artifact viewed / session bookkeeping | [config, [artifact_id]] (returns []) |
KmcKPe |
Revise Slide Deck | [[2], artifact_id, [[[0-based_index, "instruction"], ...]]] |
yyryJe |
Generate Mind Map | See Mind Map RPCs section |
CYK0Xb |
Save Mind Map | See Mind Map RPCs section |
cFji9 |
List Mind Maps | [notebook_id] |
ciyUvf |
Get Suggested Report Formats | [[2], notebook_id, [[source_id1], ...]] |
VfAZjd |
Get Report Suggestions | [notebook_id, [2]] |
tr032e |
Get Source Guide | [[[["source_id"]]]] |
JFMDGd |
Get current share status / access state (fires on notebook page load; used by notebook share status) |
[notebook_id, [2, null, [1], [1, null x9, [1, 3]]]] |
ub2Bae |
Unidentified notebook page-load call (observed 2026-09-24) | [[2, null, [1], [1, null x9, [1, 3]]]] |
sqTeoe |
Unidentified notebook page-load call (observed 2026-09-24) | [[2, null, null, [1, null x9, [1]], [[1, 4, 8, 10, 14, 2, 3, 6]]], null, 1] |
I3xc3c |
Unidentified notebook page-load call (observed 2026-09-24) | [[2, null, [1], [1, null x9, [1, 3]]], "<notebook_id>"] |
sODAg |
Unidentified dialog-open call; fires alongside EylDcb when generation dialogs open |
[] -> response [true] |
`s0tc2d` - Notebook Update RPC
This RPC handles multiple notebook update operations based on which array position is populated.
Rename Notebook
Updates the notebook title.
# Request params
[notebook_id, [[null, null, null, [null, "New Title"]]]]
# Example
["549e31df-1234-5678-90ab-cdef01234567", [[null, null, null, [null, "My New Notebook Name"]]]]
# Response
# Returns updated notebook infoConfigure Chat Settings
Configures the notebook's chat behavior - goal/style and response length.
# Request params
[notebook_id, [[null, null, null, null, null, null, null, [[goal_code, custom_prompt?], [response_length_code]]]]]
# chat_settings is at position 7 in the nested array
# Format: [[goal_code, custom_prompt_if_custom], [response_length_code]]
# Example - Default goal + Longer response:
["549e31df-...", [[null, null, null, null, null, null, null, [[1], [4]]]]]
# Example - Custom goal + Default response:
["549e31df-...", [[null, null, null, null, null, null, null, [[2, "You are an expert..."], [1]]]]]
# Example - Learning Guide + Shorter response:
["549e31df-...", [[null, null, null, null, null, null, null, [[3], [5]]]]]Goal/Style Codes
| Code | Goal | Description |
|---|---|---|
| 1 | Default | General purpose research and brainstorming |
| 2 | Custom | Custom prompt (up to 10,000 characters) |
| 3 | Learning Guide | Educational focus with learning-oriented responses |
Response Length Codes
| Code | Length | Description |
|---|---|---|
| 1 | Default | Standard response length |
| 4 | Longer | Verbose, detailed responses |
| 5 | Shorter | Concise, brief responses |
Source Types (via `izAoDd` RPC)
All source types use the same RPC but with different param structures:
URL/YouTube Source
IMPORTANT: YouTube and regular web URLs use different positions in the source_data array!
Regular Website URL
source_data = [
None,
None,
[url], # URL at position 2 for regular websites
None, None, None, None, None, None, None,
1
]
params = [[[source_data]], notebook_id, [2], settings]YouTube URL
source_data = [
None,
None,
None, # Position 2 must be None for YouTube
None, None, None, None,
[url], # URL at position 7 for YouTube
None, None,
1
]
params = [[[source_data]], notebook_id, [2], settings]Detection: Check if URL contains youtube.com or youtu.be to determine which format to use.
Pasted Text Source
source_data = [
None,
[title, text_content], # Title and content at position 1
None,
2, # Type indicator at position 3
None, None, None, None, None, None,
1
]
params = [[[source_data]], notebook_id, [2], settings]Google Drive Source
source_data = [
[document_id, mime_type, 1, title], # Drive doc at position 0
None, None, None, None, None, None, None, None, None,
1
]
params = [[[source_data]], notebook_id, [2], settings]MIME Types:
application/vnd.google-apps.document- Google Docsapplication/vnd.google-apps.presentation- Google Slidesapplication/vnd.google-apps.spreadsheet- Google Sheetsapplication/pdf- PDF files
Query Endpoint (Streaming)
Queries use a different endpoint - NOT batchexecute!
POST /_/LabsTailwindUi/data/google.internal.labs.tailwind.orchestration.v1.LabsTailwindOrchestrationService/GenerateFreeFormStreamedQuery Request Structure
params = [
[ # Source IDs - each in nested array
[[["source_id_1"]]],
[[["source_id_2"]]],
],
"Your question here", # Query text
None,
[2, None, [1]], # Config
"conversation-uuid" # For follow-up questions
]
f_req = [None, json.dumps(params)]Query Response
Streaming JSON with multiple chunks:
- Thinking steps - "Understanding...", "Exploring...", etc.
- Final answer - Markdown formatted with citations
- Source references - Links to specific passages in sources
Conversation Turns (`khqZz`)
Discovered via Chrome DevTools capture of the web UI loading a notebook's chat panel
(2026-07-22). This is a batchexecute RPC (unlike the streaming query endpoint above)
that fetches the full Q&A history for a past conversation from the server — it's
what lets the same chat re-appear after closing and reopening a notebook, and it's whatnlm chats get / chat_get / chat_export use to show real transcripts even on a
fresh CLI invocation or MCP server session (get_conversation_turns() incore/conversation.py, wrapping RPC_GET_CONVERSATION_TURNS).
Companion to hPTbtc (get_conversation_id), which only returns the conversation
UUID — khqZz is the RPC that returns the actual turn content for that UUID.
Request
params = [
[2, None, [1], [1, None, None, None, None, None, None, None, None, None, [1, 3]]], # boilerplate client-capability descriptor, same shape used by hPTbtc
None,
None,
"conversation-uuid", # from get_conversation_id()
20, # max turns to return (page size; no pagination implemented yet)
]Response
[[turn, turn, ...], "continuation_token"]Turns are returned newest-first, alternating answer then query per turn pair
(the same [answer, None, 2] / [query, None, 1] shape the client already builds
locally in _build_conversation_history() for follow-up queries):
- Answer turn:
[turn_id, [unix_sec, nanos], 2, None, content]wherecontent[0] = [answer_text, None, [conv_id, conv_id, num]]— the answer text is
nested one level insidecontent[0](content[0][0]), notcontent[0]directly.
The remainingcontententries (content[1],content[3], ...) carry rich
formatting/citation spans. - Query turn:
[turn_id, [unix_sec, nanos], 1, "query_text"]
get_conversation_turns() pairs consecutive (answer, query) entries, reverses them
to chronological order, and returns [{"turn": 1, "query": ..., "answer": ...}, ...] —
the same shape as the local-cache-based get_conversation_history(). Only plain
answer/query text is extracted; the rich formatting and citation spans are not yet
parsed. The continuation_token for fetching turns beyond the requested limit is
not yet used (no pagination).
Research RPCs (Source Discovery)
Gemini Notebook's "Research" feature discovers and suggests sources based on a query. It supports two source types (Web and Google Drive) and two research modes (Fast and Deep).
Source Types
| Type | Value | Description |
|---|---|---|
| Web | 1 |
Searches the public web for relevant sources |
| Google Drive | 2 |
Searches user's Google Drive for relevant documents |
Research Modes
| Mode | Description | Duration | Can Leave Page |
|---|---|---|---|
| Fast Research | Quick search, ~10 sources | ~10-30 seconds | No |
| Deep Research | Extended research with AI report, ~40+ sources | 3-5 minutes | Yes |
`Ljjv0c` - Start Fast Research
Initiates a Fast Research session for either Web or Drive sources.
# Request params
[["query", source_type], null, 1, "notebook_id"]
# source_type: 1 = Web, 2 = Google Drive
# Example (Web): [["What is OpenShift", 1], null, 1, "549e31df-..."]
# Example (Drive): [["sales strategy documents", 2], null, 1, "549e31df-..."]
# Response
["task_id"]
# Example: ["6837228d-d832-4e5c-89d3-b9aa33ff7815"]`QA9ei` - Start Deep Research (Web Only)
Initiates a Deep Research session with extended web crawling and AI-generated report.
# Request params
[null, [1], ["query", source_type], 5, "notebook_id"]
# The `5` indicates Deep Research mode
# source_type: 1 = Web (Drive not supported for Deep Research)
# Example: [null, [1], ["enterprise kubernetes trends 2025", 1], 5, "549e31df-..."]
# Response
["task_id", "report_id"]
# Example: ["a02dd39b-94c0-443e-b9e4-9c15ab9016c5", null]`e3bVqc` - Poll Research Results
Polls for research completion and retrieves results. Call repeatedly until status = 2.
# Request params
[null, null, "notebook_id"]
# Response structure (when completed)
[[[
"task_id",
[
"notebook_id",
["query", source_type],
research_mode, # 1 = Fast, 5 = Deep
[
# Array of discovered sources
[
"url", # Web URL or Drive URL
"title", # Source title
"description", # AI-generated description
result_type # 1 = Web, 2 = Google Doc, 3 = Slides, 8 = Sheets
],
# ... more sources
],
"summary" # AI-generated summary of sources
],
status # 1 = in progress, 2 = completed
],
[end_timestamp, nanos],
[start_timestamp, nanos]
]]
# Deep Research also includes a report in the results (long markdown document)Result Types (in poll response):
| Type | Meaning |
|---|---|
| 1 | Web URL |
| 2 | Google Doc |
| 3 | Google Slides |
| 5 | Deep Research Report |
| 8 | Google Sheets |
`LBwxtb` - Import Research Sources
Imports selected sources from research results into the notebook.
# Request params
[null, [1], "task_id", "notebook_id", [source1, source2, ...]]
# Each source structure:
# Web source:
[null, null, ["url", "title"], null, null, null, null, null, null, null, 2]
# Drive source:
[["document_id", "mime_type", null, "title"], null, null, null, null, null, null, null, null, null, 1]
# Response
# Array of created source objects with source_id, title, metadata
[[source_id, title, metadata, [null, 2]], ...]Research Flow Summary
1. Start Research
├── Fast: Ljjv0c with source_type (1=Web, 2=Drive)
└── Deep: QA9ei with mode=5 (Web only)
2. Poll Results
└── e3bVqc → repeat until status=2
3. Import Sources
└── LBwxtb with selected sources
4. Sources appear in notebook → can query themImportant Notes
- Only one active research per notebook: Starting a new research cancels any pending results
- Deep Research runs in background: User can navigate away after initiation
- Fast Research blocks navigation: Must stay on page until complete
- Drive URLs format:
https://drive.google.com/a/redhat.com/open?id=<document_id> - Web URLs: Standard HTTP/HTTPS URLs
Studio RPCs (Audio/Video Overviews)
Gemini Notebook's "Studio" feature generates audio podcasts and video overviews from notebook sources.
`R7cb6c` - Create Studio Content
Creates both Audio and Video Overviews using the same RPC, distinguished by type code.
Audio Overview Request
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
1, # STUDIO_TYPE_AUDIO
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None,
[
None,
[
focus_prompt, # Focus text (what AI should focus on)
length_code, # 1=Short, 2=Default, 3=Long
None,
[[source_id1], [source_id2], ...], # Source IDs (simpler format)
language_code, # "en", "es", etc.
None,
format_code # 1=Deep Dive, 2=Brief, 3=Critique, 4=Debate
]
]
]
]Video Overview Request
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
3, # STUDIO_TYPE_VIDEO
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None,
[
None, None,
[
[[source_id1], [source_id2], ...], # Source IDs
language_code, # "en", "es", etc.
focus_prompt, # Host/content focus text
None,
format_code, # 1=Explainer, 2=Brief, 3=Cinematic, 4=Short
visual_style_code, # 1=Auto, 3=Classic, etc. (null when using custom style text)
visual_style_prompt # Present when visual_style="custom"
]
]
]
]Short format (code 4) — verified via live capture, 2026-06-30: Short Video
Overviews have no visual style picker, so the inner options list omitsvisual_style_code/visual_style_prompt entirely (matching Cinematic), and
additionally sends language_code as null plus a trailing flag 1 whose
meaning is undocumented by Google but required for the request to succeed.
Current service behavior adds a best-effort language requirement tofocus_prompt for non-English Short requests:
[
[[source_id1], [source_id2], ...], # Source IDs
None, # language — always null for Short
focus_prompt,
None,
4, # format_code = Short
None, None, # no visual style
1, # required trailing flag (unexplained)
]Response Structure
# Returns: [[artifact_id, title, type, sources, status, ...]]
# status: 1 = in_progress, 3 = completed`gArtLc` - Poll Studio Status
Polls for audio/video generation status.
# Request
params = [[2], notebook_id, 'NOT artifact.status = "ARTIFACT_STATUS_SUGGESTED"']
# Response Structure
[
[
# Artifact Data Array
[
artifact_id, # [0] UUID
"Title", # [1] Title
type_code, # [2] Type (1=Audio, 3=Video, etc)
[source_ids], # [3] Source IDs used
wait_time_sec, # [4] Est wait time
None, # [5] (Previously thought to be prompt)
[ # [6] Custom Prompt / Focus Data
null,
[
"Custom Prompt Text", # [1][0] The actual prompt text
audience_level, # [1][1] (e.g. 2)
...
]
],
None, # [7]
[url_data], # [8] URL for audio/video (nested)
...
],
... # More artifacts
],
...
]Artifacts with type code 4 use the nested subtype at [9][1][0]:
1= flashcards2= quiz4= mind map
Mind map subtype 4 was confirmed from a live gArtLc response on 2026-07-14.
Older clients that only distinguish flashcards and quizzes will mislabel these
saved mind maps as flashcards.
`V5N4be` - Delete Studio Content
Deletes an audio or video overview artifact permanently.
# Request
params = [[2], "artifact_id"]
# Response
[] # Empty array on successWARNING: This action is IRREVERSIBLE. The artifact is permanently deleted.
`rc3d8d` - Rename Studio Artifact
Renames a studio artifact (audio, video, report, etc.).
# Request
params = [["artifact_id", "New Title"], [["title"]]]
# Response
# Returns the updated artifact data on successNote: This RPC was discovered in v0.2.8. The second array [["title"]] specifies which field(s) to update.
`KmcKPe` - Revise Slide Deck
Revises individual slides in an existing slide deck. Creates a new artifact — the original is not modified.
Request
params = [
[2], # Version/mode indicator (always [2])
artifact_id, # UUID of the existing slide deck
[
[ # Array of slide revision instructions
[0, "Make the title larger"], # [0-based slide index, instruction text]
[2, "Remove the image"], # Multiple instructions supported
]
]
]Response
Returns the same structure as R7cb6c (Create Studio Content):
result[0][0]— New artifact UUIDresult[0][2]— Title (original title + " (2)")result[0][4]— Status code (1 = in_progress, 3 = completed)result[0][20]— Original artifact UUID
Notes
- Slide index is 0-based (slide 1 = index 0)
- Multiple slides can be revised in one call
- Original deck settings (focus prompt, language, format, length) are preserved
- Poll with
gArtLc(studio_status) for completion - Only slide decks support revision (no other artifact types)
Audio Options
| Option | Values |
|---|---|
| Formats | 1=Deep Dive (conversation), 2=Brief, 3=Critique, 4=Debate |
| Lengths | 1=Short, 2=Default, 3=Long |
| Languages | BCP-47 codes, including regional values such as "es-ES", "es-US", and "es-419" |
For Audio Overviews, Gemini Notebook has been observed using the region subtag to
select the voice accent. es and es-ES produce Spain Spanish, while es-US
and es-419 produce Latin-American Spanish. Prompt text does not reliably
override the accent. This is observed behavior and may change upstream.
Video Options
| Option | Values |
|---|---|
| Formats | 1=Explainer (comprehensive), 2=Brief, 3=Cinematic, 4=Short (vertical, ~60s) |
| Visual Styles | 1=Auto-select, 2=Custom, 3=Classic, 4=Whiteboard, 5=Kawaii, 6=Anime, 7=Watercolor, 8=Retro print, 9=Heritage, 10=Paper-craft (not applicable to Cinematic or Short) |
| Languages | BCP-47 codes: "en", "es", "fr", "de", "ja", etc. Short uses best-effort prompt steering because its RPC language slot is null. |
Infographic Request
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
7, # STUDIO_TYPE_INFOGRAPHIC
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None, None, None, None, None, None, None, # 10 nulls
[[focus_prompt, language, None, orientation_code, detail_level_code, visual_style_code]] # Options at position 14
]
]Infographic Options
| Option | Values |
|---|---|
| Orientations | 1=Landscape (16:9), 2=Portrait (9:16), 3=Square (1:1) |
| Detail Levels | 1=Concise, 2=Standard, 3=Detailed (BETA) |
| Visual Styles | 1=Auto-select, 2=Sketch Note, 3=Professional, 4=Bento Grid, 5=Editorial, 6=Instructional, 7=Bricks, 8=Clay, 9=Anime, 10=Kawaii, 11=Scientific |
| Languages | BCP-47 codes: "en", "es", "fr", "de", "ja", etc. |
Slide Deck Request
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
8, # STUDIO_TYPE_SLIDE_DECK
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None, None, None, None, None, None, None, None, None, # 12 nulls
[[focus_prompt, language, format_code, length_code]] # Options at position 16
]
]Slide Deck Options
| Option | Values |
|---|---|
| Formats | 1=Detailed Deck (comprehensive), 2=Presenter Slides (key points) |
| Lengths | 1=Short, 3=Default |
| Languages | BCP-47 codes: "en", "es", "fr", "de", "ja", etc. |
Studio Flow Summary
1. Create Studio Content
├── Audio: R7cb6c with type=1 and audio options
├── Video: R7cb6c with type=3 and video options
├── Infographic: R7cb6c with type=7 and infographic options
└── Slide Deck: R7cb6c with type=8 and slide deck options
2. Returns immediately with artifact_id (status=in_progress)
3. Poll Status
└── gArtLc → repeat until status=3 (completed)
4. When complete, response includes download URLs
5. Delete (optional)
└── V5N4be with artifact_id → permanently removes contentReport RPCs
Reports use the same R7cb6c RPC with type code 2 (STUDIO_TYPE_REPORT).
Report Request Structure
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
2, # STUDIO_TYPE_REPORT
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None,
[
None,
[
"Briefing Doc", # Report title/format
"Key insights and quotes", # Short description
None,
[[source_id1], [source_id2], ...], # Source IDs (simpler format)
"en", # Language code
"Create a comprehensive...", # Full prompt/instructions
None,
True # Unknown flag
]
]
]
]Standard Report Formats
| Format | Description | Prompt Style |
|---|---|---|
| Briefing Doc | Key insights and important quotes | Comprehensive briefing with Executive Summary |
| Study Guide | Short-answer quiz, essay questions, glossary | Educational focus with test prep materials |
| Blog Post | Insightful takeaways in readable article format | Engaging, accessible writing style |
| Create Your Own | Custom format with user-defined structure | User provides custom prompt |
`ciyUvf` - Get Suggested Report Formats
Returns AI-generated suggested report topics based on notebook sources.
# Request params
params = [[2], notebook_id, [[source_id1], [source_id2], ...]]
# Response: Array of suggested reports with full prompts
[
[
"Strategy Briefing", # Title
"An analysis of...", # Description
None,
[[source_ids]], # Sources
"Synthesize the provided...", # Full AI prompt
2 # Audience level (1=beginner, 2=advanced)
],
# ... more suggestions
]`VfAZjd` - Get Notebook Summary and Report Suggestions
Returns an AI-generated summary of the notebook and suggested report topics.
# Request params
[notebook_id, [2]]
# Response structure
[
[
"The provided documents explore...", # AI-generated summary (markdown formatted)
],
[
[
[
"How do generative AI tools...", # Suggested topic question
"Create a detailed briefing..." # Full prompt for report
],
# ... more suggested topics
]
]
]Summary format: Markdown text with bold keywords highlighting key themes.
Use case: This RPC provides the notebook description shown in the Chat panel when you first open a notebook. Perfect for a notebook_describe tool to give users a high-level overview of what a notebook contains.
`tr032e` - Get Source Guide
Generates an AI summary and keyword chips for a specific source. This is the "Source Guide" feature shown when clicking on a source in the Gemini Notebook UI.
# Request params
params = [[[["source_id"]]]]
# Source ID in deeply nested arrays
# Example
params = [[[["5d318300-1b66-4bf6-ad3a-072c76f8a8eb"]]]]
# Response structure
[
[
null,
[
"This facilitator's guide outlines a specialized workshop designed to help **medical residents and fellows** leverage **generative artificial intelligence**..."
# AI-generated summary with **bold** markdown for keywords
],
[
["Medical education", "Generative AI tools", "Resident teaching skills", "Educational content creation", "Ethics and risks"]
# Array of keyword chips
],
[]
]
]Response fields:
[0][1][0]: AI-generated summary (markdown formatted with bold keywords)[0][2][0]: Array of keyword chip strings
Use case: Perfect for a source_describe tool that provides an AI-generated overview of individual sources, similar to notebook_describe for notebooks.
Interactive Report RPCs (type 11)
Interactive Reports (STUDIO_TYPE_INTERACTIVE_REPORT = 11) are long-form reports
that embed Studio elements - audio, video, mind map, infographic, flashcards,
slide deck and quiz - as suggested placeholder cards the reader can generate
in place ("Add" -> "Generate", or "Add" -> "Customize"). They are created with the standard R7cb6c create
RPC, read back with v9rmvd, and their elements are generated with an rc3d8d
field update followed by a Rytqqe kickoff.
Create request (`R7cb6c`, type 11)
params = [
[2, null, null,
[1, null, null, null, null, null, null, null, null, null, [1]], # template block ([1] = Learning Overview)
[[1, 4, 8, 10, 14, 2, 3, 6]]], # embeddable element type codes
notebook_id,
[
null, null,
11, # STUDIO_TYPE_INTERACTIVE_REPORT
[[[source_id1]], [[source_id2]], ...],
null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, # indices 4..33 are padding
[null, [custom_prompt, language]], # options block at index 34 (not 7!)
],
]| Difference from classic (type 2) reports | Detail |
|---|---|
| Config block | Gains the template selector and the element-type allowlist |
| Options block | Moves from index 7 to index 34 |
Response [2] |
11 instead of 2 |
Artifact layout
| Index | Contents |
|---|---|
[0] |
artifact id |
[1] |
title (auto-generated by the model) |
[2] |
11 |
[3] |
sources |
[4] |
status code (see below) |
[10] |
modified timestamp [seconds, nanos] |
[15] |
created timestamp [seconds, nanos] |
[21] |
base64 task id |
[34] |
[document_or_null, [prompt, language, generated?]] |
Suggested element artifacts (status 5) are 36 long and carry[title, card_description] at index [35]. The description is the text shown
on the card; a plain "Generate" click sends it as the steering prompt.
Sources: the report stores its sources at [3] as [[[""], null, 5], ...].
Suggested infographic / slide-deck elements carry the report's sources, but
suggested type-4 elements (mind map, quiz, flashcards) were observed with[3] = null. The client therefore always sends the report's source ids in
the rc3d8d update and refuses to generate when they cannot be resolved; it
never widens to every notebook source.
Status codes observed at index [4] (extends the standard set):
| Code | Meaning |
|---|---|
| 1 | in_progress (document rendering) |
| 2 | queued/generating (observed for interactive reports, mind-map, quiz, flashcard and slide-deck elements; audio keeps its completed-with-media special case) |
| 3 | completed |
| 4 | failed |
| 5 | suggested (element placeholders; excluded by the default gArtLc filter) |
Document blocks (`artifact[34][0][0][0]` once generated)
A flat list of blocks:
| Block | Shape |
|---|---|
| Heading | [null, null, [[[null, null, ["<title>"]]], [null, 5]]] |
| Paragraph | [start, end, [[[start, end, ["<text>", attrs?]], ...]]] - attrs == [true] renders bold; offsets are character positions |
| Bullet | [start, end, [[span], [null, 1], null, [null, null, 0, meta]]] with meta = {"101": bullet_char, "102": 1, "103": item_index, "104": running_counter} |
| Embed | [null x12, ["<element id>", 1]] - placeholder for a suggested Studio element (the trailing flag was observed as 1 both before and after the element was generated, so treat it as informational) |
Generating an embedded element
Two calls, mirroring the web UI:
rc3d8d- update artifact fields via field mask:
[
[element_id, null, null, [[[source_id]], ...], null, null, null, null, null,
[null, [subtype, null, steering_prompt, language]]], # subtype e.g. 4 = mind map
[["app.generation_options.free_text_steering_prompt",
"app.generation_options.language_code",
"sources"]],
null,
[2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]],
[[1, 4, 8, 10, 14, 2, 3, 6, 7]]],
]Rytqqe- kick off generation (response is the refreshed artifact). The
element id is a plain string, not wrapped in a list, and the config is
the same for every element kind:
[[2, null, null, [1, null, null, null, null, null, null, null, null, null, [1]],
[[1, 4, 8, 10, 14, 2, 3, 6]]],
"<element_id>"]Masks and option blocks are kind-specific (all captured live 2026-09-24):
Settings — payload and field mask pairs
"Generate" = plain Generate click. "Customize" = Customize panel. Codes match
the existing constants.py mappers unless noted.
| Kind | Mode | Options block | Field mask (besides sources) |
|---|---|---|---|
| Quiz | both | [9] = [null, [2, null, prompt, lang, null, null, null, [count, difficulty]]] |
app.generation_options.free_text_steering_prompt, .language_code, .quiz_generation_options.question_quantity, .quiz_difficulty |
| Flashcards | both | [9] = [null, [1, null, prompt, lang, null, null, [count, difficulty]]] |
app…free_text_steering_prompt, .language_code, .flashcards_generation_options.card_quantity, .flashcards_difficulty |
| Mind map | both | [9] = [null, [4, null, prompt, lang]] |
app…free_text_steering_prompt, .language_code |
| Infographic | both | [14] = [[prompt, lang, null, orientation, detail, style]] |
infographic.generation_options.user_steering_prompt, .language_code, .aspect_ratio, .information_density, .style |
| Slide deck | both | [16] = [[prompt, lang, format, length]] |
slides.generation_options.user_steering_prompt, .language_code, .deck_type, .length |
| Audio | Generate | [6] = [null, [prompt, null, null, null, lang, null, format]] |
audio_overview.generation_options.episode_focus, .language_code, .show_format |
| Audio | Customize | [6] = [null, [prompt, 2, null, null, lang, null, format]] |
adds .episode_length |
| Video | Generate | [8] = [null, null, [null, lang, prompt, null, format]] |
explainer_video.generation_options.language_code, .video_focus, .template_format |
| Video | Customize | [8] = [null, null, [null, lang, prompt, null, format, 1]] |
adds .video_overview_style |
| Video (Short) | Customize | [8] = [null, null, [null, lang, prompt, null, 4]] |
same as Generate: no .video_overview_style |
Value codes:
| Setting | Values (code) | Plain-Generate default |
|---|---|---|
quiz question_amount |
fewer 1, standard 2, more 3 | standard |
flashcards card_amount |
fewer 1, standard 2, more 3 | fewer (page pre-selects it though "Standard" is labelled default) |
difficulty (quiz, flashcards) |
easy 1, medium 2, hard 3 | medium |
infographic orientation |
landscape 1, portrait 2, square 3 | landscape |
infographic detail_level |
concise 1, standard 2, detailed 3 | standard |
infographic infographic_style |
existing 11-value mapper (e.g. kawaii 10) | auto_select |
slide slide_format |
detailed_deck 1, presenter_slides 2 | detailed_deck |
slide slide_length |
short 2, default 3 (report elements; regular Studio slide decks use short 1) | default |
audio audio_format |
deep_dive 1, brief 2, critique 3, debate 4 | brief |
video video_format |
explainer 1, cinematic 3, short 4 | cinematic |
Report elements expose no audio length and no video style in the UI;
the tool uses the Customize mask with the fixed values the page sends
(length 2, style 1) and does not let agents set them. Short videos are sent
without the style field, exactly as the page does.
Sources: a plain "Generate" in the web UI sends every source currently
selected in the notebook's source panel, even when the report was built from
fewer. The client deliberately sends the report's sources instead, so
elements stay scoped to what the report covers (verified 2026-09-24).
Correction (2026-09-24). An earlier version of this section said the
Rytqqekickoff only works from the web page. That was wrong: the kickoff was
being sent with the element id wrapped in a list ([element_id]), which the
server rejects withINVALID_ARGUMENT. The[2, null, [1], [..., [1, 3]]]
value previously listed as the mind-map kickoff config is the page'sx-goog-ext-525006520-jspbrequest header, not part of the body. With the
plain-string id, the kickoff succeeds from the plain HTTP client.
After generation the element also appears in the notebook Studio panel while
remaining referenced by the report's embed block.
Related observed RPCs
| RPC | Purpose | Notes |
|---|---|---|
Fxmvse |
Set artifact read state | [config, artifact_id, [null, null, null, null, 0], [["is_unread"]]]; response echoes the flag |
HpN0Ub |
Mark artifact viewed / session bookkeeping | [config, [artifact_id]], empty response |
EylDcb |
AI usage meter / pre-generation estimate | Called when the create dialog or element dialog opens |
sODAg |
Fired alongside EylDcb when generation dialogs open |
Empty params, response [true]; purpose unidentified |
Flashcard RPCs
Flashcards use the same R7cb6c RPC with type code 4 (STUDIO_TYPE_FLASHCARDS).
Flashcard Request Structure
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
4, # STUDIO_TYPE_FLASHCARDS
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None, None, # 5 nulls (positions 4-8)
[
None,
[
1, # Unknown (possibly default count)
None, None, None, None, None,
[difficulty, card_count] # [difficulty_code, card_count_code]
]
]
]
]Flashcard Options
| Option | Values |
|---|---|
| Difficulty | easy (1), medium (2), hard (3) - MCP tools accept string names |
| Card Count | Default count generated by AI |
Note: MCP tools (flashcards_create, quiz_create) accept string difficulty names which are mapped to internal codes via constants.CodeMapper.
Quiz RPCs
Quizzes use the same R7cb6c RPC with type code 4 (shared with Flashcards) but with different options structure.
Quiz Request Structure
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
4, # STUDIO_TYPE_FLASHCARDS (shared with Quiz)
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None, None, # 5 nulls (positions 4-8)
[
None,
[
2, # Format/variant code (distinguishes Quiz from Flashcards)
None, None, None, None, None, None,
[question_count, difficulty] # [questions, difficulty_level]
]
]
]
]Quiz Options
| Option | Values |
|---|---|
| Question Count | Integer (default: 2) |
| Difficulty | easy (1), medium (2), hard (3) - MCP tools accept string names |
Key Difference from Flashcards: Quiz uses format code 2 at the first position of the options array, while Flashcards use 1.
Mind maps share type code 4 too: newer mind maps are stored as type-4 studio artifacts with format code 4 at artifact[9][1][0] (older mind maps live in the notes store via the cFji9 RPC). The status parser classifies format code 4 as mind_map, and downloading one fetches the artifact's interactive HTML (v9rmvd) and extracts the mind map JSON ({"name": ..., "children": [...]}) from the data-app-data attribute — the same mechanism quiz/flashcards use.
Data Table RPCs
Data Tables use the R7cb6c RPC with type code 9 (STUDIO_TYPE_DATA_TABLE).
Excel exports of data tables are returned by the same status RPC with type
code 10 (STUDIO_TYPE_DATA_TABLE_XLSX). Their file metadata is atartifact[24] as [filename, mime_type, viewer_url, download_url]. The
download path streams download_url as binary XLSX without CSV parsing.
Data Table Request Structure
params = [
[2], # Config
notebook_id, # Notebook UUID
[
None, None,
9, # STUDIO_TYPE_DATA_TABLE
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None, None, None, None, None, None, None, # 10 nulls (positions 4-13)
None, None, None, None, # 4 more nulls (positions 14-17)
[
None,
[description, language] # ["Description of table", "en"]
]
]
]Data Table Options
| Option | Description |
|---|---|
| Description | String describing what data to extract (required) |
| Language | Language code (default: "en") |
Note: Data table options appear at position 18 in the content array, requiring 14 nulls after the sources.
Mind Map RPCs
Mind Maps use a two-step process with separate Generate and Save RPCs.
Step 1: `yyryJe` - Generate Mind Map
Generates the mind map JSON from sources.
# Request params
params = [
[[[source_id1]], [[source_id2]], ...], # Source IDs (nested arrays)
None, None, None, None,
["interactive_mindmap", [["[CONTEXT]", ""]], ""], # Type identifier
None,
[2, None, [1]] # Config
]
# Response
[
json_mind_map_string, # Hierarchical JSON with name/children structure
None,
[generation_id1, generation_id2, generation_number]
]Step 2: `CYK0Xb` - Save Mind Map
Saves the generated mind map to the notebook.
# Request params
params = [
notebook_id,
json_mind_map_string, # The full JSON structure from step 1
[2, None, None, 5, [[source_id1], [source_id2], ...]], # Metadata with sources
None,
"Mind Map Title" # Display title
]
# Response
[
mind_map_id, # UUID for the saved mind map
json_mind_map_string, # The saved JSON structure
[2, version_id, [timestamp, nanos], 5, [[source_ids]]], # Metadata
None,
"Generated Title" # AI-generated title
]`cFji9` - List Mind Maps
Retrieves all existing mind maps for a notebook.
# Request params
[notebook_id]
# Response
[
[
[mind_map_id, [
mind_map_id,
json_mind_map_string,
[2, version_id, [timestamp, nanos], 5, [[source_ids]]],
None,
"Mind Map Title"
]],
# ... more mind maps
],
[timestamp, nanos] # Last updated
]Mind Map JSON Structure
{
"name": "Root Topic",
"children": [
{
"name": "Category 1",
"children": [
{ "name": "Subcategory 1.1" },
{ "name": "Subcategory 1.2" }
]
},
{
"name": "Category 2",
"children": [
{ "name": "Subcategory 2.1" },
{
"name": "Subcategory 2.2",
"children": [
{ "name": "Leaf Node" }
]
}
]
}
]
}Mind Map Flow Summary
1. Generate Mind Map
└── yyryJe with source IDs → returns JSON structure
2. Save Mind Map
└── CYK0Xb with notebook_id, JSON, title → returns saved mind map with ID
3. List Mind Maps (optional)
└── cFji9 with notebook_id → returns all mind mapsNotes RPCs
Notes are saved AI chat responses that appear in the notebook's left panel. They share the same storage structure as mind maps (both use cFji9 for listing) but are distinguished by their content format.
Key Differences from Mind Maps:
- Notes: Plain text content
- Mind Maps: JSON structure with
"children"or"nodes"keys
`CYK0Xb` - Create Note
Creates a new note in a notebook. Same RPC as Save Mind Map, differs by parameters.
# Request params
params = [
notebook_id,
"", # Empty content (updated separately via UPDATE_NOTE)
[1], # Note type identifier
None,
"Note Title" # Display title
]
# Response
[
[note_id, "Note Title"], # Note ID and title
]Note: After creation, use cYAfTb (UPDATE_NOTE) to set the content.
`cFji9` - List Notes and Mind Maps
Retrieves all notes and mind maps for a notebook. Filter by content type to distinguish.
# Request params
[notebook_id]
# Response
[
[
# Regular note
[note_id, [
note_id,
"Note content text",
[metadata],
None,
"Note Title"
], status],
# Mind map (has JSON content)
[mind_map_id, [
mind_map_id,
'{"children": [...]}', # JSON structure
[metadata],
None,
"Mind Map Title"
], status],
# Deleted item (status = 2 or data is None)
[deleted_id, None, 2],
],
[timestamp, nanos] # Last updated
]Item Structure:
[0]: Item ID[1]: Item data (or None if deleted)[0]: Item ID (duplicate)[1]: Content (text for notes, JSON for mind maps)[2]: Metadata[3]: Always None[4]: Title
[2]: Status (2 = deleted)
Filtering Notes from Mind Maps:
for item in items:
if item[1] is None or (len(item) > 2 and item[2] == 2):
continue # Skip deleted items
content = item[1][1]
try:
parsed = json.loads(content)
if "children" in parsed or "nodes" in parsed:
# It's a mind map
else:
# It's a note
except (json.JSONDecodeError, TypeError):
# It's a note (plain text)`cYAfTb` - Update Note
Updates a note's content and/or title.
# Request params
params = [
notebook_id,
note_id,
[[[
"Updated note content",
"Updated Title",
[], # Unknown field
0 # Unknown field
]]],
]
# Response
null # Null on successImportant: Both notebook_id and note_id are required.
`AH0mwd` - Delete Note
Soft-deletes a note (clears content, keeps ID). Same RPC as Delete Mind Map.
# Request params
params = [
notebook_id,
None,
[note_id] # Can delete multiple: [note_id1, note_id2, ...]
]
# Response
null # Null on successNote: This is a soft-delete - the item appears in listings with status = 2 or data = None.
Notes Workflow
1. Create Note
└── CYK0Xb with notebook_id, title → returns note_id
2. Update Content
└── cYAfTb with notebook_id, note_id, content, title → returns null
3. List Notes
└── cFji9 with notebook_id → filter by content type (text vs JSON)
4. Delete Note
└── AH0mwd with notebook_id, note_id → returns nullStudio Type Codes Summary
| Type Code | Feature | RPC |
|---|---|---|
| 1 | Audio Overview | R7cb6c |
| 2 | Report | R7cb6c |
| 3 | Video Overview | R7cb6c |
| 4 | Flashcards / Quiz / Mind Map (subtype at [9][1][0]: 1=cards, 2=quiz, 4=mind map) |
R7cb6c |
| 7 | Infographic | R7cb6c |
| 8 | Slide Deck | R7cb6c |
| 9 | Data Table | R7cb6c |
| 10 | Data Table file export (XLSX/CSV/...) | R7cb6c |
| 11 | Interactive Report (embeds Studio elements; options at index 34) | R7cb6c + v9rmvd + rc3d8d/Rytqqe |
| N/A | Mind Map | yyryJe + CYK0Xb (separate RPCs) |
Key Findings
Filtering is client-side: The
wXbhsfRPC returns ALL notebooks. "My notebooks" vs "Shared with me" filtering happens in the browser.Unified source RPC: All source types (URL, text, Drive) use the same
izAoDdRPC with different param structures.Query is streaming: The query endpoint streams the AI's thinking process before the final answer.
Conversation support: Pass a
conversation_idfor multi-turn conversations (follow-up questions).Rate limits: Free tier has ~50 queries/day limit.
Research uses same RPC for Web and Drive: The
Ljjv0cRPC handles both Web (source_type=1) and Drive (source_type=2) Fast Research. Only the source_type parameter differs.Deep Research is Web-only: The
QA9eiRPC only supports Web sources (source_type=1). Google Drive does not have a Deep Research equivalent.
Drive Source Sync
Problem
Gemini Notebook doesn't auto-update Google Drive sources when the underlying document changes. Users must manually click each source > "Check freshness" > "Click to sync with Google Drive".
Solution
The source_list_drive and source_sync_drive tools automate this process.
For very large notebooks, source_list_drive(notebook_id, skip_freshness=True) skips
the per-source freshness RPCs and returns the source list faster, with stale status
reported as unknown.
Source Metadata Structure (from `rLM1Ne` response)
Each source in the notebook response has this structure:
[
[source_id], # UUID for the source
"Source Title", # Display title
[ # Metadata array
drive_doc_info, # [0] [doc_id, version_hash] for Docs/Slides/Sheets
byte_count, # [1] content size (0 for Drive, actual size for pasted text)
[timestamp, nanos], # [2] creation timestamp
[version_uuid, [timestamp, nanos]], # [3] last sync info
source_type, # [4] KEY FIELD: 1=Google Docs, 2=Slides/Sheets, 4=Pasted Text
null, # [5]
null, # [6]
null, # [7]
content_bytes, # [8] actual byte count (for Drive sources after sync)
drive_file_info # [9] [drive_id, version, mime_type, ""] for type-14 Drive-picker files
],
[null, 2] # Footer constant
]Source Types (metadata position 4)
The metadata example above shows a Drive source. For URL-bearing responses,
web sources (type 5) carry [url] at metadata[7]; YouTube sources (type 9)
carry [url, video_id, channel] at metadata[5] and normally leavemetadata[7] empty. The shared URL extractor checks position 7 first, then
falls back to position 5 only for YouTube. These are response metadata slots,
not the positions used in the add-source request payload.
| Type | Meaning | Drive Doc Info | Can Sync |
|---|---|---|---|
| 1 | Google Docs (Documents, including Gemini Notes) | [doc_id, version_hash] |
Yes |
| 2 | Google Slides/Sheets (Presentations & Spreadsheets) | [doc_id, version_hash] |
Yes |
| 4 | Pasted text | null |
No |
| 14 | Drive-picker file (PDF, text, Markdown, Word, PowerPoint, etc.) | [drive_id, version, mime_type, ""] at metadata[9] |
Yes, when a Drive ID is present |
Type 14 is also used for directly uploaded files. Those have no Drive metadata atmetadata[9] and are not syncable. The Drive ID must be present before a type-14
source is listed by source_list_drive or marked can_sync: true. This flag means
the source is eligible for a manual sync attempt; an individual RPC can still fail
(one stale type-14 PDF returned Google RPC code 13, while a separate PDF sync
succeeded). Google announced automatic Drive syncing for Docs, Sheets, and Slides,
but did not specify type-14 files in that update.
How We Discovered This
Method: Network Traffic Analysis
- Used Chrome DevTools MCP to automate browser interactions
- Captured network requests during each action
- Decoded
f.reqbody (URL-encoded JSON) - Analyzed response structures
- Tested parameter variations
Discovery Session Examples
Creating a notebook:
- Clicked "Create notebook" button via Chrome DevTools
- Captured POST to batchexecute with
rpcids=CCqFvf - Decoded params:
["", null, null, [2], [1,null,...,[1]]] - Response contained new notebook UUID at index 2
Adding Drive source:
- Opened Add source > Drive picker
- Double-clicked on a document
- Captured POST with
rpcids=izAoDd - Decoded:
[[[[doc_id, mime_type, 1, title], null,...,1]]] - Different from URL/text which use different array positions
Querying:
- Typed question in query box, clicked Submit
- Found NEW endpoint:
GenerateFreeFormStreamed(not batchexecute!) - Streaming response with thinking steps + final answer
- Includes citations with source passage references
Essential Cookies
The MCP needs these cookies (automatically filtered from the full cookie header):
| Cookie | Purpose |
|---|---|
SID, HSID, SSID, APISID, SAPISID |
Core auth (required) |
__Secure-1PSID, __Secure-3PSID |
Secure session variants |
__Secure-1PAPISID, __Secure-3PAPISID |
Secure API variants |
OSID, __Secure-OSID |
Origin-bound session |
__Secure-1PSIDTS, __Secure-3PSIDTS |
Timestamp tokens |
SIDCC, __Secure-1PSIDCC, __Secure-3PSIDCC |
Session cookies |
Important: Some cookies (PSIDTS, SIDCC, PSIDCC) rotate frequently. Always get fresh cookies from an active Chrome session.
Token Extraction
Three ways to get CSRF token and session ID:
From network request (fastest):
- Extracted directly from
get_network_request()data - No page fetch required
- Saves to cache for reuse
- Extracted directly from
From page fetch (slower first time):
- Client fetches
notebook.google.comusing cookies - Extracts
SNlM0e(CSRF) andFdrFJe(session ID) from HTML - Saves to cache for reuse
- ~1-2 seconds one-time delay
- Client fetches
From cache (instant):
- Subsequent requests reuse cached tokens
- No fetching needed
- Cache updates automatically when tokens are refreshed
SNlM0e is the CSRF field. FdrFJe is a session ID and cfb2h is a build
label; neither proves sign-in or substitutes for a CSRF token. Unrelatedat= text in page markup is not a token either. Interactive CDP login requires
the actual CSRF field together with the required signed-in cookies. Live
validation of a homepage without CSRF falls back to a notebook API check.
Label Management RPCs
Labels allow users to organize sources into thematic categories. Available when a notebook has 5+ sources. Sources can belong to multiple labels.
UI operations available:
- Auto-label (AI generates categories from sources)
- Create a new empty label
- Rename a label
- Set/change an emoji on a label
- Remove a label (sources are preserved, not deleted)
- Move a source to a different label (multi-label assignment via checkboxes)
- Return to flat list view
`agX4Bc` — Auto-Label / Reorganize / Create Label / List Labels
This RPC handles auto-labeling, force-reorganization, and manual label creation.
The 5th parameter (mode) controls the labeling behavior:
| Mode | Meaning |
|---|---|
[] |
Return existing labels, or auto-generate if none exist |
[0] |
Reorganize: only label sources not yet in any label (no confirmation in UI) |
[1] |
Reorganize: force-regenerate ALL labels from scratch (UI shows confirmation dialog) |
null + 6th param |
Create a new empty label with the given name/emoji |
Auto-label all sources (returns existing if already labeled)
# Request params
[[2], notebook_id, null, null, []]
# Example
[[2], "180cfc20-9d9f-4ebf-a5d3-1b50d5593b8b", null, null, []]
# Response: [null, [[label_name, [[src_id], ...], label_id, emoji], ...]]
[null, [
["User Tutorials",
[["0baf49b7-..."], ["86132bc5-..."], ["acd49c62-..."]],
"0245af0d-2663-40df-96b9-567f7ed1ce6f",
""], # emoji (empty string = no emoji)
["Enterprise Use Cases",
[["845deae5-..."], ["86132bc5-..."], ["98881af4-..."]],
"286c7cc0-39f3-4349-940f-ea50e34169eb",
""],
# ...
]]Reorganize — all sources (replaces existing labels)
# Request params — [1] = force full regeneration
[[2], notebook_id, null, null, [1]]
# Example
[[2], "180cfc20-9d9f-4ebf-a5d3-1b50d5593b8b", null, null, [1]]
# Response: same structure as auto-label — returns the new label set
# NOTE: UI shows a confirmation dialog before sending this requestReorganize — unlabeled sources only
# Request params — [0] = only label sources with no existing label assignment
[[2], notebook_id, null, null, [0]]
# Example
[[2], "180cfc20-9d9f-4ebf-a5d3-1b50d5593b8b", null, null, [0]]
# Response: same structure as auto-label — returns the full updated label set
# NOTE: UI fires this immediately without a confirmation dialogCreate a new empty label
# Request params — note position 5 contains [[label_name, emoji]]
[[2], notebook_id, null, null, null, [[label_name, ""]]]
# Example: create "My New Label" with no emoji
[[2], "180cfc20-...", null, null, null, [["My New Label", ""]]]
# Response: same as auto-label — returns full updated label list with new label
# The new label has null sources and a freshly generated label_id:
[null, [
# ... existing labels ...
["My New Label", null, "a0e1a3d0-bcc4-4619-bcd2-f152c64fe7d8", ""]
]]`le8sX` — Label Mutation (Rename / Set Emoji / Move Source)
This single RPC handles all label content mutations. The 4th parameter determines the operation type.
Rename a label
# Request params
[[2], notebook_id, label_id, [[[new_name]]]]
# Example: rename to "Enterprise Use Cases"
[[2], "180cfc20-...", "286c7cc0-...", [[["Enterprise Use Cases"]]]]
# Response: [] (empty = success)Set (or change) an emoji on a label
# Request params — null name + emoji string at position 1
[[2], notebook_id, label_id, [[[null, "🏷️"]]]]
# Example
[[2], "180cfc20-...", "6d7564e6-...", [[[null, "📊"]]]]
# Response: [] (empty = success)Move / assign a source to a label
Adds the source to the target label (multi-label — sources can be in multiple labels).
# Request params — null at position 0, source_id list at position 1
[[2], notebook_id, target_label_id, [[null, [[source_id]]]]]
# Example: move source to "Marketing Applications"
[[2], "180cfc20-...", "6d7564e6-...", [[null, [["845deae5-df8a-4c9a-9d11-53b0761823af"]]]]]
# Response: [] (empty = success)`GyzE7e` — Delete Label(s)
Deletes one or more labels. Sources belonging to the deleted labels are NOT deleted.
If removing a label that contains sources exclusively, reassign them first via le8sX.
# Request params — array of label_ids to delete
[[2], notebook_id, [label_id, ...]]
# Example: delete one label
[[2], "180cfc20-...", ["286c7cc0-39f3-4349-940f-ea50e34169eb"]]
# Example: delete multiple labels at once
[[2], "180cfc20-...", ["label-id-1", "label-id-2"]]
# Response: [] (empty = success)`LQhfEb` — Toggle Source Panel View
Saves the user's preference for label view vs flat list view. Not needed for label management itself, but fired when switching between views.
# Label view (view=1)
[null, notebook_id, [null, [null, 1]], [["notebook_lm_state.saved_source_panel_view"]]]
# List view (view=2)
[null, notebook_id, [null, [null, 2]], [["notebook_lm_state.saved_source_panel_view"]]]
# Response: [] (empty = success)Remove Label — Two-step Process
The "Remove" operation in the UI (which preserves sources) is a two-step sequence:
le8sX— Move any sources that would become orphaned to another labelGyzE7e— Delete the now-empty label
# Step 1: reassign orphaned sources to a different label
le8sX([[2], notebook_id, target_label_id, [[null, [[orphaned_source_id]]]]])
# Step 2: delete the label
GyzE7e([[2], notebook_id, [label_id_to_remove]])User Tier and Usage Limits
`ozz5Z` — Get User Subscription Tier
Returns the user's current Gemini Notebook subscription tier. Fires on the homepage (source-path=/) during page load.
Captured request params (2026-04-27, source-path=/):
# Inner JSON params sent to the RPC:
[[[[null, "1", 627], [null,null,null,null,null,null,null,null,null,[null,null,2]], 1]]]
# 627 and "1" appear to be hardcoded NotebookLM product/SKU constants.
# The same values appear in the support URL: ?ms=pt:1613;s:627
# path used: source-path=/ (homepage, no notebook context needed)Captured response (decoded, 2026-04-27):
# Tier string is at response[0][0][1][0][1][...]["NOTEBOOKLM_TIER_PRO_DASHER_END_USER"]
# Full decoded structure (abbreviated):
[[[[null,"1",627],[[1613,[..., "NOTEBOOKLM_TIER_PRO_DASHER_END_USER", ...]], 0]]]]
# Also contains:
# - "Manage subscription" link
# - Support URL: https://support.google.com/notebooklm/answer/16213268?ms=pt:1613;s:627;vp:9
# - Encoded tokens (session context): "CM0MEO4KICso..." and "CAI="Known tier strings:
| Tier String | Plan |
|---|---|
NOTEBOOKLM_TIER_STANDARD |
Free / Standard |
NOTEBOOKLM_TIER_PLUS |
Google AI Plus |
NOTEBOOKLM_TIER_PRO |
Google AI Pro |
NOTEBOOKLM_TIER_PRO_DASHER_END_USER |
Google Workspace Pro user |
NOTEBOOKLM_TIER_ULTRA |
Google AI Ultra |
Implementation note: This RPC is dual-mapped in core/utils.py as add_source_v2. When called
with the homepage params above it returns tier info. When called in a notebook context with different
params it handles the newer source-add flow. Use source-path=/ and the params above for tier detection.
`ZwVcOc` — Settings (also fires on homepage)
Also fires on every page load. Returns app settings including what appear to be account-level limits.
Captured request params (2026-04-27):
[null, [1, null, null, null, null, null, null, null, null, null, [1]]]Captured response (decoded, 2026-04-27):
[[null, [6, 500, 300, 500000, 2], [true, null, null, true, ["en", ...], ...], [[1]], [true, 2, 3, 2]]]
# [6, 500, 300, 500000, 2] — second element — suspected to be account limits:
# 500 = max notebooks (matches Pro tier: 500/user)
# 300 = max sources per notebook (matches Pro tier: 300/notebook)
# 6, 500000, 2 — unknown; could be tier code, storage, or other config
# Not confirmed — needs testing against Standard/Plus/Ultra accounts to validate.Usage Limits by Tier (from official docs, subject to change)
| Feature | Standard | Plus | Pro | Ultra |
|---|---|---|---|---|
| Notebooks | 100/user | 200/user | 500/user | 500/user |
| Sources | 50/notebook | 100/notebook | 300/notebook | 600/notebook |
| Chats | 50/day | 200/day | 500/day | 5,000/day |
| Audio Overviews | 3/day | 6/day | 20/day | 200/day |
| Video Overviews | 3/day | 6/day | 20/day | 200/day |
| Cinematic Videos | — | — | 2/day | 20/day |
| Reports | 10/day | 20/day | 100/day | 1,000/day |
| Flashcards | 10/day | 20/day | 100/day | 1,000/day |
| Quizzes | 10/day | 20/day | 100/day | 1,000/day |
| Mind Maps | Unlimited | Unlimited | Unlimited | Unlimited |
| Deep Research | 10/month | 3/day | 20/day | 200/day |
| Data Tables | Limited | More | Higher | Highest |
| Infographics | Limited | More | Higher | Highest |
| Slide Decks | Limited | More | Higher | Highest |
Notes:
- Auto-generated artifacts (on first source add) do NOT count toward limits
- Superseded for chat and Studio as of 2026-09-02 — see the compute-based
windows below. The per-notebook and per-source structural caps still apply.
`EylDcb` — Get Remaining Usage (compute windows)
On 2026-09-02 Gemini Notebook replaced fixed daily caps for chat and Studio with
a compute-based allowance. Consumption depends on prompt complexity and the
features used, not on a count of requests, so the client cannot derive what is
left by counting its own calls. This RPC reports it directly. Fires when the
usage dialog is opened from the Settings menu.
Captured request params (2026-09-12):
# The standard RPC header is the only parameter:
[[2, null, [1], [1, null, null, null, null, null, null, null, null, null, [1, 3]]]]Captured response (decoded, 2026-09-12):
[1,
[[null, null, null, null, 2, [1789697670, 16986000], 8.703406947222222, 91.29659305277778],
[null, null, null, null, 1, [1789200870, 16877000], null, 100]],
null,
[[1, true, 6, 3, 1, 11.177083333333334], ...]] # 24 entries, appears to be recent activityEach entry in response[1] describes one window:
| Index | Meaning |
|---|---|
| 4 | Window type: 1 = short rolling window (~5h), 2 = weekly window |
| 5 | Reset time as [epoch_seconds, nanoseconds] |
| 6 | Percent of the allowance used; null when nothing has been consumed |
| 7 | Percent of the allowance remaining |
Index 6 is "used" and index 7 is "remaining", confirmed two ways: the web
dialog read "0% used" while the payload carried 100, and a partly consumed
account returned 8.703406947222222 beside 91.29659305277778, which sum to
exactly 100.
⚠️ The two entries are NOT returned in a stable order. Observed as [2, 1]
in one call and [1, 2] in the next, for the same account. Always select a
window by its type code at index 4; reading by position silently swaps the
5-hour budget with the weekly one.
⚠️ An expired session is not an exhausted quota. When the saved session has
aged out, this RPC returns RPC error code 16 (UNAUTHENTICATED) and ozz5Z
returns a well-formed but empty entitlement. Neither may be read as "out of
quota" or as a free-tier account. Run nlm auth refresh and retry.
The authuser query parameter has no effect here. Omitting it and passing0, 1 or 2 all returned byte-identical payloads for the same account.
Implementation: core/usage.py (UsageMixin), services/usage.py,nlm usage, MCP tool usage_get.
File: docs/AUTHENTICATION.md
Authentication Guide
This guide explains how to authenticate with Gemini Notebook (formerly Google NotebookLM) MCP and CLI.
For public HTTP deployment or Claude web/mobile connectors, see
Remote MCP Deployment. Remote use introduces a separate MCP
endpoint authentication requirement in addition to the Google browser session
described here.
Overview
Gemini Notebook uses browser cookies for authentication (there is no official API). The CLI/MCP extracts these cookies automatically from a managed browser session:
- Chromium-family browsers use Chrome DevTools Protocol (CDP)
- Firefox uses an isolated profile and reads its cookie store directly (no CDP or WebDriver required)
Supported browsers: Google Chrome, Arc (macOS), Dia (macOS) Brave, Microsoft Edge, Microsoft Edge Beta, Chromium, Firefox, Vivaldi, Opera.
On Windows, standalone Chromium is discovered in the standard machine-wide
locations under C:\Program Files and C:\Program Files (x86), plus the
per-user %LOCALAPPDATA%\Chromium\Application\chrome.exe location. You can
select it explicitly with nlm config set auth.browser chromium.
Two authentication methods are available:
| Method | Best For | Requires |
|---|---|---|
| Auto Mode (default) | Most users | Any supported browser installed |
File Mode (--file) |
Complex setups, troubleshooting | Manual cookie extraction |
Method 1: Auto Mode (Recommended)
This method launches your browser automatically and extracts cookies after you log in.
Prerequisites
- A supported browser installed (Chrome, Arc, Dia, Brave, Edge, Edge Beta, Chromium, Firefox, Vivaldi, or Opera)
- Chromium-family browsers should be completely closed before running
Steps
# 1. Close your browser completely (Cmd+Q on Mac, or quit from taskbar)
# 2. Run the auth command (CLI or standalone)
nlm login # Recommended
# New profile? It first asks: Protected (OS keystore, recommended) or Plain file.
# See "Choosing at first login" under Protected Storage below.
# 3. Log in to your Google account in the browser window that opens
# (Ctrl+C cancels cleanly: nothing is saved and the browser window is closed)
# 4. Wait for "Successfully authenticated!"Chromium/CDP login waits up to 300 seconds for sign-in. A page atnotebook.google.com, an HTTP 200 response, or an anonymous NID cookie does
not prove authentication. Login waits for signed-in Google cookies and the
app's CSRF token before saving credentials or reporting success. A timeout
does not save anonymous credentials or replace an existing saved login.
What Happens Behind the Scenes
- The first available supported browser is detected (or your preferred browser if configured)
- A dedicated browser profile is created for authentication
- The browser launches with the appropriate automation backend
- You log in to Gemini Notebook via the browser
- Signed-in cookies and the CSRF token are extracted and cached; session fields are refreshed automatically when needed
- Builtin login closes the browser it launched after successful extraction. Externally managed CDP browsers remain open.
Recovering a Saved Session
If authentication has expired, retry login with the same profile, then check it:
nlm login --profile work
nlm login --check --profile workIn v0.15.3 and later, an anonymous session saved by an older version no longer
passes validation and blocks browser login. Upgrade first if an older version
reports success with only NID and no CSRF token. Profile deletion is not the
first recovery step. If a check is inconclusive because of a timeout or network
failure, check connectivity or try a read-only API call before re-authenticating.
When Firefox is selected, the profile is isolated under the NLM storage directory and its cookie database is read directly. Because cookie extraction cannot prove which Google account is active, re-login against an existing saved profile requires explicit nlm login --force after you confirm the account.
Browser Preference
By default, nlm login uses the first available browser. To use a specific browser:
# Set preferred browser
nlm config set auth.browser brave
# Or use an environment variable
export NLM_BROWSER=arc
# Valid values: auto, chrome, arc, brave, edge, edge-beta, chromium, firefox, vivaldi, opera
# If the preferred browser is not installed, falls back to auto-detection.Persistent Login
The dedicated browser profile persists your Google login:
- First run: You must log in to Google
- Future runs: Already logged in, just extracts fresh cookies
This profile is separate from your regular browser profile. Chromium profiles disable extensions.
Multi-Profile Support
Use multiple Google accounts by creating named profiles:
# Create profiles for different accounts
nlm login --profile work # Opens browser - log in with work account
nlm login --profile personal # Opens browser - log in with personal account
# List all profiles
nlm login profile list
# Output:
# work: [email protected]
# personal: [email protected]
# Switch default profile (no --profile flag needed)
nlm login switch personal
# Output: ✓ Switched default profile to personal
# Use profiles
nlm notebook list # Uses default (personal)
nlm notebook list --profile work # Uses work account
# Manage profiles
nlm login profile rename work company
nlm login profile delete old-profileSwitching accounts from an MCP app (no CLI needed)
AI apps that only have the MCP (for example Claude CoWork) can use the profile tool: list your saved accounts, switch for the current session, and optionally make a profile your saved default (make_default=true). A session switch lasts until the MCP server restarts. Apps like Claude Desktop share one MCP server across chats, so a switch can show up in other open chats until you quit and reopen the app. While a switch is active, every tool result names the account in use.
How Multi-Profile Works
Each profile gets:
- Separate credentials: Stored in
~/.notebooklm-mcp-cli/profiles/<name>/ - Separate browser profile: Isolated browser session in
~/.notebooklm-mcp-cli/chrome-profiles/<name>/ - Captured email: Automatically extracted during login for easy identification
This means you can stay logged into multiple Google accounts simultaneously without conflicts.
Unattended / Scheduled Refresh
A logged-in session normally self-heals: when the short-lived cookies age out,
the client automatically runs a headless-browser pass to make Google reissue
them. For unattended machines (a Mac mini or server running scheduled jobs),
you can also refresh proactively so a session never lapses between jobs:
nlm auth refresh # Refresh the default profile
nlm auth refresh --profile work # Refresh a named profilenlm auth refresh runs a headless browser against the saved profile — no
interactive login, no window to click. It exits non-zero when the refresh
fails, so a scheduler can react. Example launchd/cron keep-alive (every 30 min):
*/30 * * * * /path/to/nlm auth refresh >/dev/null 2>&1Note: This needs a saved Chrome profile (from a prior
nlm login). It
does not help whenNOTEBOOKLM_COOKIESis set as an environment variable —
that value overrides saved credentials, so update it directly instead.In v0.15.4 and later,
nlm auth refreshprovides descriptive, actionable diagnostics
indicating whether the saved profile was missing, signed out, encountered a DevTools websocket
error, timed out loading NotebookLM, or had its extracted credentials rejected by the API.
In addition, headless authentication automatically falls back to an OS-assigned ephemeral
port if default DevTools ports (e.g. 9222/9223) are occupied or conflict.
Disabling the refresh: Some Google Workspace accounts have their session
revoked server-side whenever the saved browser profile is relaunched (issue
#330). SetNOTEBOOKLM_DISABLE_HEADLESS_REFRESH=1to turn off both the
automatic self-heal andnlm auth refreshon those accounts.
Enterprise / Google Workspace
If your organization uses Gemini Notebook Enterprise, ask your Enterprise administrator for the project ID or number, the deployment location/multi-region, and confirmation that your account has access. Use the project- and location-specific host configured by your administrator (normally notebook.cloud.google.com). Set the base URL, project, and location before authenticating:
# Set the Enterprise URL and required Cloud resource context
export NOTEBOOKLM_BASE_URL=https://notebook.cloud.google.com
export NOTEBOOKLM_PROJECT_ID=your-gcp-project-id-or-number
export NOTEBOOKLM_LOCATION=global # or us / eu, as provided by your administrator
# Then authenticate as usual
nlm login --profile enterprise
nlm login switch enterprise # MCP uses the default profileAll CLI commands, MCP tools, and internal API calls will use this URL automatically. Enterprise requests require NOTEBOOKLM_PROJECT_ID; if the base URL is not set, the default personal URL (https://notebooklm.google.com) is used. The Enterprise variables apply to the current process, so use an Enterprise-only shell or MCP configuration when you also use a personal account.
Tip: Add the export to your shell profile (
~/.zshrc,~/.bashrc) so it persists across sessions.
For MCP server configuration, pass the variable in your client config:
{
"mcpServers": {
"gemini-notebook-mcp": {
"command": "notebooklm-mcp",
"env": {
"NOTEBOOKLM_BASE_URL": "https://notebook.cloud.google.com",
"NOTEBOOKLM_PROJECT_ID": "your-gcp-project-id-or-number",
"NOTEBOOKLM_LOCATION": "global"
}
}
}
}The "Gemini Notebook" rebrand (`notebook.google.com`)
Google is rolling out a rebrand of Gemini Notebook that redirects some signed-in accounts to notebook.google.com instead of notebooklm.google.com. This is handled automatically: nlm login records whichever host accepts your account (per-profile, in metadata.json), and every CLI/MCP request is routed to that host afterward. No configuration is needed for personal accounts.
Resolution order, if you need to override it manually:
NOTEBOOKLM_BASE_URLenv var, if set (see Enterprise section above).- The host your account last signed in on (auto-detected).
- The default
https://notebooklm.google.com.
Method 2: File Mode
This method lets you manually extract and provide cookies. Use this if:
- Auto mode doesn't work on your system
- You have browser extensions that interfere (e.g., Google Antigravity IDE)
- You prefer manual control
Steps
# Option A: Interactive mode (shows instructions, prompts for file path)
nlm login --manual
# Option B: Direct file path
nlm login --manual --file /path/to/cookies.txtFile mode verifies the imported cookies before saving them. For personal accounts, it checks both notebooklm.google.com and the rebranded notebook.google.com, then stores the host that accepts the session. Managed Workspace accounts should set NOTEBOOKLM_BASE_URL as described above.
To force the rebranded personal host explicitly:
NOTEBOOKLM_BASE_URL=https://notebook.google.com nlm login --manual --file /path/to/cookies.txtHow to Extract Cookies Manually
- Open Chrome and go to https://notebook.google.com
- Make sure you're logged in
- Press F12 (or Cmd+Option+I on Mac) to open DevTools
- Click the Network tab
- In the filter box, type:
batchexecute - Click on any notebook to trigger a request
- Click on a
batchexecuterequest in the list - In the right panel, scroll to Request Headers
- Find the line starting with
cookie: - Right-click the cookie value and select Copy value
- Paste into a text file and save
Cookie File Format
The cookie file should contain the raw cookie string from Chrome DevTools:
SID=abc123...; HSID=xyz789...; SSID=...; APISID=...; SAPISID=...; __Secure-1PSID=...; ...Notes:
- Lines starting with
#are treated as comments and ignored - The file can contain the cookie string on one or multiple lines
- A template file
cookies.txtis included in the repository - Cookie files are static credentials. Re-export them when the live verification reports that they were rejected.
Protected Storage (Protected Mode)
🔒 New: Protected login storage (recommended)
Your saved Google login can now be encrypted, with its key kept in your computer's keychain instead of a plain file. We highly recommend everyone switch on a personal computer:textnlm auth storage set protectedOptional: nothing changes unless you turn it on. Servers, cron, Docker and SSH setups can keep the plain file.
How it works
Overview
By default, notebooklm-mcp-cli stores login cookies in readable JSON files (file mode) with 0600 permissions.
In Protected mode, your cookies and session tokens are encrypted on disk with AES-256-GCM (credentials.enc). The encryption key is generated uniquely for your computer and stored in your operating system's native keystore:
- macOS: macOS Keychain
- Windows: Windows Credential Manager
- Linux: SecretService API (GNOME Keyring, KWallet)
Protected mode is optional. File mode remains the default, and upgrading notebooklm-mcp-cli changes nothing until you explicitly choose to enable it.
Choosing at first login
When you log in with a new profile in a terminal on a desktop computer, nlm login asks before the browser opens:
Where should your saved login live?
1) Protected - encrypted, key kept in your OS keystore (recommended)
2) Plain file - simple, readable by anything on this computerProtected mode only accepts names made of letters, numbers, -, _ and .. If you type something else (for example my work or [email protected]), the question offers a working name such as my-work and also lets you keep your name as a plain file. Choosing protected means the login is encrypted from the start and you will not be asked or reminded again. Choosing plain is respected too. Skip the question in scripts with nlm login --storage protected (or --storage file). Existing profiles keep their current mode; change it later with nlm setup (the "Credential protection" menu item) or nlm auth storage set.
Commands
Manage credential storage using the nlm auth storage subcommands:
# Check current storage status for the default profile
nlm auth storage status
# Check a named profile
nlm auth storage status --profile work
# Switch to Protected mode (encrypts credentials, removes plain files)
nlm auth storage set protected
nlm auth storage set protected --profile work
# Switch back to File mode (decrypts credentials, restores readable JSON)
nlm auth storage set file
nlm auth storage set file --profile work
# Output status or results as JSON
nlm auth storage status --json
nlm auth storage set protected --jsonConflict Resolution
If a profile has both plain-file credentials and encrypted credentials that differ (for example, if you logged in separately in both modes), nlm flags a conflict and asks you to resolve it:
# Keep the plain-file login and delete the encrypted leftover (stays in file mode)
nlm auth storage resolve file --profile work
# Keep the encrypted login and delete the plain files (switches to Protected mode)
nlm auth storage resolve protected --profile work
# Discard damaged or inaccessible credentials when the key is lost
nlm auth storage resolve file --profile work --discard-inaccessible --yes
# Clear a stuck or corrupt progress marker from an interrupted operation
nlm auth storage resolve --profile work --clear-marker --yesresolve file: Keeps your current plain-file login, deletes the encrypted leftover, and stays in file mode.resolve protected: Keeps the encrypted login, deletes the plain files, and switches to Protected mode.--discard-inaccessible: Use only when the encryption key was deleted from the OS keystore or the ciphertext file is corrupted beyond recovery. It removes the inaccessible ciphertext and resets the profile to file mode so you can log in again.--clear-marker: Clears a stuck or corrupt operation marker left behind if a migration was interrupted by a crash or power loss, provided the quarantine directory is clean.
Relocating or Copying Storage
If you move your ~/.notebooklm-mcp-cli directory to a new location on the same machine (e.g. changing your home directory path), update the installation identity:
nlm auth storage relocateWhy a copied folder refuses: Protected mode relies on your computer's OS keystore. If you copy ~/.notebooklm-mcp-cli to a different machine, the new computer does not have the hardware-backed encryption key in its keystore. The profile safely refuses to open rather than crashing. To use Gemini Notebook on a new computer, sign in directly with nlm login.
Downgrade Preparation
[!WARNING]
Older versions ofnotebooklm-mcp-clido not understandcredentials.enc. Before downgrading or installing an older version of the CLI, convert every protected profile back to file mode:textnlm auth storage set file nlm auth storage set file --profile <name>
Exporting Plain Credentials
If you need plain-text JSON credentials back (for example, to inspect them or use them with scripts that read auth.json or cookies.json), run:
nlm auth storage set file --profile <name>This decrypts the credentials and writes readable JSON files with 0600 permissions.
macOS "Always Allow" Prompt
Usually, no prompt appears during normal terminal operations. However, macOS prompts for permission when a different Python binary attempts to read an item created by another binary:
- Desktop extension users: the Claude Desktop extension uses
python3fromPATHonly to run a small launcher (run_server.py), which then starts the server withuvx --from notebooklm-mcp-cli notebooklm-mcp.uvxruns the server in its own environment, separate fromuv tool install, and may pick a different Python build. If that Python differs from the one that created the key (for example, you enabled Protected mode from theuv toolinstall ofnlm), macOS shows the Keychain access prompt once when the extension first reads it. Click Always Allow (not "Allow"). - Python upgrades: If you upgrade Python (such as when Homebrew or
uvinstalls a new Python release with a different binary path or signature), macOS detects the new binary signature and displays the prompt once more. Click Always Allow again. - Once Always Allow is clicked for each binary, macOS silently grants access for all future operations, MCP tool calls, and background token refreshes.
The prompt asks for your Mac login password: type it, then click Always Allow. If nobody answers within 60 seconds, nlm stops waiting and reports "approve the Keychain popup and retry", but the popup stays on screen. Dismiss it, rerun the command, and approve the new prompt.
Where Protected Mode Cannot Work
Protected mode requires an active, interactive desktop session with an unlocked OS keystore. Remote and headless sessions cannot use the OS keystore (Keychain on macOS, Credential Manager on Windows). Specifically, it cannot work in:
- Headless Linux servers, Docker containers, or cron jobs without a D-Bus session bus.
- Windows SSH sessions, Windows services, or Scheduled Tasks set to "Run whether user is logged on or not" (Windows error 1312: Windows Credential Manager requires an interactive logon session).
- macOS over SSH sessions or prior to user login after a reboot (the macOS Keychain is locked or unavailable over remote SSH sessions).
If you run nlm auth storage set protected in an unsupported environment, it safely refuses with a clear error message:
Cannot enable protected mode: OS credential store is unavailable or locked.
Remote/SSH sessions can't use the OS keystore (Keychain on macOS, Credential Manager on Windows). Run this from the desktop, or keep this profile in file mode. Your current setup keeps working.
Your existing setup continues working in file mode. Keep automated, remote, and server profiles in file mode.
Scheduled Refresh
User-configured scheduler jobs (launchd, cron, Windows Task Scheduler running nlm auth refresh) are the user's responsibility:
- In Protected mode, scheduled refresh only works while the user is logged into the OS desktop session with an unlocked keystore.
- Scheduled jobs running in headless environments, before login, or over SSH fail with a clear error unless the profile is kept in file mode.
- In-process token refresh and rotation (within an active CLI session or running MCP server process) is completely unaffected: it runs in the same Python process and requires no OS prompts.
Threat Model
- What it protects against: Backups, cloud sync folders (Dropbox, iCloud, Google Drive), accidental sharing of repository or storage directories, and other tools or scripts that read files from disk.
- What it does NOT protect against: Other code running as your user account on the same computer (any process running under your user session can request the key from the OS keystore).
- Other credential surfaces: Protected mode secures
notebooklm-mcp-cli's stored credentials. It does not alter browser profiles (chrome-profiles/, Firefox SQLite databases), manualcookies.txtexports left on disk,NOTEBOOKLM_COOKIESenvironment variables in shell configs, legacy~/.notebooklm-mcp/auth.jsonfiles, or authenticated debug artifacts (debug_page.html).
Environment Override (`NLM_AUTH_STORAGE`)
You can temporarily force the storage mode for testing using the NLM_AUTH_STORAGE environment variable (file or protected). If NLM_AUTH_STORAGE disagrees with the on-disk profile setting, mutating commands (set and resolve) refuse with an error until the variable is unset.
Where Tokens Are Stored
All data is stored under ~/.notebooklm-mcp-cli/:
~/.notebooklm-mcp-cli/
├── config.toml # CLI configuration
├── aliases.json # Notebook aliases
├── installation.json # Installation identity
├── locks/ # Inter-process profile locks
├── operations/ # Migration state & quarantine markers
├── profiles/ # Authentication profiles
│ ├── default/
│ │ ├── metadata.json # Email, host, timestamps, storage mode
│ │ ├── credentials.enc # Encrypted cookies & tokens (Protected mode)
│ │ └── cookies.json # Plaintext cookies (File mode only)
│ ├── work/
│ │ ├── metadata.json
│ │ └── credentials.enc
│ └── personal/
│ ├── metadata.json
│ └── cookies.json
├── auth.json # Root mirror of default profile (File mode only)
├── chrome-profile/ # Chrome profile (single-profile users)
└── chrome-profiles/ # Chrome profiles (multi-profile users)
├── work/
└── personal/- In Protected mode, credentials live in
credentials.enc. No plaintext cookies orauth.jsonfiles exist on disk. - In File mode, credentials live in
cookies.json(and the configured default profile is mirrored to rootauth.jsonfor backwards compatibility with external scripts), secured with0600permissions. - In both modes,
metadata.jsonstores non-secret profile metadata (account email, base host, build label, and storage mode setting).
After Authentication
Once authenticated, add the MCP to your AI tool:
Claude Code:
claude mcp add gemini-notebook-mcp -- notebooklm-mcpGemini CLI:
gemini mcp add gemini-notebook-mcp notebooklm-mcpManual (settings.json):
{
"mcpServers": {
"gemini-notebook-mcp": {
"command": "notebooklm-mcp"
}
}
}Then restart your AI assistant.
Token Expiration
- Cookies: Generally stable for weeks, but some rotate on each request
- CSRF token: Auto-refreshed on each MCP client initialization
- Session ID: Auto-refreshed on each MCP client initialization
When you start seeing authentication errors, simply run nlm login again to refresh.
Understanding `auth_status`
The MCP server_info tool and nlm login --check report one of fiveauth_status values from the multi-probe AuthHealthChecker. Knowing the
difference matters: a stale status means you must re-auth, but
an unverified status is a network problem, not a credential problem.
Caching note: the
server_inforesult is cached for 30 seconds
(the checker'sCACHE_TTL) and bypassed on the next call if any auth
file on disk is rewritten, so an externalnlm loginis reflected
without waiting for the TTL.nlm login --checkis always live.
| Status | Meaning | What to do |
|---|---|---|
configured |
Live check passed. Credentials are good. | Nothing. |
not_configured |
No credentials are stored at all (first-time setup). | Run nlm login. |
stale |
Credentials are known-bad: the live check was redirected to accounts.google.com (cookies expired), the on-disk profile failed to load, or the last successful validation is older than 7 days. |
Run nlm login to refresh. Subsequent API calls will fail. |
unverified |
The live check could not be completed (network timeout, DNS failure, proxy block, non-200 HTTP). Cached credentials on disk are still intact and may work for actual API calls. | Retry later, or check your network/proxy. Do not assume the user needs to re-auth — operations often still succeed. |
error |
Unexpected exception inside the check itself (very rare). | File a bug with the traceback. |
Heads up for AI agents: If you see
auth_status = "stale", prompt the
user to re-authenticate. If you seeauth_status = "unverified"while
recent operations are succeeding, treat it as a transient monitoring
failure and continue — re-auth is not required.
Troubleshooting
"Browser is running but without remote debugging enabled"
Close your browser completely and try again. On Mac, use Cmd+Q to fully quit.
Auto mode fails to connect
Try file mode instead:
nlm login --manual"401 Unauthorized" or "403 Forbidden" errors
Your cookies have expired. Run the auth command again to refresh.
Browser opens with strange branding (e.g., Antigravity IDE)
Some browser extensions or tools modify the browser's behavior. Try a different browser or use file mode:
nlm login --manualCookie file shows "missing required cookies"
Make sure you copied the cookie value, not the header name. The value should start with something like SID=... not cookie: SID=....
Authentication loop after `nlm login`
If you keep getting "Authentication expired" even after running nlm login or calling refresh_auth, check whether NOTEBOOKLM_COOKIES is set as an environment variable in your MCP config.
Why this happens: When NOTEBOOKLM_COOKIES is set in your config (e.g. claude_desktop_config.json), it takes priority over default auth sources — auth.json, profile cookies, save_auth_tokens, and nlm login. An explicit CLI --profile or MCP usage_get(profile="work") uses that saved account instead. When those hardcoded cookies expire, no recovery action can fix a running MCP process because the stale env var is baked into its environment.
How to check:
import os
print("NOTEBOOKLM_COOKIES in env:", "YES (used when no explicit profile is selected)" if os.environ.get("NOTEBOOKLM_COOKIES") else "no")How to fix (pick one):
- Update the cookie value in your MCP config file with fresh cookies, then restart your AI tool (Claude Desktop, etc.)
- Remove the
NOTEBOOKLM_COOKIESenv var from your config entirely and usenlm logininstead (recommended — this way auth recovery works automatically)
Similarly, if you have NOTEBOOKLM_CSRF_TOKEN or NOTEBOOKLM_SESSION_ID in your config, remove them — both are deprecated and auto-extracted. Stale values can prevent auto-refresh from working.
Experimental browser-backed RPC transport
nlm doctor auth-replay compares four lanes: saved cookies through httpx,
httpx after a forced cookie rotation, cookies freshly re-extracted from a
live logged-in browser (also replayed through httpx), and an in-page CDP
fetch from that same browser session. The fresh-cookie lane exists to tell
apart two failure modes that look identical if you only compare saved
cookies against the browser: ordinary expired cookies (verdictstale_cookies — run nlm login, no transport needed) versus genuine
device-bound replay (verdict browser_bound_replay — even fresh cookies
fail outside the browser).
Only opt in to the experimental CDP transport if the verdict isbrowser_bound_replay:
NOTEBOOKLM_RPC_TRANSPORT=cdp nlm notebook list
NOTEBOOKLM_RPC_TRANSPORT=cdp nlm query notebook <notebook-id> "Question?"For MCP clients, add the same environment variable to the server config:
{
"mcpServers": {
"gemini-notebook-mcp": {
"command": "notebooklm-mcp",
"env": {
"NOTEBOOKLM_RPC_TRANSPORT": "cdp"
}
}
}
}This runs supported Gemini Notebook form POSTs through fetch inside the saved
Gemini Notebook browser profile, so the browser supplies its live cookies. It is
off by default and currently targets normal batchexecute RPCs plus notebook
chat. Uploads, downloads, and artifact file transfers still use the existing
HTTP paths.
If the CDP transport cannot find a saved profile-owned browser session, runnlm login first. Do not use this flag as a general auth refresh shortcut;
use it only for suspected browser-bound replay failures.
Chromium 136+ Compatibility
Chrome 136+ (and other Chromium-based browsers at the same version) restrict remote debugging on the default profile for security reasons. This is handled automatically by:
- Using dedicated profile directories (
~/.notebooklm-mcp-cli/chrome-profiles/<name>/) - Adding the
--remote-allow-origins=*flag for WebSocket connections
No action required from users.
Security Notes
- Cookies are stored locally in
~/.notebooklm-mcp-cli/profiles/<name>/auth.json - Each browser profile contains your Google login for Gemini Notebook
- Never share your
auth.jsonfiles or commit them to version control - The
cookies.txtfile in the repo is a template - don't commit real cookies
File: docs/CLI_GUIDE.md
CLI Guide
Complete command reference for the nlm command-line interface.
Installation
# Using uv (recommended)
uv tool install notebooklm-mcp-cli
# Using pip
pip install notebooklm-mcp-cliAuthentication
nlm login # Opens browser, extracts cookies automatically
nlm login --profile work # Named profile for multiple accounts
nlm login --check # Check if authenticated
nlm login --storage protected # New profile: store the login encrypted (or 'file'); skips the question
nlm login switch # Switch default profile
nlm login profile list # List all profiles with email addresses
nlm login profile delete <name> # Delete a profile
nlm login profile rename <old> <new> # Rename a profile
nlm auth refresh # Non-interactive refresh (for schedulers)
# External CDP provider (e.g., OpenClaw-managed browser)
nlm login --provider openclaw --cdp-url http://127.0.0.1:18800For unattended machines, nlm auth refresh runs a headless-browser pass that
makes Google reissue the short-lived cookies keeping a session alive — no
interactive login. It exits non-zero on failure, so cron/launchd jobs can react.
Each profile gets its own isolated browser session (supports Chrome, Arc, Dia, Brave, Edge, Chromium, Firefox, and more), so you can stay logged into multiple Google accounts simultaneously.
Credential Storage (Protected Mode)
Encrypted credential storage using your operating system's native keystore (macOS Keychain, Windows Credential Manager, Linux SecretService):
# Check storage status
nlm auth storage status
nlm auth storage status --profile work
nlm auth storage status --json
# Set storage mode (file or protected)
nlm auth storage set protected # in a terminal: pick which saved logins
nlm auth storage set protected --all # every saved login at once
nlm auth storage set protected --profile work
nlm auth storage set file
nlm auth storage set file --profile work
# Resolve storage conflicts
nlm auth storage resolve file --profile work
nlm auth storage resolve protected --profile work
nlm auth storage resolve file --profile work --discard-inaccessible --yes
nlm auth storage resolve --profile work --clear-marker --yes
# Relocate storage after directory move
nlm auth storage relocateCommand Structure
The CLI supports two styles - use whichever feels natural:
# Noun-first (resource-oriented)
nlm notebook create "Title"
nlm source add <notebook> --url
# Verb-first (action-oriented)
nlm create notebook "Title"
nlm add url <notebook>Command Reference
Notebooks
nlm notebook list # List all notebooks
nlm notebook list --json # JSON output
nlm notebook create "Title" # Create notebook
nlm notebook get --json # Get details, including notebook emoji
nlm notebook describe # AI summary
nlm notebook rename "New Title" # Rename
nlm notebook delete --confirm --json # Delete with structured confirmation
nlm notebook query "question" --json # Response includes the original questionSources
nlm source list --json and nlm source get --json include the original URL
for web and YouTube sources. Use source list --url for an ID-to-URL list.
The MCP source_list_drive and source_get_content responses currently omit
this field; use the CLI when you need source URLs.
nlm source list <notebook> # List sources
nlm source add <notebook> --url "https://..." # Add URL
nlm source add <notebook> --url "https://..." --json # Return the new source ID as JSON
nlm source add <notebook> --url "https://..." --wait # Add and wait until ready
nlm source add <notebook> --text "content" --title "Notes" # Add text
nlm source add <notebook> --file document.pdf --wait # Upload file
nlm source add <notebook> --youtube "https://..." # Add YouTube
nlm source add <notebook> --drive <doc-id> # Add Drive doc
nlm source get # Get content
nlm source describe # AI summary
nlm source stale <notebook> # Check stale Drive sources
nlm source sync <notebook> --confirm # Sync stale sources
nlm source delete --confirm --json # Structured deletion resultStudio Content Creation
/* Detailed source-code truncated for AI context efficiency. */For Audio Overviews, Gemini Notebook (formerly Google NotebookLM) has been observed using the BCP-47 region
subtag to select the voice accent. For example, es and es-ES produce
Spain Spanish, while es-US and es-419 produce Latin-American Spanish.
Changing the focus prompt does not reliably change the accent. This is
observed Gemini Notebook behavior, not a guaranteed API contract.
Set NOTEBOOKLM_HL=es-419 to use a regional locale as the default artifact
language, or pass --language es-419 for a specific generation.
Downloads
nlm download audio <notebook> --id <artifact-id> --output podcast.m4a
nlm download video <notebook> --id <artifact-id> --output video.mp4
nlm download report <notebook> --id <artifact-id> --output report.md
nlm download mind-map <notebook> --id <artifact-id> --output mindmap.json
nlm download slide-deck <notebook> --id <artifact-id> --output slides.pdf
nlm download infographic <notebook> --id <artifact-id> --output infographic.png
nlm download data-table <notebook> --id <artifact-id> --output data.csv
# Interactive formats (quiz/flashcards)
nlm download quiz <notebook> --id <artifact-id> --format html --output quiz.html
nlm download flashcards <notebook> --id <artifact-id> --format markdown --output cards.md
# Download every completed artifact into a per-notebook folder
nlm download all <notebook> --output-dir ./exports
nlm download all --all-notebooks --output-dir ./exports --skip-existingAudio arrives as AAC in an MP4 container, so it needs a .m4a or .mp4
suffix; .mp3 and other mismatched extensions are rejected rather than
written with bytes that contradict the name. Transcode afterwards if you
need MP3:
nlm download audio <notebook> --id <artifact-id> --output raw.m4a
ffmpeg -i raw.m4a -acodec libmp3lame -q:a 2 podcast.mp3Research
nlm research start "query" --notebook-id --mode fast # Quick search
nlm research start "query" --notebook-id --mode deep # Extended research
nlm research start "query" --notebook-id --source drive # Search Drive
nlm research start "query" --notebook-id --auto-import # Start, poll, and import in one step
nlm research status <notebook> --max-wait 300 # Poll until done
nlm research import <notebook> <task-id> # Import sources
nlm research import <notebook> <task-id> --timeout 600 # Custom timeout (default: 300s)
nlm research import <notebook> <task-id> --cited-only # Import cited deep research sourcesStudio Status
nlm studio status <notebook> # Check artifact generation status
nlm studio status <notebook> --artifact-id # Poll one artifact
nlm studio status <notebook> --json --mcp-compatible # MCP-shaped paginated JSON
nlm video list <notebook> # List video artifacts only
nlm studio delete <notebook> <artifact-id> --confirm # Delete artifactThe existing --json output remains a plain list for script compatibility and
contains both id and artifact_id. --mcp-compatible returns the MCP envelope,
uses lean fields by default, and limits the response to 20 artifacts. Add--full, --limit, or --offset when detailed or later-page data is needed.
Sharing
nlm share status <notebook> # View sharing settings
nlm share public <notebook> # Enable public link
nlm share private <notebook> # Disable public link
nlm share invite <notebook> [email protected] # Invite viewer
nlm share invite <notebook> email --role editor # Invite editorBatch Operations
nlm batch query "What are the key takeaways?" --notebooks "id1,id2"
nlm batch query "Summarize" --tags "ai,research" # Query by tag
nlm batch query "Summarize" --all # Query ALL notebooks
nlm batch add-source "https://..." --notebooks "id1,id2"
nlm batch create "Project A, Project B, Project C" # Create multiple
nlm batch delete --notebooks "id1,id2" --confirm # Delete multiple
nlm batch studio audio --tags "research" # Generate across notebooksCross-Notebook Query
nlm cross query "What features are discussed?" --notebooks "id1,id2"
nlm cross query "Compare approaches" --tags "ai,research"
nlm cross query "Summarize everything" --all # Query ALL notebooksPipelines
nlm pipeline list # List available pipelines
nlm pipeline run ingest-and-podcast --notebook --input-url "https://..."
nlm pipeline run research-and-report --notebook --input-url "https://..."
nlm pipeline run multi-format --notebook # Audio + report + flashcardsBuilt-in pipelines: ingest-and-podcast, research-and-report, multi-format
Create custom pipelines: add YAML files to ~/.notebooklm-mcp-cli/pipelines/
Tag & Smart Select
nlm tag add <notebook> --tags "ai,research,llm" # Add tags
nlm tag add <notebook> --tags "ai" --title "My Notebook" # With display title
nlm tag remove <notebook> --tags "ai" # Remove tags
nlm tag list # List all tagged notebooks
nlm tag select "ai research" # Find notebooks by tag matchChat Configuration
nlm chat configure <notebook> --goal default --length default
nlm chat configure <notebook> --goal learning_guide --length longer
nlm chat configure <notebook> --goal custom --prompt "You are an expert..."Plan Usage
nlm usage # Use the configured default profile
nlm usage --profile work # Inspect a named profile without switching defaults
nlm usage -p personal --json # Short profile flag with JSON output--profile selects credentials only for this usage check; it does not changeauth.default_profile. An explicit profile takes priority overNOTEBOOKLM_COOKIES. Without --profile, environment cookies still take
priority over the configured default account.
Configuration
nlm config show # Show all settings
nlm config get auth.default_profile # Get a specific value
nlm config set auth.default_profile work # Set default profile
nlm config set output.format json # Change default output formatAvailable Settings:
| Key | Default | Description |
|---|---|---|
output.format |
table |
Default output format (table, json) |
output.color |
true |
Enable colored output |
output.short_ids |
true |
Show shortened IDs |
auth.browser |
auto |
Preferred browser for login (auto, chrome, arc, brave, edge, edge-beta, chromium, firefox, vivaldi, opera). Falls back to auto if the preferred browser is not found. |
auth.default_profile |
default |
Profile to use when --profile not specified. Note: MCP tools use the active profile: the saved default, unless the MCP profile tool switched this server (until it restarts), environment cookies are set, or usage_get(profile=...) selects another account. |
Aliases (Shortcuts)
nlm alias set myproject <notebook-id> # Create alias
nlm alias list # List all aliases
nlm alias get myproject # Resolve to UUID
nlm alias delete myproject # Remove alias
# Use aliases anywhere
nlm notebook get myproject
nlm source list myprojectSkills (AI Assistant Integration)
nlm skill list # Show installation status
nlm skill install claude-code # Install for Claude Code
nlm skill install cursor # Install for Cursor AI
nlm skill install <tool> --level project # Install at project level
nlm skill uninstall <tool> # Remove skill
nlm skill show # View skill content
nlm skill package # ~/Downloads/nlm-skill.zip for Claude Desktop / claude.ai upload
nlm skill package --output DIR # Save the zip somewhere else
# Verb-first alternatives
nlm install skill claude-code
nlm list skillsGemini CLI, Codex, and other agent CLIs all install to ~/.agents/skills/nlm-skill/ (.agents/skills/ at project level). These three targets are interchangeable aliases:
nlm skill install agents # Generic name
nlm skill install codex # Alias for OpenAI Codex CLI
nlm skill install gemini-cli # Alias for Google Gemini CLIAlef Agent (CLI target alef-agent) is separate: it installs under ~/.alef-agent/workspace/skills/nlm-skill/ with tool-specific skill frontmatter, not under .agents/skills/.
nlm skill install alef-agentSupported Tools: claude-code, cursor, agents, gemini-cli, codex, opencode, antigravity, cline, openclaw, alef-agent, other
Claude Desktop Chat/Cowork and claude.ai only load skills uploaded to your
Claude account, not local skill folders (the desktop app's Code tab is
Claude Code and uses ~/.claude/skills/). nlm skill package writesnlm-skill.zip (the nlm-skill/ folder with SKILL.md and references/) and
prints the upload steps: Customize → Skills → Add → pick the file → turn it
on. The packaged SKILL.md carries its version under metadata: as the skill
upload format requires. Re-upload after updating nlm.
Setup (MCP Server Configuration)
Configure the Gemini Notebook MCP server and optional skills for AI tools:
Running bare nlm setup opens an interactive wizard that detects installed tools,
configures MCP servers with private backups, and offers optional skill installation:
nlm setup # Guided wizard (recommended): status, add MCP/skill, remove, copy setup
nlm setup add claude-code # Direct command: Configure via `claude mcp add`
nlm setup add codex # Direct command: Configure Codex CLI / ChatGPT desktop
nlm setup add github-copilot --scope user # Configure VS Code user profile
nlm setup add github-copilot # Direct command: Configure current workspace (.vscode/mcp.json)
nlm setup add claude-desktop # Configure detected Claude Desktop profile(s)
nlm setup add claude-desktop --profile 3p # Select Relay AI / 3P explicitly
nlm setup remove claude-desktop --profile regular # Remove from regular explicitly
nlm setup add gemini # Write ~/.gemini/settings.json
nlm setup add cursor # Write ~/.cursor/mcp.json
nlm setup add windsurf # Write mcp_config.json
nlm setup add json # Generate JSON config for any tool
nlm setup remove gemini # Remove from Gemini CLI
nlm setup list # Show all clients and config statusClaude Desktop setup never creates a profile that is not detected. When both
regular and Relay AI/3P profiles are present, the command prompts for regular,
3P, or both. Removal uses the same profile selection. User-level skill installs
likewise require the target tool to be detected; project-level installs remain
explicitly scoped to the current project.
The guided wizard's main menu: Show my tools' status, Add the MCP to my
tools/agents, Add the skill to my tools/agents, Remove an MCP or skill,
Credential protection, Copy MCP setup for a tool not listed, and Exit. Press Esc on any
screen to go back (it quits from the main menu). Only detected tools are listed,
and the connect and remove lists start with nothing selected. Connections that
still use a legacy server name (notebooklm-mcp, notebooklm) show as
⚠ old name in the status view and under Needs a fix in the connect list;
ticking one renames it to gemini-notebook-mcp and keeps its other settings.
The wizard defaults MCP setup to the supported app/user-level location.
Its skill step defaults to All my projects (user level) and offers
Just this folder (project level); it pre-ticks tools you just connected and
available upgrades, and offers a Claude Desktop / claude.ai upload file. The Codex CLI and ChatGPT desktop app share
their MCP configuration; selecting this target makes the server available to
both on that host. For GitHub Copilot, the wizard targets the VS Code user
profile, while the direct command without --scope user targets the current
workspace.
Removal only offers profiles containing gemini-notebook-mcp or a recognized
legacy entry, so unrelated MCP servers are not removed.
Fully quit the selected Claude Desktop profile before adding or removing the
MCP, including when it was launched by Relay AI. The CLI detects running
regular and 3P instances and refuses to write while they are open, since Claude
may rewrite the config and discard the change. Reopen Claude Desktop after the
command completes.
Guided Removal & Safety
Running nlm setup and selecting Remove scans all recognized MCP server
configurations and installed skills, presenting a checklist grouped into
MCP connections and Skills with nothing pre-selected.
- Separate Confirmations: Removing MCP configurations requires an explicit
default-No confirmation (Remove the selected MCP entries?). Deleting skill
folders requires a separate default-No confirmation warning (Delete the listed skill folders? Personal edits in the active folders will be removed.). - Claude Desktop Profiles: Regular and Relay AI / 3P profiles appear as distinct
options and are removed cleanly without nested prompts. - Backups & Recovery: Before modifying any config file or deleting any skill
folder, a timestamped backup is saved to~/.notebooklm-mcp-cli/backups/. If a
backup cannot be created, the operation aborts safely. To restore a backup, copy
the backup file or directory from~/.notebooklm-mcp-cli/backups/back to its
original path. - JSONC / Comment Preservation: If a configuration file (such as VS Code's
user or workspacemcp.json) contains comments or custom formatting,nlm setup
safely refuses to modify it and prints the exact path with instructions for
manual removal.
Supported Clients: claude-code, claude-desktop, gemini, github-copilot, cursor, windsurf, cline, antigravity, codex, opencode
For unsupported tools: Choose Copy MCP setup for a tool not listed in nlm setup. It copies the standard snippet (full path to notebooklm-mcp) to the clipboard; Advanced options cover the other formats. nlm setup add json also generates a snippet directly. Choose between uvx or regular mode, full path or command name, and whether to include the mcpServers wrapper. The result is printed and can be copied to clipboard.
Note:
nlm setupconfigures the MCP server transport. Usenlm skill installto install skill/reference docs for AI tools that don't use MCP.
Doctor (Diagnostics)
Run diagnostics to troubleshoot installation, authentication, and configuration issues:
nlm doctor # Run all checks
nlm doctor --verbose # Include additional details (Python version, paths, etc.)Checks performed:
| Category | What it checks |
|---|---|
| Installation | Package version, nlm and notebooklm-mcp binary paths |
| Authentication | Profile status, cookies present, CSRF token, account email |
| Browser | Chromium-based browser installed, saved profiles for headless auth |
| AI Tools | MCP configuration status for each supported client |
Each issue includes a suggested fix (e.g., "Run nlm login to authenticate").
Output Formats
| Flag | Description |
|---|---|
| (none) | Rich table format |
--json |
JSON output, including source/Studio creation and deletion results |
--quiet |
IDs only |
--title |
"ID: Title" format |
--full |
All columns |
Complete Workflow Example
# 1. Authenticate and configure
nlm login
nlm setup add claude-code # One-time MCP setup
# 2. Create notebook and set alias
nlm notebook create "AI Research"
nlm alias set ai <notebook-id>
# 3. Add sources (with --wait to ensure ready)
nlm source add ai --url "https://example.com/article" --wait
nlm source add ai --file research.pdf --wait
# 4. Generate podcast
nlm audio create ai --format deep_dive --confirm
# 5. Wait for generation
nlm studio status ai
# 6. Download when ready
nlm download audio ai --id <artifact-id> --output podcast.m4aTips
- Saved cookies often remain usable for weeks. Run
nlm loginfor confirmed stale or missing credentials. - Use
--confirmfor all create/delete commands in scripts - Use
--waitwhen adding sources to ensure they're ready before querying - Use aliases for frequently-used notebooks
- Audio/video takes 1-5 minutes; poll with
nlm studio status - Use
nlm login switch <name>to change the default profile - New profiles ask plain vs protected storage before the browser opens; use
nlm login --storage protectedto skip the question in scripts - Run
nlm login profile listto see all profiles with their associated email addresses - Run
nlm doctorto diagnose installation, auth, or config issues - Use
nlm setup add <client>to quickly configure MCP for your AI tool
Scripting & Automation
Getting IDs for piping
Use --quiet when you only need IDs — it outputs one ID per line, no parsing required:
# Grab the first notebook ID in a shell script
notebook_id=$(nlm notebook list --quiet | head -1)
nlm notebook query "$notebook_id" "Summarize key points"Parsing `--json` output
When you need more than just IDs (title, source count, etc.), use --json. It emits
clean JSON to stdout — always parse it properly rather than splitting strings:
import json
import subprocess
result = subprocess.run(
["nlm", "notebook", "list", "--json"],
capture_output=True, text=True
)
notebooks = json.loads(result.stdout)
notebook_id = notebooks[0]["id"] # UUID like "abc12345-..."File: docs/FILE_UPLOAD_IMPLEMENTATION.md
File Upload Implementation
Overview
File upload uses HTTP Resumable Upload – the reliable, headless method.
HTTP Resumable Upload
Implementation
Uses Google's 3-step resumable upload protocol:
- Register source intent (RPC:
o4cbdc) → get SOURCE_ID - Start upload session (POST to
/upload/_/) → get upload URL - Stream file content → upload URL (memory-efficient streaming)
Code Structure
SourceMixin._register_file_source()- Step 1: Register and get SOURCE_IDSourceMixin._start_resumable_upload()- Step 2: Get upload URLSourceMixin._upload_file_streaming()- Step 3: Stream file contentSourceMixin.add_file()- Public API that orchestrates all 3 steps
Usage
Python:
from notebooklm_tools.core.client import NotebookLMClient
with NotebookLMClient() as client:
result = client.add_file(notebook_id="...", file_path="document.pdf")
# Returns: {"id": "source-id", "title": "document.pdf"}CLI:
nlm source add <notebook-id> --file document.pdfMCP:
source_add(notebook_id="...", source_type="file", file_path="/path/to/file.pdf")Supported File Types
The local gate admits the official 43-extension contract case-insensitively:
OFFICIAL_FILE_EXTENSIONS: .pdf, .txt, .md, .docx, .csv, .pptx, .epub, .avif, .bmp, .gif, .heic, .heif, .ico, .jp2, .jpe, .jpeg, .jpg, .png, .tif, .tiff, .webp, .3g2, .3gp, .aac, .aif, .aifc, .aiff, .amr, .au, .avi, .cda, .m4a, .mid, .mp3, .mp4, .mpeg, .ogg, .opus, .ra, .ram, .snd, .wav, .wma
This establishes local admission eligibility only. NotebookLM may still reject or
fail to process corrupt files, misleading extensions, inaccessible media, or
reference-only formats such as some .cda and .ram files. Those outcomes are
provider ingestion errors, not local unsupported-extension errors.
Advantages
- ✅ No Chrome dependency
- ✅ Works in headless/server environments
- ✅ Memory-efficient streaming for large files
- ✅ Faster and more reliable
- ✅ No browser fingerprinting issues
Testing
# Create a test file
echo "Test content" > test.txt
# Upload the file
nlm source add <notebook-id> --file test.txt
# Verify it was added
nlm source list <notebook-id>Troubleshooting
"File not found"
- Cause: The file path doesn't exist or is inaccessible
- Fix: Check the file path spelling and permissions
"Unsupported file type"
- Cause: File extension is not in the supported list
- Fix: Convert the file to a supported format
Upload times out
- Cause: Large file or slow connection
- Fix: The upload may still complete on the backend; check notebook sources before retrying
Historical Note
A browser-based upload fallback using Chrome automation was previously available (--browser flag) but has been removed. Gemini Notebook (formerly Google NotebookLM)'s UI now uses the File System Access API which cannot be automated via CDP. The HTTP method is more reliable anyway.
File: docs/GETTING_STARTED.md
Getting Started
A practical guide to going from "just installed" to "Gemini Notebook (formerly Google NotebookLM) is wired
into my agent." For background on what the project is and the full feature
list, see the README. For deep command/tool reference, see
the CLI Guide and MCP Guide.
Contents
First-time setup
If you have never used notebooklm-mcp-cli before, the path is:
- Install —
uv tool install notebooklm-mcp-cli. This gives you both
thenlmCLI and thenotebooklm-mcpserver binary. See the
README → Installation section for
alternatives (uvx,pip,pipx, source install). - Authenticate —
nlm login. The CLI extracts your Google cookies
from a managed browser session. See the
Authentication Guide for the supported methods
and how multi-profile auth works.🔒 Tip: On a personal computer,
nlm loginasks before the browser opens whether to keep your login encrypted in your OS keystore (recommended) or as a plain file. Already logged in? Switch any time withnlm setup→ Credential protection ornlm auth storage set protected. See Protected Storage. - Connect an agent — run the setup wizard and choose your app:Choose Add the MCP to my tools/agents, tick your apps with Space, andbash
nlm setup
press Enter. The wizard configures MCP at user/app scope, then offers the
optional skill for all projects or the current folder. Use Show my tools'
status to check what is connected, and Copy MCP setup for a tool not
listed for a client without a built-in installer. For Claude Desktop's
Chat and Cowork tabs (and claude.ai), tick Claude Desktop / claude.ai in
Add the skill to get an upload file (~/Downloads/nlm-skill.zip). - Verify — restart your agent and call
notebook_list(MCP) ornlm notebook list(CLI). If you see your existing notebooks, you are
good to go.
For deeper coverage, jump to the relevant guide:
- CLI Guide — every command, every flag
- MCP Guide — every tool, every parameter
- Authentication Guide — login, profiles, token
lifecycle,auth_statusstate meanings
Migrating from another Gemini Notebook MCP
If you previously used a browser-automation–based Gemini Notebook MCP (or any
other third-party Gemini Notebook server) and want to switch tonotebooklm-mcp-cli for direct API access, follow these steps. Most agent
frameworks (Hermes Agent, Claude Code, Cursor, etc.) get confused when two
Gemini Notebook servers are configured at the same time because their tool
names overlap (notebook_create, source_add, …), so a clean swap is
recommended.
1. Install the unified CLI/MCP
uv tool install notebooklm-mcp-cliThis installs both nlm and the notebooklm-mcp server binary.
2. Authenticate once
nlm loginYour Google cookies are extracted from a managed browser session and
cached in ~/.notebooklm-mcp-cli/profiles/default/auth.json. Thenlm login --check command verifies that the cached creds still work.
3. Register the new MCP server
Recommended: run the guided wizard to configure supported clients and
optionally install the skill:
nlm setupThe skill defaults to all projects (user level); choose the current folder for
project scope. For direct configuration, the existing client-specific commands
remain available. For example:
# Hermes Agent
nlm skill install hermes
# Claude Code
claude mcp add gemini-notebook-mcp -- notebooklm-mcp
# Gemini CLI
gemini mcp add --scope user gemini-notebook-mcp -- notebooklm-mcp
# Claude Desktop (detects regular and Relay AI/3P profiles)
nlm setup add claude-desktop
# Or select Relay AI / Claude 3P explicitly
nlm setup add claude-desktop --profile 3pFor any other MCP client, generate a config snippet:
nlm setup add jsonRecommended server name:
gemini-notebook-mcp. The executable remainsnotebooklm-mcp. Avoid
generic names likenotebooklmif you also have a legacy server
registered, or your agent will mix their tools up.
For Claude Desktop, fully quit the selected profile before running setup. If
both regular and Relay AI/3P profiles are detected, the CLI asks which one to
configure; if no profile exists, it creates nothing. Reopen Claude Desktop
after setup completes.
User-level skill installation also requires the target tool to be detected;
use nlm skill install <tool> --level project when you intentionally want a
project-local skill without changing user-level tool directories.
4. Remove the old MCP server
This is the step most people forget. Leaving both configured is the #1
cause of "Hermes picked the wrong tool" symptoms:
# Claude Code
claude mcp remove notebooklm # (or whatever the old name was)
# Gemini CLI
gemini mcp remove notebooklm
# Hermes / others: edit the client config directlyOnly removing a different Gemini Notebook server needs this step. If the old
entry is this project's own pre-rebrand name (notebooklm-mcpornotebooklm),nlm setupshows it as ⚠ old name and renames it togemini-notebook-mcpfor you: Add the MCP to my tools/agents → Needs a fix.
If you are not sure what is registered, list everything:
claude mcp list # Claude Code
gemini mcp list # Gemini CLI5. Restart your agent
Restart Claude Code / Cursor / Gemini / Hermes so the new tool list is
reloaded. Verify with a no-op call such as notebook_list (via the MCP)
or nlm notebook list (via the CLI).
Troubleshooting
- Auth setup issues — see the
Authentication Guide → Troubleshooting. - "Hermes picked the wrong tool" — you almost certainly have two
Gemini Notebook servers registered. See step 4 of the
migration section above. auth_statussays"stale"— re-runnlm login. See
Understandingauth_status
for the difference betweenstaleandunverified.- Run the full diagnostic —
nlm doctorchecks storage, auth, browser,
and MCP wiring in one go.
File: docs/KNOWN_ISSUES.md
Known Issues and Fragility
This document describes known limitations and potential failure points in the Gemini Notebook (formerly Google NotebookLM) MCP. Since this project uses undocumented internal APIs, certain breakages are expected over time.
1. Build Label (`bl`) Parameter
What it is
The bl (build label) parameter is a frontend version identifier required by Gemini Notebook's batchexecute API. It looks like:
boq_labs-tailwind-frontend_20260219.16_p2Current status (v0.3.11+)
Resolved. The bl value is now auto-extracted from the Gemini Notebook page during nlm login and during CSRF token refresh. It stays current automatically without manual intervention.
Manual override
If you need to force a specific value, set the NOTEBOOKLM_BL environment variable:
export NOTEBOOKLM_BL="boq_labs-tailwind-frontend_YYYYMMDD.XX_pN"The priority order is: env var > auto-extracted > hardcoded fallback.
2. Cookie Expiration
What it is
Authentication uses browser cookies extracted from an active Chrome session. These cookies have a limited lifespan.
When it breaks
Cookies typically expire after 2-4 weeks. Symptoms:
ValueError: Cookies have expired. Please re-authenticate...- API calls redirect to Google login page
- Authentication errors on previously working operations
How to fix
Re-extract fresh cookies using one of these methods:
Option A: nlm login CLI (recommended)
The built-in authentication CLI automatically launches Chrome, navigates to Gemini Notebook, and extracts cookies:
nlm loginIf Chrome is not running, it will be launched automatically. If you're not logged in, the CLI waits for you to complete login in the browser window. Tokens are cached to your profile directory.
Option B: Chrome DevTools MCP
If your AI assistant has Chrome DevTools MCP available:
- Navigate to
notebook.google.comin Chrome - Use Chrome DevTools MCP to extract cookies from any network request
- Call
save_auth_tokens(cookies=<cookie_header>)
Option C: Manual extraction
- Open Chrome DevTools on
notebook.google.com - Network tab → find any request → copy Cookie header
- Set
NOTEBOOKLM_COOKIESenvironment variable
3. Rate Limits
What it is
The free tier of Gemini Notebook has usage limits enforced server-side.
Current limits
- ~50 queries per day (approximate, not officially documented)
- Studio content generation may have separate limits
Symptoms when exceeded
- API returns rate limit errors
- Operations start failing mid-session
Mitigation
- Space out operations when possible
- Create videos sequentially. Brief automatic retries only cover transient
failures; after a Studio rate-limit error, wait 1-2 minutes before retrying. - Avoid tight polling loops
- Poll a known Studio artifact by ID instead of repeatedly listing the notebook.
- Consider batching queries where the API supports it
4. API Instability (Undocumented Internal APIs)
What it is
This MCP uses internal, undocumented APIs that Google can change at any time without notice.
What can break
- RPC IDs (e.g.,
wXbhsffor list notebooks) may be renamed - Request/response structure may change
- New required parameters may be added
- Endpoints may be deprecated or moved
Symptoms
- Parsing errors (unexpected response shape)
Noneresults from previously working operations- New error messages from the API
What to do when it breaks
- Check if the issue is widespread (Google may have deployed changes)
- Use Chrome DevTools to capture current request/response format
- Update the relevant RPC handling in
api_client.py - Submit a PR or issue if you discover the fix
5. CSRF Token and Session ID
What it is
The MCP auto-extracts CSRF token (SNlM0e) and session ID (FdrFJe) from the Gemini Notebook homepage on first use.
When it breaks
- If the homepage structure changes, extraction may fail
- Tokens are per-session and must be refreshed if the page is not accessible
Symptoms
ValueError: Could not extract CSRF token from page- Debug HTML saved to
~/.notebooklm-mcp-cli/debug_page.html
How to fix
If auto-extraction fails:
- Manually extract tokens from Chrome DevTools Network tab
- Pass them via
save_auth_tokens(cookies=..., request_body=..., request_url=...)
6. Claude Desktop profile setup
Symptoms
- The MCP does not appear in Claude Desktop after setup.
nlm setup add claude-desktop --profile 3psays Claude is still running even
though the window was closed.
Cause
Claude Desktop keeps separate regular and Relay AI/3P profiles. Claude or Relay
AI can also rewrite its configuration while the app is open. macOS may leave a
Crashpad helper process behind after the window closes; that helper is not an
active Claude Desktop instance and is ignored by current nlm versions.
Fix
- Fully quit the selected Claude Desktop profile and stop the Relay AI launcher
if it owns the 3P instance. - Run
nlm setup add claude-desktop --profile 3p(orregular). - Reopen the selected Claude Desktop profile and check its Developer settings.
The CLI never creates a missing profile. Removal only offers profiles that
contain gemini-notebook-mcp or a recognized legacy entry, leaving unrelated
MCP servers unchanged.
7. Claude Desktop Chat or Cowork doesn't use the skill
Symptoms
/nlm-skillworks in Claude Desktop's Code tab but the skill is missing
from Customize → Skills, and Chat or Cowork don't use it.
Cause
The Code tab is Claude Code: it reads skills from ~/.claude/skills/, wherenlm setup installs them. Chat, Cowork, and claude.ai only load skills
uploaded to your Claude account. (The MCP itself reaches Chat and Cowork through
the normal Claude Desktop connection.)
Fix
- Run
nlm skill package(ornlm setup→ Add the skill to my tools/agents
→ tick Claude Desktop / claude.ai). It saves~/Downloads/nlm-skill.zip. - In Claude Desktop or claude.ai open Customize → Skills → Add, pick the
file, and turn the skill on. - After updating
nlm, repeat both steps —nlm setupcan't see skills
uploaded to your account, so it can't flag an outdated copy.
8. Protected Mode & Keystore Environments
Symptoms
BackendUnavailableError: Cannot enable protected mode: OS credential store is unavailable or locked- On macOS: authorization popup asking for Keychain access when running the desktop extension.
- On Windows/macOS: refusal over SSH or remote non-interactive sessions.
- On Linux:
D-Bus connection refusedor keystore locked on headless servers or Docker containers. - Downgrade error: older versions of
notebooklm-mcp-clicannot readcredentials.enc.
Causes & Fixes
- macOS "Always Allow" & Desktop Extension: Usually, no prompt appears during normal terminal operations. However, the Claude Desktop extension starts the server through
uvx(itspython3launcher only locatesuvx), which runs in its own environment and may use a different Python build than theuv toolinstallation. If it does, macOS prompts for Keychain access once when the extension first reads a key created by the other Python. Enter your Mac login password and click Always Allow. Likewise, if Python is upgraded (e.g. via Homebrew or uv), do this once more. If the prompt goes unanswered for 60 seconds, the call fails with "approve the Keychain popup and retry" while the popup stays open: dismiss it, retry, and approve the new prompt. - Remote / SSH Sessions (macOS and Windows): Remote/SSH sessions cannot use the OS keystore (Keychain on macOS, Credential Manager on Windows). When attempting migration over SSH,
nlm auth storage set protectedsafely refuses. Run from the desktop session, or keep the profile in file mode. - Headless Linux / Docker: Protected mode requires an interactive desktop session with an accessible secret service daemon (D-Bus). For headless machines, servers, or Docker containers, keep the profile in file mode (
nlm auth storage set file). - Downgrading to Older Versions: Before installing an older version of
notebooklm-mcp-cli, convert any protected profile back to file mode usingnlm auth storage set file --profile <name>.
Reporting Issues
When reporting issues, include:
- The specific tool/operation that failed
- The error message (redact any sensitive info)
- Whether the operation worked before
- The current date (to correlate with potential Google deployments)
File: docs/MCP_CLI_TEST_PLAN.md
Gemini Notebook (formerly Google NotebookLM) MCP - Comprehensive Test Plan
Purpose: Verify all 53 MCP tools work correctly.
Version: 2.7 (Updated 2026-09-27 - synchronized current MCP surface)
Changes from v2.6:
- Current tool count: 53 tools — adds
profile,aliasandchat_save_to_note, pluspipelinecreate and profile info inserver_info(v0.15.0).
Historical changes from v2.1:
- The test plan previously covered 39 tools, including consolidated notes, labels, async query, batch, pipeline, tags, and server_info.
Historical changes from v1:
- Tools were consolidated from 45+ to 29 (-36%) before the current 43-tool surface.
source_add(source_type=...)replaces 4 source toolsstudio_create(artifact_type=...)replaces 9 creation toolsdownload_artifact(artifact_type=...)replaces 9 download tools
Prerequisites:
- MCP server installed:
uv cache clean && uv tool install --force . - Valid authentication cookies saved
- Restart IDE MCP server after installation
Automated Testing with AI Tools
Simply ask your AI assistant to:
Run the automated tests from docs/MCP_TEST_PLAN.md. For the Drive sync test,
pause before checking freshness, ask me to make a change to the doc, then
verify both staleness detection and sync functionality work correctly.How Automation Works
- Execute tests sequentially - Run through each test group automatically
- Track progress - Use a todo list to show what's being tested
- Pause for user input - Stop at Drive sync test to ask you to modify a document
- Report results - Provide a comprehensive summary of all tests
Test Group 1: Authentication & Basic Operations
Test 1.1 - Refresh Auth
Tool: refresh_auth
CLI: nlm login --check (validates tokens)
Prompt:
Refresh authentication tokens for NotebookLM.Expected: Success message with auth status.
Test 1.1b - Non-Interactive Session Refresh (unattended)
CLI: nlm auth refresh (headless refresh; --profile <name> for a named profile)
Prompt:
Refresh my NotebookLM session without opening a browser window.Expected: With a saved Chrome profile and a working login, prints✓ Session refreshed for profile '<name>' and exits 0. With no saved profile
or a dead login, prints a clear failure and exits non-zero (usable in
cron/launchd). Refuses up front when NOTEBOOKLM_COOKIES is set in the
environment (that value overrides saved credentials).
Test 1.1c - Credential Storage Management (Protected Mode)
CLI: nlm auth storage status, nlm auth storage set protected, nlm auth storage set file
Prompt:
Check my NotebookLM credential storage mode and test switching between file mode and protected mode.Expected:
nlm auth storage status: Shows profile name, storage mode (fileorprotected), and presence of ciphertext or legacy files.nlm auth storage set protected: Encrypts credentials tocredentials.encusing the OS keystore (macOS Keychain, Windows Credential Manager, Linux SecretService) and removes plaintext files.nlm auth storage set file: Decrypts credentials back tocookies.jsonwith 0600 permissions.
Test 1.1d - Profiles (`profile` tool)
Tool: profile (action: list, status, switch)
Prompt:
List my Gemini Notebook profiles, switch to my other profile for this session, then tell me how many notebooks I have. Do not change my default.Expected:
profile(action="list")returns every saved profile with email,storage_mode,is_saved_default,is_active.profile(action="switch", name=...)returnsscope: "session"; later tools use that account and results carryactive_profile_note.- The saved default (
nlm config get auth.default_profile) is unchanged. - Quitting and reopening the app returns to the saved default.
make_default=truechangesdefault_profilein~/.notebooklm-mcp-cli/config.toml(switch it back afterwards).- With
NOTEBOOKLM_COOKIESset in the server's environment, switching returns an error.
Test 1.1e - Aliases, chat-to-note, pipeline create
Tools: alias, chat_save_to_note, pipeline (action: create)
Expected: alias(action="set", name="nb", value="<notebook id>") then notebook_get(notebook_id="nb") resolves the alias; chat_save_to_note creates a note from a chat; pipeline(action="create", ...) saves a pipeline that pipeline(action="list") shows.
Test 1.2 - Interactive Login (Primary)
Tool: save_auth_tokens (Fallback)
CLI: nlm login (Launches Chrome for automated extraction)
WSL2 CLI: nlm login --wsl (Supports NAT and mirrored networking modes)
Prompt:
I need to authenticate with NotebookLM.Expected: Chrome opens, logs in, and tokens are saved. On WSL2, Windows Chrome opens and
the CDP connection succeeds in both NAT and mirrored networking modes.
Anonymous-session regression (#350):
- Run login with a new profile. Before signing in, the browser must remain open;
a public landing page, build label, or anonymousNIDcookie must not finish login. - Complete Google sign-in. Login saves signed-in cookies and a CSRF token, then
closes the browser it launched. Verify the profile withnlm login --check. - If sign-in times out, login fails without saving anonymous credentials.
- For a profile previously saved with only
NID, rerunning login opens the
sign-in browser.--checkmust not report success based on a public HTTP 200 page. - An API authentication rejection must fail validation; a genuine network timeout
remains inconclusive and must not be mislabeled as credential expiry.
Test 1.3 - List Notebooks
Tool: notebook_list
CLI: nlm notebook list
Prompt:
List all my NotebookLM notebooks.Expected: List of notebooks with counts (owned, shared).
Test 1.3E - Enterprise List and Query
Before running this test, set NOTEBOOKLM_BASE_URL, NOTEBOOKLM_PROJECT_ID,
and NOTEBOOKLM_LOCATION for a Gemini Notebook Enterprise deployment. The
project setting is required; the location defaults to global. Authenticate
the profile and make it the default profile used by MCP:
nlm login --profile enterprise
nlm login switch enterprise
nlm notebook list
nlm notebook query <enterprise-notebook-id> "Summarize the sources" --new-conversationExpected: Login opens the configured regional project URL, list returns
Enterprise notebooks, and the query uses the Enterprise streamed endpoint.
This test is not complete until it is run against an Enterprise deployment with
a redacted request/response capture; the automated suite only verifies routing
and payload contracts.
Test 1.4 - Create Notebook
Tool: notebook_create
CLI: nlm notebook create "MCP Test Notebook"
Prompt:
Create a new notebook titled "MCP Test Notebook".Expected: New notebook created with notebook_id and URL.
Save: Note the notebook_id for subsequent tests.
Test 1.5 - Get Notebook Details
Tool: notebook_get
CLI: nlm notebook get [notebook_id]
Prompt:
Get the details of notebook [notebook_id from Test 1.4].Expected: Notebook details with empty sources list, timestamps.
Test 1.6 - Rename Notebook
Tool: notebook_rename
CLI: nlm notebook rename [notebook_id] "MCP Test - Renamed"
Prompt:
Rename notebook [notebook_id] to "MCP Test - Renamed".Expected: Success with updated title.
Test Group 2: Adding Sources (Consolidated source_add)
Test 2.1 - Add URL Source
Tool: source_add
CLI: nlm source add [notebook_id] --url https://en.wikipedia.org/wiki/Artificial_intelligence
Prompt:
Add a URL source to notebook [notebook_id]:
- source_type: url
- url: https://en.wikipedia.org/wiki/Artificial_intelligenceExpected: Source added with source_id, source_type: url.
Test 2.2 - Add Text Source
Tool: source_add
CLI: nlm source add [notebook_id] --text "This is a test document..." --title "Test Document"
Prompt:
Add a text source to notebook [notebook_id]:
- source_type: text
- title: "Test Document"
- text: "This is a test document about machine learning. Machine learning is a subset of artificial intelligence."Expected: Text source added with source_id, source_type: text.
Test 2.3 - Add Drive Source (Optional)
Tool: source_add
CLI: nlm source add [notebook_id] --drive [doc_id] --type doc --title "My Drive Doc"
Prompt:
Add a Drive document to notebook [notebook_id]:
- source_type: drive
- document_id: [your_doc_id]
- title: "My Drive Doc"
- doc_type: docExpected: Drive source added (or skip if no Drive doc available).
Test 2.4 - Add File Source (Optional)
Tool: source_add
CLI: nlm source add [notebook_id] --file /path/to/document.pdf
Prompt:
Add a file to notebook [notebook_id]:
- source_type: file
- file_path: /path/to/document.pdfExpected: File uploaded with source_id, method: resumable.
Test 2.5 - List Sources with Drive Status
Tool: source_list_drive
CLI: nlm source list [notebook_id] --drive
CLI (fast list): nlm source list [notebook_id] --drive --skip-freshness
Prompt:
List all sources in notebook [notebook_id] and check their Drive freshness status.Expected: List showing sources by type, Drive sources show freshness.
Large notebook variant: Call source_list_drive with skip_freshness=True, or usenlm source list [notebook_id] --drive --skip-freshness. Expected: sources are listed
without per-source freshness checks; stale status may be unknown.
Drive-picker file variant: Include a PDF, text, Markdown, Word, or PowerPoint file
that was imported from Google Drive. Expected: type-14 files with Drive metadata appear
in drive_sources with their Drive IDs and can_sync: true; directly uploaded type-14
files without that metadata remain in other_sources. can_sync marks eligibility to
attempt a manual sync, not a guarantee that every source's RPC will succeed. Only
manually sync entries the tool marks can_sync: true.
Save: Note a source_id for next tests.
Test 2.6 - Describe Source
Tool: source_describe
CLI: nlm source describe [source_id]
Prompt:
Get an AI-generated summary of source [source_id].Expected: AI summary with keywords.
Test 2.7 - Get Source Content
Tool: source_get_content
CLI: nlm source content [source_id]
Prompt:
Get the raw text content of source [source_id].Expected: Raw text content with title, source_type, char_count.
Test 2.8 - Delete Source
Tool: source_delete
CLI: nlm source delete [source_id] --confirm
Prompt:
Delete source [source_id] with confirm=True.Expected: Source permanently deleted.
Test Group 3: AI Features
Test 3.1 - Describe Notebook
Tool: notebook_describe
CLI: nlm notebook describe [notebook_id]
Prompt:
Get an AI-generated summary of what notebook [notebook_id] is about.Expected: AI summary with suggested topics.
Test 3.2 - Query Notebook
Tool: notebook_query
CLI: nlm query [notebook_id] "What is artificial intelligence?"
Prompt:
Ask notebook [notebook_id]: "What is artificial intelligence?"Expected: AI answer with conversation_id.
Test 3.2a - Query Timeout and Async Fallback
Tool: notebook_query, notebook_query_start, notebook_query_status
CLI: nlm query [notebook_id] "Summarize all sources" --timeout 45
Prompt:
Ask notebook [notebook_id] a source-heavy question with timeout=45.
If it times out, retry with timeout=180. Also verify the async workflow:
notebook_query_start(..., timeout=180), then poll notebook_query_status(query_id).Expected:
- A timed-out synchronous query returns a structured error with
debug_code=query_deadline_exceeded, a retryable flag, and a hint to use a
longer timeout. - The retry with
timeout=180can complete when the notebook needs more than
the default 120-second budget. - The async workflow returns a
query_idimmediately and reachescompleted
or a structurederrorstatus when polled.
Test 3.3 - Configure Chat (Learning Guide)
Tool: chat_configure
CLI: nlm chat configure [notebook_id] --goal learning_guide --response-length longer
Prompt:
Configure notebook [notebook_id] chat settings:
- goal: learning_guide
- response_length: longerExpected: Settings updated successfully.
Test 3.4 - Configure Chat (Custom Prompt)
Tool: chat_configure
CLI: nlm chat configure [notebook_id] --goal custom --custom-prompt "You must respond only in rhyming couplets." --response-length default
Prompt:
Configure notebook [notebook_id] chat settings:
- goal: custom
- custom_prompt: "You must respond only in rhyming couplets."
- response_length: defaultExpected: Settings updated with custom_prompt echoed back.
Test 3.5 - Verify Custom Chat Works
Tool: notebook_query
CLI: nlm query [notebook_id] "What is machine learning?"
Prompt:
Ask notebook [notebook_id]: "What is machine learning?"Expected: AI response should be in rhyming couplets.
Test Group 4: Research
Test 4.1 - Start Fast Research (Web)
Tool: research_start
CLI: nlm research start "OpenShift container platform" --mode fast --source web
Prompt:
Start fast web research for "OpenShift container platform" in notebook [notebook_id].
- mode: fast
- source: webExpected: Research task started with task_id.
Save: Note the task_id.
Test 4.2 - Check Research Status
Tool: research_status
CLI: nlm research status [notebook_id]
Prompt:
Check research status for notebook [notebook_id]. Poll until complete.Expected:
status: completedmode: fastsource_count: ~10 sources
Test 4.3 - Import Research Sources
Tool: research_import
CLI: nlm research import [notebook_id] [task_id]
Prompt:
Import all discovered sources from research task [task_id] into notebook [notebook_id].Expected: Sources imported successfully.
Test 4.4 - Start Deep Research (Background)
Tool: research_start
CLI: nlm research start "AI ROI return on investment" --mode deep
Prompt:
Start deep web research for "AI ROI return on investment" in notebook [notebook_id].
- mode: deepExpected: Research task started (takes 3-5 minutes).
IMPORTANT: Continue with Test Group 5-7 while deep research runs.
Test Group 5: Studio Creation (Consolidated studio_create)
Test 5.1 - Create Audio Overview
Tool: studio_create
CLI: nlm audio create [notebook_id] --format brief --length short
Prompt:
Create an audio overview for notebook [notebook_id]:
- artifact_type: audio
- format: brief
- length: short
- confirm: False (show settings first)Follow-up:
Confirmed. Create with confirm=True.Expected: Audio generation started with artifact_id.
Test 5.1b - Create Audio with Custom Prompt (Verify Extraction)
Tool: studio_create
CLI: nlm audio create [notebook_id] --focus "Explain this to a 5 year old." --confirm
Prompt:
Create an audio overview for notebook [notebook_id] with a focus prompt:
- artifact_type: audio
- focus_prompt: "Explain this to a 5 year old."
- confirm: TrueExpected: Audio generation started. We will verify the prompt "Explain this to a 5 year old" appears in Test 5.10.
Test 5.2 - Create Video Overview
Tool: studio_create
CLI: nlm video create [notebook_id] --format brief --style classic --confirm
Prompt:
Create a video overview for notebook [notebook_id]:
- artifact_type: video
- format: brief
- visual_style: classic
- confirm: TrueExpected: Video generation started.
Test 5.3 - Create Report
Tool: studio_create
CLI: nlm report create [notebook_id] --format "Briefing Doc" --confirm
Prompt:
Create a report for notebook [notebook_id]:
- artifact_type: report
- report_format: Briefing Doc
- confirm: TrueExpected: Report generation started.
Test 5.3b - Create Interactive Report
Tool: studio_create
CLI: nlm report create [notebook_id] --format Interactive --template learning_overview --prompt "Summarize the key ideas with a quiz" --confirm
Prompt:
Create an interactive report for notebook [notebook_id]:
- artifact_type: report
- report_format: Interactive
- report_template: learning_overview
- custom_prompt: Summarize the key ideas with a quiz
- confirm: TrueExpected: Interactive report generation starts (status "queued" -> "in_progress"
-> "completed" in studio_status). The artifact type shows as interactive_report.
Test 5.3c - Read Interactive Report
Tool: report (action=get)
CLI: nlm report get [notebook_id] [artifact_id] (or --json, --output report.md)
Prompt:
Read interactive report [artifact_id] in notebook [notebook_id]:
- call report(action="get")
- show the first section of the markdown and the element listExpected: Markdown content, prompt and the embedded elements
(mind map / infographic / flashcards / slide deck / quiz) with status
"suggested" before they are generated.
Test 5.3d - List and Generate Report Elements
Tool: report (action=elements, generate)
CLI: nlm report elements [notebook_id] [artifact_id] thennlm report element create [notebook_id] [artifact_id] --type quiz --confirm
Prompt:
List the embedded elements of interactive report [artifact_id], then generate
the quiz element.Expected: Element list shows ids/types/statuses; generating one flips its
status from "suggested" to generation states and the element also appears in the
notebook Studio panel.
Test 5.3e - Report Element Settings Sweep
Tool: report (action=generate, one-item plan)
CLI: nlm report element create [notebook_id] [artifact_id] --type quiz --setting difficulty=hard --setting question_amount=more --confirm
Prompt:
Generate an interactive report quiz element with settings difficulty=hard and question_amount=more.Expected: Generation starts with custom settings passed into generation options (difficulty=3, question_amount=3).
Test 5.3f - Batch Element Plan Validation (Invalid Item Stops All)
Tool: report (action=generate)
CLI: nlm report element create-batch [notebook_id] [artifact_id] --plan invalid_plan.json --confirm
Prompt:
Validate and attempt to run a batch plan containing 2 valid element IDs and 1 non-existent or invalid element ID:
[
{"element_id": "valid-id-1"},
{"element_id": "invalid-id-xyz"}
]Expected: Validation fails before any RPC mutation occurs; no elements change status or start generation.
Test 5.3g - Batch Generation Stops on Quota Exhaustion
Tool: report (action=generate)
CLI: nlm report element create-batch [notebook_id] [artifact_id] --plan plan.json --confirm
Prompt:
Run a multi-element batch plan when quota is exhausted.Expected: Generation terminates immediately when encountering RESOURCE_EXHAUSTED; result reports stopped_reason="quota" and remaining planned items stay not_started.
Test 5.3h - Element Bounded Waiting and Timeout
Tool: report (action=elements)
CLI: nlm report elements [notebook_id] [artifact_id] --wait [element_id] --timeout 10
Prompt:
Wait on an in-progress element for up to 10 seconds.Expected: Returns status before timeout if completed/failed, or returns timed_out: true with current status if timeout elapses without raising an error.
Test 5.3i - Inline Content for Element Review
Tool: report (action=elements)
CLI: nlm report elements [notebook_id] [artifact_id] --content
Prompt:
List report elements with include_content=True to review completed quiz, flashcard, or mind map elements.Expected: Returns inline structured content for review with label "Checked against the plan and the report section, not against the original sources."
Test 5.4 - Create Flashcards
Tool: studio_create
CLI: nlm flashcards create [notebook_id] --difficulty medium --focus "Focus on definitions" --confirm
Prompt:
Create flashcards for notebook [notebook_id]:
- artifact_type: flashcards
- difficulty: medium
- confirm: TrueExpected: Flashcards generation started.
Test 5.5 - Create Quiz
Tool: studio_create
CLI: nlm quiz create [notebook_id] --count 2 --difficulty 2 --focus "Focus on integration testing" --confirm
Prompt:
Create a quiz for notebook [notebook_id]:
- artifact_type: quiz
- question_count: 2
- difficulty: medium
- confirm: TrueExpected: Quiz generation started.
Test 5.6 - Create Infographic
Tool: studio_create
CLI: nlm infographic create [notebook_id] --orientation landscape --detail standard --style professional --confirm
Prompt:
Create an infographic for notebook [notebook_id]:
- artifact_type: infographic
- orientation: landscape
- detail_level: standard
- infographic_style: professional
- confirm: TrueExpected: Infographic generation started with professional visual style.
Visual Styles Available: auto_select, sketch_note, professional, bento_grid, editorial, instructional, bricks, clay, anime, kawaii, scientific
Test 5.7 - Create Slide Deck
Tool: studio_create
CLI: nlm slides create [notebook_id] --format detailed_deck --length short --confirm
Prompt:
Create a slide deck for notebook [notebook_id]:
- artifact_type: slide_deck
- format: detailed_deck
- length: short
- confirm: TrueExpected: Slide deck generation started.
Test 5.8 - Create Mind Map
Tool: studio_create
CLI: nlm mindmap create [notebook_id] --title "AI Concepts" --confirm
Prompt:
Create a mind map for notebook [notebook_id]:
- artifact_type: mind_map
- title: "AI Concepts"
- confirm: TrueExpected: Mind map created immediately.
Test 5.9 - Create Data Table
Tool: studio_create
CLI: nlm data-table create [notebook_id] "Key features and capabilities" --confirm
Prompt:
Create a data table for notebook [notebook_id]:
- artifact_type: data_table
- description: "Key features and capabilities"
- confirm: TrueExpected: Data table generation started.
Test 5.10 - Check Studio Status
Tool: studio_status
CLI: nlm studio status [notebook_id]
Prompt:
Check studio content generation status for notebook [notebook_id].Expected:
- Default MCP response contains at most 20 artifacts and only lean status fields.
paginationcontainsreturned,offset,limit, andhas_more.- Polling with
artifact_idreturns only the selected artifact. - Calling with
include_details=Trueexposes rich fields and the artifact from
Test 5.1b showscustom_instructions: "Explain this to a 5 year old". nlm studio status [notebook_id] --jsonremains a plain list containing bothidandartifact_id.nlm studio status [notebook_id] --json --mcp-compatiblereturns the MCP envelope.nlm video list [notebook_id] --jsonreturns only video artifacts.
Test 5.11 - Rename Studio Artifact
Tool: studio_status (with action="rename")
CLI: nlm studio rename [artifact_id] "My Renamed Podcast"
Prompt:
Rename artifact [artifact_id] in notebook [notebook_id] to "My Renamed Podcast".
- action: rename
- artifact_id: [artifact_id from studio_status]
- new_title: "My Renamed Podcast"Expected: Artifact renamed successfully.
Test 5.12 - Revise Slide Deck
Tool: studio_revise
CLI: nlm slides revise <artifact_id> --slide '1 Make the title larger' --confirm
Prompt:
Revise the slide deck artifact [artifact_id] in notebook [notebook_id]:
- slide_instructions: [{"slide": 1, "instruction": "Make the title larger and bolder"}]
- confirm: trueExpected:
- New artifact created with status "in_progress"
- New artifact_id returned (different from original)
- Original artifact unchanged
- Verify: Run
studio_status— new deck appears with "(2)" suffix in title.
Test Group 6: Downloads (Consolidated download_artifact)
Test 6.1 - Download Report
Tool: download_artifact
CLI (Verb): nlm download report [notebook_id] --output /tmp/report.md
Prompt:
Download the report from notebook [notebook_id]:
- artifact_type: report
- output_path: /tmp/report.mdExpected: Report downloaded as markdown.
Test 6.2 - Download Flashcards (JSON)
Tool: download_artifact
CLI: nlm download flashcards [notebook_id] --format json --output /tmp/flashcards.json
Prompt:
Download flashcards from notebook [notebook_id]:
- artifact_type: flashcards
- output_path: /tmp/flashcards.json
- output_format: jsonExpected: Flashcards downloaded as JSON.
Test 6.3 - Download Quiz (Markdown)
Tool: download_artifact
CLI: nlm download quiz [notebook_id] --format markdown --output /tmp/quiz.md
Prompt:
Download quiz from notebook [notebook_id]:
- artifact_type: quiz
- output_path: /tmp/quiz.md
- output_format: markdownExpected: Quiz downloaded as markdown.
Test 6.4 - Download Audio
Tool: download_artifact
CLI: nlm download audio [notebook_id] --output /tmp/podcast.m4a
Prompt:
Download audio from notebook [notebook_id]:
- artifact_type: audio
- output_path: /tmp/podcast.m4aExpected: Audio downloaded as M4A/MP4.
Test 6.5 - Download Slide Deck
Tool: download_artifact
CLI: nlm download slide-deck [notebook_id] --output /tmp/slides.pdf
Prompt:
Download slide deck from notebook [notebook_id]:
- artifact_type: slide_deck
- output_path: /tmp/slides.pdfExpected: Slides downloaded as PDF.
Test 6.6 - Download Data Table Excel Export
Tool: download_artifact
Prompt:
Download the Excel data-table export from notebook [notebook_id]:
- artifact_type: data_table_xlsx
- output_path: /tmp/data-table.xlsxExpected: The completed type-10 artifact is downloaded byte-for-byte as XLSX.
Test 6.7 - Download All Artifacts
Tool: download_all_artifacts
CLI: nlm download all [notebook_id] --output-dir /tmp/exports
Prompt:
Download all completed artifacts from notebook [notebook_id]:
- output_dir: /tmp/exportsExpected: A subdirectory named after the notebook title created under the
output dir, containing every completed artifact named after its title
(report → .md, mind_map → .json, video → .mp4, slide_deck → .pdf, ...).
In-progress/failed artifacts are listed as skipped; result reports
downloaded/failed/skipped counts. Also test --types video,report filtering.
Test 6.7 - Sweep All Notebooks (Incremental)
Tool: download_all_artifacts with all_notebooks=True, skip_existing=True
CLI: nlm download all --all-notebooks --skip-existing --output-dir /tmp/exports
Prompt:
Download all artifacts from every notebook into /tmp/exports, skipping
files that already exist.Expected: One subdirectory per notebook. On a second run withskip_existing, previously downloaded artifacts are reported as skipped
("already downloaded") and only new artifacts are fetched. A failure on one
notebook doesn't stop the sweep; result includes per-notebook counts anderrored_notebooks.
Test Group 7: Sharing
Test 7.1 - Get Share Status
Tool: notebook_share_status
CLI (Noun): nlm share status [notebook_id]
Prompt:
Get sharing status for notebook [notebook_id].Expected: Status with is_public, access_level, collaborators.
Test 7.2 - Enable Public Link
Tool: notebook_share_public
CLI: nlm share public [notebook_id]
Prompt:
Enable public link for notebook [notebook_id].
- is_public: TrueExpected: Public link returned.
Test 7.3 - Disable Public Link
Tool: notebook_share_public
CLI: nlm share private [notebook_id]
Prompt:
Disable public link for notebook [notebook_id].
- is_public: FalseExpected: Public link disabled.
Test 7.4 - Invite Collaborator (Optional)
Tool: notebook_share_invite
CLI: nlm share invite [notebook_id] [email protected] --role viewer
Prompt:
Invite collaborator to notebook [notebook_id]:
- email: [email protected]
- role: viewerExpected: Invitation sent (or error if email invalid).
Test Group 8: Drive Sync (Optional)
Test 8.1 - Sync Drive Sources
Tool: source_sync_drive
CLI (Verb): nlm source sync [notebook_id] --confirm
Prompt:
Check if any Drive sources in notebook [notebook_id] are stale using source_list_drive.
If any are stale, sync them using source_sync_drive with confirm=True.Expected: Sources synced if any were stale.
Test Group 9: Deep Research Verification
TIMING: By now, deep research from Test 4.4 should be complete.
Test 9.1 - Check Deep Research Status
Tool: research_status
CLI (Noun): nlm research status [notebook_id]
Prompt:
Check deep research status for notebook [notebook_id] with max_wait=60.Expected:
status: completedmode: deepsource_count: ~40-50 sourcesreportfield present
Test 9.2 - Import Deep Research Sources
Tool: research_import
CLI (Noun): nlm research import [notebook_id] [task_id]
Prompt:
Import all deep research sources from task [task_id] into notebook [notebook_id].Expected: Sources imported successfully.
Test Group 10: Cleanup
Test 10.1 - Delete Studio Artifacts
Tool: studio_delete
CLI (Noun): nlm studio delete [notebook_id] [artifact_id] --confirm
Prompt:
Get studio status for notebook [notebook_id], then delete each artifact with confirm=True.Expected: All artifacts deleted.
Test 10.2 - Delete All Sources
Tool: source_delete
CLI (Noun): nlm source delete [source_id] --confirm
Prompt:
List sources in notebook [notebook_id], then delete each with confirm=True.Expected: All sources deleted.
Test 10.3 - Delete Notebook
Tool: notebook_delete
CLI (Noun): nlm notebook delete [notebook_id] --confirm
Prompt:
Delete notebook [notebook_id] with confirm=True.Expected: Notebook deleted successfully.
Test Group 11: Notes Management (Unified `note` tool)
Test 11.1 - Create Note
Tool: note
CLI (Noun): nlm note create [notebook_id] --content "Test" --title "Title"
Prompt:
Create a note in notebook [notebook_id]:
- action: create
- content: "This is a test note about AI"
- title: "AI Note"Expected: Note created with note_id.
Save: Note the note_id for subsequent tests.
Test 11.2 - List Notes
Tool: note
CLI (Noun): nlm note list [notebook_id]
Prompt:
List all notes in notebook [notebook_id]:
- action: listExpected: Array of notes including the one just created, with previews.
Test 11.3 - Update Note
Tool: note
CLI (Noun): nlm note update [notebook_id] [note_id] --content "Updated"
Prompt:
Update note [note_id] in notebook [notebook_id]:
- action: update
- note_id: [note_id]
- content: "Updated: AI is transforming technology"Expected: Success confirmation.
Test 11.4 - Delete Note
Tool: note
CLI (Noun): nlm note delete [notebook_id] [note_id] --confirm
Prompt:
Delete note [note_id] in notebook [notebook_id]:
- action: delete
- note_id: [note_id]
- confirm: TrueExpected: Deletion confirmation message.
Test Group 12: Server Info
Test 12.1 - Get Server Info
Tool: server_info
Prompt:
Get Gemini Notebook MCP server version and check for updates.Expected:
version: Current versionlatest_version: Latest PyPI version (or null if offline)update_available: Booleanupdate_command: Upgrade command
Test Group 13: Plan Usage
Test 13.1 - Get Remaining Usage
Tool: usage_get
CLI (Noun): nlm usage (add --json for raw output)
Prompt:
How much of my Gemini Notebook usage allowance is left?Expected:
windows: two entries,rollingfirst thenweekly- each with
percent_used,percent_remainingandresets_at(ISO 8601 UTC) tier: subscription tier string, e.g.NOTEBOOKLM_TIER_PRO_CONSUMER_USER
Also verify:
- Run it twice. The reported order stays
rolling,weeklyeven though the API
returns the two entries in an unstable order. percent_usedandpercent_remainingsum to 100 for a partly consumed window.- With an expired session it reports an authentication error and hints at
nlm auth refresh. It must NOT report the allowance as exhausted.
Test 13.2 - Check Work and Personal Profiles
Tool: usage_get(profile="work"), then usage_get(profile="personal")
CLI: nlm usage --profile work, then nlm usage -p personal --json
Expected:
- Each call reads the selected saved account's usage, even when
NOTEBOOKLM_COOKIESis set for a different account. - The configured default and the account used by other MCP tools are unchanged.
- Missing profiles return an error without falling back to environment cookies
or the default account. - Omitting the profile keeps the existing environment/default authentication.
Test Group 14: Setup Wizard (CLI only)
Automated: uv run pytest -m wizard_e2e drives the real nlm setup in a
pseudo-terminal against a sandboxed HOME (fake claude/codex/ps/pbcopy/open) and checks the files written. Run it after any change tocli/commands/setup.py, setup_wizard.py, skill.py or cli/skill_package.py.
The manual checks below cover what the sandbox can't: the real claude andcodex CLIs, and the real Claude Desktop upload. Run nlm setup in a real
terminal (not an agent's shell — it refuses non-interactive sessions).
Test 14.1 - Status and Esc
CLI: nlm setup → Show my tools' status
Expected:
- Only installed tools are listed, with ✓ set up / ✗ not yet / ⚠ old name and
the skill version (⬆ when an upgrade exists). - Esc on every screen returns to the main menu within ~0.1s; Esc on the main
menu quits.
Test 14.2 - Connect and rename
CLI: nlm setup → Add the MCP to my tools/agents
Expected:
- Nothing is pre-ticked; already-connected tools are shown but not selectable.
- Tools whose entry is still
notebooklm-mcpappear under Needs a fix;
ticking them reportsrepaired · Renamed to gemini-notebook-mcp. - Verify:
claude mcp listshowsgemini-notebook-mcp ✔ Connected;~/.codex/config.tomlhas[mcp_servers.gemini-notebook-mcp]and kept any
extra keys (e.g.enabled = true); Claude Desktop configs (both profiles if
chosen) containgemini-notebook-mcpwith the full binary path. - With Claude Desktop open, connecting it is refused ("still running").
Test 14.3 - Skill and Claude Desktop upload file
CLI: nlm setup → Add the skill to my tools/agents (or nlm skill package)
Expected:
- No "Add the skill?" question — it opens on "Where should the skill live?".
- The picker includes Claude Desktop / claude.ai · creates a file to upload,
not pre-ticked. Ticking it saves~/Downloads/nlm-skill.zip, reveals it in
Finder, and shows the upload steps last. - Upload via Claude Desktop Customize → Skills → Add: accepted; the skill
appears on the Skills page and loads in Chat and Cowork.
Test 14.4 - Remove and copy setup
CLI: nlm setup → Remove an MCP or skill, then Copy MCP setup for a tool not listed
Expected:
- Remove: grouped MCP connections / Skills, nothing pre-ticked, two separate
default-No confirmations; only ticked items change; backups appear in~/.notebooklm-mcp-cli/backups/. - Copy: the clipboard holds
{"mcpServers": {"gemini-notebook-mcp": {"command": "<full path>/notebooklm-mcp"}}}.
Test 14.5 - Credential protection
CLI: nlm setup → Credential protection
Expected:
- With no saved logins: "No saved logins yet. Run 'nlm login' first."
- With plain logins: lists each as
plainand offers Protect saved logins (recommended); with several logins a checkbox picker appears. - With protected logins: offers Restore protected logins to plain files (asks to confirm, default No); never offers Protect for an already-protected login.
- Mixed: offers both.
- Afterwards
nlm auth storage statusmatches, and no protection tip appears for a protected login.
Test 14.6 - First login on a new profile (new-user flow)
CLI: nlm login on a machine (or sandbox) with no saved logins
Expected:
- Before the browser opens: "Where should your saved login live?" with Protected (recommended) and Plain file.
- Choosing 1 stores the login encrypted (
credentials.enc, nocookies.json) and ends with "✓ Login stored in protected mode." with no further tips or questions. - A profile name protected mode can't use (
my work,[email protected]) offers a working name (my-work) or keeping the name as a plain file. - Ctrl+C or closing the flow leaves no profile folder, no saved answer, and closes the Chrome window it opened.
Summary: 31 Consolidated Tools
| Category | Tools | Count |
|---|---|---|
| Auth | refresh_auth, save_auth_tokens |
2 |
| Notebooks | notebook_list, notebook_get, notebook_describe, notebook_create, notebook_rename, notebook_delete |
6 |
| Sources | source_add, source_list_drive, source_sync_drive, source_delete, source_describe, source_get_content |
6 |
| Sharing | notebook_share_status, notebook_share_public, notebook_share_invite |
3 |
| Research | research_start, research_status, research_import |
3 |
| Studio | studio_create, studio_status, studio_delete, studio_revise |
4 |
| Downloads | download_artifact, download_all_artifacts |
2 |
| Exports | export_artifact |
1 |
| Chat | notebook_query, chat_configure |
2 |
| Notes | note (unified: list, create, update, delete) |
1 |
| Server | server_info |
1 |
| Total | 31 |
Quick Copy-Paste Test Prompts
/* Detailed source-code truncated for AI context efficiency. */