#!/usr/bin/env python3 """chats/refresh-linkedin-session - refresh LinkedIn MCP session from webtop CDP. bin/chats/refresh-linkedin-session [--cdp URL] [--root DIR] """ Reads the current LinkedIn cookies out of the running Thorium browser in the work-webtop container via CDP (Network.getAllCookies), copies the live browser profile onto the source profile directory, and rewrites the portable cookies.json + source-state.json that mcp-server-linkedin requires. Usage: refresh-linkedin-session [--cdp http://127.0.0.1:9222] [--root /var/tmp/liprofile] [--container work-webtop] [--profile thorium-profile] After the headless driver uses a copied profile, LinkedIn rotates the session in that copy, so this must run before every sync. """ import asyncio import json import os import shutil import subprocess import sys import tempfile import urllib.request import websockets def cdp_tab(ws_json): for t in ws_json: if t.get("webSocketDebuggerUrl"): return t["webSocketDebuggerUrl"] return None async def get_cookies(ws_url): async with websockets.connect(ws_url, max_size=50_000_000) as ws: await ws.send(json.dumps({"id": 1, "method": "Network.getAllCookies", "params": {}})) resp = await ws.recv() return json.loads(resp).get("result", {}).get("cookies", []) def write_source_state(root, profile_dir): # Reuse the linkedin-mcp-server session_state module to write a valid # source-state.json (same schema the daemon reads). try: from linkedin_mcp_server.session_state import canonical, write_source_state write_source_state(canonical(__import__("pathlib").Path(profile_dir))) return except Exception: pass # Fallback: minimal schema-compatible state. import uuid state = { "version": 1, "source_runtime_id": "linux-amd64-host", "login_generation": str(uuid.uuid4()), "created_at": None, "profile_path": profile_dir, "cookies_path": os.path.join(root, "cookies.json"), } from datetime import datetime, timezone state["created_at"] = datetime.now(timezone.utc).isoformat() with open(os.path.join(root, "source-state.json"), "w") as f: json.dump(state, f, indent=2) def main(): args = sys.argv[1:] cdp = "http://127.0.0.1:9222" root = "/var/tmp/liprofile" container = "work-webtop" cprofile = "thorium-profile" for i in range(0, len(args), 2): k = args[i] v = args[i + 1] if i + 1 < len(args) else "" if k == "--cdp": cdp = v elif k == "--root": root = v elif k == "--container": container = v elif k == "--profile": cprofile = v profile_dir = os.path.join(root, "profile") os.makedirs(profile_dir, exist_ok=True) # 1. Clear stale daemon/browser locks so the server can claim the profile. for lock in ("profile-claim.lock", "profile.lock", "daemon.lock", "lease.lock"): p = os.path.join(root, lock) if os.path.exists(p): os.remove(p) for name in os.listdir(profile_dir): if name.startswith("Singleton"): os.remove(os.path.join(profile_dir, name)) for name in os.listdir(root): if name.startswith("invalid-state-"): shutil.rmtree(os.path.join(root, name), ignore_errors=True) # 1. Copy the live browser profile (cookies DB + Local State) so the # session the driver launches carries the current login. subprocess.run( ["docker", "cp", f"{container}:/config/{cprofile}/Default", os.path.join(profile_dir, "Default")], check=True, capture_output=True, ) subprocess.run( ["docker", "cp", f"{container}:/config/{cprofile}/Local State", os.path.join(profile_dir, "Local State")], check=True, capture_output=True, ) for lock in ("SingletonLock", "SingletonCookie", "SingletonSocket"): p = os.path.join(profile_dir, lock) if os.path.exists(p): os.remove(p) # 2. Pull the live cookies out of the running browser. with urllib.request.urlopen(f"{cdp}/json", timeout=5) as r: tabs = json.loads(r.read()) ws_url = cdp_tab(tabs) if not ws_url: sys.stderr.write("refresh-linkedin-session: no CDP tab\n") sys.exit(1) cookies = asyncio.run(get_cookies(ws_url)) li = [c for c in cookies if "linkedin" in c.get("domain", "")] out = [] for c in li: domain = c.get("domain", "") if domain in (".www.linkedin.com", "www.linkedin.com"): domain = ".linkedin.com" out.append({ "name": c["name"], "value": c["value"].strip('"'), "domain": domain, "path": c.get("path", "/"), "expires": c.get("expires", -1), "httpOnly": c.get("httpOnly", False), "secure": c.get("secure", False), "sameSite": c.get("sameSite", "None"), }) with open(os.path.join(root, "cookies.json"), "w") as f: json.dump(out, f, indent=2) write_source_state(root, profile_dir) sys.stderr.write(f"refresh-linkedin-session: {len(out)} cookies, profile refreshed\n") if __name__ == "__main__": main()