rc-mcp
Read-only browsing and searching of a Rocket.Chat workspace, including end-to-end encrypted rooms. Decrypts messages locally using the user's PAT and E2EE password, keeps a local full-text index, and exposes rooms, threads, messages and files as tools. Supports room listing, message history and thread retrieval, full-text search (with optional server-side search for unencrypted rooms), incremental sync, and file/attachment retrieval with decryption, plus permalinks to quote messages back.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@rc-mcpfind the postmortem thread about the checkout outage in #incidents"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
rocketchat-e2ee-mcp
A read-only MCP server and CLI (rc-mcp) that lets Claude Code browse and
search a Rocket.Chat workspace, including end-to-end encrypted (E2EE) rooms. It decrypts on your machine with
your own keys (personal access token + E2EE password), the same way the official web client does, keeps a local
full-text index, and exposes rooms, threads, messages and files as MCP tools. It cannot send, edit, react, mark as
read or accept keys.
Contents: Why · How it works · Security model · Limitations · Quickstart · Tools · CLI · Configuration · Troubleshooting · Development · License
Why
Teams keep how-tos, decisions and runbooks in chat threads instead of a wiki. Finding them later means scrolling and guessing search terms. Many of the useful ones live in E2EE private groups and DMs, where the server only stores ciphertext. Server-side tools therefore can't read them. That includes server-side search and Rocket.Chat's own MCP endpoint, which ships with 8.9 as part of the enterprise AI add-on and runs on the server.
rc-mcp runs as you, on your machine. It reads the rooms you can already read, decrypts them locally, and lets
Claude search them, follow threads and quote them back to you with permalinks.
Related MCP server: mcp-room
How it works
Key chain
The derivation is a port of the 8.9.0 web client (apps/meteor/client/lib/e2ee) to WebCrypto. Every step is
checked against data produced by the real client.
RC_E2EE_PASSWORD your E2EE password / generated passphrase, taken as Latin-1 bytes like the client does
│
│ PBKDF2-HMAC-SHA256 → 256-bit key
│ stored key v2: salt "v2:<userId>:<uuid>", 100 000 iterations → AES-GCM
│ stored key v1: salt "<userId>", 1 000 iterations → AES-CBC (legacy)
▼
AES master key ──decrypts──▶ your stored private key (e2e.fetchMyKeys → private_key)
▼
RSA-OAEP-2048 private key (JWK, kept in memory only)
│
│ RSA-OAEP-SHA256 unwraps, per room (from subscriptions.get):
│ E2EKey current room key
│ oldRoomKeys[] previous keys after room key resets (the server keeps at most 10)
│ E2ESuggestedKey key offered by another member: used in memory, never accepted
│ (with its suggestedOldRoomKeys[] when you have no oldRoomKeys)
▼
room keys A256GCM (legacy A128CBC), picked per message by key id:
│ the oldRoomKeys entry whose e2eKeyId matches, else the current key
│
│ rc.v2.aes-sha2 AES-GCM-256 rc.v1.aes-sha2 / bare `msg` AES-CBC-128 (legacy)
▼
message content EJSON { msg, attachments, file, files }
│
│ file messages carry a per-file AES-CTR-256 key, a 16-byte counter block
│ and the SHA-256 of the plaintext inside the encrypted content
▼
attachment bytes downloaded ciphertext → AES-CTR → SHA-256 verifiedData flow
Rocket.Chat server (stores only ciphertext for E2EE rooms)
│ HTTPS GET to an allowlist of endpoints, PAT in X-User-Id / X-Auth-Token headers
▼
rc-api zod-validated responses, honours 429 / X-RateLimit-*, serialised per route
▼
e2ee decrypts with your keys in memory; failures become `undecryptable` + reason
▼
index node:sqlite + FTS5: plaintext, search index, sync cursors
│ <dataDir>/index.sqlite (0600), or RAM with RC_MCP_EPHEMERAL=1
▼
MCP tools over stdio · rc-mcp CLI
▼
Claude Code ──▶ LLM provider (receives whatever the tools return)Rooms and keys: one
subscriptions.getplus onerooms.get.Backfill:
rooms.history, 100 messages per page, newest first.Incremental:
chat.syncMessagesreturns new, edited and deleted messages, one call per changed room. Rooms whose last update did not move are skipped. Rocket.Chat purges its deletion log after 30 days, so a room whose cursor is older than 29 days gets a full resync.Freshness: there is no websocket and no background polling. Data syncs when a tool needs it:
get_messagesandget_threadrefresh a room whose last sync is older than 60 s.get_messagesreturns only history that the index holds without gaps; paging back withbeforebackfills older messages as needed.searchwithroomsrefreshes those rooms the same way. Withoutrooms, it syncs changed rooms at most every 5 minutes.refresh: falseskips all network work (no sync, and no server search unlessinclude_server_search: trueis passed explicitly).
Missing keys: messages that can't be decrypted yet are kept as ciphertext. Once the room's keys change (for example after a member shares the key), the next sync of that room decrypts them again locally, without extra REST calls.
Cold index: when the index lacks a message or a thread's replies,
get_messageandget_threadfetch them live (chat.getMessage,chat.getThreadMessages).get_filealways downloads the file and re-derives its key, from the re-fetched message or, when no message is known, from the room's upload record, because per-file keys are never stored.Server search: for unencrypted rooms,
searchalso queries Rocket.Chat'schat.searchwhen you passrooms(up to 10 of the named rooms), or, withinclude_server_search: true, for the 10 most recently active unencrypted rooms. It waits at most 2 s for a rate limit; otherwise it skips the server search and says so innotes.Offline: when Rocket.Chat can't be reached (network error, timeout, HTTP 5xx, 401 or 429) and the index already has data,
list_rooms,get_messages,get_thread,get_message(for indexed messages) andsearchanswer from the index and add anotesentry saying the results may be stale. After such a failure they skip their network work for 30 s.
Design: docs/architecture.md, the decision records starting at ADR 0001, and the crypto formats in docs/e2ee-protocol.md.
Security model
Rocket.Chat E2EE guarantees that only room members' devices see plaintext.rc-mcp adds two places where
plaintext ends up: a file on your disk and your LLM provider. Read this section before you point it at
a real workspace.
Decrypted text goes to your LLM provider. Every message, thread or file a tool returns becomes part of the conversation. It is sent to the model provider your MCP client uses, and it sits in anything that client stores itself. That widens the E2EE trust boundary for every participant of every room you read, and those people did not opt in. Get the consent of the people in those rooms, follow your organisation's policy on confidential data and AI tools, and leave out rooms where that isn't settled. Don't use it on rooms whose members expect their messages to stay on their devices.
The local index stores plaintext. It is needed for fast full-text search across rooms.
It lives in
<dataDir>/index.sqlite, with SQLite's-waland-shmfiles next to it. The directory is mode 0700 and is tightened if it is wider. The files are 0600, and the process runs withumask 077, so new files are private to you too.The data directory must be an absolute path (a leading
~is expanded). A relative path is rejected, so the index can't end up in whatever directory your MCP client started the server in.Mitigations:
RC_MCP_EPHEMERAL=1keeps the index in memory, andget_filethen writes nothing to disk.rc-mcp purgedeletes the index and saved files. It also works whileRC_MCP_EPHEMERALis set, to remove what earlier non-ephemeral runs left behind.rc-mcp doctorwarns if the index file has become group- or world-readable.Full-disk encryption is recommended.
get_filereturns text withinmax_inline_bytesand images up to 4 MiB inline, without writing them. Other files:normal mode: saved (0600) to
<dataDir>/files/(0700), and the path is returned;rc-mcp purgeremoves them;ephemeral mode: returned inline as an MCP embedded resource (up to 10 MiB), never written to disk.
rc-mcp exportwrites plaintext Markdown to stdout, or with-oto a file that it sets to mode 0600, also when the file already existed.
Never stored:
your E2EE password;
your private key, in either decrypted or stored form;
room keys;
per-file keys:
get_filefetches and decrypts the message again each time;your access token.
Attachment metadata in the index has its key material stripped. For messages that couldn't be decrypted yet, the index keeps the original ciphertext so they can be retried later.
You only get what you can already read.
rc-mcpuses your PAT and your keys and sees only rooms your account belongs to.An encrypted room is readable only if your account holds its key, or was offered one (a suggested key). Suggested keys get the same trust as in the official client, which accepts any suggested key that decrypts with your private key.
rc-mcpuses them in memory and never accepts them on the server.It can't read content from before you had the key, or content encrypted with a key older than the 10 the server keeps.
Read-only by construction.
The REST client has no generic request method. It only issues
GETs to a fixed allowlist of endpoints, plus authenticated downloads under/file-upload/.It avoids endpoints that change state even on
GET:GET /api/v1/logoutrevokes the calling token, PATs included;chat.ignoreUserchanges your subscription;im.history/im.messagescalled with a username can create a DM;e2e.acceptSuggestedGroupKeywould accept a key on the server.
Every tool is annotated
readOnlyHint: true,destructiveHint: false.
Secrets handling.
Where secrets come from: the environment, or a config file that
rc-mcprefuses to read unless only you can access it (mode 0600).Logs: JSON lines on stderr, with secrets scrubbed by key name and by value. Tool errors are scrubbed too.
Doctor: shows only the token's length.
Where the token is sent: to
RC_URLonly. File downloads follow up to 4 redirects to any host, and redirects to another origin are followed without credentials. Use anhttps://URL:http://is accepted for local testing but sends the token in cleartext.Treat the PAT like a password. Rocket.Chat tokens have no scopes, so a PAT can do anything your account can. Revoke it in My Account → Personal Access Tokens when you stop using
rc-mcp.
Chat content is untrusted input. Anyone in a room you read can write text aimed at the model. rc-mcp itself
can't act on it, but other tools in the same Claude Code session might, such as the shell or file edits. Review
actions Claude proposes after reading chat content.
Limitations
Rocket.Chat versions
Rocket.Chat | Status | Evidence |
8.9.0 | Tested | The committed fixtures in |
Legacy formats (older clients) | Formats verified | Private key v1, legacy bare- |
Any other version | Untested | Likely to work if the formats and REST shapes are unchanged. Regenerate the fixtures to confirm (see Development). |
Matrix federation rooms ( | Not supported | Messages come back as |
Other limitations
Old keys: after room key resets the server keeps at most 10 old room keys per member. Messages encrypted with an older key, or sent before you received the room key, report
room-key-missing. The official client can't read them either.Suggested keys are used read-only in memory and never accepted on the server. Open the room once in the official client to accept the key;
rc-mcp doctorlists the rooms where that is needed. When a suggested key comes with old keys (suggestedOldRoomKeys) and your subscription has nooldRoomKeys, those old keys are used too, as the server would adopt them on acceptance, so history from before a room key reset stays readable.No key shared yet (
no-room-key): until another member's official client is online and shares the key, that room is unreadable.Rate limits for non-admin tokens: Rocket.Chat limits REST calls per client IP and route. The default is 10 calls per 60 s;
rooms.history,chat.getThreadMessagesandchat.getThreadsListallow 20 per 10 s.rc-mcpwaits for the reset and retries. The CLI retries up to 5 times, gives up on a single wait longer than 120 s and times out a request after 30 s. The MCP server (serve) is less patient so tool calls don't stall: 2 retries, at most 45 s of waiting, 20 s timeout.The first sync of a large workspace can take minutes. Running
rc-mcp synconce from a terminal backfills every room completely, with the CLI's longer waits.Other clients behind the same IP share the budget.
Roles with
api-bypass-rate-limit(admin, bot, app by default) are not limited.
No live updates: there is no push or websocket; data is synced when a tool runs (see Data flow).
Subscribed rooms only: public channels you haven't joined are not listed.
Search coverage:
Encrypted rooms are searchable only through the local index, and only for history that has been synced.
Undecryptable messages are not searchable.
Server-side search covers unencrypted rooms only and, by default, runs only when you pass
rooms(see Data flow). Unencrypted rooms are in the local index anyway.
Files:
Downloads are capped at 50 MiB.
Text is returned inline up to 256 KiB by default (
max_inline_bytes, at most 1 MiB).Images up to 4 MiB are returned as MCP image content and not saved.
Anything else is saved to disk. In ephemeral mode it is returned inline as an embedded resource instead, up to 10 MiB; larger files fail with
file-too-large.
Deletions hidden by "Keep History": with
Message_KeepHistoryon andMessage_ShowDeletedStatusoff, Rocket.Chat hides deleted messages without reporting the deletion through any sync endpoint. Such messages stay in the local index, and in search results, until you re-index the room withrc-mcp sync <room> --full(or thesynctool withfull: true).Account E2E key reset:
users.resetE2EKeyinvalidates all your login tokens, PATs included, and removes every room key from your subscriptions (E2EKey,E2ESuggestedKeyandoldRoomKeys). Create a new PAT. After the official client has created your new keys, rooms showmissinguntil a member re-shares the room key. History from before the reset is readable only if a member also re-shares the old keys (suggestedOldRoomKeys, whichrc-mcpuses); otherwise it reportsroom-key-missing.One account per server process.
Platform: Node.js 24 or newer is required (
node:sqlite). Tested on Linux, including WSL2. On Windows the config-file and data-directory permission checks are skipped;doctor's index-file mode check may still warn there.
Quickstart
Prerequisites
Node.js 24+ and Claude Code.
A Rocket.Chat account that has set up E2EE (logged in to the official client once with E2EE enabled), and its E2EE password.
A personal access token, created by hand in the Rocket.Chat UI: avatar → My Account → Personal Access Tokens. Enter a name, set the two-factor option to Ignore Two Factor Authentication, then Add. Copy the Token and Your user Id shown in the dialog; the token is shown only once.
Without "Ignore Two Factor Authentication", calls can hit a 2FA challenge (
totp-required).Creating tokens needs the
create-personal-access-tokenspermission, which theuserandadminroles have by default.
Commands (bash or zsh)
# 1. Credentials for this shell (see the note below to keep them out of shell history)
export RC_URL=https://chat.example.com RC_USER_ID=<your-user-id> RC_AUTH_TOKEN=<your-pat> RC_E2EE_PASSWORD='<your E2EE password>'
# 2. Check connectivity, token, E2EE keys and which encrypted rooms are readable
npx -y rocketchat-e2ee-mcp doctor
# 3. Register the server with Claude Code (local scope)
claude mcp add rocketchat -e RC_URL="$RC_URL" -e RC_USER_ID="$RC_USER_ID" -e RC_AUTH_TOKEN="$RC_AUTH_TOKEN" -e RC_E2EE_PASSWORD="$RC_E2EE_PASSWORD" -- npx -y rocketchat-e2ee-mcp
# 4. Start Claude Code
claudeThe leading space in step 1 keeps the line out of history in bash (HISTCONTROL=ignorespace) and zsh
(setopt HIST_IGNORE_SPACE). Alternatively read the secrets without echo: read -rs RC_AUTH_TOKEN,
read -rs RC_E2EE_PASSWORD, then export RC_AUTH_TOKEN RC_E2EE_PASSWORD.
Then ask, for example:
"Search Rocket.Chat for the deploy runbook and summarise the steps, with links."
"Read this thread and list the decisions: https://chat.example.com/group/handbook/thread/<id>"
"What did @bob write about electionTimeoutMillis?"
A healthy doctor run looks like this:
rc-mcp doctor
✓ config server https://chat.example.com, user id <your-user-id>, token set (43 chars)
✓ e2ee password RC_E2EE_PASSWORD is set (not shown)
· index stored in /home/you/.local/share/rc-mcp (plaintext of decrypted messages)
✓ node Node.js 24.22.0
✓ server reachable, Rocket.Chat 8.9
✓ authentication token valid for @alice (roles: user)
· rate limits non-admin token: Rocket.Chat limits most REST routes to ~10 calls/min per IP by default; rc-mcp waits and retries
✓ e2ee keys private key unlocked (stored format v2)
✓ rooms 3 subscribed rooms, 2 end-to-end encrypted
✓ encrypted rooms all encrypted rooms are readable
✓ index file /home/you/.local/share/rc-mcp/index.sqlite (mode 600): 0 messages in 3 rooms
Encrypted rooms:
✓ handbook [private-group] key: ok
✓ Bob Brown [direct-message] key: ok
Result: OKdoctor exits with 1 when a check fails (server unreachable, bad token, wrong E2EE password, …) and with 0
otherwise. Warnings, such as an unset E2EE password, unreadable or suggested-key rooms, or an index file with
widened permissions, are printed but don't change the exit code.
Before the npm release
Until rocketchat-e2ee-mcp is published to npm, use one of these in place of npx -y rocketchat-e2ee-mcp, in
steps 3 and 4 and everywhere below:
GitHub release tarball:
npx -y https://github.com/alien2003/rocketchat-e2ee-mcp/releases/download/v0.1.0/rocketchat-e2ee-mcp-0.1.0.tgzSource build (needs mise):
git clone https://github.com/alien2003/rocketchat-e2ee-mcp && cd rocketchat-e2ee-mcp mise install && mise run setup && mise run buildThen run
node /path/to/rocketchat-e2ee-mcp/dist/cli/main.js. With Claude Code, that isclaude mcp add rocketchat -e … -- node /path/to/rocketchat-e2ee-mcp/dist/cli/main.js.
Once the package is on npm, you can pin a version with npx -y rocketchat-e2ee-mcp@0.1.0.
Other ways to register with Claude Code
The syntax is claude mcp add [options] <name> <commandOrUrl> [args...]. Repeat -e/--env KEY=value for each
variable, and pick a scope with -s/--scope local|user|project.
With literal values, the same as step 4:
claude mcp add rocketchat -e RC_URL=https://chat.example.com -e RC_USER_ID=<id> -e RC_AUTH_TOKEN=<pat> -e RC_E2EE_PASSWORD='<e2ee password>' -- npx -y rocketchat-e2ee-mcpValues passed with -e are stored in Claude Code's own configuration, outside your repository.
Keep secrets in a 0600 config file instead, so Claude Code's configuration holds only a path. See Configuration for the file format.
claude mcp add rocketchat -e RC_MCP_CONFIG="$HOME/.config/rc-mcp/config.json" -- npx -y rocketchat-e2ee-mcpProject scope, shared with your team. -s project writes .mcp.json in your project, which you commit.
Never put secrets in it: Claude Code expands ${VAR} in .mcp.json env values from the environment claude
starts in.
Either copy
.mcp.json.exampleto.mcp.json, or let Claude Code write it. Single quotes keep the shell from expanding${…}:claude mcp add rocketchat -s project -e RC_URL='${RC_URL}' -e RC_USER_ID='${RC_USER_ID}' -e RC_AUTH_TOKEN='${RC_AUTH_TOKEN}' -e RC_E2EE_PASSWORD='${RC_E2EE_PASSWORD}' -- npx -y rocketchat-e2ee-mcpEach teammate exports their own
RC_*values before startingclaude, for example from a private.envloaded withset -a; . ./.env; set +a(see.env.example). Each person reads only what their own account can read.
Tools
All tools are read-only (readOnlyHint: true, destructiveHint: false) and return JSON text. You can refer to a
room by id, name (handbook), display name, or @username for a DM. Message ids can be any id-like token
(imported messages, for example from Slack, have longer ids), and pasted URLs with ?msg=<id> or /thread/<id>
work wherever a message or thread id is expected (get_message, get_thread, get_file.message_id,
search.thread_id). file_id must be a bare file id.
Dates are ISO 8601: 2026-03-01 (midnight UTC), 2026-03-01T12:00:00Z or 2026-03-01T14:00:00+02:00. A
date-time without Z or an offset is read in the local time zone of the machine running rc-mcp. Values are
validated and converted to UTC; an invalid value returns invalid-argument. after is inclusive and before is
exclusive, so before: "2026-03-01" stops at 2026-03-01 00:00 UTC and leaves out 1 March. The CLI's --after /
--since and --before / --until work the same way.
Tool | Parameters ( | Returns |
|
|
|
|
| One page, oldest first: |
|
|
|
|
|
|
|
|
|
|
|
|
|
| Metadata first: |
|
|
|
get_file only downloads a file it can find in a message or in a room's upload list (groups.files,
channels.files, im.files); anything else is file-not-found. file_id must match [A-Za-z0-9_-]{1,64}. If
you pass both ids, the message must carry that file.
Resources: rocketchat://room/{rid} returns a Markdown transcript of the room's 50 most recent top-level
messages, decrypted. Thread replies are left out; thread roots show their reply count and thread id. The resource
list contains every subscribed room.
Message objects have these fields:
id,room_id,room,ts,author,text,permalink;thread_id,reply_count,edited_at,system_type;quotes[],attachments[](file_id,name,type,size, …),encrypted;undecryptable {reason, hint, key_id?}in place of text when a message can't be decrypted. Tools never guess that content.
Errors come back with isError: true and one shape:
{ "error": { "code": "room-not-found", "message": "No room matching \"ops\" among the rooms this account is subscribed to.", "hint": "Call list_rooms to see room ids and names." } }The codes are:
room-not-found,invalid-argument,message-deleted;from
get_file:file-not-found,file-key-missing,file-too-large(ephemeral mode), andundecryptablewhen the file's message or metadata can't be decrypted;rocketchat-<code>, for examplerocketchat-unauthorized,rocketchat-rate-limited,rocketchat-networkorrocketchat-timeout;e2ee-<reason>;internal-error.
Arguments that don't match a tool's schema (for example limit: 1000) are rejected by the MCP SDK before the tool
runs. That error is plain text, such as MCP error -32602: Input validation error: …, still with isError: true.
Examples
These are abridged results from the 8.9.0 fixtures. The server URL and the room name (handbook-mva1woss) are
simplified.
search with { "query": "deploy runbook", "rooms": ["handbook"] }:
{
"query": "deploy runbook",
"count": 1,
"results": [
{
"id": "Zd97mTM23F4efdEg8",
"room_id": "6ad1459708d9d91a69c55ade",
"room": "handbook",
"ts": "2026-10-15T21:28:59.374Z",
"author": "Alice Anders (@alice)",
"text": "Deploy runbook: run `make deploy` from the release branch, then check /healthz on every node.",
"reply_count": 2,
"encrypted": true,
"permalink": "https://chat.example.com/group/handbook?msg=Zd97mTM23F4efdEg8",
"snippet": "«Deploy» «runbook»: run `make «deploy»` from the release branch, then check /healthz on every node.",
"source": "index"
}
],
"index": { "rooms": 3, "messages": 19 }
}get_thread with { "url": "https://chat.example.com/group/handbook/thread/Zd97mTM23F4efdEg8" }. The first reply
was encrypted before a room key reset and the second one after it; both decrypt, the first via oldRoomKeys:
{
"room": { "id": "6ad1459708d9d91a69c55ade", "name": "handbook", "type": "private-group", "encrypted": true, "key_status": "ok" },
"root": { "id": "Zd97mTM23F4efdEg8", "author": "Alice Anders (@alice)", "text": "Deploy runbook: run `make deploy` from the release branch, then check /healthz on every node.", "reply_count": 2 },
"total_replies": 2,
"returned": 2,
"replies": [
{ "author": "Bob Brown (@bob)", "text": "Thread reply: remember to bump the version in package.json before `make deploy`.", "thread_id": "Zd97mTM23F4efdEg8" },
{ "author": "Bob Brown (@bob)", "text": "Post-reset thread reply (bob): version bump is now automated by CI.", "thread_id": "Zd97mTM23F4efdEg8" }
]
}get_file with { "message_id": "<id of a file message>" } returns two content items. The first is the metadata:
{ "file_id": "<file id>", "name": "release-checklist.md", "type": "text/markdown", "size": 96, "encrypted": true, "integrity": "verified", "message_id": "<id>", "inline": true }The second is the decrypted file text.
A message the account can't decrypt:
{ "id": "<id>", "text": "", "encrypted": true, "undecryptable": { "reason": "room-key-missing", "hint": "Encrypted with a room key this account does not hold (e.g. sent before you got access, or before a room key reset older than the 10 kept keys).", "key_id": "<key id>" } }CLI
Every CLI command uses the same configuration as the server. The command is rc-mcp after
npm install -g rocketchat-e2ee-mcp. Otherwise run npx -y rocketchat-e2ee-mcp <command>, or
node dist/cli/main.js <command> from a source build.
rc-mcp [serve] MCP server over stdio (the default command)
rc-mcp doctor [--json] connectivity, token, E2EE key state, unreadable rooms and why
rc-mcp sync [room] [--full] [--max-messages <n>] [--json]
rc-mcp search <query...> [-r, --room <room...>] [-f, --from <user...>] [--after <date>] [--before <date>]
[-n, --limit <n>] [--no-refresh] [--json]
rc-mcp export <room> [-t, --thread <id>] [--since <date>] [--until <date>] [--format md] [-o, --output <file>]
rc-mcp purge [-y, --yes]
rc-mcp --version | --helpCommand | Notes |
| Speaks MCP over stdio and logs only to stderr. Answers |
| Never prints secrets. Exit code 1 means a check failed; warnings alone exit with 0. |
| Without |
| Put the query words before the options, because |
| Markdown with dates and authors, for a room or for one thread of that room ( |
| Deletes |
Numeric options (--max-messages, -n) must be positive integers.
rc-mcp sync # bring the index up to date
rc-mcp search postgres failover -r handbook -n 5
rc-mcp export handbook -t Zd97mTM23F4efdEg8 -o runbook.md2026-10-15 20:23 handbook Alice Anders (@alice)
«Deploy» «runbook»: run `make «deploy»` from the release branch, then check /healthz on every node.
https://chat.example.com/group/handbook?msg=Zd97mTM23F4efdEg8
1 result(s)The output above is from rc-mcp search deploy runbook.
Exit codes: 0 success, 1 error (also a failed doctor check or a room that failed in sync), 2
configuration error.
Configuration
Settings are read from environment variables and an optional JSON config file. Environment variables take
precedence over the file. rc-mcp does not read .env files by itself; see .env.example.
Variable | Config file key | Default | Meaning |
|
| (required) | Server base URL, e.g. |
|
| (required) | Your user id. It is shown with the token when you create a PAT. |
|
| (required) | Personal access token, created with "Ignore Two Factor Authentication". |
|
| unset | E2EE password or passphrase. Without it, encrypted rooms are listed but not decrypted. |
|
|
| Where the index and saved files live. Must be an absolute path; a leading |
|
|
|
|
|
|
|
|
| (none) |
| Path to the config file. The default path is used only if the file exists; a path set here must exist. |
Config file. The file must not be readable or writable by group or others; rc-mcp refuses it otherwise and
tells you to run chmod 600. Unknown keys are rejected. rc-mcp never writes this file.
mkdir -p ~/.config/rc-mcp && install -m 600 /dev/null ~/.config/rc-mcp/config.json && $EDITOR ~/.config/rc-mcp/config.json{
"url": "https://chat.example.com",
"userId": "<your-user-id>",
"authToken": "<your-personal-access-token>",
"e2eePassword": "<your-e2ee-password>",
"ephemeral": false,
"logLevel": "info"
}Where data lives:
Path | Contents |
| Created on first run. Its permissions are tightened to 0700 if they are wider. |
| Rooms, sync cursors, decrypted message text, the full-text index, and attachment metadata with keys stripped. Also the ciphertext of messages that couldn't be decrypted yet. |
| Attachments saved by |
Removing local data:
rc-mcp purge(orpurge --yesin scripts) deletes the index and saved files, also whileRC_MCP_EPHEMERALis set.RC_MCP_EPHEMERAL=1avoids writing the index and attachments at all; each server start then syncs from scratch.
Troubleshooting
Run rc-mcp doctor first. It checks each step and says what to fix.
Symptom | Cause | Fix |
| Variables not set in the environment the process runs in. When run through Claude Code, that is the | Set them (see Configuration). For |
| The config file is readable by others. |
|
| A relative data directory. Values in a JSON config file or in | Use an absolute path, or one starting with |
| The server can't be reached or didn't answer in time. Read tools keep answering from the index (also after HTTP 5xx, 401 or 429 errors) and skip network work for 30 s after a failure. | Check |
| Wrong user id or token, or the PAT was revoked. An account E2E key reset ( | Check both values. Create a new PAT if needed. |
| The PAT was created with "Require Two Factor Authentication". | Create a new PAT with Ignore Two Factor Authentication. |
doctor: |
| Use your current E2EE password, the one that unlocks E2EE in the official client. |
doctor: | The value has characters above U+00FF. The client uses the password as Latin-1 bytes and rejects such characters too, so this can't be your E2EE password. Often a copy/paste artefact, such as typographic quotes or dashes. | Re-enter the password exactly. |
doctor: | No | Set it. Until then, encrypted rooms are listed but not decrypted. |
doctor: | E2EE was never set up for this account. | Log in once with the official client and set up E2EE. |
Room | No member has shared the room key with your account yet. Keys are shared by another member's official client while it is online. | Ask a member to open the room in the official client. Then run |
Room | Another member offered the key, and your official client hasn't accepted it yet. | It's readable already. Open the room once in the official client to accept the key; |
Old messages show | Encrypted with a key you never had (sent before you got access), a key older than the 10 the server keeps, or a key that was removed by an account E2E key reset and not re-shared. The official client shows "cannot be decrypted due to incorrect encryption key". | Can't be fixed on your side; that history is not readable with your keys. |
Room | A room key shared with your account doesn't unwrap with your current private key. | Ask a member to re-share by opening the room in the official client. |
Slow responses; | A non-admin token hit the per-IP, per-route REST limit. | Wait; |
| The room isn't fully indexed ( | Run |
| The server uses "Keep History" without "Show Deleted Status", so the deletion is never reported (see Limitations). |
|
| Ephemeral mode returns files inline only, up to 10 MiB. | Run without |
doctor: | The index file's permissions were widened. |
|
Claude Code reports the server failed or disconnected; JSON parse errors | Something wrote to stdout, or the process is waiting for input. On stdio, stdout belongs to the MCP protocol. Typical culprits: a wrapper script that prints, or | Launch |
npm warns | Older Node.js. npm only warns about the | Install Node.js 24+. |
Dev stack ( | Compose needs a Docker-compatible socket. | Always go through |
Reason reference. The first four reasons describe the account's E2EE state: they appear in list_rooms
(e2ee.reason), in a room's key_detail, in rc-mcp doctor and as e2ee-<reason> error codes. The others are
per-message undecryptable.reason values (and file errors).
Reason | Meaning |
| Account: |
| Account: no E2EE key pair on the server. |
| Account: the password doesn't unlock the private key. |
| Account: the password has characters the client can't use (above U+00FF). |
| No usable room key: none shared with this account yet, or E2EE is not unlocked (check |
| No key for the message's key id. |
| A shared room key doesn't unwrap with your current private key. |
| The format isn't implemented, e.g. Matrix megolm. |
| Structurally invalid ciphertext. |
| Wrong key or corrupted data. |
| Decrypted bytes aren't a message payload. |
| A file message carries no key. |
| The decrypted file doesn't match the uploader's SHA-256. |
Development
mise install && mise run setup # Node, pnpm, compose CLI; then dependencies + headless Chromium into node_modules
mise run check # lint + typecheck + unit testsTo run several tasks in one call, separate them with :::, as in mise run lint ::: typecheck. Without the
separator, mise run lint typecheck would pass typecheck to lint as an argument.
There are three test layers. No test encrypts with this project's code to prove compatibility.
Unit (
mise run test, offline):crypto is checked against the committed real-client fixtures and Rocket.Chat's legacy vectors;
the index and sync run against a fake Rocket.Chat that serves the captured responses;
MCP tools are tested through an in-memory transport;
a guard checks that
rc-mcp servewrites only JSON-RPC to stdout.
Integration (
mise run rc:up, thenmise run test:ground-truthonce, thenmise run test:integration): runs against a Rocket.Chat 8.9.0 + MongoDB compose stack as alice, bob, carol and the adminrcadmin. The ground-truth run gives the users real E2EE keys and data and records itself in.local/stack.json, which the integration tests require. Covers REST pagination,chat.syncMessages, downloads, the real rate limiter, andrc-mcp serveover stdio driven by the MCP client SDK.Ground truth (
mise run test:ground-truth): Playwright drives the real web client through E2EE setup, messages, uploads and a room key reset. The suite then checks that the decrypted output equals what was typed and what the client rendered.
Other tasks: mise run dev (server from source with auto-reload), mise run build, mise run rc:down,
mise run fixtures:generate.
Supporting another Rocket.Chat version:
Set
RC_VERSIONin.mise.toml.Run
mise run rc:down && mise run rc:up && mise run fixtures:generate, then the tests.Read the source diff of
apps/meteor/client/lib/e2eebetween the two versions.
See CONTRIBUTING.md for the contribution workflow and docs/adr/0001-architecture.md for the design.
License
MIT © alien2003
Available Tools
8 toolsget_fileGet an attachmentARead-onlyIdempotent
Download an attachment and decrypt it locally if it was uploaded to an encrypted room (integrity checked against the uploader's SHA-256). Text-like files (markdown, code, json, csv, txt…) up to max_inline_bytes are returned as text; images up to 4 MiB as images; other files are saved to the private data directory and the path is returned (in ephemeral mode they are returned inline as a binary resource instead, never written to disk). Identify the file by file_id (from a message's attachments) or by message_id.
| Name | Required | Description | Default |
|---|---|---|---|
| file_id | No | The attachment file_id. | |
| message_id | No | The message carrying the file. | |
| max_inline_bytes | No | Inline text limit (default 262144). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnly/idempotent/non-destructive annotations, it discloses decryption of encrypted-room uploads, integrity verification against the uploader's SHA-256, per-type size thresholds, the side effect of writing to the private data directory, and the ephemeral-mode exception where nothing is written to disk. This is exactly the extra behavioral context annotations cannot express.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core action is front-loaded and every clause carries information — type-based return behavior, size limits, disk-write policy, and identifier sourcing. It is one dense paragraph rather than scannable bullets, but there is essentially no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description carries the return-value burden and does so fully: text for text-like files, images under 4 MiB, a file path for other files, or an inline binary resource in ephemeral mode. An agent has everything needed to call and interpret the result.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: file_id and message_id are presented as alternative identifying routes, and max_inline_bytes is tied specifically to the text-like-file threshold. It doesn't document the default value or bounds for max_inline_bytes, which remain schema-only.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource ('Download an attachment and decrypt it locally'), and the mechanics (decrypt encrypted rooms, SHA-256 integrity check) make it unmistakable against siblings like get_message or get_user. An agent can tell exactly what this returns (text, image, or a saved path) without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It tells the agent how to source the identifiers — 'file_id (from a message's attachments) or by message_id' — which is genuine usage context. It does not state when to prefer this over get_message or otherwise route between alternatives, so no explicit exclusions are provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_messageGet one messageARead-onlyIdempotent
Fetch a single message by id or by a pasted Rocket.Chat permalink (…?msg=), decrypted. Messages that quote other messages list them in quotes, and the quoted messages are resolved and returned in quoted_messages.
| Name | Required | Description | Default |
|---|---|---|---|
| id | No | Message id. | |
| url | No | A Rocket.Chat message URL containing ?msg=<id>. | |
| resolve_quotes | No | Resolve quoted messages (default true). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only, idempotent, open-world, and non-destructive traits. The description adds meaningful behavior beyond them: it says the message is returned decrypted and explains that quoted messages appear in `quotes` with resolved messages in `quoted_messages`.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences with no wasted words. The core action is front-loaded, followed immediately by the special quote-resolution return behavior.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only single-message fetch with rich annotations and full schema coverage, the description covers purpose, input forms, decryption, and quote resolution. It stops short of clarifying whether id or url is required when both are optional in the schema, and it does not relate the tool to thread or list siblings.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents id, url, and resolve_quotes. The description restates the id/permalink lookup but adds no syntax or behavior beyond the schema, so the baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb ('Fetch') and resource ('a single message'), and explicitly distinguishes the singular fetch from the plural sibling get_messages. It also names the two supported lookup forms (id or Rocket.Chat permalink), so the agent can identify the tool without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage when you have a message id or permalink, but it does not state when to prefer this over get_messages, get_thread, or search, nor does it give exclusions. The context is clear enough for basic selection but lacks any explicit alternative routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_messagesRead messages in a roomARead-onlyIdempotent
Read messages of one room, decrypted, oldest-first within the returned page (newest page by default). Each message has id, ts, author, text, thread_id/reply_count, attachments and a permalink. Page backwards with before = the returned older_than_cursor. Thread replies are excluded unless include_thread_replies is true (use get_thread for a whole thread). The room is synced incrementally before reading.
| Name | Required | Description | Default |
|---|---|---|---|
| room | Yes | Room id, room name (e.g. "dev-handbook"), display name, or "@username" for a direct message. | |
| after | No | Only messages at or after this instant. | |
| limit | No | Messages to return (default 30, max 200). | |
| before | No | Only messages strictly before this instant (pass older_than_cursor to page back). | |
| include_thread_replies | No | Also include replies that live in threads. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With annotations covering read-only/idempotent safety, the description adds substantial behavioral context: decryption, ordering and pagination semantics, connection to the older_than_cursor, default exclusion of thread replies, and the fact that the room is synced incrementally before reading.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense and front-loaded: purpose first, then return shape, pagination, thread behavior, and sync note. Every sentence adds operational value for an agent.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given there is no output schema, the description helps by naming the returned fields and explaining pagination and thread behavior. Combined with annotations and a fully documented schema, it covers what an agent needs to call the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description adds useful meaning for before (linking it to the returned older_than_cursor) and for include_thread_replies (clarifying the default exclusion), though it does not add details for after or limit beyond what the schema already provides.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource ('Read messages of one room') with rich scope: decrypted, oldest-first within a page, newest page by default. It distinguishes itself from siblings by naming get_thread for whole threads and clarifying that thread replies are excluded by default.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives clear context for reading room messages and explicitly routes thread retrieval to get_thread. However, it does not compare against search or get_message, and it does not state when not to use this tool beyond the thread-reply case.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_threadRead a threadARead-onlyIdempotent
Read a whole thread: the root message and all replies in chronological order, decrypted. Accepts the root message id, any reply id, or a pasted Rocket.Chat thread/message URL. Threads are where most how-tos and decisions are discussed.
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | A Rocket.Chat URL such as https://chat.example.com/group/ops/thread/<id>. | |
| limit | No | Maximum replies (default 500). | |
| thread_id | No | Root message id (or any reply id) of the thread. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover readOnly, idempotent, non-destructive, and open-world traits, so the safety profile is handled. The description adds genuinely useful behavior beyond that: results are decrypted and returned in chronological order as a complete thread, which tells the agent what it will actually receive.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, zero fat, with the core operation front-loaded. The closing sentence is softer motivation rather than instruction, which slightly dilutes conciseness but still earns its place as retrieval context.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description responsibly characterizes the return shape (root plus chronological replies). Annotations cover safety, and parameters are fully documented, so the only minor omission is pagination/limit behavior for large threads.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents url, limit, and thread_id. The description restates that the input may be a root id, any reply id, or a pasted URL, adding slight clarity but no format or default details beyond the schema; baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Read a whole thread'), plus the exact scope returned (root message plus all replies, chronological, decrypted). An agent can distinguish this from get_message (single message) and search without ambiguity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The trailing sentence ('Threads are where most how-tos and decisions are discussed') gives motivational context for why a thread read matters, but there is no explicit when-to-use vs when-not guidance and no named alternatives such as get_message. Usage is implied rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_userLook up a userARead-onlyIdempotent
Look up a Rocket.Chat user by username or id: display name, status, status text, bio, roles and timezone offset when visible to this account.
| Name | Required | Description | Default |
|---|---|---|---|
| user_id | No | User id. | |
| username | No | Username, with or without @. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, openWorld, and non-destructive, so the safety profile is covered. The description adds a genuine behavioral nuance beyond that: fields are returned 'when visible to this account,' signaling permission-based visibility filtering.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no filler; the resource and return fields are stated immediately and nothing is redundant.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description usefully enumerates returned fields, and annotations cover the safety profile, so an agent has enough to invoke it. The only gap is that both parameters are optional with no stated behavior when neither is provided.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% with only two parameters, so the schema already documents both user_id and username. The description's 'by username or id' merely restates the parameter set without adding format or precedence rules (e.g., what happens if both or neither are supplied).
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Look up a Rocket.Chat user') and enumerates the fields returned, making it clearly distinguishable from siblings like get_message, list_rooms, and get_thread.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage by naming the two lookup keys, but never states when to prefer this tool over siblings such as search or get_messages, nor any exclusion. Usage is inferable rather than explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_roomsList roomsARead-onlyIdempotent
List the rooms (channels, private groups, direct messages) this account belongs to, with type, whether the room is end-to-end encrypted, the E2EE key status (ok | suggested | missing | undecryptable | not-encrypted | e2ee-unavailable), last activity and how much of it is indexed locally. Use it to find room ids/names for the other tools.
| Name | Required | Description | Default |
|---|---|---|---|
| type | No | Restrict to one room type. | |
| limit | No | Maximum rooms to return (default 200). | |
| filter | No | Case-insensitive substring of the room name or topic. | |
| encrypted_only | No | Only end-to-end encrypted rooms. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the read-only, idempotent, non-destructive nature of the call. The description adds what is returned (room type, E2EE status, last activity, local indexing) and enumerates E2EE key states, which is valuable context beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the verb and resource, followed by a short routing sentence. The first sentence is dense but every clause earns its place by listing returned fields and E2EE statuses.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description appropriately explains what the list contains, including the E2EE key status values. It does not cover pagination or ordering, but the schema already documents the limit parameter with its default.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all four parameters are documented in the input schema itself. The description reinforces room types but does not add syntax or behavioral details for limit, filter, or encrypted_only, so the baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb (list) and resource (rooms), and scopes it to rooms this account belongs to. It names the room types and returned attributes, making it clearly distinguishable from message, user, or file tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says to use it to find room ids/names for the other tools, which gives clear context for when to reach for it. It does not state when not to use it or name alternative discovery tools, but no sibling appears to overlap.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
searchSearch messagesARead-onlyIdempotent
Full-text search over the locally indexed, decrypted messages of all rooms (including end-to-end encrypted ones). All words must match; accents are ignored; append * for prefix search (deploy*). Filter by rooms, authors (usernames), date range or thread. Results include a «highlighted» snippet, thread info and permalink. When specific unencrypted rooms are given, the Rocket.Chat server search is also queried and merged (source: index | server | index+server). Stale rooms are synced first unless refresh=false; if Rocket.Chat is unreachable the local index still answers and notes says so.
| Name | Required | Description | Default |
|---|---|---|---|
| from | No | Limit to messages by these usernames. | |
| after | No | Only messages at or after this instant. | |
| limit | No | Maximum results (default 20). | |
| query | Yes | Words to search for, e.g. "deploy runbook" or "postgres failover". | |
| rooms | No | Limit to these rooms. | |
| before | No | Only messages strictly before this instant. | |
| refresh | No | Sync stale rooms before searching (default true). | |
| thread_id | No | Limit to one thread (root id). | |
| include_server_search | No | Also query Rocket.Chat server search for unencrypted rooms (default: only when `rooms` is given; unencrypted rooms are indexed locally anyway). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent, but the description adds substantial behavior beyond them: stale-room syncing before search, merging of local index and server results with a `source` field, and graceful offline degradation where the local index still answers and `notes` reports the condition. This is exactly the extra context annotations cannot convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Dense but every clause carries information (query syntax, filters, result shape, server-merge behavior, sync/offline fallback). It is front-loaded with the core purpose and search syntax before operational caveats, though the run-on parentheticals make it slightly heavy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 9-parameter search tool with no output schema, the description compensates by describing the return shape (highlighted snippet, thread info, permalink) and result provenance, plus sync and offline behavior. An agent has everything needed to call it and interpret results.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds query-string semantics not in the schema (all words must match, accents ignored, `*` for prefix search) and clarifies the rooms/include_server_search interaction. Some parameters (after/before, thread_id, limit) get no elaboration beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Starts with a specific verb+resource ('Full-text search over the locally indexed, decrypted messages of all rooms') and immediately scopes what is searchable, including E2E rooms. This clearly distinguishes it from siblings like get_messages, get_thread and list_rooms, which retrieve rather than search.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives strong operational context: when server search is additionally queried (only for specific unencrypted rooms), and that stale rooms are synced unless refresh=false. It does not explicitly name an alternative tool or state when NOT to use search versus get_messages, so it falls short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
syncSync the local indexARead-only
Fetch new, edited and deleted messages into the local index of decrypted messages, for one room or for all rooms. Older history is backfilled in chunks of up to max_messages (default 2000) per room per call: if note says history is incomplete, call again. Returns what changed per room. Only reads from Rocket.Chat.
| Name | Required | Description | Default |
|---|---|---|---|
| full | No | Discard the local copy and re-index from scratch. | |
| room | No | Sync only this room (default: all rooms that changed). | |
| max_messages | No | Backfill at most this many older messages per room in this call. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint=false, and destructiveHint=false; the description usefully explains why non-idempotency exists (incremental chunked backfill up to max_messages, default 2000) and what comes back ('Returns what changed per room'). It does not address the side effect of the `full` flag discarding the local copy, which is a noticeable omission for a tool whose local state is its core resource.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four short sentences, each carrying distinct load: purpose, backfill/retry mechanics, return value, and read scope. The core purpose is front-loaded with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description competently covers the return shape ('what changed per room') and the retry loop, and annotations cover safety. Slightly more detail on the response or on the `full` re-index behavior would close the remaining gap for an incremental-sync tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds meaning beyond the schema: it reveals the default chunk size (2000) and the per-room chunking behavior of max_messages, plus the room-default semantics. These behavioral defaults are not documented in the schema properties.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Fetch new, edited and deleted messages into the local index of decrypted messages') with clear scope ('for one room or for all rooms'). None of the sibling names (get_message, search, list_rooms, etc.) describe an indexing/sync operation, so the agent can distinguish this tool immediately.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives actionable operational guidance: 'if `note` says history is incomplete, call again,' which tells the agent how to handle a partial backfill rather than guessing. It lacks explicit when-not-to-use or direct sibling comparisons, but the read-only scope note signals it is not a search/retrieval alternative.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
8 tool updates
v0.1.0- First observed
get_file - First observed
get_message - First observed
get_messages - First observed
get_thread - First observed
get_user - First observed
list_rooms - First observed
search - First observed
sync
TDQS
Scored across 8 tools
Each tool targets a distinct resource or action: user lookup, single-message fetch, room timeline, thread, global search, room listing, attachment download, and index sync. The singular/plural pair get_message/get_messages is clearly differentiated by descriptions, and no tools appear to do the same thing.
Six tools follow a clear verb_noun pattern (get_user, get_message, get_file, get_messages, get_thread, list_rooms), while search and sync use bare verbs without nouns. This is a minor deviation from an otherwise predictable convention.
Eight tools is well-scoped for a read-focused Rocket.Chat client. Each tool earns its place by covering a distinct retrieval granularity (user, room, message, thread, search, file) plus index synchronization.
The read surface is comprehensive: users, rooms, messages, threads, full-text search, attachments, and index sync are all covered. The main gap is the absence of any write/send operations, and there is no tool for room member lists or bulk user listing, but core read workflows have no dead ends.
Maintenance
Related MCP Connectors
Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only
Work on your own computers from Claude or ChatGPT: run commands, edit and search files.
Search and reason over your Obsidian-style Markdown vault, right from ChatGPT.
Let Claude or ChatGPT search, read and send your WhatsApp messages over MCP. OAuth sign-in.
Related MCP Servers
- AlicenseAqualityDmaintenanceConnects Claude with Matrix/Element to read and search messages across rooms. Enables listing rooms, viewing room information, retrieving message history, and searching conversation content from your Matrix account.41MIT
- AlicenseAqualityBmaintenanceEnables Claude chats to communicate across machines or sessions via shared encrypted rooms, with tools to create, join, send, read, wait, search, and delete room messages.113 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables Claude to search locally indexed exported chat history and project docs, retrieving full messages and conversation IDs via full-text search.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables Claude to full-text search, list, and read your exported ChatGPT and Claude conversations from a local conversations.json file, running locally with read-only file access and no network requests. Supports both official export formats with automatic detection.MIT