Skip to main content
Glama

local-notebook

本機版 NotebookLM 替代工具(第一版)。把 PDF / TXT / MD(含逐字稿)匯入 SQLite, 透過 MCP server 讓 Claude Code 查詢,回答時附上可追查的原始出處 (檔名 + 頁碼 / 行號 / 時間戳)。

PDF / TXT / MD / 逐字稿
        ↓  (src/ingest.py,唯讀開啟原始檔,只複製不修改)
Python 匯入
        ↓
SQLite + FTS5 (data/notebook.db)
        ↓
MCP Server (src/server.py)
        ↓
Claude Code 查詢
        ↓
回答 + 原始來源 + 頁碼/時間戳

第一版範圍(依原始規格):不做向量資料庫、不做 OCR、不做 Docker,全部跑在 .venv 內,不需要系統管理員權限。


1. 需求與已驗證環境

【確認】以下是本次實際建置與測試所用的環境(在本機沙箱內執行並以 pytest 全數通過,共 22 項測試,其中 1 項因沒有 CJK 字型而 skip):

  • Python 3.11.15

  • SQLite 3.45.1(內建於該 Python,支援 fts5 的 trigram tokenizer)

  • mcp 2.2.0(pip install mcp 目前抓到的版本;此版把舊版 FastMCP 類別 改名為 MCPServer,import 路徑是 from mcp.server.mcpserver import MCPServer。 如果你的環境裝到 1.x 版 mcp,需要改回 from mcp.server.fastmcp import FastMCP,API 用法相同(.tool() 裝飾器、 .run()),但本專案的 src/server.py 是針對 2.x 寫的,尚未針對 1.x 測試。)

  • pypdf 6.19.0

  • pytest 9.1.1(測試用)

【確認】目標環境 Windows 11 + Python 3.12.10(pypdf 6.19.0)也已實際跑過 pytest:151 項通過、1 項 skip(同樣是缺 CJK 字型的那項;含網頁 UI 的測試)。


Related MCP server: fable MCP server

2. 安裝(Windows,比照你原本的規劃)

cd D:\local-notebook
py -3.12 -m venv .venv
.venv\Scripts\pip install --upgrade pip
.venv\Scripts\pip install -e .
.venv\Scripts\pip install -e ".[dev]"    # 想跑測試才需要

專案檔案、虛擬環境和套件都裝在 D:\local-notebook\ 底下。Python/pip 執行時 仍可能寫入使用者層級的暫存位置(例如 %TEMP%、pip 快取 %LOCALAPPDATA%\pip\Cache),但不碰:

  • C:\Program Files

  • Windows Service

  • 系統 PATH / Registry

  • Docker / PostgreSQL

  • 全域 pip install(沒加 -e . 就用系統 pip 的話才會裝到全域,這裡用的是 專案自己的 .venv)

若公司電腦的權限有疑慮,先用你原本規劃的三步驟測試:

py -3.12 -m venv D:\python_test
D:\python_test\Scripts\python.exe -m pip --version
D:\python_test\Scripts\python.exe -m pip install pytest

三步都成功,代表一般 Python 開發環境可用。


3. 使用方式

3.1 匯入檔案

