Skip to main content
Glama
README.md
# NotebookBrain MCP

Cited project memory for agents: an MCP server with three tools, `add_source`, `ask` and
`list_sources`, behind a pluggable backend. Every backend returns the same answer shape: text
with `[n]` markers and a list of numbered citations that quote the source.

[![CI](https://github.com/SkilledDragonCorp/notebookbrain-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/SkilledDragonCorp/notebookbrain-mcp/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)

## The problem

Agents need to answer from a team's own notes (ADRs, runbooks, meeting notes) and show where
each claim came from, without anyone running a vector database. The community
[notebooklm-py](https://github.com/teng-lin/notebooklm-py) client makes NotebookLM usable from
code, but it drives undocumented Google endpoints with your browser session, so it needs a
hard opt-in and a filter that keeps secrets and personal data out. Where an official Google
path exists, it should be the default remote option.

## What was built

| Backend | `NOTEBOOKBRAIN_BACKEND` | Kind | Needs | Status here |
| --- | --- | --- | --- | --- |
| Local BM25 + extractive answers | `local` (default) | local | nothing | runs, measured below |
| Gemini API File Search (REST) | `gemini` | official | `GEMINI_API_KEY` | built and tested against a mocked REST transport; live run skipped (HTTP 402, see Limits) |
| notebooklm-py 0.8.5 | `notebooklm-py` | unofficial | ToS flag + notebook id + your own `notebooklm login` | off by default; tested with a fake client and a signature contract test against the real package |

- **MCP server** (`notebookbrain serve`, stdio, MCP Python SDK `mcp==2.3.0`): `add_source(title,
  text)`, `list_sources()`, `ask(question, top_k)`. Results are structured JSON.
- **One citation format** (`src/notebookbrain/models.py`): `{n, source_id, source_title, quote,
  start, end}`. `normalize()` renumbers citations by first marker, drops dangling markers and
  merges duplicates, for every backend. `tests/test_parity.py` checks the three backends return
  identical keys.
- **Risk gate** (`src/notebookbrain/gate.py`) runs before any backend call, on sources and
  questions. Policy by backend kind:
  - `local`: reports findings, never refuses (nothing leaves the machine).
  - `official`: refuses secrets (cloud keys, tokens, private keys, password assignments, JWTs).
  - `unofficial`: also refuses emails, Luhn-valid card numbers, US SSNs, confidentiality
    markers and sources over 200,000 characters. Findings never include the matched value.
- **ToS flag**: the unofficial backend refuses to start unless
  `NOTEBOOKBRAIN_ACCEPT_UNOFFICIAL=i-accept-unofficial-google-api-risk` is set exactly.
- **Clean skips**: missing key, HTTP 401/402/403, no notebooklm-py login or no ToS flag all
  return `{"ok": false, "status": "unavailable", "error": "skipped: ..."}` instead of crashing.
- **CLI**: `notebookbrain {serve, add, add-dir, list, ask, gate-check, info, version}`.
- **Skill**: [`.claude/skills/notebookbrain-mcp/SKILL.md`](.claude/skills/notebookbrain-mcp/SKILL.md).

```mermaid
flowchart LR
  Agent -- MCP stdio --> Server[notebookbrain serve]
  Server --> Gate{risk gate}
  Gate -- local --> BM25[(local BM25 store)]
  Gate -- official --> FS[Gemini File Search]
  Gate -- "unofficial + ToS flag" --> NB[notebooklm-py]
```

## Is there an official NotebookLM API? (checked 2026-10-21)

- **NotebookLM Enterprise API exists, but only for managing notebooks and sources.** Google
  Cloud documents `v1alpha` REST methods on
  `https://{us|eu|global}-discoveryengine.googleapis.com/v1alpha/projects/PROJECT_NUMBER/locations/LOCATION/notebooks`
  (`notebooks.create`, `get`, `listRecentlyViewed`, `batchDelete`, `share`) and
  `.../notebooks/NOTEBOOK_ID/sources:batchCreate` / `sources:uploadFile` / `sources:batchDelete`,
  authenticated with an OAuth bearer token (`gcloud auth print-access-token`). The pages are
  marked Preview (Pre-GA terms) and need a NotebookLM Enterprise license. Sources:
  [Create and manage notebooks (API)](https://docs.cloud.google.com/gemini/enterprise/notebooklm-enterprise/docs/api-notebooks),
  [Add and manage data sources (API)](https://docs.cloud.google.com/gemini/enterprise/notebooklm-enterprise/docs/api-notebooks-sources).
- **We found no documented method to ask a notebook a question** through that API. Without an
  `ask` call it cannot back this server, so the official backend uses the Gemini API instead.
- **Gemini API File Search** is documented with stores, document upload and citations:
  `POST /v1beta/fileSearchStores`, `POST /upload/v1beta/{store}:uploadToFileSearchStore`,
  `GET /v1beta/{store}/documents`, and `generateContent` with `tools[].fileSearch`, whose
  response carries `groundingMetadata.groundingChunks[].retrievedContext` and
  `groundingSupports[].segment` (byte offsets). Sources:
  [File Search guide](https://ai.google.dev/gemini-api/docs/file-search),
  [File Search stores reference](https://ai.google.dev/api/file-search/file-search-stores),
  [Documents reference](https://ai.google.dev/api/file-search/documents),
  [generateContent reference](https://ai.google.dev/api/generate-content).
- **notebooklm-py**: latest release `v0.8.5` (published 2026-10-12, MIT, tag commit
  `50493ddf9f9eb9230de998d3dff8565243383078`, PyPI `notebooklm-py 0.8.5`). Auth is a browser
  login through Playwright, cookies imported from a signed-in browser, or a "master token";
  credentials live under `~/.notebooklm/`. Its README calls itself an unofficial library that
  uses undocumented Google APIs, not affiliated with Google, that can break at any time.

## Measured

Keyless run through the real stdio MCP server with the local backend, over the synthetic notes
in `fixtures/notes/` and the questions in `fixtures/questions.yaml` (some have no answer in the
notes on purpose). The gate rows use the synthetic cases in `fixtures/gate-cases.yaml`, whose
secret-shaped values are assembled at runtime. Every number below is written by
`uv run python scripts/report.py`; none is typed by hand.

<!-- measured:start -->
| Metric (keyless run, `uv run python scripts/report.py`) | Result |
| --- | --- |
| MCP tools exposed over stdio | add_source, ask, list_sources |
| Synthetic notes added / listed back | 8 / 8 |
| Expected note is citation [1] (hit@1) | 10/12 |
| Expected note among citations (hit@3) | 12/12 |
| Cited quote from that note contains the key fact | 11/12 |
| Unanswerable questions declined with no citations | 3/3 |
| Citations whose quote equals source[start:end] | 23/23 |
| Answers where every [n] maps to a citation | 15/15 |
| Risk gate, local policy: refused / allowed | 0/0 refused, 12/12 allowed |
| Risk gate, official policy: refused / allowed | 5/5 refused, 7/7 allowed |
| Risk gate, unofficial policy: refused / allowed | 9/9 refused, 3/3 allowed |
| `gemini` backend with no key / no ToS flag | unavailable: skipped: no GEMINI_API_KEY |
| `notebooklm-py` backend with no key / no ToS flag | unavailable: disabled: the unofficial notebooklm-py backend needs NOTEBOOKBRAIN_ACCEPT_UNOFFICIAL=i-accept-unofficial-google-api-risk (read SECURITY.md first: undocumented Google endpoints, Terms of Service and account risk are yours) |
<!-- measured:end -->

Per-question detail: [reports/notebookbrain.md](reports/notebookbrain.md). These numbers
measure the local BM25 backend on our own synthetic notes. They say nothing about Gemini File
Search or NotebookLM answer quality, which we could not run here.

## Quickstart

Requires Python 3.12 and [uv](https://docs.astral.sh/uv/).

```bash
uv sync --frozen
uv run notebookbrain add-dir fixtures/notes
uv run notebookbrain ask "What canary error rate triggers a rollback?"
uv run notebookbrain list
bash scripts/check.sh            # the full local gate (same as CI)
```

Use it from an MCP client (Claude Code, Cursor, any stdio client):

```json
{
  "mcpServers": {
    "notebookbrain": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/notebookbrain-mcp", "notebookbrain", "serve"],
      "env": { "NOTEBOOKBRAIN_BACKEND": "local" }
    }
  }
}
```

Official backend: put `GEMINI_API_KEY` in `.env` (copy `.env.example`), then run with
`--env-file .env --backend gemini` (for MCP: `"args": [..., "notebookbrain", "--env-file", ".env",
"--backend", "gemini", "serve"]`). Nothing reads `.env` unless you pass `--env-file`. The first `add_source` creates a File Search store and saves
its name in `.notebookbrain/gemini-store.json`. Default model is `gemini-3.8-flash`, the id
used in the File Search guide's samples; override with `GEMINI_MODEL`.

Unofficial backend (read [SECURITY.md](SECURITY.md) first): `uv sync --extra unofficial`, run
`notebooklm login` yourself, then set `NOTEBOOKBRAIN_BACKEND=notebooklm-py`,
`NOTEBOOKBRAIN_NOTEBOOK_ID=<id>` and
`NOTEBOOKBRAIN_ACCEPT_UNOFFICIAL=i-accept-unofficial-google-api-risk`.

## Keys and live tests

| Need | Keyless (CI) | Live |
| --- | --- | --- |
| Local backend, MCP server, CLI, gate, report | yes | n/a |
| Gemini File Search | mocked REST transport | `GEMINI_API_KEY`; `uv run pytest -m live -rs` |
| notebooklm-py | fake client + contract test on the real package | ToS flag + notebook id + your own login; run by hand |

Live tests write to `.recorded-local/` (gitignored), never to `reports/`.

## Limits

- **The Gemini backend has not answered a live question.** On this machine the key returns
  `HTTP 402` ("prepayment credits are depleted") on `generateContent`, so the live test skips.
  An earlier live attempt uploaded a note into a fresh store: the upload call succeeded but the
  document ended in `STATE_FAILED`; we did not confirm why (unpaid embeddings is a guess, not a
  finding). That store was deleted. The REST shapes the code relies on come from Google's
  reference pages and the `google-genai` 2.32.0 SDK, and are tested against synthetic payloads.
- **Gemini citations have no offsets.** File Search returns the retrieved chunk text, not a span
  in your document, so `start`/`end` are `null` for that backend; `source_id` is matched by
  document title.
- **The unofficial backend was never run live here** (by design: no Google sign-in, no cookies).
  Mapping is tested with a fake client; the contract test only checks that notebooklm-py 0.8.5
  still has the methods and fields we call. notebooklm-py reports citation offsets in UTF-16
  units, which we pass through unchanged.
- **Terms of Service**: we did not verify whether automating consumer NotebookLM with
  notebooklm-py is allowed for your account. The ToS flag records that you accept that risk; it
  does not make the use compliant.
- **The risk gate is a regex scan.** It catches the listed patterns and nothing else: no names,
  addresses, phone numbers or free-text secrets. Treat it as a seatbelt, not proof.
- **The local backend is extractive**: it returns whole sentences from your notes, it does not
  write new prose, and BM25 misses paraphrases (compare hit@1 with hit@3 in Measured).
- NotebookLM Enterprise is not wired in: its documented API has no ask method (see above).
- Educational minilab; not regularly maintained.

## Not affiliated

Not affiliated with, endorsed by, or sponsored by Google (NotebookLM, Gemini) or the
notebooklm-py authors. Names are used only to describe compatibility.

## Credits

Builds on [minilab-notebooklm-py-skill](https://github.com/SkilledDragonCorp/minilab-notebooklm-py-skill)
(the notebooklm-py pin) and the repo layout of SkilledDragon's agent-flight-recorder and
decisiongate-mcp. See [NOTICE](NOTICE).

---

Built by [SkilledDragon LLC](https://skilleddragon.com). Questions: hello@skilleddragon.com.
License: [Apache-2.0](LICENSE).

TDQS

A3.9/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a distinct action and resource: add_source writes a source, list_sources enumerates sources, and ask queries the accumulated sources. There is no plausible overlap in agent selection.

Naming Consistency4/5

add_source and list_sources follow a clear verb_noun snake_case convention, and ask is also snake_case but is a bare verb. This is mostly consistent with a minor deviation.

Tool Count5/5

The set is intentionally small and well-scoped for a source-backed question-answering server: ingestion, listing, and querying are the three core operations. Each tool earns its place and three tools fit within the recommended range.

Completeness3/5

The surface covers adding sources, listing them, and asking grounded questions, but lacks update/delete operations and a way to inspect or retrieve an individual source's contents. These are notable lifecycle gaps, though the core ask workflow is present.

Maintenance

ActivityMaintained
ResponsivenessNo issues