api/src/mob/adapters/discord/events.py). Slack has such an API, so mob
stores no Slack content at all; the Slack section covers that
path.
What enters the index
The bot’s channel permissions set the boundary. Set up your Discord app covers how a private channel enters that coverage, and Authorization covers the second check at query time, where mob intersects the bot’s channels with the ones the requesting member can see. Inside those channels mob indexes:- message text, plus one line for each attachment filename, embed title and description, sticker name, and poll question
- public threads and forum posts, stored under their parent channel so they inherit its visibility
- messages from every author, apps and bots included
When messages become searchable
A live message is indexed as it arrives. History arrives behind it: each channel keeps a cursor recording the newest and oldest message stored, and a background task walks older pages one at a time, rotating across channels so recent coverage lands everywhere before deep history lands anywhere (api/src/mob/adapters/discord/backfill.py).
A server that was just installed answers searches over its recent messages
first, and older history keeps filling in for as long as the walk takes.
The walk paces itself against Discord’s rate limits and resumes from the
stored cursors after a restart.
When the gateway reconnects, each channel re-indexes a short overlap behind
its stored frontier, which absorbs edits made while the connection was away
and removes messages deleted inside that window.
Attachment contents
Every attachment is searchable by filename from the moment its message is indexed. A second pass then downloads the file and adds its contents (api/src/mob/mirror/enrich.py):
- text files, including source code and configuration formats, are decoded
- CSV and TSV files contribute their header, a sample of rows, and a row count
- PDFs contribute their text layer, so a scanned PDF carrying no text layer stays findable by filename
- PNG, JPEG, WebP, and GIF images go through one vision completion on
MOB_SMALL_MODEL, which transcribes legible text and describes what the image shows
api/src/mob/mirror/extract.py).
How results are ranked
Every query runs Postgres full-text search over the indexed chunks. When the deployer configured a model endpoint, mob also embeds the query, runs a vector search, and fuses the two rankings (api/src/mob/mirror/search.py). Self-hosting
covers the settings.
Both legs degrade instead of failing. A query whose embedding call fails
returns full-text results. Chunks whose embedding call failed during
ingest are stored without a vector and stay findable through full-text
search until a later edit or catch-up re-indexes them.
A long message is split into chunks that are ranked individually, and each
result carries the message it came from with a permalink.
How messages leave the index
- An edit replaces the message’s chunks, and an edit that shortens a message drops the chunks past its new end.
- A delete removes them.
- A member running
/mob deleteinside the server removes every message that member authored from that server’s index. - Losing bot access to a channel, or deleting the channel or thread, purges its rows.
- Disabling the installation from the console, removing the bot from the server, and uninstalling the app each drop the whole mirror for that installation.
Slack
mob stores no Slack messages. Each query callsassistant.search.context
with the requesting member’s own Slack token, Slack applies its own
visibility rules, and the response stays in request memory until the MCP
response completes. Slack indexes file contents itself, which is what
search:read.files adds to the same call.