.venv\Scripts\python -m src.ingest --notebook "弱電SOP" --tags "SOP,弱電" `
    C:\path\to\CableTray-SOP.pdf C:\path\to\meeting_transcript.txt

整個資料夾一次匯入時,不要直接寫 C:\path\to\*.pdf。PowerShell 不會替外部 程式展開萬用字元,Python 收到的是字面上的 *.pdf,只會印出一行 [跳過] 找不到檔案,什麼都沒匯入(已實測)。請先用 Get-ChildItem 展開:

$files = Get-ChildItem "C:\path\to\*.pdf" | ForEach-Object FullName
.venv\Scripts\python -m src.ingest --notebook "弱電SOP" --tags "SOP,弱電" $files
  • --notebook:notebook 名稱,不存在會自動建立。

  • --tags:逗號分隔的標籤,存在 notebook 上(notebook 已存在時,有給 --tags 會覆蓋舊標籤,沒給則保留)(v1 只存一個字串欄位,不做多對多標籤)。

  • --force:即使檔案內容雜湊(sha256)跟已匯入的重複,也重新匯入並取代舊紀錄(刪舊與寫新在同一個交易內,中途失敗會 rollback,舊資料不會遺失)。

  • 支援的副檔名:.pdf .txt .md。檔案不存在、型別不支援、或 pypdf 判定 PDF 損毀(PyPdfError)時會印出 [跳過] 並繼續下一個檔案。其他錯誤(例如資料庫 錯誤,或 pypdf 對某些畸形 PDF 丟出的一般例外)仍會中止整批匯入;中止前已 rollback,且這次匯入剛複製進 originals/ 的檔案會被刪除,不留孤兒檔。

  • notebook 名稱含 Windows 不允許的字元(< > : " / \ | ? *)時,originals/ 底下的資料夾名會把它們換成 _;名稱是 Windows 裝置名(CON、NUL、 COM1 等)時資料夾名前面會加 _。資料庫裡的 notebook 名稱不受影響。

  • 【確認】原始檔只會被「唯讀開啟」計算雜湊與擷取文字,然後用 shutil.copy2 複製一份到 data/originals/<notebook>/ 底下,不會修改、 不會搬移原始檔(tests/test_ingest_and_search.py::test_original_file_never_modified 有驗證匯入前後原始檔的位元組完全一致)。

  • 同一個 notebook 內,同雜湊的檔案預設會被跳過(status: skipped_duplicate), 避免重複灌資料。

3.2 啟動 MCP Server

.venv\Scripts\python -m src.server

這個指令本身不會印出東西就繼續掛著等待——它是用 stdio 跟 MCP client(例如 Claude Desktop / Claude Code)溝通,不是要你手動互動。正常關閉方式是讓呼叫它 的 client 結束連線。

Claude 啟動 server 時,Python 必須找得到 src 套件,也就是專案根目錄 (src\ 的上一層)要在搜尋路徑上。在其他資料夾直接執行 python -m src.server 會出現 ModuleNotFoundError: No module named 'src'(已實測;editable 安裝加進 搜尋路徑的是 src\ 本身,不是專案根目錄)。

所以設定裡用 PYTHONPATH 指定專案根目錄,不依賴 client 啟動 server 時的工作 目錄。以下以專案在 F:\github\local-notebook\local-notebook 為例,請換成你實際 的路徑。

Claude Code(一行指令註冊):

claude mcp add local-notebook -e PYTHONPATH=F:\github\local-notebook\local-notebook -- F:\github\local-notebook\local-notebook\.venv\Scripts\python.exe -m src.server

Claude Desktop(claude_desktop_config.json;Claude Code 的 .mcp.json 也是 同樣的寫法):

{
  "mcpServers": {
    "local-notebook": {
      "command": "F:\\github\\local-notebook\\local-notebook\\.venv\\Scripts\\python.exe",
      "args": ["-m", "src.server"],
      "env": { "PYTHONPATH": "F:\\github\\local-notebook\\local-notebook" }
    }
  }
}

【確認】在 C:\ 底下設好上面的 PYTHONPATH 再執行,src.server 可以正常匯入, 資料庫也指向專案裡的 data\notebook.db(資料庫路徑由程式檔位置決定,和工作 目錄無關)。

【未驗證】還沒有在 Claude Code/Claude Desktop 裡實際註冊並呼叫過。舊版範例用的 cwd 欄位已拿掉,因為不確定 client 會不會讀它,改用 PYTHONPATH 就不需要它。 註冊後在 Claude 裡問一句「列出我的 notebook」,就能確認有沒有接通。

3.2.1 網頁 UI(匯入、搜尋、匯出)

雙擊專案根目錄的 start-ui.bat,瀏覽器會自動開啟 http://127.0.0.1:8765。 也可以在 PowerShell 執行:

.venv\Scripts\python.exe -m src.ui

畫面上可以:

  1. 建立 notebook,或更新已存在 notebook 的 tags(tags 留空會保留原值)。

  2. 拖放或選擇 .pdf .txt .md 匯入。每個檔案各自顯示結果:已匯入、重複已跳過、或失敗原因;一個檔案失敗不影響其他檔案。

  3. 搜尋,並把結果匯出成 CSV(Excel 可直接開啟中文)或 Markdown(附檔名、頁碼、行號、時間戳)。

注意:

  • 只接受本機連線(127.0.0.1),區網其他電腦連不進來。

  • 8765 埠已被使用時會顯示錯誤;通常代表 UI 已經開著,直接開啟上面的網址即可。

  • 關閉瀏覽器不會停止伺服器,要關閉 start-ui.bat 開出的命令視窗。

  • UI 匯入不支援 --force;需要強制重新匯入時用命令列。

  • CSV 中以 =、+、-、@ 等開頭的內容前面會加 ',避免 Excel 當成公式執行。

  • 匯入失敗時畫面只顯示簡短原因,不顯示本機路徑;完整錯誤記在命令視窗裡。

  • UI、命令列、MCP 共用同一個 data\notebook.db,任一邊匯入的資料其他兩邊都查得到。

3.3 MCP 工具一覽

工具

參數

回傳

list_notebooks

無

所有 notebook:id、name、tags、created_at、source_count、chunk_count

list_sources

notebook

該 notebook 內所有來源檔:id、filename、file_type、file_hash、imported_at、chunk_count

search

notebook, query, limit(預設 10)

符合的段落:chunk_id、seq、page、line_start、line_end、timestamp、text、snippet、source_file、source_id、notebook、score

get_chunk

chunk_id

單一段落完整原文 + 來源;查無資料回傳 null

get_source

source_id

來源檔中繼資料 + 其所有段落(含 80 字預覽)

每筆 search / get_chunk 結果都帶有 page(PDF)或 line_start/line_end

  • timestamp(TXT/MD,逐字稿才會有 timestamp),讓 Claude 回答時可以寫成 「根據 CableTray-SOP.pdf 第 12 頁……」或「根據 meeting.txt 第 3 行 (00:01:10)……」。

注意 PDF 頁碼:page 是 PDF 檔案從第一頁開始數的頁序(src/ingest.py 的 chunk_pdf 逐頁編號),不一定等於頁面上印出的頁碼。文件有封面、目錄、 羅馬數字前置頁時,兩者會不同。正式引用工程規範前,請開啟原始 PDF 核對 印刷頁碼與條文編號。


4. 執行測試

.venv\Scripts\pip install -e ".[dev]"
.venv\Scripts\python -m pytest tests\ -v

【確認】實際執行結果(Windows 11 + Python 3.12.10):151 個測試通過、1 個 skip(見下方明細),涵蓋:

  • tests/test_ingest_and_search.py(7 項):TXT/MD 分段、原始檔未被修改、 重複匯入跳過與 --force 覆蓋、get_chunk/get_source 往返、 notebook 之間互不干擾、不支援的副檔名會丟出錯誤、list_notebooks 計數正確。

  • tests/test_pdf_ingest.py(2 項):用 fpdf2 產生真的 PDF(一份純英文、 一份用系統 Noto CJK 字型產生的繁體中文 PDF)餵給 pypdf 驗證逐頁分段、 頁碼、以及底下第 5 節提到的 NUL byte 問題確實被修好。

  • tests/test_mcp_server.py(1 項):直接呼叫真正的 MCPServer.call_tool() 把 5 個工具都跑過一次。

  • tests/test_mcp_stdio_subprocess.py(1 項):真的把 python -m src.server 開成子行程,用 mcp.ClientSession 走 stdio 協定 跟它對話(list_tools + 呼叫 list_notebooks、search),這是唯一會 抓到「python -m src.server 這個進入點本身跑不起來」或「有東西污染了 stdout 導致 JSON-RPC 協定壞掉」這類問題的測試。

  • tests/test_optimizations.py(11 項):審查時找到的 bug 的回歸測試,包括 list_notebooks 段落數不被放大、limit<=0、notebook 名稱含非法字元或 Windows 裝置名、損毀 PDF 不中斷整批、--tags 更新、--force 失敗時保留 舊資料,以及匯入失敗不留孤兒檔。

  • tests/test_ui_app.py(127 項,含參數化展開):網頁 UI 的 API,用 Starlette TestClient 走真正的 HTTP 路徑。涵蓋 Host/Origin/token 檢查、notebook 與 tags、上傳檔名清理與路徑不外洩、逐檔錯誤隔離、搜尋參數驗證、CSV(BOM、 公式防護)與 Markdown 匯出,以及前端不使用 innerHTML。

  • tests/test_ui_launcher.py(3 項):埠號被佔用時不開瀏覽器並回報錯誤; 開瀏覽器的那一刻 server 一定已經接受連線。

fpdf2 與 fonttools 只有跑測試才需要(用來產生測試用的假 PDF), ingest.py / server.py 本身完全不依賴它們。若你的機器上沒有系統 CJK 字型, test_pdf_traditional_chinese_extraction_and_search 會自動 skip(不會判定失敗), 其餘測試不受影響。


5. 已知限制(實測發現,不是猜的)

  1. FTS5 trigram tokenizer 對短查詢無效 【確認】trigram tokenizer 是把內容切成重疊的 3 字元窗格來建索引,直接在 這個沙箱用 sqlite3 驗證:對內容 "電纜橋架施工規範 Cable Tray" 查詢 "橋架"(2 個中文字)回傳 0 筆,查詢 "橋架施"(3 個字)回傳 1 筆; 英文查詢 "Ca"(2 字元)也是 0 筆,"Cable" 才找得到。 這代表很多常見的兩字中文詞(例如「規範」「載重」)用 FTS5 搜不到,即使 內文明明有出現。 處理方式:src/search.py 的 search() 已經加了判斷——查詢字串去除 前後空白後若少於 3 個字元,改用 SQL LIKE '%...%' 子字串比對(結果的 snippet 與 score 會是 null,因為 LIKE 沒有 bm25 排序);3 字元以上 才走原本的 FTS5 trigram 路徑。這個行為由 tests/test_ingest_and_search.py::test_txt_ingest_chunking_and_search 實際驗證(對「橋架」查詢會確實找到兩個含此詞的段落)。

  2. pypdf 從某些內嵌 CJK 字型的 PDF 擷取文字時,字元間會夾雜 NUL byte(\x00) 【確認】用 fpdf2 + 系統內的 Noto Sans CJK 字型產生一份含繁體中文的 PDF,pypdf.extract_text() 擷取出來的結果是 "\x00第\x00一\x00頁..." 這種每個字中間插一個 NUL byte 的形式,不是乾淨的 中文字串。 處理方式:src/ingest.py 的 _clean_pdf_text() 在每一頁擷取後都會 先 .replace("\x00", "") 再存進資料庫,tests/test_pdf_ingest.py::test_pdf_traditional_chinese_extraction_and_search 有驗證清理後存入的文字與搜尋結果都不含 NUL byte。 【無法驗證】這個現象是不是所有中文 PDF 的通病,還是只跟這次測試用的 字型/產生方式有關——我沒有另一份「正常」的中文 PDF 可以對照測試。已加上 的清理是保險措施,不代表所有中文 PDF 擷取出來的文字品質都沒問題,正式 匯入你手上的 PDF 之後,建議用 get_source / get_chunk 抽查幾段內容, 確認擷取出來的文字跟原文一致。

  3. (已修正)原始檔複製目的地曾經跟資料庫路徑脫鉤:早期版本裡, ingest.py 決定「原始檔要複製到哪個 originals/ 資料夾」是看環境變數 LOCAL_NOTEBOOK_DB,跟實際傳進來的 SQLite 連線指向哪個 .db 檔案無關。 寫測試時就是因為這樣,才發現用自訂路徑開連線(測試常見寫法)時,原始檔 卻被複製進了專案本身的 data/originals/,而不是測試用的暫存資料夾。 已改成 db.originals_dir_for(conn):用 PRAGMA database_list 直接讀這個 連線實際打開的資料庫檔案路徑,再取其同層的 originals/,保證 data/notebook.db 跟 data/originals/ 永遠對應同一份資料庫,不會因為呼叫方式不同而分裂。修好之後重新整批跑測試,專案的 data/ 目錄在測試前後都是空的(測試只會寫進各自的暫存目錄)。

  4. 沒有 OCR:掃描圖檔式的 PDF(頁面上沒有文字層)會被整頁跳過, ingest_file 回傳的 chunk_count 可能是 0 或比實際頁數少很多,不會報錯, 但你不會拿到那些頁的任何內容——這是規格明講「第一版不做 OCR」的預期行為, 不是 bug。

  5. PDF 一頁一個 chunk,不做細切:符合規格「第一版不做向量資料庫」的簡化 設計;一頁內容很長時,search() 回傳的 text 會是整頁,snippet 欄位可以用來快速定位關鍵字在頁面中的位置。

  6. 沒有語意搜尋:search 是關鍵字(trigram 子字串)比對,不是向量相似度, 同義詞或换句話說的查詢不會自動找到相關段落——這也是規格明講的第一版範圍。


專案結構

local-notebook/
├─ .venv/
├─ data/
│  ├─ originals/        # 匯入時複製進來的原始檔(依 notebook 分資料夾)
│  └─ notebook.db        # SQLite(metadata + 擷取文字 + FTS5 索引)
├─ src/
│  ├─ db.py               # schema、連線、notebook 建立
│  ├─ ingest.py            # CLI:匯入 PDF/TXT/MD
│  ├─ search.py             # list_notebooks / list_sources / search / get_chunk / get_source 的實作
│  ├─ server.py              # MCP server,把上面幾個函式包成 5 個 tool
│  └─ ui/
│     ├─ app.py              # 網頁 UI 的 API(只包裝 db / ingest / search)
│     ├─ launcher.py         # 綁定埠號、啟動 server、就緒後開瀏覽器
│     ├─ __main__.py         # python -m src.ui
│     └─ static/             # index.html / app.js / style.css
├─ tests/
│  ├─ test_ingest_and_search.py
│  ├─ test_pdf_ingest.py
│  ├─ test_mcp_server.py
│  ├─ test_mcp_stdio_subprocess.py
│  ├─ test_optimizations.py
│  ├─ test_ui_app.py
│  └─ test_ui_launcher.py
├─ samples/
│  └─ sample_transcript.txt   # 範例逐字稿,可用來快速試跑 ingest
├─ start-ui.bat              # 雙擊啟動網頁 UI
├─ pyproject.toml
└─ README.md

data/notebook.db 與 data/originals/ 不會隨程式碼一起打包(都是執行時才 產生的資料),第一次執行 src/ingest.py 時會自動建立。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables Claude Desktop to search and query personal document collections (PDF, Word, Markdown, text) using semantic search and conversational AI with full context preservation across exchanges.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables Claude to search, recall, and remember its own past conversations by indexing them into a local SQLite vault, providing direct access to the full context of previous sessions.
    11
    13
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables natural language interaction with a personal knowledge base stored locally on your computer, supporting semantic search, note reading, and writing through Claude Code or mobile apps.
    11
    MIT