Files
socialmag/MANIFEST.md
T
juxtapo 1ac06c1bbe v0.3: dashboard local mode, openbridge tab reuse, tab activation fix
- 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
2026-09-11 10:18:38 +08:00

7.9 KiB

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

# 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

  • get_notifications(platform) — Threads activity via CDP scraping (v0.2)
  • get_feed — Twitter timeline + search (v0.2)
  • reply_tweet (v0.2)
  • Web dashboard — one view for all platform activity (v0.3)
  • 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.