A single-page local dashboard for running a multi-platform livestream (Twitch, Kick, YouTube, plus X/Instagram/Facebook/LinkedIn via a browser bridge) with OBS previews, a combined chat, live stats, and an MCP server that lets an AI update your stream title, category and tags on all platforms at once. Zero dependencies — everything is one HTML file and stdlib-only Python.
- MAIN merged chat (top-left, always visible) — Twitch + Kick + YouTube chat merged live, plus X/Instagram/Facebook/LinkedIn comments via the bridge extension. Persistent across refreshes (SQLite). Click a username to reply; hover a message to delete it (✕); "clear ⌫" wipes the log (two-click confirm).
- Post bar — send a message to all or selected platforms from a dropdown. Twitch/Kick/YouTube go out directly via their APIs; X/IG/FB are typed into their open popups by the extension (you press send).
- Stats & stream info panel — per-platform row with a clickable stream link, live dot, concurrent viewers, message/chatter counts, uptime, and the current title · category · tags with inline ✎ editing (single platform or all at once). "⟳ refresh" re-fetches everything on demand (bypassing the server's 15 s stats cache); hovering the chatters count lists the individual users with their message counts.
- Live detection from three sources — platform APIs (Twitch/Kick/YouTube), OBS output state (X etc.), and browser-tab presence (Instagram lives started from the phone app, Facebook and LinkedIn lives — detected when you open the live in a Chrome tab).
- Desktop notifications for new MAIN chat messages (prompted on load, toggleable via the 🔔 pill, rate-limited).
- Join events — Twitch joins via IRC membership, Instagram joins scraped from the live comment feed ("👋 joined").
- OBS chat overlay —
chat.htmlis a transparent, bottom-pinned merged-chat page to add as an OBS Browser source (?size=?max=?solid=1). - OBS previews — landscape and vertical (9:16) scene previews via the OBS WebSocket, fullscreen toggle, hideable via the 🎬 pill.
- Collapsible layout — side chat panels collapse to slim tabs; everything fits one screen at 100% zoom. MAIN chat and the stats panel split the left column 50/50.
- YouTube chat without an API key — the server polls YouTube's innertube live-chat endpoint; the live video ID comes from the YouTube API (active broadcast) with a scrape fallback.
- AI-managed stream info — an MCP server so Claude can read/update titles, categories and tags (see below).
- Persistent stream titles — every title/tags/category applied through the dashboard (or
/streamupdate) is saved tostream_settings.json(gitignored) and a background keeper re-applies it every 5 minutes if a platform drifts — e.g. YouTube creating a fresh broadcast with a default title, or Kick blanking the title between streams.POST /reapplyforces a pass. "✎ all" also queues the title for X: the bridge extension types it into the broadcast-title field of an open X studio/producer tab. - Auto-opened scraper popups — when X/Instagram/Facebook/LinkedIn goes live and no browser tab is feeding its chat into MAIN, the dashboard opens the platform's popup automatically (allow popups for
localhost:8425once; a pulsing pill means Chrome blocked it — click to open). - Lag fixes — OBS preview screenshots pause while the tab is hidden or the preview strip is collapsed; collapsed side chat panels fully unload their embedded-chat iframes; history polling slows down in hidden tabs.
| File | Purpose |
|---|---|
index.html |
The dashboard UI (all styles and JS inline) |
chat.html |
Merged-chat overlay page for an OBS Browser source |
server.py |
Local HTTP server (Python 3 stdlib only) serving the pages and JSON endpoints |
mcp_server.py |
MCP server exposing stream title/category/tag tools for Twitch, Kick, YouTube |
extension/ |
Chrome MV3 bridge for X/Instagram/Facebook/LinkedIn tabs (scrape, reply typing, live presence) + YouTube chat banner hider |
Stream Dashboard.command |
Double-clickable macOS launcher: opens the browser and starts the server |
.env.example |
Template for the (gitignored) .env holding all secrets and tokens |
All served on http://localhost:8425 (bound to 127.0.0.1 only):
/— the dashboard ·/chat.html— the OBS overlay/ytid— current YouTube live video ID (API-first, scrape fallback)/ytchat— new YouTube live chat messages since the last poll/kickid— Kick chatroom ID/vuuid— map of vertical-canvas scene name → OBS source UUID (read from the OBS scene collection file)/config— secrets the dashboard needs (currently the OBS WebSocket password), read from.env/mcp— the MCP-server install command / Claude Desktop config snippet/stats— live state, viewers, uptime per platform + chat message/chatter counts (incl. top chatters per platform) + tab presence;?fresh=1bypasses the 15 s cache/streaminfo— current title/category/tags per platform ·/streamupdate— apply title/tags/category to selected platforms (also saved tostream_settings.json)/settings— the saved (persistent) stream settings ·POST /reapply— force one re-apply pass now/post— send a chat message to selected platforms (API for Twitch/Kick/YouTube, outbox for X/IG/FB)/history·/log— persistent chat log (SQLite,chat_log.db, gitignored) ·/clear·/delete— log management/reply·/outbox·/outbox_ack— reply queue delivered by the bridge extension/presence— extension heartbeat marking a platform live when its tab shows a live video
- macOS with Python 3 (no packages to install)
- OBS Studio with the WebSocket server enabled on port 4455
- Scene collection file at
~/Library/Application Support/obs-studio/basic/scenes/Main_Setup.json(path configurable inserver.py)
Everything below is the complete, tested procedure this repo was set up with (last done 2026-07-25).
cp .env.example .envSet OBS_PASSWORD in .env (OBS → Tools → WebSocket Server Settings). Then start the dashboard — double-click Stream Dashboard.command, or:
python3 server.py # then open http://localhost:8425The dashboard fetches the OBS password from the localhost-only /config endpoint; no secrets live in the HTML.
Per-channel settings if adapting for a different channel: CHANNEL in server.py and CHANNEL / X_HANDLE / IG_HANDLE in index.html; SCENES_FILE in server.py; the OBS output names in PLATFORMS in index.html (from OBS's GetOutputList).
Each platform needs an app you create in its developer portal. All three use the same OAuth redirect URL: http://localhost:8426/callback (served temporarily by mcp_server.py during auth — the dashboard itself uses 8425).
- https://dev.twitch.tv/console/apps/create
- Name: anything · OAuth Redirect URL:
http://localhost:8426/callback· Category: anything · Client Type: Public (public + device-code flow means no client secret is needed). - Copy the Client ID into
.envasTWITCH_CLIENT_ID.
- https://dev.kick.com → Settings → Developer → create an app.
- Redirect URL:
http://localhost:8426/callback. Tick the scopes Read channel information, Update channel information and Write to chat (channel:read,channel:write,chat:write— the last one is needed by the dashboard's post bar). - Copy Client ID →
KICK_CLIENT_IDand Client Secret →KICK_CLIENT_SECRETin.env.
- https://console.cloud.google.com → create a project.
- Enable YouTube Data API v3 (search it in the top bar → Enable).
- Configure the OAuth consent screen (External is fine; the app stays in Testing mode).
- Crucial: add the Google account that owns your YouTube channel as a Test user (Console → APIs & Services → OAuth consent screen / Audience → Test users). Skipping this gives
Error 403: access_deniedat login. - Create an OAuth client — Desktop app or Web application both work; if Web, register
http://localhost:8426/callbackas an authorized redirect URI (authorized JS origins are not needed). - Copy Client ID →
YT_CLIENT_IDand Client Secret →YT_CLIENT_SECRETin.env(or from the downloadedclient_secret_….json: theweb/installed→client_id/client_secretfields).
Run each of these once; a browser tab opens, you approve, tokens are saved back into .env automatically:
python3 mcp_server.py auth twitch # device-code flow: page opens pre-filled, click Authorize
python3 mcp_server.py auth kick # click Authorize, redirects back to localhost
python3 mcp_server.py auth youtube # pick the channel account, Continue past the
# "Google hasn't verified this app" warningScopes requested: Twitch channel:manage:broadcast user:write:chat; Kick channel:read channel:write chat:write; YouTube full youtube (covers live chat). If you authorized before chat-sending existed, re-run the Twitch and Kick auth once to pick up the chat scopes.
Access tokens auto-refresh from the stored refresh tokens; you never repeat this step. (TWITCH_USER_ID is discovered and cached in .env on first API call.)
Click ✨ Install MCP in the dashboard header (copies the command), or run:
claude mcp add --scope user stream-dashboard -- python3 /path/to/stream-dashboard/mcp_server.py--scope user makes it available in every Claude Code session, not just this folder. Verify with claude mcp list — it should show ✔ Connected. For Claude Desktop, the JSON config snippet is served at http://localhost:8425/mcp.
Optional — keep it registered but off by default: in a session, run /mcp and toggle stream-dashboard off; Claude Code remembers this per folder (stored in disabledMcpServers in ~/.claude.json). Toggle it back on via /mcp whenever you're about to stream. This setup uses that mode: registered user-wide, disabled everywhere by default, enabled on demand.
chrome://extensions → enable Developer mode → Load unpacked → select the extension/ folder. It runs only in X/Instagram/Facebook/LinkedIn tabs and YouTube live-chat frames, and only talks to localhost:8425. Reload the extension (⟳) after pulling changes, then refresh the affected tabs.
In any new Claude Code session:
"Check my stream status, then update all titles, categories and tags to maximise viewers — it's a Claude Code live-coding stream"
Tools exposed: stream_status · search_twitch_category · search_kick_category · update_twitch (title/category/≤10 tags) · update_kick (title/category) · update_youtube (title/description/tags/category — auto-targets the active or upcoming broadcast, or pass video_id).
Notes: Twitch's usual category for coding streams is Software and Game Development; YouTube categoryId 28 = Science & Technology, 20 = Gaming. Updates apply immediately, including mid-stream. Titles/tags/categories can also be edited by hand in the dashboard's stats panel (✎ per row, or "✎ all").
- Persistent chat log — every message in the MAIN merged chat is written to
chat_log.db(SQLite, created automatically, gitignored). The dashboard renders chat from this log, so a page refresh restores the last 300 messages. Messages carry platform message IDs (Twitch IRC tags, Kick Pusher IDs, YouTube innertube IDs), so duplicates are dropped even with multiple dashboard tabs open. - Bridge extension (
extension/) — scrapes visible comments in X/Instagram/Facebook/LinkedIn tabs into the chat log (X only on a tweet's own/status/page, skipping your own posts); detects Instagram/Facebook/LinkedIn lives in open tabs and reports presence; surfaces IG "user joined" events; types queued replies/posts into the site's compose box — it never presses send; you do. It also hides YouTube live chat's "Welcome to live chat!" banner (yt.css). The DOM selectors incontent.jsare best-effort and may need updating when those sites change their markup. - Replying from MAIN — click a username, type in the reply bar, hit Send. X/IG/FB replies are queued and typed into the matching popup by the extension.
- Posting to chats — the post bar at the bottom of MAIN sends to any selection of platforms: Twitch (
POST /helix/chat/messages), Kick (POST /public/v1/chat), YouTube (liveChatMessages.insert), and the X/IG/FB outbox.
Add a Browser source: URL http://localhost:8425/chat.html, ~480×800. Transparent background, new messages slide in at the bottom. Options: ?size=24 (font px) · ?max=15 (messages kept) · ?solid=1 (opaque background).
- Google
Error 403: access_denied— your account isn't a Test user on the OAuth consent screen (step 2.4). - YouTube "No active or upcoming broadcast found" — expected when you're not live and nothing is scheduled.
- Any API returns 401 repeatedly — re-run the matching
python3 mcp_server.py auth <platform>. - Post bar says a platform lacks the chat scope — re-run that platform's auth (the flows now request the chat scopes).
- Port 8425 already in use — a dashboard server is already running (the server now says so cleanly):
kill $(lsof -ti :8425). - Extension errors "Extension context invalidated" — harmless leftovers from tabs opened before an extension reload; refresh those tabs.
- X/IG/FB messages stop appearing — reload the extension after updates, and keep the platform's tab/popup open; X is only scraped on a tweet's own page.
.envformat — plainKEY=VALUElines only; no inline comments after values.