A Model Context Protocol server and local HTTP API for Douyin web direct messages. Playwright remains the login and verified-conversation fallback, while authenticated message reads and learned sends prefer direct HTTP/Protobuf requests.
The project uses Douyin's current standalone chat page:
https://www.douyin.com/chat?isPopup=1
Login and account status checks still use the Douyin home page. /messages currently returns a 404 page and is not used for automation.
DOUYIN_ALLOW_SENDdefaults totrue, so real sending is enabled by default unless you set it tofalse.send_messagedefaults todryRun: true. Dry runs validate the current snapshot without opening a conversation or changing page state.- A real send requires both dry-run to be disabled and
DOUYIN_ALLOW_SEND=true. - Before reading or a real send, the server verifies that the nickname is unique, the conversation position and exact nickname still match, and the open chat title matches.
- Duplicate nicknames are marked
targetable: falseand are refused by both MCP tools and the nickname-based CLI. - Once a confirmed page send has exposed the account's request shape, later sends first replay its matching
creator.douyin.com/aweme/v1/im/consistency/action/reportrequest with the new conversation, message, and action time, then use the profile-local HTTP/Protobuf send template. An explicit protocol rejection safely falls back to the verified page and relearns both templates; an unknown send outcome returnsSEND_STATUS_UNKNOWNand is never retried automatically. A top-levelOKis not treated as delivery success unless the nested message status, check code, andcheckMessage.status_codeare all present and zero; missing fields and delivery blocks such as 8101/8610 are never reported as sent. - Each browser profile has an exclusive filesystem lock to prevent concurrent Chromium instances from corrupting it. MCP, the HTTP API, and the operator CLI cannot run at the same time against the same
DOUYIN_PROFILE. - All page operations are serialized to prevent cross-conversation reads or sends.
- The project does not modify browser fingerprints or bypass verification challenges. Optional protocol reads bind the authenticated initialization snapshot to rendered conversations by exact conversation ID and verified preview text, then replay the request with Node's HTTP client and current profile cookies. Direct sends are learned only from this account's own successful request and retain the same target verification and rate limit as page sends.
- Logs are written to stderr and redact message bodies, cookies, and password fields.
Douyin's rendered conversation DOM does not expose a supported stable conversation ID, user ID, sec_uid, or stable profile link. Therefore:
conversationKeyis opaque and valid only for the latestlist_conversationssnapshot.- Calling
list_conversationscreates new keys and immediately expires every key from the previous snapshot. - Every conversation returns
stableKey: false; duplicate nicknames additionally returntargetable: false. - Call
list_conversationsbefore callingread_messagesorsend_message, then use a key from that exact result. - The conversation list contains only items currently rendered by the browser;
completeis alwaysfalse. - Fuzzy nickname matching, bulk sending, stranger search, and search-to-send fallbacks are intentionally unsupported.
Detailed live-page evidence is recorded in RESEARCH.md.
- Node.js 20 or newer
- npm
- A desktop environment capable of displaying Chromium for the initial QR-code login
npm install
npx playwright install chromium
npm run build| Environment variable | Default | Description |
|---|---|---|
DOUYIN_PROFILE |
default |
Profile name; letters, numbers, underscores, and hyphens only |
DOUYIN_HEADLESS |
false |
Run Chromium headlessly; keep this false for initial login |
DOUYIN_ALLOW_SEND |
true |
Allow real message sends |
DOUYIN_DEBUG |
false |
Enable debug logging |
DOUYIN_PROTOCOL_PROBE |
false |
Log redacted HTTP/WebSocket protocol shapes for private-message research |
DOUYIN_PROTOCOL_READ |
true |
Enable authenticated protocol I/O: verified Protobuf reads and learned HTTP sends, with page fallback |
DOUYIN_NAVIGATION_TIMEOUT_MS |
60000 |
Navigation timeout in milliseconds |
DOUYIN_ACTION_TIMEOUT_MS |
10000 |
Page action timeout in milliseconds |
DOUYIN_MIN_SEND_INTERVAL_MS |
3000 |
Minimum interval between send attempts |
DOUYIN_API_HOST |
127.0.0.1 |
HTTP API bind address |
DOUYIN_API_PORT |
3000 |
HTTP API port |
DOUYIN_API_KEY |
unset | Bearer key, minimum 16 characters; required for non-loopback binding |
These variables are read from the process environment. The project does not load .env. Use .env.example as a reference, then export the values in your shell or set them in the MCP client env block.
Browser data is stored in:
.data/profiles/<DOUYIN_PROFILE>
After message direction has been verified against the rendered conversation, the service stores
the sender ID in .sender-identity.json inside that profile. The file contains no cookies: it binds
the sender ID to a SHA-256 fingerprint of the profile's authenticated session. A different login
invalidates the cached identity and safely returns reads to DOM calibration.
After a confirmed page send, the service also stores the paired action-report and send requests in
direct-send-template.json in the same ignored profile directory. The file contains no cookies: it
binds the learned requests to a SHA-256 fingerprint of the profile's authenticated session, is
written only after the page send is confirmed, and is deleted automatically when Douyin rejects it
or a different login is detected. Do not copy it between accounts.
This directory contains authentication data. Do not commit or share it.
For first use or an expired session, run:
npm run loginScan the displayed QR code with Douyin. After login, the script prints structured status, closes Chromium safely, and keeps the authenticated session in the persistent profile.
Check the current session:
npm run statusExample successful result:
{
"ok": true,
"browserRunning": true,
"loggedIn": true,
"currentUrl": "https://www.douyin.com/jingxuan"
}The compiled entry point is:
node dist/index.jsCodex CLI example:
codex mcp add douyin-dm -- node /absolute/path/to/douyin-dm-mcp/dist/index.jsGeneric MCP client configuration:
{
"mcpServers": {
"douyin-dm": {
"command": "node",
"args": ["/absolute/path/to/douyin-dm-mcp/dist/index.js"],
"env": {
"DOUYIN_PROFILE": "default",
"DOUYIN_ALLOW_SEND": "true"
}
}
}
}For an authorized real send, ensure DOUYIN_ALLOW_SEND is true for that MCP process. Set it to false to disable sending, and restart the process for environment changes to take effect.
Do not start this process while the HTTP API or CLI already holds the same profile lock.
Compiled entry point. Run npm run build first:
npm run apiFrom source:
npm run dev:apiDo not start this process while MCP or the CLI already holds the same profile lock.
The default base URL is http://127.0.0.1:3000. The unauthenticated health check is:
curl http://127.0.0.1:3000/healthAPI routes:
| Method | Path | Input | Purpose |
|---|---|---|---|
GET |
/health |
None | Process liveness; no auth, no browser |
GET |
/api/v1/status |
None | Login / browser session |
GET |
/api/v1/conversations?limit=20 |
Query parameter limit, 1–100 |
Current rendered snapshot + new keys |
POST |
/api/v1/messages/read |
JSON { "conversationKey": "...", "limit": 20 } |
Visible messages for a snapshot key |
POST |
/api/v1/messages/send |
JSON { "conversationKey": "...", "text": "...", "dryRun": true } |
Dry-run by default; real send needs both gates |
POST requests require Content-Type: application/json. Sending remains a dry run by default. A real send still requires both "dryRun": false and DOUYIN_ALLOW_SEND=true.
Example:
curl "http://127.0.0.1:3000/api/v1/conversations?limit=20"
curl -X POST http://127.0.0.1:3000/api/v1/messages/read \
-H "Content-Type: application/json" \
-d '{"conversationKey":"fallback:...:0","limit":20}'Loopback access does not require an API key. Binding to any other host is refused unless DOUYIN_API_KEY is set to at least 16 characters. When configured, send it on every /api/v1/* request:
curl http://127.0.0.1:3000/api/v1/status \
-H "Authorization: Bearer YOUR_API_KEY"The API returns the same structured success and Douyin error objects as MCP. Request parsing errors use INVALID_REQUEST, INVALID_JSON, UNSUPPORTED_MEDIA_TYPE, or PAYLOAD_TOO_LARGE; authentication failures use UNAUTHORIZED.
Checks whether the persistent Douyin browser profile is authenticated.
Input: none.
Opens the standalone chat page and returns currently rendered conversations with opaque conversationKey values for the new snapshot.
{
"limit": 20
}Conversation fields:
conversationKeystableKey, currently alwaysfalsepositionnicknameavatarUrl, the rendered counterpart avatar URL when availablepreviewtimestamptargetable,falsewhen duplicate nicknames make safe selection impossible
Reads currently visible messages from a conversation returned by list_conversations.
{
"conversationKey": "fallback:550e8400-e29b-41d4-a716-446655440000:0",
"limit": 20
}Message fields:
direction:incomingoroutgoing, from verified sender-side DOM evidencetype:text, orunsupportedfor unrecognized message typescontent: visible text, ornullwhen empty- Returned oldest to newest.
limitselects the newest N currently rendered messages.
Conversations with targetable: false are refused.
Sends one message to a verified conversation.
{
"conversationKey": "fallback:550e8400-e29b-41d4-a716-446655440000:0",
"text": "Test message",
"dryRun": true
}A real send requires all of the following:
DOUYIN_ALLOW_SEND=true.dryRun=false.- The target nickname is unique in the current snapshot.
- The conversation position and exact nickname still match the snapshot.
- A verified protocol target is available for direct sending; otherwise the open chat title must exactly match the target nickname.
- The message has no leading or trailing whitespace.
- The logical Slate editor text exactly matches the requested text.
The first send without a learned protocol template uses the page and records its paired action-report and send requests only after that page send is confirmed. Later sends replay the action report first and only call the send endpoint when that report succeeds. If Douyin explicitly rejects either stale template before sending, the service safely returns to the page once and relearns both. A top-level protocol OK is only accepted when the nested message status, check code, and JSON status_code are all present and indicate success; missing fields return SEND_STATUS_UNKNOWN, and delivery blocks such as 8101/8610 return SEND_FAILED without a DOM retry. If either path has an unknown outcome, it returns SEND_STATUS_UNKNOWN; callers must inspect the conversation manually instead of retrying automatically. The minimum send interval is retained across conversation-list refreshes.
List currently rendered conversations:
npm run chat -- listRead messages by an exact, unique nickname:
npm run chat -- read "Exact nickname"Real sends also require DOUYIN_ALLOW_SEND. PowerShell example:
$env:DOUYIN_ALLOW_SEND="true"
npm run chat -- send "Exact nickname" "Test message"
Remove-Item Env:DOUYIN_ALLOW_SENDThe CLI accepts exact nicknames only and refuses to continue when no match or multiple matches are found.
Do not run the CLI while MCP or the HTTP API already holds the same profile lock.
npm run lint
npm test
npm run build
npm run smoke:mcpTests cover configuration parsing, structured errors, profile locking, page-operation serialization, snapshot expiry, duplicate refusal, target verification, message direction, dry-run isolation, composer rollback, successful send confirmation, unknown send status, persistent rate limiting, and package-safe defaults.
src/
browser/ Browser lifecycle, profile locking, and operation serialization
douyin/ DouyinService, centralized selectors, and page objects
index.ts MCP stdio server
api.ts HTTP API process entry point
api/ Versioned HTTP routes, validation, and authentication
scripts/
login.ts QR-code login
status.ts Authentication status check
chat.ts Operator CLI
mcp-smoke.ts MCP transport smoke check
tests/unit/ Repeatable behavioral tests
RESEARCH.md Live-page evidence and engineering research
Licensed under the permissive MIT License.