thought-search
MCP server for searching files on macOS by name or content using semantic and lexical search, providing file paths and line numbers for agents to read from.
README
thought-search
Find any file on your Mac by a vague memory of it. Spotlight for names, a
local semantic index for meaning, and results shaped for agents: a path:line
to start reading from, not a chunk dump. Nothing leaves the machine.
$ thought grep "the essay about calculating the punch force that could snap a jaw"
~/Documents/college/fermi-estimates-draft.pdf:118 (0.68) — assume the fist decelerates over ~2cm of jaw...
$ thought find "financial memo" --kind presentation
Found 2 in your work folders (newest first):
1. ~/Documents/Acme Financial - One Page Memo.pptx — 3d ago — 4.2MB
2. ~/Downloads/memo-draft-old.pptx — 2mo ago — 3.9MB
This is the file-search layer of Apprentice, a macOS AI apprentice, extracted as a standalone tool. It shipped to real users first; the code here is the shipped code, war-story comments included.
The problem
You know the file exists. You wrote it. You just can't produce the one thing every search interface demands: its name.
Two tools already live on your Mac, and each is broken alone:
- Spotlight is fast and literal. It finds
fermi-estimates-draft.pdfif you type "fermi". You didn't remember "fermi". You remembered punch force and jaw. - An agent with shell access can
ls,cat, andgrepits way around — powerful, but blind. Every wrong folder it opens burns seconds and context window, and an unscopedmdfindorfind /is a 30-60 second walk.
Semantic search should fix this, and in RAG form it half does: one query pulls content by meaning. But it hands back top-K chunks ripped out of their files — an answer fragment with no neighbors, no thread to pull, nowhere to stand.
The whole point is: you shouldn't have to pick.
The bet
Supermemory's SMFS work put numbers on the right design: let semantic search land on a path, then let the agent read, grep, and reason from that path with the tools it already has. Reach of semantic search, control of agentic search, one motion. (Their write-up is worth your time.)
SMFS mounts a cloud memory backend as a filesystem, so half their system is a sync engine: push queues, delta pulls, watermarks, FUSE/NFS mounts.
We took the other half of the idea and inverted the premise: your Mac is already the filesystem. The files are already local, already canonical, already yours. What's missing is not a mount — it's the semantic layer over what's there. So thought-search is the retrieval model without the cloud: no daemon, no sync, no account. An index in SQLite, a matrix in RAM, and two primitives.
Two primitives
find_file — the lexical leg. Name + kind + time + folder, straight off
the OS's own Spotlight index (mdfind, always scoped with -onlyin, never a
directory walk). Multi-word queries are tokenized so "financial memo" matches
Acme Financial - One Page Memo.pptx. Temporal queries use
kMDItemLastUsedDate — when you last opened the file, not when some
background process touched it — so "the deck I worked on yesterday" means what
you meant. On a miss it escalates: work folders → whole home directory →
external drives. It never dead-ends silently; it tells you where it looked.
semantic_grep — the semantic leg. File contents, chunked and embedded
locally (ONNX MiniLM, no PyTorch, ~15MB of runtime), searched by cosine over
an in-RAM matrix. A hit is path:line (score) — excerpt: a launch point. Open
the file, read around the line, follow the thread.
The router between them is not code — it's the model. find_file for
name/metadata references, semantic_grep for content references, and the
agent picks per query, exactly the way you reach for grep vs grep -r.
docs/AGENT_PROMPT.md is the battle-tested routing
doctrine to put in your agent's system prompt.
Install
pip install git+https://github.com/Reppin123/thought-search.git
thought index # crawl Downloads/Desktop/Documents, embed locally
thought grep "that doc about our pricing model"
thought find "contract" --since 'last week'
thought status
The embedding model (~90MB, all-MiniLM-L6-v2, Apache-2.0) downloads once on first use. After that the tool runs fully offline. macOS only — the lexical leg is Spotlight.
For agents (MCP)
pip install "thought-search[mcp]"
claude mcp add thought-search -- thought-mcp
Any MCP client works: {"mcpServers": {"thought-search": {"command": "thought-mcp"}}}.
Then wire docs/AGENT_PROMPT.md into your system
prompt — the tools are half the product; the routing doctrine is the other
half.
As a library
import asyncio
from thought_search import find_files, index_paths, search, start_background_index
index_paths(["~/Documents"]) # foreground crawl
start_background_index() # or: polite daemon-thread crawl
hits = search("unit economics assumptions") # [{path, score, snippet, start_line}]
res = asyncio.run(find_files("financial memo", kind="presentation"))
What makes it fast (and keeps it fast)
Everything below was a measured failure first. The numbers come from the machine this shipped on — a real corpus that grew to 7,207 files / 280k chunks / 1.2GB of index — not a synthetic benchmark.
Content-hash freshness. Every file's text is hashed with a settings fingerprint (model, dims, chunking params). Identical bytes never re-embed: unchanged files skip on an mtime+size gate without even being read, and a moved/renamed/copied file reuses its vectors for free. Steady-state re-crawls cost approximately nothing.
Progressive commits. The index commits every 20 embedded files. The first version committed at the end of the crawl — 26 minutes in, the WAL held 71MB and every search said "index empty." Partial results should be searchable during the first crawl, and a killed process should keep its progress.
A polite background crawl. The first cut pegged ~4 cores for 26 minutes.
Now: os.nice(10) plus a per-embed throttle. An index that makes the laptop
fans spin is an index that gets uninstalled.
One matrix in RAM. Brute-force cosine is a single matmul — if the
vectors are already in memory. Reloading 388MB from SQLite per query cost
3.5-5.8s; cached, a query is ~40ms at 252k chunks. The cache key is the chunk
count, not SQLite's data_version — that pragma is per-connection, and
keying on it silently rebuilt the cache on every single query.
Best-chunk-per-file, lazily. Results are files, not chunks: argsort once, walk until top-k distinct files, early-exit. The cache holds no Python strings — at 252k rows, six 252k-element Python lists were a 73s warm-up cost by themselves. Snippets are fetched from SQLite only for the winners.
No type filter on semantic search. An inferred file-type filter is a
silent excluder — a wrong kind guess once dropped the exact PDF the query
was about, and the agent thrashed for 8 turns on a file the index had.
semantic_grep returns every type and shows the extension; choosing is the
caller's job.
bf16 storage: evaluated, rejected. Halving vector RAM sounds free until you notice numpy has no BLAS f16 matmul — you pay a 2-3s per-query upcast or you upcast at load and the RAM comes back. Honest dead ends stay in the comments so nobody re-walks them.
What it deliberately doesn't do
- No cloud, no sync engine, no account. SMFS needs push queues and delta pulls because it mirrors to a backend. There is no backend here. Your files never leave your machine; neither do their embeddings.
- No daemon, no FUSE/NFS mount. It's a library, a CLI, and an MCP server. The OS filesystem is already mounted.
- No hardcoded query planner. The model decomposes "the pdf about X from last week" into parameters inline. Speed comes from the indexes; smarts stay in the model.
Status and honest limitations
- macOS only. The lexical leg is Spotlight. A Linux leg would need
locate/plocateor similar. - Index freshness is crawl-based (session start /
thought index), not a live file-watcher. Watcher is the natural next step. - Text, code, PDF, and docx are indexed today. Images/audio/video want
transcription siblings (
talk.mp3→talk.mp3.transcript.md) — designed, not built. - MiniLM is the floor. 384-dim, 256-token context; it lands on the right files but scores are modest. The embedder is a deliberate seam — swapping in a longer-context model is a two-constant change plus a re-index.
start_lineis chunk-start, not an exact span.- The eval is an A/B gate, not a benchmark.
evals/find_file_ab.pyscores hit@1 / found / latency against a bare-lscontrol on your files; there is no xAFS-style public corpus here yet. Numbers above are one real machine, disclosed as such.
Credits
The retrieval philosophy — semantic search as a navigation aid that lands on paths — is SMFS's (technical report), here reimplemented local-first. Embeddings by all-MiniLM-L6-v2 over ONNX Runtime. Extracted from Apprentice.
MIT.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。