Skip to content

MCP: search_chats finds content across every chat - #640

Open
katulevskiy wants to merge 1 commit into
zeronsh:mainfrom
katulevskiy:mcp-search-chats
Open

katulevskiy wants to merge 1 commit into
zeronsh:mainfrom
katulevskiy:mcp-search-chats

Conversation

@katulevskiy

@katulevskiy katulevskiy commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

An agent can list_chats and read_chat one transcript at a time, but it cannot find "the chat where we talked about X" without reading every chat. Titles were the only searchable field, and only by exact match.

Change

New MCP tool search_chats searches the content of every chat: messages, the one-line tool ledger, and titles.

  • words mode (default) is forgiving. Each query word scores per message: exact 1.0, substring 0.8, typo within a length-scaled edit budget 0.6/0.5. Rare words weigh more than ones present in most chats. A chat is listed when it covers min_match (default 0.5) of the query, and the words may land in different messages, so a loose description finds the chat.
  • regex mode is ripgrep-style, case-insensitive unless case_sensitive.
  • Each hit has a snippet and offsetFromNewest, which is the read_chat offset that lands on that message.
  • Filters: project, device, parent, role, include_archived, include_tools, include_reasoning. The calling chat is skipped unless include_self.

Showcase

Real output of the tool on five fictional chats (highlighting added for the image). Top: a typo'd query finds a chat whose title never mentions throttling. Middle: "race condition" never appears in the chat, yet it is found via races, flaky, test. Bottom: a case-sensitive regex.

search_chats results

The image lives on a separate pr-assets-mcp-search branch of my fork, so it isn't part of this diff.

Design notes

  • There is no engine-side index. The tool reads each candidate chat's transcript the way read_chat does (8 at a time, newest activity first, capped by max_chats, default 200, max 1000) and scores it in the MCP process. The reply reports candidateChats, scannedChats, unreadableChats and truncated, so a partial scan is never mistaken for a full one. If this gets slow on large histories, the next step is an engine-side index (SQLite FTS5; rusqlite is already in the tree).
  • Not semantic: it will not find a chat that expresses the same idea in different words.
  • list_chats and search_chats now share one filtered_chats helper (project/device/archived/parent filters); behaviour of list_chats is unchanged.
  • Adds regex as a workspace dependency; it was already in the build tree indirectly.

Tests

  • cargo test -p zeron-mcp: 27 pass. New: unit tests for tokenising, match weights, ranking (rare terms, min_match, terms spanning messages, title-only), regex case handling and UTF-8-safe snippets; two tool tests through the in-memory engine stub (typo/fragment search, the offset feeding straight into read_chat, regex, role filter, invalid input, self-skip, truncation reporting).
  • cargo clippy -p zeron-mcp --all-targets and cargo fmt --check are clean.
  • Also run by hand through zeron mcp against a live engine (scanned all chats, ranked the relevant ones first).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Agents could list chats and read one transcript at a time, but had no way to
find "the chat where we talked about X" short of reading them all. Titles
were the only searchable field.

search_chats scans transcript content (messages, tool lines, titles):

- words mode (default) is forgiving: per-word exact/substring/typo scoring,
  rare words weigh more, and a chat only has to cover min_match of the query,
  with words allowed to land in different messages.
- regex mode is ripgrep-style, case-insensitive unless case_sensitive.
- Hits carry a snippet and offsetFromNewest, which drops straight into
  read_chat's offset.
- Filters: project, device, parent, role, archived, tools, reasoning. The
  calling chat is skipped unless include_self.

There is no engine-side index: transcripts are read 8 at a time, newest
activity first, capped by max_chats (default 200), and the reply reports
truncated/unreadableChats so a partial scan is never mistaken for a full one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant