matrix-nio-mcp

matrix-nio-mcp

A Model Context Protocol server for Matrix that gives AI assistants read/write access to Matrix rooms, semantic search over message history, and real-time message notifications.

Category
访问服务器

README

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 room
  • search_messages — search indexed message history by semantic similarity (OpenAI embeddings + cosine similarity), fuzzy sender name, time range, or any combination
  • get_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 room
  • send_message — send a text message to any joined room
  • LLM 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

Requirements

  • A Matrix account with a long-lived access token and a stable device ID
  • An OpenAI API key (for text-embedding-3-small embeddings)
  • 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 --build

The 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
MATRIX_HOMESERVER_URL yes — Homeserver URL, e.g. https://matrix.example.org
MATRIX_ACCESS_TOKEN yes — Long-lived access token
MATRIX_USER_ID yes — Full MXID, e.g. @bot:example.org
MATRIX_DEVICE_ID yes — Device ID — must be stable across restarts for E2EE
MATRIX_STORE_PATH no ~/.cache/nio-mcp/store Path for the Olm E2EE crypto database (created if absent)
MATRIX_KEY_BACKUP_FILE no — Path to an Element-exported E2EE key file; see Decrypting historical messages
MATRIX_KEY_BACKUP_PASSPHRASE no — Passphrase chosen when exporting; required when MATRIX_KEY_BACKUP_FILE is set
QDRANT_HOST no localhost Qdrant hostname (qdrant inside Docker Compose)
QDRANT_PORT no 6333 Qdrant port
QDRANT_COLLECTION no matrix_messages Qdrant collection name
OPENAI_API_KEY yes — OpenAI API key for embeddings
EMBEDDING_MODEL no text-embedding-3-small OpenAI embedding model; dimensions is only supported by text-embedding-3-* models
EMBEDDING_VECTOR_SIZE no 1536 Output dimension requested from the model and used for the Qdrant collection; see note below
WEBHOOK_URL no — OpenAI-compatible base URL for the LLM callback (e.g. https://api.openai.com/v1)
WEBHOOK_BEARER_TOKEN no — Bearer token sent in the Authorization header
WEBHOOK_PROMPT_HEADER no New Matrix messages: Text prepended once before all per-message lines
WEBHOOK_PROMPT_PER_MSG no {sender_name} ({sender}) in {room_name} ({room}): {message} Template rendered once per buffered message
WEBHOOK_MODEL no gpt-4o-mini Model name passed to the LLM
WEBHOOK_COOLDOWN_SECONDS no 300 Seconds of silence before the LLM is called; multiple messages within the window are batched
WEBHOOK_TOOLS no — Optional JSON string of tools/parameters merged into the chat completions request body
BACKFILL_LIMIT no 100 Messages fetched per page per room during startup backfill
BACKFILL_PAGES_MAX no 10 Maximum backfill pages per room; 0 = full history
MESSAGE_BUFFER_SIZE no 500 In-memory ring buffer size for get_recent_messages
MATRIX_SYNC_TIMEOUT_MS no 30000 Matrix /sync long-poll timeout in milliseconds
SSE_QUEUE_MAXSIZE no 100 Per-subscriber SSE event queue cap (oldest dropped when full)
MCP_PORT no 8000 Port for the HTTP server; MCP at /mcp, Matrix event SSE at /events, health at /health
HTTP_AUTH_TOKEN no — If set, requires Authorization: Bearer <token> for all endpoints
ALLOW_SEND_MESSAGE no false Set to true to enable the send_message tool
IGNORED_ROOMS no — Comma-separated list of Matrix room IDs to exclude from indexing, backfill, and live sync (e.g. !abc:example.org,!def:example.org); leave empty to include all rooms

Changing EMBEDDING_MODEL or EMBEDDING_VECTOR_SIZE requires 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 point QDRANT_COLLECTION at a new name), delete MATRIX_STORE_PATH/backfill_complete, then restart.

Obtaining credentials

Access token and device ID — the easiest way is via Element:

  1. Log in to Element as your bot account
  2. Go to Settings → Security & Privacy → Session Manager
  3. 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 query is omitted, up to limit matching messages are returned newest-first by timestamp with score: 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
{message} Body of the message
{sender_name} Display name of the sender
{sender} MXID of the sender
{room_name} Display name of the room
{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/events

Each 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:

  1. os.makedirs ensures the Olm store directory exists
  2. restore_login() loads credentials from env vars (works on first run — no prior session needed)
  3. Initial sync(full_state=True) anchors the sync token
  4. Backfill: for each joined room, paginate backwards from the initial sync's prev_batch token until end is absent (Matrix spec) or BACKFILL_PAGES_MAX is reached
  5. Register 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

  1. In Element, go to Settings → Security & Privacy → Export E2E room keys.

  2. Choose a passphrase and save the exported .txt file.

  3. 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-passphrase
    
  4. Start (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 to MATRIX_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_FILE and MATRIX_KEY_BACKUP_PASSPHRASE must 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_imported and 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/ -v

Integration 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.py

Do 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 coverage

Connecting 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

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选