matrix-nio-mcp
Provides tools for interacting with Matrix, enabling AI assistants to read/write messages, search message history, and receive real-time notifications in Matrix rooms.
Uses OpenAI embeddings for semantic search and optionally calls an OpenAI-compatible API for message summarization and action-item extraction.
Click on "Install 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., "@matrix-nio-mcpShow me recent messages in the dev room."
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.
nio-mcp
A Model Context Protocol server for Matrix, built on matrix-nio. Gives AI assistants read/write access to Matrix rooms, semantic search over message history, and real-time message notifications.
Features
get_recent_messages— fetch the most recent messages across all joined rooms, with optional filtering by exact MXID sender or roomsearch_messages— search indexed message history by semantic similarity (OpenAI embeddings + cosine similarity), fuzzy sender name, time range, or any combinationget_message_context— retrieve messages surrounding a specific event (useful after a search hit)get_room_info— return the friendly display name and full member list (MXID + display name) for a roomsend_message— send a text message to any joined roomLLM callback — call any OpenAI-compatible endpoint with a configurable prompt after a cooldown period; multiple messages are batched; also stream events via SSE
E2EE support — works with encrypted rooms via libolm
Backfill on startup — indexes historical messages from all joined rooms before going live
Related MCP server: Lspace MCP Server
Requirements
A Matrix account with a long-lived access token and a stable device ID
An OpenAI API key (for
text-embedding-3-smallembeddings)Docker and Docker Compose (for running the server and Qdrant)
Quick start
git clone <this repo>
cd nio-mcp
cp .env.example .env
# Edit .env — fill in MATRIX_*, OPENAI_API_KEY at minimum
docker compose up --buildThe MCP server is available at http://localhost:8000/mcp (Streamable HTTP transport). The Matrix event SSE stream is at http://localhost:8000/events.
Configuration
All configuration is via environment variables. Copy .env.example to .env and fill in the required values.
Variable | Required | Default | Description |
| yes | — | Homeserver URL, e.g. |
| yes | — | Long-lived access token |
| yes | — | Full MXID, e.g. |
| yes | — | Device ID — must be stable across restarts for E2EE |
| no |
| Path for the Olm E2EE crypto database (created if absent) |
| no | — | Path to an Element-exported E2EE key file; see Decrypting historical messages |
| no | — | Passphrase chosen when exporting; required when |
| no |
| Qdrant hostname ( |
| no |
| Qdrant port |
| no |
| Qdrant collection name |
| yes | — | OpenAI API key for embeddings |
| no |
| OpenAI embedding model; |
| no |
| Output dimension requested from the model and used for the Qdrant collection; see note below |
| no | — | OpenAI-compatible base URL for the LLM callback (e.g. |
| no | — | Bearer token sent in the |
| no |
| Text prepended once before all per-message lines |
| no |
| Template rendered once per buffered message |
| no |
| Model name passed to the LLM |
| no |
| Seconds of silence before the LLM is called; multiple messages within the window are batched |
| no | — | Optional JSON string of tools/parameters merged into the chat completions request body |
| no |
| Messages fetched per page per room during startup backfill |
| no |
| Maximum backfill pages per room; |
| no |
| In-memory ring buffer size for |
| no |
| Matrix |
| no |
| Per-subscriber SSE event queue cap (oldest dropped when full) |
| no |
| Port for the HTTP server; MCP at |
| no | — | If set, requires |
| no |
| Set to |
| no | — | Comma-separated list of Matrix room IDs to exclude from indexing, backfill, and live sync (e.g. |
Changing
EMBEDDING_MODELorEMBEDDING_VECTOR_SIZErequires wiping the Qdrant collection and re-syncing from scratch. The collection is created at startup with the configured vector size; vectors already stored at a different dimension will cause Qdrant errors that cannot be recovered without dropping the collection. To reset: stop the server, delete the Qdrant collection (or pointQDRANT_COLLECTIONat a new name), deleteMATRIX_STORE_PATH/backfill_complete, then restart.
Obtaining credentials
Access token and device ID — the easiest way is via Element:
Log in to Element as your bot account
Go to Settings → Security & Privacy → Session Manager
Copy the access token and session/device ID for your current session
Alternatively, call the Matrix login endpoint directly:
curl -XPOST 'https://matrix.example.org/_matrix/client/v3/login' \
-H 'Content-Type: application/json' \
-d '{"type":"m.login.password","user":"@bot:example.org","password":"secret"}'The response contains access_token and device_id.
MCP tools
get_recent_messages
Returns the k most recent messages from the in-memory buffer (populated by backfill and live sync). During the initial startup backfill, this endpoint returns an empty array until the backfill phase has finished populating the buffer.
{
"k": 20,
"sender": "@alice:example.org",
"room_id": "!abc123:example.org"
}sender and room_id are optional filters. sender must be an exact MXID (e.g. @alice:example.org) — partial names are not matched. Returns a list of message objects:
[
{
"event_id": "$abc:example.org",
"room_id": "!abc123:example.org",
"sender": "@alice:example.org",
"body": "Hello!",
"timestamp": 1700000000000
}
]search_messages
Search indexed messages by semantic similarity, sender, time range, or any combination of those. At least one of query, sender, after_ts, or before_ts must be provided.
{
"query": "project standup notes",
"sender": "fred",
"limit": 10,
"after_ts": 1700000000000,
"before_ts": 1700086400000
}query— natural-language search; embedded with OpenAI and matched by cosine similarity against Qdrant.sender— fuzzy sender filter. A full MXID (@alice:example.org) is matched exactly; anything else (e.g.alice,fred) uses word search against the sender's MXID, display name, and localpart variants.after_ts/before_ts— Unix millisecond timestamps (optional). Filter results to a time window.If
queryis omitted, up tolimitmatching messages are returned newest-first by timestamp withscore: 0.
Returns the same message fields as get_recent_messages plus a score (cosine similarity, 0–1, or 0 for time-only queries). Use the returned event_id and room_id with get_message_context to retrieve surrounding messages.
get_message_context
Fetches messages before and after a specific event via the Matrix /context endpoint.
{
"room_id": "!abc123:example.org",
"event_id": "$found_event:example.org",
"before": 5,
"after": 5
}get_room_info
Returns the friendly display name and full member list for a room, read from nio's in-memory room state populated during initial sync.
{
"room_id": "!abc123:example.org"
}Returns:
{
"room_id": "!abc123:example.org",
"name": "My Room",
"members": [
{"user_id": "@alice:example.org", "display_name": "Alice"},
{"user_id": "@bob:example.org", "display_name": "Bob"}
]
}send_message
Sends a plain-text message to a room.
{
"room_id": "!abc123:example.org",
"body": "Hello from the MCP server!"
}Webhooks
LLM callback
When WEBHOOK_URL is set, an OpenAI-compatible chat-completions request is sent after a configurable cooldown period with no new messages (default 5 minutes). Multiple messages arriving within the cooldown window are batched into a single call.
POST {WEBHOOK_URL}/chat/completions
Authorization: Bearer {WEBHOOK_BEARER_TOKEN}
Content-Type: application/json
{
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": "<rendered prompt>" }]
}WEBHOOK_PROMPT_HEADER is prepended once. WEBHOOK_PROMPT_PER_MSG is rendered for every buffered message and the results are joined with newlines. Braces inside message bodies are never re-interpreted as placeholders.
Placeholder | Value |
| Body of the message |
| Display name of the sender |
| MXID of the sender |
| Display name of the room |
| Room MXID |
Example:
WEBHOOK_PROMPT_HEADER=Summarize these Matrix messages and list action items:
WEBHOOK_PROMPT_PER_MSG={sender_name} said: {message}Produces a user message like:
Summarize these Matrix messages and list action items:
Alice said: Can we move the standup?
Bob said: Sure, how about 10am?SSE stream
Connect to http://localhost:8000/events to receive a live stream of new messages:
curl -N http://localhost:8000/eventsEach event is a JSON-encoded message object. Multiple clients can connect simultaneously — each gets its own independent stream. If a client falls behind by more than SSE_QUEUE_MAXSIZE events, the oldest queued events are dropped (the stream remains live but is lossy under load).
Health check
curl http://localhost:8000/health
# {"status":"ok"}Architecture
┌──────────────────────────────────────────────────────────┐
│ nio-mcp process │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ FastAPI :8000 │ │
│ │ /mcp (MCP Streamable HTTP) │ │
│ │ /events (Matrix message SSE fan-out) │ │
│ │ /health │ │
│ └───────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ MatrixMCPClient │ │
│ │ (nio AsyncClient) │ │
│ └──────┬─────────────────┘ │
│ │ │
│ ┌────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────▼───┐ ┌─────▼────┐ ┌─────▼───────────┐ │
│ │Qdrant │ │ OpenAI │ │WebhookDispatcher│ │
│ │vector │ │embeddings│ │ LLM call + SSE │ │
│ │store │ │ │ │ per-subscriber │ │
│ └────────┘ └──────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────────┘Startup sequence:
os.makedirsensures the Olm store directory existsrestore_login()loads credentials from env vars (works on first run — no prior session needed)Initial
sync(full_state=True)anchors the sync tokenBackfill: for each joined room, paginate backwards from the initial sync's
prev_batchtoken untilendis absent (Matrix spec) orBACKFILL_PAGES_MAXis reachedRegister live message callback, then
sync_forever(since=<initial token>)— no gap between backfill and live
E2EE note: On a brand-new deployment the Olm store is empty, so messages encrypted before this device joined cannot be decrypted. New messages in encrypted rooms will be decryptable once device trust is established. Plaintext rooms are unaffected. To recover historical encrypted messages see Decrypting historical messages.
Decrypting historical messages in encrypted rooms
Matrix Megolm session keys are distributed once at send-time to all devices present in the room. Because the bot's device wasn't present when those sessions were created, it cannot decrypt historical ciphertext — the homeserver only stores encrypted blobs.
Element (and other standard clients) let you export all session keys you hold to an encrypted file. Importing that file into nio-mcp gives the bot the keys it needs to decrypt backfilled history.
One-time setup
In Element, go to Settings → Security & Privacy → Export E2E room keys.
Choose a passphrase and save the exported
.txtfile.Mount the file into the container and set the two config variables:
services: nio-mcp: volumes: - ./element_keys.txt:/data/element_keys.txt:ro environment: MATRIX_KEY_BACKUP_FILE: /data/element_keys.txt MATRIX_KEY_BACKUP_PASSPHRASE: your-export-passphraseStart (or restart) the bot. On the first run the keys are imported into the Olm store and a sentinel file (
key_backup_imported) is written toMATRIX_STORE_PATH. Subsequent restarts skip the import automatically — the env vars and volume mount can be left in place or removed; either way the import will not run again.
Notes
Both
MATRIX_KEY_BACKUP_FILEandMATRIX_KEY_BACKUP_PASSPHRASEmust be set together.The import happens before backfill, so session keys are available when historical messages are paginated.
If the file path or passphrase is wrong, startup fails with a clear error rather than silently skipping messages.
To force a re-import (e.g. after exporting a newer key file), delete
MATRIX_STORE_PATH/key_backup_importedand restart.
Development
Running tests
Unit tests have no external dependencies:
python -m venv .venv
source .venv/bin/activate
pip install matrix-nio mcp qdrant-client openai fastapi "uvicorn[standard]" \
pydantic-settings httpx anyio sse-starlette \
pytest pytest-asyncio pytest-mock respx
pytest tests/unit/ -vIntegration tests must be run through the helper script, because it brings up the Docker-backed Matrix homeserver and Qdrant before invoking pytest:
scripts/test-matrix-integration.sh
# Still use the script entry point even when focusing on one integration file
scripts/test-matrix-integration.sh tests/integration/test_qdrant_integration.pyDo not invoke the integration tests with bare pytest unless you have already recreated that environment yourself. The script is the supported entry point; it starts docker-compose.integration.yml, waits for Synapse and Qdrant, runs pytest tests/integration -v "$@", and tears the stack down again.
Project layout
src/nio_mcp/
├── config.py # Pydantic Settings
├── models.py # MessageRecord, SearchResult
├── embeddings.py # OpenAI embedding client
├── vector_store.py # Qdrant wrapper
├── matrix_client.py # nio AsyncClient wrapper
├── webhook.py # HTTP POST + SSE dispatcher
└── server.py # MCP server + FastAPI app
tests/
├── unit/ # All external I/O mocked
└── integration/ # Real Qdrant + optional real Matrix homeserver coverageConnecting to Claude Desktop
Start the server with docker compose up --build, then point Claude Desktop at the HTTP endpoint by adding to claude_desktop_config.json:
{
"mcpServers": {
"matrix": {
"url": "http://localhost:8000/mcp"
}
}
}License
MIT
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/james-choncholas/matrix-nio-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server