notebooklm-mcp-cli Documentation

Programmatic access to Gemini Notebook - via command-line interface (CLI), Model Context Protocol (MCP) server, and AI agent skills.

5,786 stars Python Python 1 file · ~57,884 tokens
RAW Doc

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:

text
nlm 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.com host has been live-verified with a project-qualified global deployment; 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:

  1. Install (gives you nlm and the MCP server):
bash
uv tool install notebooklm-mcp-cli
  1. Sign in to your Google account:
bash
nlm login
  1. Connect your AI tools:
bash
nlm setup

The 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.zip and shows where to upload it (Customize → Skills → Add). Or run nlm 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:

bash
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 link

Run nlm --ai for comprehensive AI-assistant documentation.

🤖 MCP Server (for AI Agents)

Connect AI assistants (Claude, Gemini, Cursor, etc.) to Gemini Notebook:

bash
nlm setup                       # Guided wizard — see Quick Start above
nlm setup add codex             # Or connect one client directly

Then 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 (.mcpb file) → double-click → done! One-click install, no config needed.

Install from PyPI. This single package includes both the CLI and MCP server:

bash
uv tool install notebooklm-mcp-cli

Using uvx (Run Without Install)

bash
uvx --from notebooklm-mcp-cli nlm --help
uvx --from notebooklm-mcp-cli notebooklm-mcp

Using pip

bash
pip install notebooklm-mcp-cli

Using pipx

bash
pipx install notebooklm-mcp-cli

After installation, you get:

  • nlm — Command-line interface
  • notebooklm-mcp — Gemini Notebook MCP server for AI assistants

Alternative: Install from Source

bash
# 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

bash
# Using uv
uv tool upgrade notebooklm-mcp-cli

# Using pip
pip install --upgrade notebooklm-mcp-cli

# Using pipx
pipx upgrade notebooklm-mcp-cli

After upgrading, restart your AI tool to reconnect to the updated MCP server:

  • Claude Code: Restart the application, or use /mcp to 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

bash
uv tool list | grep notebooklm

Legacy 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

bash
# Remove old CLI package (if installed)
uv tool uninstall notebooklm-cli

# Remove old MCP package (if installed)
uv tool uninstall notebooklm-mcp-server

Step 3: Reinstall the Unified Package

After removing legacy packages, reinstall to fix symlinks:

bash
uv tool install --force notebooklm-mcp-cli

Why --force? When multiple packages provide the same executable, uv can leave broken symlinks after uninstalling. The --force flag ensures clean symlinks.

Step 4: Verify Installation

bash
uv tool list | grep notebooklm

You should see only:

text
notebooklm-mcp-cli v0.2.0
- nlm
- notebooklm-mcp

Step 5: Re-authenticate

Your existing cookies should still work, but if you encounter auth issues:

bash
nlm login

Note: The configured MCP server name is now gemini-notebook-mcp. The executable remains notebooklm-mcp for 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:

bash
# 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-cli

Also remove from your AI tools:

bash
nlm setup remove claude-code
nlm setup remove cursor
# ... or any configured tool

Authentication

Before using the CLI or MCP, you need to authenticate with Gemini Notebook:

bash
# 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:18800

New 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:

bash
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 profile

From 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:

bash
nlm auth refresh                     # Headless, no interaction; exits non-zero on failure
nlm auth refresh --profile work      # Refresh a named profile

Run it on a timer (cron/launchd). It needs a saved Chrome profile from a prior
nlm 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.

  1. Set Base URL, GCP Project ID & Location:

    bash
    export 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"
  2. Save an Enterprise Auth Profile:
    This opens the configured Enterprise URL in an isolated browser profile. Sign in with your organization account:

    bash
    nlm login --profile enterprise
    nlm login switch enterprise

    You can switch back to a personal profile at any time:

    bash
    unset NOTEBOOKLM_BASE_URL NOTEBOOKLM_PROJECT_ID NOTEBOOKLM_LOCATION
    nlm login switch personal
  3. Configure MCP Server with Enterprise Environment Variables:
    When adding the MCP server to AI assistants (Claude, Cursor, etc.), pass NOTEBOOKLM_BASE_URL, NOTEBOOKLM_PROJECT_ID, and optionally NOTEBOOKLM_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):

bash
nlm login              # Auto mode (launches browser)
nlm login --manual     # Manual file mode

How 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-mcp to toggle. To keep it on but expose only a subset, see Selective tool exposure.

The setup wizard is the recommended way to connect tools — see
Quick Start for what each menu option does:

bash
nlm setup

MCP 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:

bash
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 status

Claude 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 example
nlm 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.

bash
# Install skill files
nlm skill install cline
nlm skill install openclaw
nlm skill install codex
nlm skill install antigravity

# Update skills
nlm skill update

User-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:

bash
nlm skill package                  # saves ~/Downloads/nlm-skill.zip
nlm skill package --output ~/Desktop

