Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

**Programmatic access to Gemini Notebook** — via command-line interface (CLI) or Model Context Protocol (MCP) server.

> **Note:** Tested with Pro/free and Google AI Ultra ($249/mo) tier accounts. May work with Gemini Notebook Enterprise accounts but has not been tested.
> **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](https://buymeacoffee.com/jacobbd).**
> 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! 🙏
Expand Down Expand Up @@ -314,7 +314,50 @@ nlm login profile delete <profile> # Delete a profile
nlm login profile rename <old> <new> # Rename a profile
```

Each profile gets its own isolated browser session, so you can be logged into multiple Google accounts simultaneously.
### 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

Expand Down
39 changes: 39 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,45 @@ prompt rather than an undocumented payload change.
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:

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

```
Expand Down
21 changes: 13 additions & 8 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,20 @@ This means you can stay logged into multiple Google accounts simultaneously with

## Enterprise / Google Workspace

If your organization uses **Google Workspace** with a managed Gemini Notebook instance (e.g., `notebooklm.cloud.google.com` instead of `notebook.google.com`), set the `NOTEBOOKLM_BASE_URL` environment variable before authenticating:
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
export NOTEBOOKLM_BASE_URL=https://notebooklm.cloud.google.com
# 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
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. If the variable is not set, the default personal URL (`https://notebooklm.google.com`) is used.
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.

Expand All @@ -158,9 +161,11 @@ For MCP server configuration, pass the variable in your client config:
"mcpServers": {
"gemini-notebook-mcp": {
"command": "notebooklm-mcp",
"env": {
"NOTEBOOKLM_BASE_URL": "https://notebooklm.cloud.google.com"
}
"env": {
"NOTEBOOKLM_BASE_URL": "https://notebook.cloud.google.com",
"NOTEBOOKLM_PROJECT_ID": "your-gcp-project-id-or-number",
"NOTEBOOKLM_LOCATION": "global"
}
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions docs/MCP_CLI_TEST_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ List all my NotebookLM notebooks.

---

### 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"`
Expand Down
2 changes: 2 additions & 0 deletions docs/MCP_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ pipeline(action="run", notebook_id="abc", pipeline_name="ingest-and-podcast", in
| `NOTEBOOKLM_HL` | Interface language and default artifact locale, including regional BCP-47 values such as `es-419` (default: en) |
| `NOTEBOOKLM_QUERY_TIMEOUT` | Query timeout (seconds) |
| `NOTEBOOKLM_BASE_URL` | Override base URL for Enterprise/Workspace (default: `https://notebooklm.google.com`) |
| `NOTEBOOKLM_PROJECT_ID` | Required GCP project ID or number for Gemini Notebook Enterprise |
| `NOTEBOOKLM_LOCATION` | Enterprise location/multi-region (`global`, `us`, or `eu`; default: `global`) |
| `NOTEBOOKLM_DOWNLOAD_DIR` | Optional directory boundary for artifact downloads. Unset preserves the default behavior. |
| `NOTEBOOKLM_ALLOWED_FILE_DIRS` | Optional OS-separated list of directories allowed for local file sources. Unset means unrestricted. |
| `NOTEBOOKLM_DISABLED_GROUPS` | Comma-separated tool groups to hide (see [Selective tool exposure](#selective-tool-exposure)) |
Expand Down
4 changes: 2 additions & 2 deletions src/notebooklm_tools/cli/commands/studio.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from notebooklm_tools.core.exceptions import NLMError
from notebooklm_tools.services import ServiceError, ValidationError
from notebooklm_tools.services import studio as studio_service
from notebooklm_tools.utils.config import get_base_url, get_default_language
from notebooklm_tools.utils.config import get_default_language, get_notebook_url

console = make_console()

Expand Down Expand Up @@ -190,7 +190,7 @@ def studio_status(
"limit": result["limit"],
"has_more": result["has_more"],
},
"notebook_url": f"{get_base_url()}/notebook/{notebook_id}",
"notebook_url": get_notebook_url(notebook_id),
}
)
return
Expand Down
14 changes: 13 additions & 1 deletion src/notebooklm_tools/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from urllib.parse import quote

from notebooklm_tools.utils.config import get_base_url

Expand Down Expand Up @@ -726,9 +727,20 @@ def _fetch_notebooklm_homepage(
headers["Cookie"] = cookie_header

url = base_url or get_base_url()
from notebooklm_tools.utils.config import get_enterprise_location, get_enterprise_project_id

loc = get_enterprise_location()
prefix = ""
if "vertexaisearch.cloud.google.com" in url:
prefix = f"/notebooklm/{loc}"
elif "cloud.google.com" in url:
prefix = f"/{loc}"

project_id = get_enterprise_project_id()
project_query = f"?project={quote(project_id, safe='')}" if project_id and prefix else ""

with httpx.Client(follow_redirects=True, timeout=timeout, headers=headers) as client:
return client.get(f"{url}/")
return client.get(f"{url}{prefix}/{project_query}")


def check_auth(
Expand Down
55 changes: 53 additions & 2 deletions src/notebooklm_tools/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,26 +193,56 @@ class BaseClient:
def _get_base_url(self) -> str:
return get_base_url(getattr(self, "_base_host", "") or None)

def _is_enterprise(self) -> bool:
"""Return True if connected to Gemini Notebook Enterprise (Vertex AI Search / Cloud)."""
base_url = self._get_base_url()
return "vertexaisearch.cloud.google.com" in base_url or "cloud.google.com" in base_url

def _get_enterprise_location(self) -> str:
"""Return the enterprise location/region (e.g. global, us, eu)."""
from notebooklm_tools.utils.config import get_enterprise_location

return getattr(self, "_location", None) or get_enterprise_location()

def _get_enterprise_prefix(self) -> str:
base_url = self._get_base_url()
loc = self._get_enterprise_location()
if "vertexaisearch.cloud.google.com" in base_url:
return f"/notebooklm/{loc}"
# notebooklm.cloud.google.com uses /{location}
return f"/{loc}"

def _get_batchexecute_url(self) -> str:
if self._is_enterprise():
return f"{self._get_base_url()}{self._get_enterprise_prefix()}/_/CloudNotebookLmUi/data/batchexecute"
return f"{self._get_base_url()}/_/LabsTailwindUi/data/batchexecute"

def _get_upload_url(self) -> str:
if self._is_enterprise():
return f"{self._get_base_url()}{self._get_enterprise_prefix()}/upload/_/"
return f"{self._get_base_url()}/upload/_/"

def _get_query_endpoint(self) -> str:
if self._is_enterprise():
return f"{self._get_enterprise_prefix()}/_/CloudNotebookLmUi/data/google.cloud.notebooklm.v1main.NotebookService/GenerateFreeFormStreamed"
return self.QUERY_ENDPOINT

# Keep class-level attributes for backward compatibility with code that
# reads them directly (e.g. tests). These are the defaults; runtime code
# should use the _get_*() methods which respect NOTEBOOKLM_BASE_URL.
BASE_URL = "https://notebooklm.google.com"
BATCHEXECUTE_URL = f"{BASE_URL}/_/LabsTailwindUi/data/batchexecute"
UPLOAD_URL = "https://notebooklm.google.com/upload/_/"
_BL_FALLBACK = "boq_labs-tailwind-frontend_20260108.06_p0"
_BL_FALLBACK_ENTERPRISE = "boq_cloud-ml-notebooklm-ui_20260816.08_p0"

# =========================================================================
# Known RPC IDs
# =========================================================================

# Notebook operations
RPC_LIST_NOTEBOOKS = "wXbhsf"
RPC_LIST_NOTEBOOKS_ENTERPRISE = "rG2vCb"
RPC_GET_NOTEBOOK = "rLM1Ne"
RPC_CREATE_NOTEBOOK = "CCqFvf"
RPC_RENAME_NOTEBOOK = "s0tc2d"
Expand Down Expand Up @@ -408,6 +438,8 @@ def __init__(
build_label: str = "",
base_host: str = "",
profile_name: str | None = None,
location: str | None = None,
project_id: str | None = None,
):
"""
Initialize the base client.
Expand All @@ -421,16 +453,27 @@ def __init__(
(optional - falls back to NOTEBOOKLM_BASE_URL or the default host)
profile_name: Auth profile that owns these credentials. Uses the
configured default when omitted.
location: GCP region for Enterprise (e.g. global, us, eu). Defaults to NOTEBOOKLM_LOCATION or 'global'.
project_id: GCP project ID for Enterprise. Defaults to NOTEBOOKLM_PROJECT_ID.
"""
import time as _time

from notebooklm_tools.utils.config import get_enterprise_location, get_enterprise_project_id

self.cookies = cookies
self.csrf_token = csrf_token
self._client: httpx.Client | None = None
self._session_id = session_id
self._bl = build_label
self._base_host = base_host
self._profile_name = profile_name
self._location = location or get_enterprise_location()
self._enterprise_project_id = project_id or get_enterprise_project_id()
if self._is_enterprise() and not self._enterprise_project_id:
raise ValueError(
"NOTEBOOKLM_PROJECT_ID is required when NOTEBOOKLM_BASE_URL points to "
"Gemini Notebook Enterprise."
)
self._created_at: float = _time.time()

# Conversation cache for follow-up queries.
Expand Down Expand Up @@ -471,6 +514,12 @@ def __init__(
# Apply any runtime RPC-ID overrides (hot-patch for rotated method IDs).
self._apply_rpc_overrides()

# In Enterprise mode, ensure we have enterprise-compatible tokens and session ID
if self._is_enterprise() and self._bl and "tailwind" in self._bl:
self._bl = ""
self._session_id = ""
self.csrf_token = ""

# Only refresh CSRF token if not provided - tokens actually last hours/days, not minutes
# The retry logic in _call_rpc() handles expired tokens gracefully
if not self.csrf_token:
Expand Down Expand Up @@ -634,10 +683,11 @@ def _build_request_body(self, rpc_id: str, params: Any) -> str:

def _build_url(self, rpc_id: str, source_path: str = "/") -> str:
"""Build the batchexecute URL with query params."""
fallback_bl = self._BL_FALLBACK_ENTERPRISE if self._is_enterprise() else self._BL_FALLBACK
params = {
"rpcids": rpc_id,
"source-path": source_path,
"bl": os.environ.get("NOTEBOOKLM_BL") or getattr(self, "_bl", "") or self._BL_FALLBACK,
"bl": os.environ.get("NOTEBOOKLM_BL") or getattr(self, "_bl", "") or fallback_bl,
"hl": os.environ.get("NOTEBOOKLM_HL", "en"),
"rt": "c",
}
Expand Down Expand Up @@ -1161,7 +1211,8 @@ def _refresh_auth_tokens(self) -> None:
from .cookie_rotation import rotate_google_cookies

rotate_google_cookies(client)
response = client.get(f"{self._get_base_url()}/")
home_path = f"{self._get_enterprise_prefix()}/" if self._is_enterprise() else "/"
response = client.get(f"{self._get_base_url()}{home_path}")

# Check if redirected to login (cookies expired)
if "accounts.google.com" in str(response.url):
Expand Down
Loading
Loading