imessage-index

imessage-index

A local-first iMessage retrieval system that serves hybrid full-text and vector search over an MCP server, enabling an AI assistant to answer questions about your correspondence.

Category
访问服务器

README

imessage-index

A local-first iMessage retrieval system. Extracts your full message history, resolves identities, segments conversations topically, embeds and indexes them, and serves hybrid full-text + vector search over an MCP server — so an AI assistant can actually answer questions about your own correspondence.

Designed to run unattended on a headless Mac, with all derived state on an encrypted volume and nothing leaving the machine unless you explicitly allowlist it.


⚠️ Read this before you clone

This code has never run against a real corpus. 836 tests pass (639 without a database, plus 197 integration tests that need a live PostgreSQL and skip cleanly without one), the CLI works, migrations apply against real PostgreSQL + pgvector — and no message has ever been indexed by it.

It ships deterministic fake model providers. The embedding, captioning, OCR, transcription, and segmentation providers are correctly-dimensioned stubs (marked PLACEHOLDER in cli.py) that let the pipeline run end to end in tests. Run the pipeline as-is and every stage will report success while your search results are meaningless. Replacing them is the first real task — see Replacing the model providers.

Treat this as a thoroughly tested skeleton with a complete design behind it, not a working product. If you want something that works today, this isn't it. If you want a rigorous starting point that has already made and documented the non-obvious decisions, read on.

What is genuinely done

Area State
Schema + migrations Complete; applied against live PostgreSQL 17 + pgvector
Pipeline stages (snapshot → extract → identity → segment → enrich → embed → sync → export) Implemented; unit + integration tested
Hybrid retrieval (BM25 + text vector + multimodal vector, RRF-fused, reranked) Implemented
Local MCP surface (stdio) Implemented
Public MCP surface (StreamableHTTP + OAuth) Implemented; never exposed
Export gate (default-deny, plan/approve/push) Implemented; transport never wired to a live API
Eval harness (nDCG@k, recall@k, MRR) Implemented; metrics verified against hand-computed fixtures
Real model providers Not implemented
Any run against real data Never happened

Architecture in one pass

chat.db ──.backup──▶ snapshot ──▶ extract ──▶ identity resolution
                                                     │
                                              segmentation
                                                     │
                                  ┌──────────────────┴─────────────┐
                             text bodies                     attachments
                                                                   │
                                          ┌────────────────────────┼───────────┐
                                    PDF text layer              images    audio/video
                                          └────────────────────────┼───────────┘
                                                                   │
                                      embed ──▶ pgvector + FTS5 + multimodal vector
                                                                   │
                                          ┌────────────────────────┴──────────┐
                                    local MCP                         filtered export
                                   (full corpus)                       (allowlisted)

Four choices that shaped everything else:

  • Segments, not messages. Retrieval returns topically coherent conversation segments. Message-granular results flood a model's context with "sounds good" and prevent reconstructing what was actually discussed.
  • Everything becomes text first. OCR output, captions, transcripts, and PDF text layers all embed into one text space, so failures stay debuggable — when something doesn't surface you can read the extracted text and see why. A second multimodal vector runs alongside for visual similarity.
  • Identity resolution precedes segmentation. Nothing downstream keys on a raw handle. One person legitimately has many numbers, emails and aliases across a decade.
  • Default deny on export. Nothing reaches an external index unless explicitly allowlisted, and a group thread requires every participant allowlisted.

Requirements

  • macOS on Apple Silicon. The pipeline depends on macOS-only APIs: Full Disk Access to Messages, the Contacts framework, Apple Vision.
  • Python 3.12+ and uv.
  • PostgreSQL 17 + pgvector, as a dedicated instance.
  • Rust toolchain, to build the extraction shim.
  • ≥ 32 GB unified memory for 8B-class local models; smaller models work with reduced quality.

Getting started

git clone https://github.com/msg43/imessage_mcp.git imessage-index
cd imessage-index && uv sync
cargo build --release --manifest-path tools/imsg-dump/Cargo.toml
uv run pytest        # 639 passed, 197 skipped (integration tests need a live DB)

Copy config.example.yaml, fill it in, and point the CLI at it:

export IMSG_CONFIG=/path/to/your/config.yaml
uv run imsg check-permissions && uv run imsg migrate

Secrets are never stored in config — they resolve from the macOS Keychain (keychain:<item>) or the environment (env:<VAR>), and config validation rejects anything that looks like a literal secret.