Then in Claude Desktop (or claude.ai) open Customize → Skills → Add, pick
nlm-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

bash
nlm setup remove claude-code

Using uvx (No Install Required)

If you don't want to install the package, you can use uvx to run on-the-fly:

bash
# Run CLI commands directly
uvx --from notebooklm-mcp-cli nlm setup add cursor
uvx --from notebooklm-mcp-cli nlm login

For tools that use JSON config, point them to uvx:

json
{
  "mcpServers": {
    "gemini-notebook-mcp": {
      "command": "uvx",
      "args": ["--from", "notebooklm-mcp-cli", "notebooklm-mcp"]
    }
  }
}

Manual Setup (if you prefer)

Tip: Run nlm setup add json for an interactive wizard that generates the right JSON snippet for your tool.

Claude Code / Gemini CLI support adding MCP servers via their own CLI:

bash
claude mcp add --scope user gemini-notebook-mcp notebooklm-mcp
gemini mcp add --scope user gemini-notebook-mcp notebooklm-mcp

Cursor / Windsurf resolve commands from your PATH, so the command name is enough:

json
{
  "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:

json
{
  "servers": {
    "gemini-notebook-mcp": {
      "command": "notebooklm-mcp",
      "args": []
    }
  }
}

Claude Desktop may not resolve PATH — use the full path to the binary:

json
{
  "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:

  1. Refreshes CSRF tokens immediately when expired
  2. Reloads cookies from disk if updated externally
  3. 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-cli installs an older version (e.g., 0.1.5 instead of 0.1.9)
  • uv cache clean doesn'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:

bash
uv tool install --force notebooklm-mcp-cli

This bypasses any cached constraints and installs the absolute latest version from PyPI.

Verify:

bash
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 usage to 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_content tool 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 setup and nlm doctor commands 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_HL environment variable (PR #59).
  • JumpLao (@JumpLao) for extended audio, video, and image format support (PR #82).
  • cbruyndoncx (@cbruyndoncx) for including cited_text passages in query output (PR #81).
  • zxyasfas (@zxyasfas) for cited-only research import (PR #188).
  • Serdar Akın (@SERDAR-AKIN) for the multi-probe AuthHealthChecker that fixes false "stale" reports for semi-stale cookies (PR #219).

Star History

License

MIT 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-replay to compare:
    • saved-cookie replay through normal httpx
    • httpx replay after a forced Google RotateCookies call
    • optional in-page CDP fetch from the saved Chrome profile
  • Added verdicts that distinguish ordinary cookie freshness failures from likely browser-bound replay behavior.
  • Added best-effort Google RotateCookies support during auth token refresh.

Fixed

  • Matched the auth replay diagnostic HTTP probes to the normal Gemini Notebook RPC headers to avoid false browser_bound_replay verdicts from bare httpx requests.
  • Confirmed nlm login --check homepage login redirects with a real Gemini Notebook RPC before reporting cookies as expired. Thanks to laofun for 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.com cookie values when flattening duplicate browser cookies. Thanks to laofun for 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 -q
  • uv run pytest -q
  • uv cache clean && uv tool install --force .
  • Live smoke test:
    • nlm login --check
    • nlm notebook list --title
    • nlm 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=cdp to route normal batchexecute RPCs through
    an in-page fetch inside 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 httpx transport unchanged. The CDP path is off by default.

Notes

Use nlm doctor auth-replay first. Only try
NOTEBOOKLM_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 -q
  • uv cache clean && uv tool install --force .
  • Live smoke test:
    • nlm login --check
    • nlm notebook list --title
    • nlm doctor auth-replay --timeout 20
    • NOTEBOOKLM_RPC_TRANSPORT=cdp nlm notebook list --title
    • NOTEBOOKLM_RPC_TRANSPORT=cdp nlm query notebook <notebook-id> "Answer in one short sentence: what is this notebook about?" --json
    • MCP stdio notebook_list with NOTEBOOKLM_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

python
notebooks = notebook_list()

Create and Query

python
# 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 poll
notebook_query_status until it completes. A deadline error includes a retry
hint and the query_deadline_exceeded debug code.

Configure Chat Settings

python
# 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

python
# 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

python
# 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

python
# 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

python
# 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

python
# Delete a source from notebook (after user confirmation)
source_delete(source_id="source-uuid", confirm=True)

Research and Import Sources

python
# 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 seconds
  • deep + web: Extended research with AI report, ~40 sources in 3-5 minutes
  • fast + drive: Quick Google Drive search, ~10 sources in ~30 seconds

Generate Audio/Video Overviews

python
# 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

text
POST https://notebook.google.com/_/LabsTailwindUi/data/batchexecute

Gemini 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:

text
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:

json
["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}:

json
[
  [[["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

text
Content-Type: application/x-www-form-urlencoded

f.req=&at=<CSRF token>

The f.req structure:

json
[[["<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 — an
x-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

text
)]}'

<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.

python
# 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 info

Configure Chat Settings

Configures the notebook's chat behavior - goal/style and response length.

python
# 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

python
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

python
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

python
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

python
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 Docs
  • application/vnd.google-apps.presentation - Google Slides
  • application/vnd.google-apps.spreadsheet - Google Sheets
  • application/pdf - PDF files

Query Endpoint (Streaming)

Queries use a different endpoint - NOT batchexecute!

text
POST /_/LabsTailwindUi/data/google.internal.labs.tailwind.orchestration.v1.LabsTailwindOrchestrationService/GenerateFreeFormStreamed

Query Request Structure

python
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:

  1. Thinking steps - "Understanding...", "Exploring...", etc.
  2. Final answer - Markdown formatted with citations
  3. 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 what
nlm chats get / chat_get / chat_export use to show real transcripts even on a
fresh CLI invocation or MCP server session (get_conversation_turns() in
core/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

python
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

text
[[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] where
    content[0] = [answer_text, None, [conv_id, conv_id, num]] — the answer text is
    nested one level inside content[0] (content[0][0]), not content[0] directly.
    The remaining content entries (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.

python
# 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.

python
# 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.

python
# 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.

python
# 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

text
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 them

Important 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

python
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

python
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 omits
visual_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 to
focus_prompt for non-English Short requests:

python
[
    [[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

python
# Returns: [[artifact_id, title, type, sources, status, ...]]
# status: 1 = in_progress, 3 = completed

`gArtLc` - Poll Studio Status

Polls for audio/video generation status.

python
# 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 = flashcards
  • 2 = quiz
  • 4 = 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.

python
# Request
params = [[2], "artifact_id"]

# Response
[]  # Empty array on success

WARNING: This action is IRREVERSIBLE. The artifact is permanently deleted.

`rc3d8d` - Rename Studio Artifact

Renames a studio artifact (audio, video, report, etc.).

python
# Request
params = [["artifact_id", "New Title"], [["title"]]]

# Response
# Returns the updated artifact data on success

Note: 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

python
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 UUID
  • result[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

python
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

python
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

text
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 content

Report RPCs

Reports use the same R7cb6c RPC with type code 2 (STUDIO_TYPE_REPORT).

Report Request Structure

python
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.

python
# 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.

python
# 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.

python
# 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)

python
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:

  1. rc3d8d - update artifact fields via field mask:
python
[
    [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]]],
]
  1. 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:
python
[[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
Rytqqe kickoff 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 with INVALID_ARGUMENT. The [2, null, [1], [..., [1, 3]]]
value previously listed as the mind-map kickoff config is the page's
x-goog-ext-525006520-jspb request 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.

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

python
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

python
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 at
artifact[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

python
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.

python
# 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.

python
# 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.

python
# 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

json
{
  "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

text
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 maps

Notes 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.

python
# 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.

python
# 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:

python
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.

python
# Request params
params = [
    notebook_id,
    note_id,
    [[[
        "Updated note content",
        "Updated Title",
        [],  # Unknown field
        0    # Unknown field
    ]]],
]

# Response
null  # Null on success

Important: 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.

python
# Request params
params = [
    notebook_id,
    None,
    [note_id]  # Can delete multiple: [note_id1, note_id2, ...]
]

# Response
null  # Null on success

Note: This is a soft-delete - the item appears in listings with status = 2 or data = None.

Notes Workflow

text
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 null

Studio 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

  1. Filtering is client-side: The wXbhsf RPC returns ALL notebooks. "My notebooks" vs "Shared with me" filtering happens in the browser.

  2. Unified source RPC: All source types (URL, text, Drive) use the same izAoDd RPC with different param structures.

  3. Query is streaming: The query endpoint streams the AI's thinking process before the final answer.

  4. Conversation support: Pass a conversation_id for multi-turn conversations (follow-up questions).

  5. Rate limits: Free tier has ~50 queries/day limit.

  6. Research uses same RPC for Web and Drive: The Ljjv0c RPC handles both Web (source_type=1) and Drive (source_type=2) Fast Research. Only the source_type parameter differs.

  7. Deep Research is Web-only: The QA9ei RPC 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:

python
[
  [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 leave
metadata[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 at
metadata[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

  1. Used Chrome DevTools MCP to automate browser interactions
  2. Captured network requests during each action
  3. Decoded f.req body (URL-encoded JSON)
  4. Analyzed response structures
  5. Tested parameter variations

Discovery Session Examples

Creating a notebook:

  1. Clicked "Create notebook" button via Chrome DevTools
  2. Captured POST to batchexecute with rpcids=CCqFvf
  3. Decoded params: ["", null, null, [2], [1,null,...,[1]]]
  4. Response contained new notebook UUID at index 2

Adding Drive source:

  1. Opened Add source > Drive picker
  2. Double-clicked on a document
  3. Captured POST with rpcids=izAoDd
  4. Decoded: [[[[doc_id, mime_type, 1, title], null,...,1]]]
  5. Different from URL/text which use different array positions

Querying:

  1. Typed question in query box, clicked Submit
  2. Found NEW endpoint: GenerateFreeFormStreamed (not batchexecute!)
  3. Streaming response with thinking steps + final answer
  4. 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:

  1. From network request (fastest):

    • Extracted directly from get_network_request() data
    • No page fetch required
    • Saves to cache for reuse
  2. From page fetch (slower first time):

    • Client fetches notebook.google.com using cookies
    • Extracts SNlM0e (CSRF) and FdrFJe (session ID) from HTML
    • Saves to cache for reuse
    • ~1-2 seconds one-time delay
  3. 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. Unrelated
at= 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)

python
# 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)

python
# 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 request

Reorganize — unlabeled sources only

python
# 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 dialog

Create a new empty label

python
# 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

python
# 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

python
# 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).

python
# 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.

python
# 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.

python
# 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:

  1. le8sX — Move any sources that would become orphaned to another label
  2. GyzE7e — Delete the now-empty label
python
# 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=/):

python
# 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):

python
# 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):

python
[null, [1, null, null, null, null, null, null, null, null, null, [1]]]

Captured response (decoded, 2026-04-27):

python
[[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):

python
# 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):

python
[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 activity

Each 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 passing
0, 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

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

bash
# 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 at
notebook.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

  1. The first available supported browser is detected (or your preferred browser if configured)
  2. A dedicated browser profile is created for authentication
  3. The browser launches with the appropriate automation backend
  4. You log in to Gemini Notebook via the browser
  5. Signed-in cookies and the CSRF token are extracted and cached; session fields are refreshed automatically when needed
  6. 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:

bash
nlm login --profile work
nlm login --check --profile work

In 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:

bash
# 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:

bash
# 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-profile

Switching 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:

bash
nlm auth refresh                 # Refresh the default profile
nlm auth refresh --profile work  # Refresh a named profile

nlm 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):

bash
*/30 * * * * /path/to/nlm auth refresh >/dev/null 2>&1

Note: This needs a saved Chrome profile (from a prior nlm login). It
does not help when NOTEBOOKLM_COOKIES is set as an environment variable —
that value overrides saved credentials, so update it directly instead.

In v0.15.4 and later, nlm auth refresh provides 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). Set NOTEBOOKLM_DISABLE_HEADLESS_REFRESH=1 to turn off both the
automatic self-heal and nlm auth refresh on 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:

bash
# 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 profile

All 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:

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"
        }
    }
  }
}

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:

  1. NOTEBOOKLM_BASE_URL env var, if set (see Enterprise section above).
  2. The host your account last signed in on (auto-detected).
  3. 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

bash
# 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.txt

File 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:

bash
NOTEBOOKLM_BASE_URL=https://notebook.google.com nlm login --manual --file /path/to/cookies.txt

How to Extract Cookies Manually

  1. Open Chrome and go to https://notebook.google.com
  2. Make sure you're logged in
  3. Press F12 (or Cmd+Option+I on Mac) to open DevTools
  4. Click the Network tab
  5. In the filter box, type: batchexecute
  6. Click on any notebook to trigger a request
  7. Click on a batchexecute request in the list
  8. In the right panel, scroll to Request Headers
  9. Find the line starting with cookie:
  10. Right-click the cookie value and select Copy value
  11. Paste into a text file and save

The cookie file should contain the raw cookie string from Chrome DevTools:

text
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.txt is 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:

text
nlm auth storage set protected

Optional: 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:

text
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 computer

Protected 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:

bash
# 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 --json

Conflict 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:

bash
# 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 --yes
  • resolve 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:

bash
nlm auth storage relocate

Why 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 of notebooklm-mcp-cli do not understand credentials.enc. Before downgrading or installing an older version of the CLI, convert every protected profile back to file mode:

text
nlm 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:

bash
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:

  1. Desktop extension users: the Claude Desktop extension uses python3 from PATH only to run a small launcher (run_server.py), which then starts the server with uvx --from notebooklm-mcp-cli notebooklm-mcp. uvx runs the server in its own environment, separate from uv 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 the uv tool install of nlm), macOS shows the Keychain access prompt once when the extension first reads it. Click Always Allow (not "Allow").
  2. Python upgrades: If you upgrade Python (such as when Homebrew or uv installs 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.
  3. 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), manual cookies.txt exports left on disk, NOTEBOOKLM_COOKIES environment variables in shell configs, legacy ~/.notebooklm-mcp/auth.json files, 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/:

text
~/.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 or auth.json files exist on disk.
  • In File mode, credentials live in cookies.json (and the configured default profile is mirrored to root auth.json for backwards compatibility with external scripts), secured with 0600 permissions.
  • In both modes, metadata.json stores 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:

bash
claude mcp add gemini-notebook-mcp -- notebooklm-mcp

Gemini CLI:

bash
gemini mcp add gemini-notebook-mcp notebooklm-mcp

Manual (settings.json):

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 five
auth_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_info result is cached for 30 seconds
(the checker's CACHE_TTL) and bypassed on the next call if any auth
file on disk is rewritten, so an external nlm login is reflected
without waiting for the TTL. nlm login --check is 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 see auth_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:

bash
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:

bash
nlm login --manual

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:

python
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):

  1. Update the cookie value in your MCP config file with fresh cookies, then restart your AI tool (Claude Desktop, etc.)
  2. Remove the NOTEBOOKLM_COOKIES env var from your config entirely and use nlm login instead (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 (verdict
stale_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 is
browser_bound_replay:

bash
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:

json
{
  "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, run
nlm 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:

  1. Using dedicated profile directories (~/.notebooklm-mcp-cli/chrome-profiles/<name>/)
  2. 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.json files or commit them to version control
  • The cookies.txt file 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

bash
# Using uv (recommended)
uv tool install notebooklm-mcp-cli

# Using pip
pip install notebooklm-mcp-cli

Authentication

bash
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:18800

For 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):

bash
# 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 relocate

Command Structure

The CLI supports two styles - use whichever feels natural:

bash
# 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

bash
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 question

Sources

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.

bash
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 result

Studio Content Creation

text
/* 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

bash
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-existing

Audio 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:

bash
nlm download audio <notebook> --id <artifact-id> --output raw.m4a
ffmpeg -i raw.m4a -acodec libmp3lame -q:a 2 podcast.mp3

Research

bash
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 sources

Studio Status

bash
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 artifact

The 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

bash
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 editor

Batch Operations

bash
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 notebooks

Cross-Notebook Query

bash
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 notebooks

Pipelines

bash
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 + flashcards

Built-in pipelines: ingest-and-podcast, research-and-report, multi-format

Create custom pipelines: add YAML files to ~/.notebooklm-mcp-cli/pipelines/

Tag & Smart Select

bash
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 match

Chat Configuration

bash
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

bash
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 change
auth.default_profile. An explicit profile takes priority over
NOTEBOOKLM_COOKIES. Without --profile, environment cookies still take
priority over the configured default account.

Configuration

bash
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 format

Available 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)

bash
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 myproject

Skills (AI Assistant Integration)

bash
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 skills

Gemini CLI, Codex, and other agent CLIs all install to ~/.agents/skills/nlm-skill/ (.agents/skills/ at project level). These three targets are interchangeable aliases:

bash
nlm skill install agents      # Generic name
nlm skill install codex       # Alias for OpenAI Codex CLI
nlm skill install gemini-cli  # Alias for Google Gemini CLI

Alef 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/.

bash
nlm skill install alef-agent

Supported 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 writes
nlm-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:

bash
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 status

Claude 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 workspace mcp.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 setup configures the MCP server transport. Use nlm skill install to install skill/reference docs for AI tools that don't use MCP.

Doctor (Diagnostics)

Run diagnostics to troubleshoot installation, authentication, and configuration issues:

bash
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

bash
# 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.m4a

Tips

  • Saved cookies often remain usable for weeks. Run nlm login for confirmed stale or missing credentials.
  • Use --confirm for all create/delete commands in scripts
  • Use --wait when 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 protected to skip the question in scripts
  • Run nlm login profile list to see all profiles with their associated email addresses
  • Run nlm doctor to 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:

bash
# 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:

python
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:

  1. Register source intent (RPC: o4cbdc) → get SOURCE_ID
  2. Start upload session (POST to /upload/_/) → get upload URL
  3. Stream file content → upload URL (memory-efficient streaming)

Code Structure

  • SourceMixin._register_file_source() - Step 1: Register and get SOURCE_ID
  • SourceMixin._start_resumable_upload() - Step 2: Get upload URL
  • SourceMixin._upload_file_streaming() - Step 3: Stream file content
  • SourceMixin.add_file() - Public API that orchestrates all 3 steps

Usage

Python:

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:

bash
nlm source add <notebook-id> --file document.pdf

MCP:

python
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

bash
# 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:

  1. Install — uv tool install notebooklm-mcp-cli. This gives you both
    the nlm CLI and the notebooklm-mcp server binary. See the
    README → Installation section for
    alternatives (uvx, pip, pipx, source install).
  2. 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 login asks 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 with nlm setup → Credential protection or nlm auth storage set protected. See Protected Storage.

  3. Connect an agent — run the setup wizard and choose your app:
    bash
    nlm setup
    Choose Add the MCP to my tools/agents, tick your apps with Space, and
    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).
  4. Verify — restart your agent and call notebook_list (MCP) or
    nlm notebook list (CLI). If you see your existing notebooks, you are
    good to go.

For deeper coverage, jump to the relevant guide:


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 to
notebooklm-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

bash
uv tool install notebooklm-mcp-cli

This installs both nlm and the notebooklm-mcp server binary.

2. Authenticate once

bash
nlm login

Your Google cookies are extracted from a managed browser session and
cached in ~/.notebooklm-mcp-cli/profiles/default/auth.json. The
nlm 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:

bash
nlm setup

The 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:

bash
# 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 3p

For any other MCP client, generate a config snippet:

bash
nlm setup add json

Recommended server name: gemini-notebook-mcp. The executable remains
notebooklm-mcp. Avoid
generic names like notebooklm if 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:

bash
# Claude Code
claude mcp remove notebooklm          # (or whatever the old name was)

# Gemini CLI
gemini mcp remove notebooklm

# Hermes / others: edit the client config directly

Only removing a different Gemini Notebook server needs this step. If the old
entry is this project's own pre-rebrand name (notebooklm-mcp or
notebooklm), nlm setup shows it as ⚠ old name and renames it to
gemini-notebook-mcp for you: Add the MCP to my tools/agents → Needs a fix.

If you are not sure what is registered, list everything:

bash
claude mcp list                       # Claude Code
gemini mcp list                       # Gemini CLI

5. 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_status says "stale" — re-run nlm login. See
    Understanding auth_status
    for the difference between stale and unverified.
  • Run the full diagnostic — nlm doctor checks 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:

text
boq_labs-tailwind-frontend_20260219.16_p2

Current 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:

bash
export NOTEBOOKLM_BL="boq_labs-tailwind-frontend_YYYYMMDD.XX_pN"

The priority order is: env var > auto-extracted > hardcoded fallback.


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:

bash
nlm login

If 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:

  1. Navigate to notebook.google.com in Chrome
  2. Use Chrome DevTools MCP to extract cookies from any network request
  3. Call save_auth_tokens(cookies=<cookie_header>)

Option C: Manual extraction

  1. Open Chrome DevTools on notebook.google.com
  2. Network tab → find any request → copy Cookie header
  3. Set NOTEBOOKLM_COOKIES environment 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., wXbhsf for 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)
  • None results from previously working operations
  • New error messages from the API

What to do when it breaks

  1. Check if the issue is widespread (Google may have deployed changes)
  2. Use Chrome DevTools to capture current request/response format
  3. Update the relevant RPC handling in api_client.py
  4. 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:

  1. Manually extract tokens from Chrome DevTools Network tab
  2. 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 3p says 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

  1. Fully quit the selected Claude Desktop profile and stop the Relay AI launcher
    if it owns the 3P instance.
  2. Run nlm setup add claude-desktop --profile 3p (or regular).
  3. 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-skill works 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/, where
nlm 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

  1. Run nlm skill package (or nlm setup → Add the skill to my tools/agents
    → tick Claude Desktop / claude.ai). It saves ~/Downloads/nlm-skill.zip.
  2. In Claude Desktop or claude.ai open Customize → Skills → Add, pick the
    file, and turn the skill on.
  3. After updating nlm, repeat both steps — nlm setup can'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 refused or keystore locked on headless servers or Docker containers.
  • Downgrade error: older versions of notebooklm-mcp-cli cannot read credentials.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 (its python3 launcher only locates uvx), which runs in its own environment and may use a different Python build than the uv tool installation. 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 protected safely 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 using nlm auth storage set file --profile <name>.

Reporting Issues

When reporting issues, include:

  1. The specific tool/operation that failed
  2. The error message (redact any sensitive info)
  3. Whether the operation worked before
  4. 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, alias and chat_save_to_note, plus pipeline create and profile info in server_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 tools
  • studio_create(artifact_type=...) replaces 9 creation tools
  • download_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:

text
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

  1. Execute tests sequentially - Run through each test group automatically
  2. Track progress - Use a todo list to show what's being tested
  3. Pause for user input - Stop at Drive sync test to ask you to modify a document
  4. 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:

text
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:

text
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:

text
Check my NotebookLM credential storage mode and test switching between file mode and protected mode.

Expected:

  1. nlm auth storage status: Shows profile name, storage mode (file or protected), and presence of ciphertext or legacy files.
  2. nlm auth storage set protected: Encrypts credentials to credentials.enc using the OS keystore (macOS Keychain, Windows Credential Manager, Linux SecretService) and removes plaintext files.
  3. nlm auth storage set file: Decrypts credentials back to cookies.json with 0600 permissions.

Test 1.1d - Profiles (`profile` tool)

Tool: profile (action: list, status, switch)

Prompt:

text
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:

  1. profile(action="list") returns every saved profile with email, storage_mode, is_saved_default, is_active.
  2. profile(action="switch", name=...) returns scope: "session"; later tools use that account and results carry active_profile_note.
  3. The saved default (nlm config get auth.default_profile) is unchanged.
  4. Quitting and reopening the app returns to the saved default.
  5. make_default=true changes default_profile in ~/.notebooklm-mcp-cli/config.toml (switch it back afterwards).
  6. With NOTEBOOKLM_COOKIES set 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:

text
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):

  1. Run login with a new profile. Before signing in, the browser must remain open;
    a public landing page, build label, or anonymous NID cookie must not finish login.
  2. Complete Google sign-in. Login saves signed-in cookies and a CSRF token, then
    closes the browser it launched. Verify the profile with nlm login --check.
  3. If sign-in times out, login fails without saving anonymous credentials.
  4. For a profile previously saved with only NID, rerunning login opens the
    sign-in browser. --check must not report success based on a public HTTP 200 page.
  5. 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:

text
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:

bash
nlm login --profile enterprise
nlm login switch enterprise
nlm notebook list
nlm notebook query <enterprise-notebook-id> "Summarize the sources" --new-conversation

Expected: 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:

text
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:

text
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:

text
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:

text
Add a URL source to notebook [notebook_id]:
- source_type: url
- url: https://en.wikipedia.org/wiki/Artificial_intelligence

Expected: 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:

text
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:

text
Add a Drive document to notebook [notebook_id]:
- source_type: drive
- document_id: [your_doc_id]
- title: "My Drive Doc"
- doc_type: doc

Expected: 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:

text
Add a file to notebook [notebook_id]:
- source_type: file
- file_path: /path/to/document.pdf

Expected: 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:

text
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 use
nlm 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:

text
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:

text
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:

text
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:

text
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:

text
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:

text
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=180 can complete when the notebook needs more than
    the default 120-second budget.
  • The async workflow returns a query_id immediately and reaches completed
    or a structured error status 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:

text
Configure notebook [notebook_id] chat settings:
- goal: learning_guide
- response_length: longer

Expected: 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:

text
Configure notebook [notebook_id] chat settings:
- goal: custom
- custom_prompt: "You must respond only in rhyming couplets."
- response_length: default

Expected: 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:

text
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:

text
Start fast web research for "OpenShift container platform" in notebook [notebook_id].
- mode: fast
- source: web

Expected: 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:

text
Check research status for notebook [notebook_id]. Poll until complete.

Expected:

  • status: completed
  • mode: fast
  • source_count: ~10 sources

Test 4.3 - Import Research Sources

Tool: research_import
CLI: nlm research import [notebook_id] [task_id]

Prompt:

text
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:

text
Start deep web research for "AI ROI return on investment" in notebook [notebook_id].
- mode: deep

Expected: 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:

text
Create an audio overview for notebook [notebook_id]:
- artifact_type: audio
- format: brief
- length: short
- confirm: False (show settings first)

Follow-up:

text
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:

text
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: True

Expected: 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:

text
Create a video overview for notebook [notebook_id]:
- artifact_type: video
- format: brief
- visual_style: classic
- confirm: True

Expected: Video generation started.


Test 5.3 - Create Report

Tool: studio_create
CLI: nlm report create [notebook_id] --format "Briefing Doc" --confirm

Prompt:

text
Create a report for notebook [notebook_id]:
- artifact_type: report
- report_format: Briefing Doc
- confirm: True

Expected: 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:

text
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: True

Expected: 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:

text
Read interactive report [artifact_id] in notebook [notebook_id]:
- call report(action="get")
- show the first section of the markdown and the element list

Expected: 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] then
nlm report element create [notebook_id] [artifact_id] --type quiz --confirm

Prompt:

text
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:

text
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:

text
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:

text
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:

text
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:

text
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:

text
Create flashcards for notebook [notebook_id]:
- artifact_type: flashcards
- difficulty: medium
- confirm: True

Expected: 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:

text
Create a quiz for notebook [notebook_id]:
- artifact_type: quiz
- question_count: 2
- difficulty: medium
- confirm: True

Expected: 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:

text
Create an infographic for notebook [notebook_id]:
- artifact_type: infographic
- orientation: landscape
- detail_level: standard
- infographic_style: professional
- confirm: True

Expected: 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:

text
Create a slide deck for notebook [notebook_id]:
- artifact_type: slide_deck
- format: detailed_deck
- length: short
- confirm: True

Expected: Slide deck generation started.


Test 5.8 - Create Mind Map

Tool: studio_create
CLI: nlm mindmap create [notebook_id] --title "AI Concepts" --confirm

Prompt:

text
Create a mind map for notebook [notebook_id]:
- artifact_type: mind_map
- title: "AI Concepts"
- confirm: True

Expected: 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:

text
Create a data table for notebook [notebook_id]:
- artifact_type: data_table
- description: "Key features and capabilities"
- confirm: True

Expected: Data table generation started.


Test 5.10 - Check Studio Status

Tool: studio_status
CLI: nlm studio status [notebook_id]

Prompt:

text
Check studio content generation status for notebook [notebook_id].

Expected:

  • Default MCP response contains at most 20 artifacts and only lean status fields.
  • pagination contains returned, offset, limit, and has_more.
  • Polling with artifact_id returns only the selected artifact.
  • Calling with include_details=True exposes rich fields and the artifact from
    Test 5.1b shows custom_instructions: "Explain this to a 5 year old".
  • nlm studio status [notebook_id] --json remains a plain list containing both
    id and artifact_id.
  • nlm studio status [notebook_id] --json --mcp-compatible returns the MCP envelope.
  • nlm video list [notebook_id] --json returns 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:

text
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:

text
Revise the slide deck artifact [artifact_id] in notebook [notebook_id]:
- slide_instructions: [{"slide": 1, "instruction": "Make the title larger and bolder"}]
- confirm: true

Expected:

  • 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:

text
Download the report from notebook [notebook_id]:
- artifact_type: report
- output_path: /tmp/report.md

Expected: 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:

text
Download flashcards from notebook [notebook_id]:
- artifact_type: flashcards
- output_path: /tmp/flashcards.json
- output_format: json

Expected: 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:

text
Download quiz from notebook [notebook_id]:
- artifact_type: quiz
- output_path: /tmp/quiz.md
- output_format: markdown

Expected: Quiz downloaded as markdown.


Test 6.4 - Download Audio

Tool: download_artifact
CLI: nlm download audio [notebook_id] --output /tmp/podcast.m4a

Prompt:

text
Download audio from notebook [notebook_id]:
- artifact_type: audio
- output_path: /tmp/podcast.m4a

Expected: 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:

text
Download slide deck from notebook [notebook_id]:
- artifact_type: slide_deck
- output_path: /tmp/slides.pdf

Expected: Slides downloaded as PDF.


Test 6.6 - Download Data Table Excel Export

Tool: download_artifact

Prompt:

text
Download the Excel data-table export from notebook [notebook_id]:
- artifact_type: data_table_xlsx
- output_path: /tmp/data-table.xlsx

Expected: 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:

text
Download all completed artifacts from notebook [notebook_id]:
- output_dir: /tmp/exports

Expected: 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:

text
Download all artifacts from every notebook into /tmp/exports, skipping
files that already exist.

Expected: One subdirectory per notebook. On a second run with
skip_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 and
errored_notebooks.


Test Group 7: Sharing

Test 7.1 - Get Share Status

Tool: notebook_share_status
CLI (Noun): nlm share status [notebook_id]

Prompt:

text
Get sharing status for notebook [notebook_id].

Expected: Status with is_public, access_level, collaborators.


Tool: notebook_share_public
CLI: nlm share public [notebook_id]

Prompt:

text
Enable public link for notebook [notebook_id].
- is_public: True

Expected: Public link returned.


Tool: notebook_share_public
CLI: nlm share private [notebook_id]

Prompt:

text
Disable public link for notebook [notebook_id].
- is_public: False

Expected: Public link disabled.


Test 7.4 - Invite Collaborator (Optional)

Tool: notebook_share_invite
CLI: nlm share invite [notebook_id] [email protected] --role viewer

Prompt:

text
Invite collaborator to notebook [notebook_id]:
- email: [email protected]
- role: viewer

Expected: 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:

text
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:

text
Check deep research status for notebook [notebook_id] with max_wait=60.

Expected:

  • status: completed
  • mode: deep
  • source_count: ~40-50 sources
  • report field present

Test 9.2 - Import Deep Research Sources

Tool: research_import
CLI (Noun): nlm research import [notebook_id] [task_id]

Prompt:

text
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:

text
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:

text
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:

text
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:

text
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:

text
List all notes in notebook [notebook_id]:
- action: list

Expected: 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:

text
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:

text
Delete note [note_id] in notebook [notebook_id]:
- action: delete
- note_id: [note_id]
- confirm: True

Expected: Deletion confirmation message.


Test Group 12: Server Info

Test 12.1 - Get Server Info

Tool: server_info

Prompt:

text
Get Gemini Notebook MCP server version and check for updates.

Expected:

  • version: Current version
  • latest_version: Latest PyPI version (or null if offline)
  • update_available: Boolean
  • update_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:

text
How much of my Gemini Notebook usage allowance is left?

Expected:

  • windows: two entries, rolling first then weekly
  • each with percent_used, percent_remaining and resets_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, weekly even though the API
    returns the two entries in an unstable order.
  • percent_used and percent_remaining sum 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_COOKIES is 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 to
cli/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 and
codex 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-mcp appear under Needs a fix;
    ticking them reports repaired · Renamed to gemini-notebook-mcp.
  • Verify: claude mcp list shows gemini-notebook-mcp ✔ Connected;
    ~/.codex/config.toml has [mcp_servers.gemini-notebook-mcp] and kept any
    extra keys (e.g. enabled = true); Claude Desktop configs (both profiles if
    chosen) contain gemini-notebook-mcp with 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 plain and 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 status matches, 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:

  1. Before the browser opens: "Where should your saved login live?" with Protected (recommended) and Plain file.
  2. Choosing 1 stores the login encrypted (credentials.enc, no cookies.json) and ends with "✓ Login stored in protected mode." with no further tips or questions.
  3. 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.
  4. 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

text
/* Detailed source-code truncated for AI context efficiency. */