tiktok
TikTok — Navigation via chrome-bridge MCP
TikTok is a client-rendered React SPA on top of a logged-in Chrome session. The bridge profile owns the cookies. Do not attempt programmatic login. If /profile ends up at /login, stop and tell the user — the session expired.
TikTok's stable selector hooks are data-e2e="..." attributes; CSS class names contain rotating hashes (e.g. epprvxn10) and should only be matched as substrings via [class*="..."].
How this skill is laid out
Each capability has a focused JS file under scripts/. To use one:
Readthe script file to load it as a string.- Pass the contents as the
codeargument tomcp__chrome-bridge__execute_script. - The script returns a structured object — see the header comment at the top of each file for the exact return shape.
Scripts are pure JS, no escaping needed; the comment block at the top of each file documents pre-conditions and return shape so you don't need to grep through the body.
Capability map
| Task | Pre-condition | Script | Notes |
|---|---|---|---|
| Verify login | navigate('https://www.tiktok.com/profile') + 3-4s |
scripts/login_check.js | URL after redirect is the authoritative signal — do NOT rely on top-login-button absence. |
| Run a search | navigate('https://www.tiktok.com/search?q=' + encodeURIComponent(query)) + 4-5s |
scripts/search_results.js | Direct URL only. The DOM input flow is React-controlled and dom_click on the submit button silently no-ops. Tab variants: /search/video?q=, /search/user?q=. |
| Video stats + author profile stats (single nav) | navigate('https://www.tiktok.com/@<handle>/video/<id>') + 5s |
scripts/video_metadata.js | Reads from rehydration JSON — exact integer counts, includes playCount (not in DOM) and full author stats. |
| Video stats — DOM fallback | same as above | scripts/video_metadata_dom.js + scripts/parse_count.js | Use only if the rehydration JSON path returns ok:false. Display strings need parseCount. |
| Author profile stats | navigate('https://www.tiktok.com/@<handle>') + 5s |
scripts/profile_stats.js | Skip this nav if you already have a video page open — video_metadata.js returns the same author stats. |
| Author profile stats — DOM fallback | same as above | scripts/profile_stats_dom.js + scripts/parse_count.js | Display strings; use parse_count.js. |
| Author latest N videos | navigate('https://www.tiktok.com/@<handle>') + 5s |
scripts/profile_latest_videos.js | The post grid is NOT in the rehydration JSON — DOM scrape is the only path. Filters out pinned posts so first 3 are truly latest. |
| All comments + replies (preferred) | navigate('https://www.tiktok.com/@<handle>/video/<id>') + 5s |
scripts/comments_api.js | Calls TikTok's web API directly. No _signature needed; cookies + device_id are enough. ~100× faster than DOM scraping. |
| All comments — DOM fallback | navigate + click [data-e2e="comment-icon"] + 2s |
scripts/comments_dom_extract.js + scripts/comments_dom_step.js | Use only if the API path returns ok:false. Loop pattern below. |
DOM comments fallback loop
When comments_api.js returns { ok:false }, drive the DOM fallback like this:
# Pre-condition: comment panel open (dom_click on [data-e2e="comment-icon"], wait ~2s)
seen = {}
plateau = 0
HARD_CAP = 1000
MAX_PASSES = 80
for i in range(MAX_PASSES):
execute_script(code=<contents of scripts/comments_dom_step.js>)
# wait 2-3s — too fast and TikTok skips a load batch
rows = execute_script(code=<contents of scripts/comments_dom_extract.js>)
before = len(seen)
for r in rows:
seen.setdefault(r['key'], r) # synthesized dedup key
if len(seen) == before:
plateau += 1
if plateau >= 2: # two passes with zero new rows → done
break
else:
plateau = 0
if len(seen) >= HARD_CAP:
break
comments = list(seen.values())
Each pass adds roughly 20 top-level comments. Coverage check: if the sum of level-2 rows under a parent is less than the parent's replyCountAdvertised, you haven't expanded everything yet — keep stepping.
Why the API path doesn't need _signature
TikTok's web app reads comments through /api/comment/list/ and /api/comment/list/reply/. Both are GET endpoints. No _signature / X-Bogus is required for these read paths — auth rides entirely on session cookies (msToken, ttwid, etc.) plus a single device_id URL param. Because chrome-bridge runs inside a real logged-in Chrome session, fetch(..., credentials: 'include') from execute_script carries those cookies automatically. device_id is read from the rehydration JSON (webapp.app-context.wid), with localStorage.ttwid as fallback.
Verified URL shape:
GET https://www.tiktok.com/api/comment/list/?aweme_id=<id>&cursor=<n>&count=<k>&aid=1988&device_id=<wid>
GET https://www.tiktok.com/api/comment/list/reply/?item_id=<id>&comment_id=<cid>&cursor=<n>&count=<k>&aid=1988&device_id=<wid>
If TikTok ever adds signature gating, the API path will return status_code != 0 — fall back to the DOM path.
Critical rules
- Stable selectors only.
data-e2e="..."is stable; class names rotate per deploy and must be substring-matched via[class*="..."]. - Direct-URL search > input flow. Navigate to
/search?q=...rather than typing in the search box. - For comments, prefer the API path. Only fall back to DOM if the API returns
status_code != 0. - DOM comments dedup. No comment ID exists in the DOM (verified — wrappers carry only a class attribute). Use the synthesized
keyfromcomments_dom_extract.js. /profileredirect is the login signal.top-login-buttonis rendered on public video pages even when logged in; ignore it.- Wait 4–6s after navigation. TikTok hydrates slowly, especially the first time.
- Never log in programmatically. If
/profilelands at/login, stop and tell the user. - Close tabs when done. TikTok pages are heavy (autoplaying video).
Common failures
| Signal | Cause | Fix |
|---|---|---|
| Search button click does nothing | React form handler expects keypress | Navigate to /search?q=... directly |
comments_api.js returns status_code != 0 |
Stale cookies, bot-detection, or signature gating | Fall back to the DOM loop |
| Zero comments via DOM path | Comment panel hasn't been opened | dom_click('[data-e2e="comment-icon"]') then wait ~2s |
| DOM comment count plateaus at 20 | Comment list isn't being scrolled | comments_dom_step.js scrolls the right ancestor; window scroll doesn't work |
comment-level-1 text is empty |
Sticker / image-only comment | Expected — keep the row, accept null text |
/profile lands on /login |
Session expired | Stop; ask user to re-authenticate Chrome manually |
| DOM empty right after navigate | Page not hydrated yet | Wait 5–6s; re-query |
| Profile page rehydration JSON missing post list | TikTok loads it via async API after first paint | Use profile_latest_videos.js (DOM scrape) instead |
Example: top 5 comments on the first cat-video search result
# 1. Search
navigate(url='https://www.tiktok.com/search/video?q=' + encodeURIComponent('cat videos'))
# wait ~5s
results = execute_script(code=<contents of scripts/search_results.js>)
firstUrl = results[0]['url']
# 2. Open the video and pull all comments via the API
navigate(url=firstUrl)
# wait ~5s
out = execute_script(code=<contents of scripts/comments_api.js>)
if out['ok']:
top5 = out['topLevel'][:5]
else:
# API path failed; fall back to DOM loop (see "DOM comments fallback loop" above)
...