NotebookBrain MCP
# 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.
[](https://github.com/SkilledDragonCorp/notebookbrain-mcp/actions/workflows/ci.yml)
[](./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
Scored across 3 tools
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.
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.
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.
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.