Skip to main content
Glama
juanQNav

mcp-notebooklm

by juanQNav
README.md
# mcp-notebooklm

MCP server that exposes [Google NotebookLM](https://notebooklm.google.com/)
notebooks as tools for AI assistants (opencode).

Thin wrapper over [notebooklm-py](https://github.com/teng-lin/notebooklm-py)
using [`FastMCP`](https://github.com/modelcontextprotocol/python-sdk).

## What it implements

A stdio MCP server with four tools:

| Tool                                             | Purpose                                                                    |
| ------------------------------------------------ | -------------------------------------------------------------------------- |
| `list_notebooks()`                               | List every notebook with `id`, `title`, `source_count`.                    |
| `find_notebook(title)`                           | Case-insensitive partial match over notebook titles.                       |
| `ask_notebook(notebook_id, question)`            | Ask a question; returns the grounded answer from NotebookLM.               |
| `generate_quiz(notebook_id, num_questions, ...)` | Generate quiz JSON with multiple_choice / true_false questions in batches. |

The service (`src/mcp_notebooklm/service.py`) wraps the async `NotebookLMClient`
and applies two safety rails so the host LLM never hangs:

- **Concurrency limit** — one in-flight query at a time
  (`asyncio.Semaphore(1)`), with a 10s queue timeout. If another query is
  running, the tool returns
  `[RETRY_NEEDED] NotebookLM is busy... call again in ~60 seconds.`
- **Execution timeout** — 60s hard cap on the underlying `chat.ask()`. Timeouts
  and transport failures surface as `[RETRY_NEEDED]` / `[ERROR]` prefixes so the
  LLM can react instead of looping.

Authentication state is persisted to `data/auth.json` and reloaded on every
request via `NotebookLMClient.from_storage()`.

## Prerequisites

- Python ≥ 3.12
- [`uv`](https://docs.astral.sh/uv/) (recommended) or `pipx`
- A Google account with NotebookLM access

## 1. Install

From the project root:

```bash
uv sync
```

This installs the project (including the `notebooklm-py[browser]` extra, which
pulls Playwright) and exposes the `mcp-notebooklm` console script.

## 2. Install Playwright browser (one-time)

notebooklm-py[browser] installs the Playwright Python package, but the Chromium
browser binary must be downloaded separately.

uv run playwright install chromium

This is a one-time step (~170 MB) and must be done before the first login.

## 3. Authenticate with NotebookLM

The first run downloads Chromium (~170 MB) and opens a Google sign-in window.
Auth state is written to `data/auth.json` and reused on subsequent calls.

```bash
# one-time login (interactive — finishes in the browser)
uv run notebooklm login --storage-path ./data/auth.json
```

The login command is provided by the upstream `notebooklm-py` CLI; see
[its README](https://github.com/teng-lin/notebooklm-py) for browser options
(`--browser msedge`, `--browser-cookies chrome`, multi-account `--profile`,
etc.).

> `data/auth.json` is git-ignored. Back it up somewhere safe — it is the only
> thing standing between you and a fresh login.

To refresh cookies silently (cron / launchd / systemd):

```bash
notebooklm auth refresh --quiet
```

## 4. Register with opencode

Add the server to `~/.config/opencode/opencode.json`:

```jsonc
{
  "mcp": {
    "notebooklm": {
      "command": [
        "uv",
        "run",
        "--project",
        "<your-path>/mcp-notebooklm",
        "mcp-notebooklm",
      ],
      "timeout": 120000,
      "type": "local",
    },
  },
}
```

Restart opencode. The four tools (`list_notebooks`, `find_notebook`,
`ask_notebook`, `generate_quiz`) appear as `notebooklm__*` and are available
immediately.

> The `timeout` (120s) covers the worst-case ask path: 10s queue + 60s ask +
> overhead. Raise it if you see transport resets on slow networks.

## 5. Use it

From inside opencode (or any MCP host):

```text
list all my NotebookLM notebooks
```

```text
find the notebook about <topic>
```

```text
ask notebook <notebook_id>: <question grounded in that notebook's sources>
```

Typical flow the LLM will follow:

1. `list_notebooks()` → choose the right `notebook_id`.
2. `ask_notebook(id, question)` → get a cited answer.
3. If the response starts with `[RETRY_NEEDED]`, call the tool again.

### Quiz generation

Generate structured quizzes that bypass NotebookLM's ~20 question limit by
batching requests:

```text
generate_quiz(
    notebook_id = "abc123",
    num_questions = 50,
    topic = "sorting algorithms",
    difficulty = "mixed",
    output_path = "~/quizzes/algorithms.json",
    cumulative = true,
    language = "es"
)
```

**Parameters:**

| Param           | Required | Default         | Description                                                       |
| --------------- | -------- | --------------- | ----------------------------------------------------------------- |
| `notebook_id`   | yes      | —               | Notebook to query                                                 |
| `num_questions` | yes      | —               | Total questions to generate                                       |
| `topic`         | no       | `"all sources"` | Specific topic or full notebook                                   |
| `difficulty`    | no       | `"mixed"`       | `easy` / `medium` / `hard` / `mixed`                              |
| `output_path`   | no       | —               | Save JSON to this path (creates directories if needed)            |
| `cumulative`    | no       | `false`         | If `true` and file exists, merge new questions with existing ones |
| `language`      | no       | `"es"`          | Language for questions, options, and explanations                 |

**How batching works:**

Questions are generated in batches of 15. For 50 questions, the tool makes 4
calls to NotebookLM (15 + 15 + 15 + 5), parses each response, and merges them
into a single JSON. If a batch fails (timeout, parse error), it's skipped and
`failed_batches` in metadata tells you how many were lost.

**Output format:**

```json
{
  "metadata": {
    "notebook_id": "abc123",
    "notebook_title": "Algorithms",
    "generated_at": "2026-06-24T10:30:00Z",
    "topic": "sorting algorithms",
    "difficulty": "mixed",
    "total_questions": 50,
    "failed_batches": 0
  },
  "questions": [
    {
      "id": 1,
      "type": "multiple_choice",
      "question": "What is the average time complexity of quicksort?",
      "options": [
        {
          "text": "O(n)",
          "rationale": "Incorrect. Linear time only applies to specific cases like searching in unsorted arrays."
        },
        {
          "text": "O(n log n)",
          "rationale": "Correct. Quicksort averages O(n log n) with good pivot selection and balanced partitions."
        },
        {
          "text": "O(n²)",
          "rationale": "Incorrect. This is the worst-case complexity when the pivot selection is poor (e.g., already sorted array with first/last element as pivot)."
        },
        {
          "text": "O(log n)",
          "rationale": "Incorrect. Logarithmic time applies to operations like binary search, not full sorting algorithms."
        }
      ],
      "correct_answer": 1
    },
    {
      "id": 2,
      "type": "true_false",
      "question": "Mergesort is a stable sorting algorithm.",
      "correct_answer": true,
      "explanation": "Mergesort preserves the relative order of equal elements, making it stable."
    }
  ]
}
```

**Cumulative mode:**

When `cumulative = true` and `output_path` exists, new questions are appended to
the existing array and IDs are renumbered sequentially. This lets you build up a
question bank over multiple calls.

## Project layout

```text
src/mcp_notebooklm/
├── __init__.py
├── main.py        # entry point → server.main()
├── server.py      # FastMCP tool definitions (list, find, ask, generate_quiz)
└── service.py     # NotebookLMClient wrapper + concurrency / timeout guards + quiz generation
data/
└── auth.json      # notebooklm-py session storage (git-ignored)
pyproject.toml     # deps, entry point: mcp-notebooklm
```

## Development

```bash
uv run ruff check src/        # lint
uv run flake8 src/           # style
```

## Notes & limits

- The upstream library uses **undocumented Google APIs** — endpoints can break
  without notice.
- Heavy usage is rate-limited; the 1-concurrent semaphore is intentional, not a
  bug. Quiz generation with many questions will take time due to sequential
  batches.
- `chat.ask` is the only endpoint used. Source management, artifact generation,
  etc. are not wired into the MCP surface.
- The server is stdio-only. For HTTP, look at the upstream `notebooklm-py` REST
  server.

TDQS

A4.1/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a distinct, non-overlapping purpose: listing notebooks, finding by title, asking questions, and generating quizzes. No ambiguity.

Naming Consistency5/5

All tools follow a consistent verb_noun snake_case pattern (list_notebooks, find_notebook, ask_notebook, generate_quiz), making predictions easy.

Tool Count5/5

With 4 tools, the server is well-scoped for interacting with NotebookLM notebooks—covering essential operations without excess.

Completeness4/5

Covers listing, searching, asking, and quiz generation. Minor gaps like notebook creation/deletion or source retrieval, but these are outside the server's apparent focus.

Maintenance

ActivityStale
ResponsivenessNo issues