MCP server that gives Claude Code full access to your MAX messenger account — read chats, dump channels, send messages and files.
MAX (max.ru) is a Russian messenger by VK with ~50 M users. This server uses a user session (not a bot token), so it can access your DM list, arbitrary channels, and post on your behalf — things the official Bot API cannot do.
Model Context Protocol is an open standard by Anthropic that lets AI assistants call external tools. This server speaks the MCP stdio transport and plugs directly into Claude Code (claude CLI / desktop app).
The official MAX Bot API (platform-api.max.ru) can only act as a bot: it receives messages directed at the bot and replies in its own chats. It cannot:
- list your personal DMs
- read channels you didn't create
- send messages as you (not as a bot)
For SMM analytics and content workflows you need a user-level session — the same identity as your own MAX account. This server achieves that through maxapi-python (PyMax), an unofficial wrapper that speaks MAX's internal WebSocket protocol.
Risk: PyMax technically violates MAX Terms of Service. Use a dedicated SMM account, not your personal one.
~/Documents/claude-projects/max-mcp/ ← code (permanent)
pyproject.toml
uv.lock
src/max_mcp/
server.py # FastMCP stdio entrypoint
client.py # WebClient/Client singleton + lifespan
auth.py # CLI for login-qr / login-sms
normalize.py # pymax models → plain JSON dicts
tools/
chats.py # list_chats, get_chat
messages.py # read_messages, search_messages
channels.py # list_channel_posts, dump_channel
send.py # send_message, send_file
~/.max-mcp/ ← session (permanent, secret)
session.db # pymax SQLite session — chmod 600
session.kind # "web" or "sms" — picks client type at start
session.phone # E.164 phone if kind=sms — chmod 600
Code and session live in separate locations on purpose: the code can be deleted and rebuilt without losing your MAX login.
- macOS / Linux
- Python 3.11 – 3.13
- uv (
brew install uvorcurl -LsSf https://astral.sh/uv/install.sh | sh) - Claude Code CLI (
npm install -g @anthropic-ai/claude-code)
git clone https://github.com/renosaza/max-mcp.git ~/Documents/claude-projects/max-mcp
cd ~/Documents/claude-projects/max-mcp
uv syncPick one method:
QR code (requires mobile MAX app):
uv run python -m max_mcp.auth login-qrAn ASCII QR appears in your terminal. In the MAX mobile app: Settings → Devices → Link Device → scan. On success you'll see:
Logged in as: Your Name (id=123456789)
SMS + password (no phone app needed):
uv run python -m max_mcp.auth login-sms --phone +79991234567PyMax prompts interactively for the SMS code (arrives in MAX or as SMS) and, if 2FA is enabled, your password.
claude mcp add max-mcp --scope user \
-- uv run --directory ~/Documents/claude-projects/max-mcp python -m max_mcp.serverVerify it's connected:
claude mcp list
# max-mcp: uv run --directory ... — ✓ ConnectedIn an active Claude Code session: /mcp → reconnect, or open a new chat.
All IDs (chat_id, channel_id) are integers. Discover them via list_chats.
Returns your chat list. Cursor-paginated: pass next_marker from the previous response to get the next page.
{
"chats": [
{
"id": 123456789,
"title": "Team chat",
"type": "CHAT",
"last_event_time": 1717520000000,
"participants_count": 12,
"description": null,
"owner_id": 987654321
}
],
"next_marker": 1717510000000
}Chat type values: DIALOG, CHAT, CHANNEL.
API note: last_event_time is a unix timestamp in milliseconds. The marker cursor is timestamp-based (not ID-based).
Full info on a single chat or channel.
{
"id": -69531237780213,
"title": "My Channel",
"type": "CHANNEL",
"last_event_time": 1717520000000,
"participants_count": 4200,
"description": "Channel description",
"owner_id": 100500
}Message history, newest-first. Paginate backward by passing next_before_time from the previous response as before_time.
{
"messages": [
{
"id": 116691985249148380,
"chat_id": null,
"sender": 5267903,
"text": "Hello!",
"time": 1717520000123,
"type": "USER",
"reply_to_id": null,
"attaches": [...]
}
],
"next_before_time": 1717519000000
}next_before_time is null when the history is exhausted.
API notes:
timeis unix milliseconds.senderis a numeric user ID (no name — useget_chatfor group metadata).chat_idin messages may benull(DMs don't include it in pymax 2.1.2).attachesis only present when the message has attachments. Shape varies by type:PHOTO,VIDEO,FILE,CALL,INLINE_KEYBOARD,SHARE,CONTROL.reply_to_idis populated only whentype == "REPLY"(usesprev_message_idfield).
Client-side substring search — MAX has no native server-side search.
Walks message history backward in batches of 100, filters case-insensitively. Returns matching messages up to limit.
{
"messages": [...],
"scanned": 342,
"scan_exhausted": false
}scan_exhausted: true means the scan hit scan_limit without reaching the beginning of history. Increase scan_limit and paginate.
API note: This can be slow on large chats (1 API call per 100 messages). For chats with thousands of messages, use a reasonable scan_limit (e.g. 2000) and expect a few seconds of latency.
Channel posts with reactions and stats. Same pagination as read_messages.
{
"posts": [
{
"id": 116691985249148380,
"sender": 5267903,
"text": "Post text",
"time": 1717520000123,
"type": "USER",
"reaction_info": {...},
"stats": {"views": 1234}
}
],
"next_before_time": 1717519000000
}API note: stats shape is opaque — pymax surfaces the raw dict from the MAX server. The views key is present on most posts, but not guaranteed. reaction_info is also raw.
Bulk dump for analytics. Hard cap of 1000 posts per call.
{
"posts": [...],
"count": 1000,
"stopped_reason": "max_posts"
}stopped_reason values:
max_posts— hit themax_postscap (call again with smallerbefore_time)since_time— reached the requested time boundaryexhausted— reached the beginning of channel history
Full archive pattern:
# pseudocode for Claude to iterate
before_time = None
all_posts = []
while True:
result = dump_channel(channel_id=X, max_posts=1000, before_time=before_time)
all_posts.extend(result["posts"])
if result["stopped_reason"] != "max_posts":
break
before_time = min(p["time"] for p in result["posts"]) - 1Send a text message. Optionally reply to a specific message by ID.
{
"id": 116692035197621360,
"sender": 309991366,
"text": "Hello!",
"time": 1717520000000,
"type": "USER",
"reply_to_id": null
}Send a file. Attachment type is chosen by extension:
| Extensions | MAX type |
|---|---|
.jpg .jpeg .png .gif .webp .bmp |
Photo |
.mp4 .mov .webm |
Video |
| anything else | File |
file_path must be an absolute path to a regular file (no symlinks).
Security: By default only paths inside ~/Downloads, ~/Documents, and /tmp are allowed. Paths under ~/.ssh, ~/.aws, ~/.max-mcp, /etc, /var, and macOS Keychains are always denied. Override the allowlist:
export MAX_MCP_SEND_ROOTS=~/Desktop:/data/exports| Field | Type | Notes |
|---|---|---|
id |
int | Message / post ID (int64) |
sender |
int | Sender user ID |
time |
int | Unix timestamp, milliseconds |
type |
str | "USER", "REPLY", "BOT", etc. |
reply_to_id |
int | null | Populated only when type == "REPLY" |
attaches |
list | Present only if attachments exist |
last_event_time |
int | Chat's last activity, milliseconds |
participants_count |
int | Members in chat/channel |
rm ~/.max-mcp/session.db ~/.max-mcp/session.kind ~/.max-mcp/session.phone
uv run --directory ~/Documents/claude-projects/max-mcp python -m max_mcp.auth login-qr
# or login-smsDelete all session files (above) and re-run the desired auth flow. The session type is stored in ~/.max-mcp/session.kind and determines whether WebClient (QR) or Client (SMS) is used at startup.
uv run --directory ~/Documents/claude-projects/max-mcp python -m max_mcp.serverErrors surface to stderr — pymax exceptions are not swallowed.
Then in Claude Code: /mcp → reconnect.
The default MCP stdio frame warns at ~10 k tokens and caps around 25 k. For dump_channel(max_posts=1000) on busy channels this can be exceeded. Set in your environment:
export MAX_MCP_OUTPUT_TOKENS=80000| Limitation | Workaround |
|---|---|
| No native server-side search | search_messages scans client-side. Increase scan_limit for deeper history coverage. |
marker for list_chats is timestamp-based (assumed from pymax internals) |
Works in practice; may misbehave if MAX ever changes the semantics. |
stats / reaction_info schema is undocumented |
Inspect the raw output on real channel posts. |
reply_to_id requires type == "REPLY" (string, uppercased) |
If pymax changes the enum serialization, replies will always return null. |
| Sessions don't auto-refresh indefinitely | Re-run login-qr / login-sms when the session expires. |
dump_channel hard cap = 1000 posts/call |
Call repeatedly with decreasing before_time. |
sender is a numeric ID, not a name |
MAX has no public user-lookup endpoint in the user-session API. |
chat_id is null in DM messages |
This is a pymax 2.1.2 behaviour for direct messages. |
~/.max-mcp/is createdchmod 700; files arechmod 600.- Session files are written with
O_CREAT|O_EXCL|O_NOFOLLOW(no TOCTOU, no symlink follow). send_filevalidates paths against a denylist (~/.ssh,~/.aws, etc.) and an allowlist (~/Downloads,~/Documents,/tmpby default). Symlinks are rejected at the leaf.- The server runs locally over stdio — no network port is opened.
mcp[cli] >= 1.0— Anthropic MCP SDK (FastMCP, stdio transport)maxapi-python >= 2.1.2— PyMax, unofficial MAX WebSocket wrapper (MIT)pydantic >= 2.6- Python 3.11 – 3.13
uv.lock pins the full transitive closure (e.g. mcp==1.27.2, qrcode==8.2).
PyMax uses MAX's internal WebSocket API. Practical risks:
- Account suspension — VK may detect automation patterns and suspend the account.
- API drift — MAX can change its protocol without notice; PyMax may break until updated.
- Rate limits — not publicly documented; pace your
dump_channelcalls.
Mitigation: use a dedicated SMM account, not your personal one. Don't send high-volume outgoing messages.
MIT