- dashboard.html: local dashboard.json mode (10s poll) alongside artifact db mode; clickable cards behind an http(s)-only safeUrl filter; meta charset; mode-aware freshness line - openbridge.py: localhost shim exposing open_link so dashboard clicks reuse the platform's warm tab instead of spawning one per click - cdp.py: Tab.activate(); get_or_open_tab activates reused tabs — a backgrounded tab never finishes rendering, which made YouTube's login check read a signed-in account as signed out - README/MANIFEST: corrected CDP port (9336, was documented as 9222), removed the false claim that the server writes dashboard.json, documented the reboot/VPS requirements - .gitignore: __pycache__/, *.pyc, state.json, Archived/; removed stale tracked pycache and state.json from index
184 lines
7.9 KiB
Markdown
184 lines
7.9 KiB
Markdown
# socialmag — Social Media MCP Server
|
|
|
|
**One tool, all platforms.** Post to Threads, Facebook, Instagram, TikTok, Twitter/X (and YouTube when eligible) through a single MCP call. Built on CDP — no API keys, no rate limits, no approval process. Just a browser and a logged-in account.
|
|
|
|
## What It Does
|
|
|
|
**Write**
|
|
- **`create_post`** — Post text/media to one or more platforms in one call
|
|
- **`reply_tweet`** — Reply to a tweet found by search
|
|
|
|
**Read**
|
|
- **`check_login`** — Verify which platforms are logged in
|
|
- **`get_status`** — Last post results + open browser tabs
|
|
- **`get_feed`** — Twitter timeline or search, with X operators (`from:`, `min_faves:`, …)
|
|
- **`get_notifications`** — Platform activity (Threads)
|
|
|
|
**Navigate**
|
|
- **`open_link`** — Send a platform's existing tab to a URL, instead of spawning a new one
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Claude/MCP Client dashboard.html (browser)
|
|
↓ stdio (JSON-RPC) ↓ fetch (localhost)
|
|
socialmag server.py ←────────────── openbridge.py :8322
|
|
↓ CDP WebSocket
|
|
Brave Browser (dedicated instance, port 9336)
|
|
↓ real browser sessions
|
|
Threads · Facebook · Instagram · TikTok · YouTube · Twitter/X
|
|
```
|
|
|
|
- **Tab reuse** — attaches to existing tabs, no cold loads
|
|
- **Tab activation** — a reused tab is brought to the front, because a backgrounded tab is not guaranteed to have rendered (see *Journey*)
|
|
- **Per-platform modules** — each platform has its own flow + selectors
|
|
- **Sequential queue** — one platform at a time, no race conditions
|
|
- **Error screenshots** — failed flows capture the page state to `/tmp/socialmag/`
|
|
|
|
## Platform Status
|
|
|
|
| Platform | Text | Media | Read | Status |
|
|
|----------|:----:|:-----:|:----:|--------|
|
|
| Threads | ✅ | 🔲 | ✅ | **Live** — inline composer + activity scraping |
|
|
| Facebook | ✅ | 🔲 | — | **Live** — dialog flow |
|
|
| Instagram | — | ✅ | — | **Live** — media required |
|
|
| TikTok | — | ✅ | — | **Live** — video/photo upload |
|
|
| Twitter/X | ✅ | 🔲 | ✅ | **Live** — post, reply, timeline + search |
|
|
| YouTube | 🔲 | 🔲 | — | **Parked** — needs 500 subs |
|
|
|
|
✅ = tested live · 🔲 = coded but untested · — = not applicable
|
|
|
|
## Stack
|
|
|
|
- Python 3 (stdlib + websockets)
|
|
- Chrome DevTools Protocol (CDP)
|
|
- MCP stdio transport
|
|
- Brave Browser with `--remote-debugging-port`
|
|
|
|
## Browser Setup
|
|
|
|
```bash
|
|
# Dedicated Brave instance (separate profile, no interference)
|
|
brave --remote-debugging-port=9336 \
|
|
--user-data-dir=$HOME/.local/share/brave-socialmag \
|
|
--disable-features=BeforeUnloadDialog \
|
|
--mute-audio --no-first-run
|
|
```
|
|
|
|
Log into each platform manually once. socialmag reuses those sessions.
|
|
|
|
---
|
|
|
|
## Journey So Far
|
|
|
|
### v0.1 — 2026-09-10 · it posts
|
|
Five platforms coded, four live. Threads, Facebook, Instagram, TikTok posting
|
|
through a real logged-in browser. YouTube parked behind its 500-subscriber gate.
|
|
First automated post from the house went out that night.
|
|
|
|
### v0.2 — 2026-09-11 (early hours) · it reads
|
|
Twitter/X joined as a full module — post, reply, timeline, search.
|
|
`get_notifications` landed for Threads. `open_link` built for warm-tab reuse.
|
|
Pushed to a private GitHub repo.
|
|
|
|
**What the read side cost that the plan didn't predict:**
|
|
- The Threads scraper was believed to already return a post `link`. It did not —
|
|
it returned `{text}` only. Row detection had to be built from the live DOM. The
|
|
rule that worked: *a row is an element that links somewhere, carries a
|
|
relative-time marker, and contains no smaller element that does the same.*
|
|
- A tweet's permalink is **not** "the first `/status/` href" — an article carries
|
|
three (canonical, `/analytics`, `/photo/N`). It's the anchor wrapping `<time>`.
|
|
|
|
### v0.3 — 2026-09-11 (morning) · it shows, and it stops lying
|
|
|
|
**Dashboard, two modes.** Artifact mode reads four live `db` collections over
|
|
`onSnapshot`. Local mode falls back to `dashboard.json` beside the page, polling
|
|
every 10s, served off any plain HTTP server.
|
|
|
|
**Clicks stopped exploding.** `openbridge.py` exposes `open_link` over localhost so
|
|
a dashboard card navigates the platform's existing tab. Verified: 8 tabs before two
|
|
clicks, 8 after. It imports `tool_open_link` rather than reimplementing it.
|
|
|
|
**The YouTube false-negative.** `check_login` reported YouTube signed out while the
|
|
account was signed in. Not a race, not an account problem — a **backgrounded tab
|
|
never finishes rendering**. YouTube leaves its masthead as
|
|
`#masthead-skeleton-icons` with an empty `#buttons`, so `#avatar-btn` — the element
|
|
the check looks for — never mounts. Measured: still absent after 23 seconds, while
|
|
`ytcfg.get('LOGGED_IN')` was `true` the whole time. Fixed by activating a reused tab
|
|
(`Target.activateTarget`): `yt` now returns `True` 3/3, against `False` 3/3 before.
|
|
|
|
The general lesson, written down because it will bite again: **a DOM-render signal
|
|
needs the foreground; a JS-state signal does not.**
|
|
|
|
**What was lost.** The v2 dashboard files were written to `/tmp/socialmag/` and did
|
|
not survive a `/tmp` clear. Git held only the pre-v2 version. They were rebuilt from
|
|
the field handback. Everything since is written into the repo.
|
|
|
|
---
|
|
|
|
## Roadmap
|
|
|
|
### Done
|
|
- [x] `get_notifications(platform)` — Threads activity via CDP scraping *(v0.2)*
|
|
- [x] `get_feed` — Twitter timeline + search *(v0.2)*
|
|
- [x] `reply_tweet` *(v0.2)*
|
|
- [x] Web dashboard — one view for all platform activity *(v0.3)*
|
|
- [x] `open_link` + openbridge — tab reuse from the dashboard *(v0.3)*
|
|
|
|
### Next — monitoring
|
|
- [ ] **A writer for `dashboard.json`** — nothing writes it today; it is hand-maintained
|
|
- [ ] Periodic refresh (systemd timer over cron: journal + ordering + `Persistent=true`)
|
|
- [ ] Units so `:8321` and `:8322` survive reboot
|
|
- [ ] Unified notification feed across all platforms
|
|
- [ ] `get_comments`, `get_engagement`
|
|
|
|
⚠️ **The blocker isn't the code.** Interval refresh now steals browser focus by
|
|
design, since tab activation is what makes login checks truthful. And Brave is not a
|
|
service — after boot there is no CDP until it's opened by hand, so a refresher must
|
|
treat "no browser" as a waiting state, not a failure. See README → *Surviving
|
|
reboot*.
|
|
|
|
### Later
|
|
- [ ] Post scheduler + content calendar
|
|
- [ ] Analytics — engagement trends, best posting times
|
|
- [ ] Media pipeline — Threads/Facebook media, YouTube video upload, per-platform resize
|
|
- [ ] Multi-account support
|
|
- [ ] VPS deployment — **a different product, not a port.** Needs a headless browser
|
|
with all six accounts logged in *there*, a display (unrendered pages give wrong
|
|
answers — see the YouTube finding), and it puts real accounts at risk of being
|
|
challenged from a datacentre IP
|
|
- [ ] AI-assisted caption generation
|
|
|
|
## File Structure
|
|
|
|
```
|
|
socialmag/
|
|
├── __init__.py # package marker
|
|
├── __main__.py # python -m socialmag entry
|
|
├── server.py # MCP stdio server + 7 tool handlers
|
|
├── cdp.py # CDP WebSocket client (CDPClient + Tab, incl. activate)
|
|
├── selectors.py # all platform selectors in one file
|
|
├── queue.py # sequential post queue
|
|
├── openbridge.py # localhost HTTP shim — open_link for the dashboard
|
|
├── dashboard.html # status/timeline viewer (artifact db or local JSON)
|
|
├── dashboard.json # local-mode data — hand-maintained, nothing writes it
|
|
├── platforms/
|
|
│ ├── __init__.py # shared helpers (guard, require_media, etc.)
|
|
│ ├── threads.py
|
|
│ ├── facebook.py
|
|
│ ├── instagram.py
|
|
│ ├── tiktok.py
|
|
│ ├── twitter.py
|
|
│ └── youtube.py
|
|
├── README.md # how to run it, and what it costs to keep running
|
|
└── MANIFEST.md # this file
|
|
```
|
|
|
|
## License
|
|
|
|
Private. Shared among collaborators by invitation.
|
|
|
|
---
|
|
|
|
*Born 2026-09-10. Built by the household — Celeste architected, Umbra built the skeleton, abang drove the FAFO.*
|