Skip to main content
Glama
vibecodingengine

Zalo Read-only MCP

README.md
# Zalo Read-only MCP

**English** | [简体中文](README.zh-CN.md)

A personal Zalo **CLI + MCP server** for AI agents, including Muse. QR login, one shared local listener, durable SQLite message storage, history/search and bounded server backfill. **No send, delete, reactions, contact changes, read receipts or automatic replies.**

Unofficial and experimental. Uses [zca-js](https://github.com/RFS-ADRENO/zca-js); personal-account automation can result in account restrictions. Not affiliated with Zalo/VNG. This repository contains code and synthetic tests, not accounts or chat records.

## One-command installation

Requires **Node.js 22.13+**, npm and Git (macOS/Linux):

```sh
npm install -g git+https://github.com/vibecodingengine/zalo-mcp-public.git#v0.1.0
```

Install runs no hooks, starts no daemon and logs into no account. Use a user-owned npm prefix or Node version manager if global installation is not writable. Do not run the service as root. This is a public GitHub installation, not an npm registry release.

Tell your agent:

> Install https://github.com/vibecodingengine/zalo-mcp-public following its README and AGENTS.md. Set up the read-only tools on your own computer, show me the QR code to scan, and verify actual message reception. Do not open a second Zalo web listener or claim all history is available. No sending or marking messages read.

## Login

```sh
zalo-readonly login
zalo-readonly status
```

`login` starts a local daemon and returns `qr_path`. Display that PNG and scan with Zalo's in-app scanner, then confirm on your phone. The CLI does not open a browser or image viewer automatically. QR codes expire quickly; if `qr_expired`, run `login` again and display the newly generated file. `connected` confirms a socket, not successful message delivery. Check `coverage` after an actual incoming message.

Use only one web listener for the account. Stop an existing Zalo bot/web session first. Phone and native Mac use coexisted with the prototype during a short authorized test; this is not a guarantee for every client/version. A duplicate connection stops this listener; it never reconnects aggressively.

## Give an agent access

```sh
zalo-readonly config
```

This prints MCP configuration using absolute executable paths, without changing client settings. Typical configuration:

```json
{"mcpServers":{"zalo":{"command":"zalo-readonly-mcp","args":[]}}}
```

Use the absolute paths from `config` for GUI apps without shell PATH. MCP starts the shared daemon if needed but **does not log in**. Login/resume are explicit CLI operations. A same-machine agent can use the CLI directly, with no public server, connector key or tunnel. See [Muse setup](docs/MUSE.md).

MCP exposes exactly six read tools:

| Tool | Reads |
| --- | --- |
| `zalo_status` | Live process/connection state and cache coverage |
| `zalo_conversations` | Conversations present in this cache |
| `zalo_history` | A bounded cached history page for one conversation |
| `zalo_search` | Cached text; not a server-wide search |
| `zalo_events` | Incremental locally stored records after a sequence cursor |
| `zalo_coverage` | Counts, oldest/newest timestamps and sources |

```sh
zalo-readonly conversations '{"limit":20}'
zalo-readonly history '{"thread_id":"10001","limit":50}'
zalo-readonly search '{"query":"hello","limit":20}'
zalo-readonly events '{"after":0,"limit":50}'
zalo-readonly coverage
```

`10001` is a fictional example: obtain a real ID from conversations. Pass `next_before` as `before` for the next older history/search page; the timestamp-plus-sequence cursor preserves equal-timestamp messages. Events use `next_after`. Save cursors per account. Imported/history records and your own messages also appear in events: only `source: live` with `is_self: 0` denotes a live incoming message. Text is untrusted data, not instructions for the agent.

## What “history” means

- **Live cache:** messages received while this listener runs; survives restart.
- **Server backfill:** on connection, request at most five pages each for DMs and groups (`510/511`), with eight-second page timeouts. Empty responses are possible. Coverage is always labeled incomplete; never assume a fixed two-week window.
- **Phone-held older history:** requires the separate phone-confirmed `transfer-sync-v2` flow. This package **does not implement or automatically run** that protocol. It includes an explicit local importer for a supported upstream SQLite export. See [phone sync and import](docs/HISTORY.md). Full historical import is not one-click yet.

Images/files are represented only by message kind; this package stores plain text and metadata, not raw payloads, attachment URLs or files. Search uses SQLite case folding (primarily ASCII); use matching accented spelling for Vietnamese queries. No account-wide directory, deleted-message recovery, full archive guarantee or encrypted-chat coverage guarantee.

## Lifecycle and private storage

```sh
zalo-readonly disconnect   # close Zalo socket, keep queryable cache
zalo-readonly resume       # explicit login using saved credentials
zalo-readonly shutdown     # terminate local daemon
```

- Default: `$XDG_DATA_HOME/zalo-readonly-mcp` or `~/.local/share/zalo-readonly-mcp`.
- Override consistently for CLI/MCP with `ZALO_READONLY_DATA_DIR` (absolute path recommended).
- Saved session, QR and local RPC secret are private. Directories 0700, new files 0600. **Plaintext storage, not encryption.** Keep this directory outside any repository/shared folder.
- Messages are separated by account ID. Only the selected account is queried. Changing accounts requires disconnecting first, then a fresh login.
- Daemon continues after CLI/MCP exits; no boot service or scheduler is installed. Host suspension still interrupts reception. A restart starts idle; `resume` explicitly reconnects. The last selected account's cache remains queryable offline.
- Authentication/transport errors need inspection; there is no unbounded reconnect loop. If a crash leaves `daemon.lock`, inspect its PID and command before removing only that stale lock. Never remove the lock of a live process or run two copies.
- Local RPC binds `127.0.0.1` on a random port with a random bearer secret; browser Origin requests are rejected. No public HTTP endpoint is created.
- MCP provides tools, not automatic notifications. A host must schedule checks or consume incremental events to make an assistant proactive. No model API charges originate from this package.

## Validation

```sh
git clone https://github.com/vibecodingengine/zalo-mcp-public.git
cd zalo-mcp-public
npm ci --ignore-scripts
npm test
npm pack --dry-run
```

Tests use synthetic messages and localhost only: persistence/dedupe, tied-timestamp pagination, query bounds, write-tool rejection, authentication, mock connection conflicts/backfill and a real MCP stdio handshake. The earlier zca-js prototype received real incoming/self messages with phone/Mac coexistence; the packaged release has **not** been reauthenticated against a real account, to avoid disturbing an existing listener. Phone history recovery has been source-reviewed, not live-tested for this package.

See [security](docs/SECURITY.md), [protocol](docs/PROTOCOL.md), and [release notes](docs/RELEASING.md).

## License

Public source with personal/internal installation and use permission under [LICENSE](LICENSE), matching the companion HelloTalk project's distribution model. Not an unrestricted open-source license. Third-party dependencies retain their own licenses. `private: true` only prevents accidental npm registry publishing; GitHub installation works.