Two macOS gotchas that will each cost you an hour. PostgreSQL needs export LC_ALL=C or the postmaster dies at startup with "postmaster became multithreaded during startup" — which reads like a corrupt installation and is not. And Full Disk Access cannot be granted over SSH: TCC prompts require a GUI session, and the grant goes to the binary that launches the job, not to Messages.

Then uv run imsg --help. Every stage supports --dry-run.

Replacing the model providers

This is the work between "tests pass" and "it does something." The interfaces already exist and are correctly dimensioned — implement them and inject your implementations where cli.py currently constructs the fakes.

Interface What it needs
TextEmbeddingProvider embed_documents() (bare) and embed_query() (instruction-prefixed); 2048-dim, L2-normalized
MultimodalEmbeddingProvider embed_images() and embed_text() (paired towers); 1280-dim
BoundaryProvider Topical boundary indices for a window of messages
OcrProvider / CaptionProvider / TranscriptionProvider One method each

The reference design uses local MLX-hosted models throughout, on the reasoning that sending a decade of personal messages to a hosted API is a categorically different decision from indexing them on your own machine — and that as of mid-2026 the leading open text-embedding models top the benchmarks anyway, so there is little quality left to trade for it. Nothing in the code requires that choice; the interfaces are provider-agnostic.

⚠️ The dimensions are load-bearing — see the first item below.


Things that will bite you

Learned the expensive way; written down so you don't have to.

  • pgvector's index caps are lower than its type limits. The vector and halfvec types accept up to 16,000 dimensions, but HNSW/IVFFlat indexes cap at 2,000 (vector) and 4,000 (halfvec). A column can be perfectly legal DDL whose index can never be created — the error surfaces at CREATE INDEX, and ignoring it means silently falling back to sequential scan. scripts/lint_ddl.py exists solely to catch this. An earlier revision of this project specified an unbuildable halfvec(4096) for exactly this reason.
  • Audience validation and subject validation are not redundant. On the public surface the subject check answers "is this the owner?" and the audience check answers "was this token minted for this system?" A user's OAuth subject is identical across every app they sign into, so subject-checking alone does not stop a token minted for another application being replayed here. Both, or neither works.
  • Applied migrations are immutable, enforced by hash. Correct a mistake in a later migration; never edit a shipped one.
  • updated_at is enforced by trigger, not convention. Re-segmentation keys off it, so a writer that forgot to bump it would strand chats out of reprocessing — no error, stale results the only symptom.
  • Filters must overfetch, not post-filter. Post-filtering a fixed top-K silently starves results when filters are selective: you get few or zero hits and it looks like "nothing matched."
  • Ingest-time and query-time text normalization must match exactly. If they drift, exact-phrase search silently stops working.

Layout

src/imsg/
  config/      config surface + validation (enforces the safety rules)
  db/          connection, migrations, cluster fingerprint
  stages/      snapshot, extract, identity, sync
  segment/  backfill/  enrich/  embed/    indexing pipeline
  retrieval/   hybrid query flow, RRF fusion, reranking
  mcp/         auth boundary, local + public surfaces, tools
  export/      default-deny eligibility, plan/approve/push
  eval/        metrics, runner, diff
  verify/      seed completeness, attachment reconciliation
migrations/         schema, applied in order by a hash-checked runner
tools/imsg-dump/    GPL-3.0 Rust extraction shim (subprocess only)

Development

uv run ruff check . && uv run mypy . && uv run pytest

Integration tests run against a live PostgreSQL when one is reachable and skip cleanly when it isn't; the unit suite never needs a database.

Licensing

The core is MIT — see LICENSE. Component licensing and the reasoning behind the split are in NOTICE.

In short: tools/imsg-dump/ is GPL-3.0 and carries its own LICENSE. It links the GPL imessage-database crate to parse the attributedBody typedstream format, which is not optional — since Big Sur much of a message's text is not in the text column at all, and readers that only query that column silently return empty strings for large portions of modern history.

It is invoked strictly across a process boundary — spawned as a subprocess, never linked into the Python code. That boundary is deliberate and load-bearing for the licensing split. Vendor it differently and that is yours to reason about.

What isn't here

Instance configuration, by design: real config values, contact seed data, allowlists and eval queries live in a separate private overlay you supply and point at with IMSG_CONFIG. This repo is public-safe by construction — no real names, hosts or secrets have ever been committed to it, and config.example.yaml ships placeholders only.

The design record — architecture rationale, full build spec, and the decision log explaining why each choice above was made — is kept private, since it's written against a specific deployment.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选