115 lines
4.5 KiB
Python
115 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Skald launcher for the LinkedIn MCP connector.
|
|
|
|
The server itself is the upstream `mcp-server-linkedin` package (pinned in
|
|
requirements.txt); this file exists because a marketplace connector must ship an
|
|
entry file for `mcp_config.args[0]` to name. It does two things the package
|
|
cannot do for itself on a headless box:
|
|
|
|
1. Pins the paths that must live inside the connector directory. Skald re-copies
|
|
a connector's shipped files on every update but never deletes anything else,
|
|
so state written here survives both updates and container recreates — which
|
|
the upstream defaults (`~/.linkedin-mcp`, `~/.cache/ms-playwright`) do not.
|
|
|
|
2. Materializes the session bundle from the cookie the user pasted into the
|
|
configuration form (see session.py). Upstream expects that bundle to be
|
|
produced by signing in through a visible browser and copied over by hand;
|
|
there is no display here, so we build it from the form instead.
|
|
|
|
stdout belongs to JSON-RPC. Everything this script says goes to stderr.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
|
|
# The package derives its auth root from the *parent* of USER_DATA_DIR, so
|
|
# pointing the profile at `auth/profile` puts `cookies.json`, `source-state.json`
|
|
# and the derived `runtime-profiles/` together under `auth/`.
|
|
AUTH_DIR = HERE / "auth"
|
|
|
|
# Chromium is ~350 MB and is NOT shipped: patchright downloads its own pinned
|
|
# build on first use. Keeping it here means it survives updates instead of being
|
|
# re-fetched into every fresh container.
|
|
BROWSERS_DIR = HERE / "browsers"
|
|
|
|
# Records which credential the current bundle was built from, so a browser
|
|
# session is only rebuilt when the user actually pastes a new cookie.
|
|
STAMP = AUTH_DIR / ".credential"
|
|
|
|
|
|
def log(message: str) -> None:
|
|
print(f"[linkedin] {message}", file=sys.stderr, flush=True)
|
|
|
|
|
|
def sync_session() -> None:
|
|
"""Rebuild the session bundle when the configured cookie has changed.
|
|
|
|
Rewriting on every start would be wasteful but harmless; the reason to guard
|
|
it is that a rewrite mints a new `login_generation`, which discards the
|
|
runtime profile the package built from the previous one — throwing away a
|
|
warmed-up, LinkedIn-accepted session for no reason.
|
|
"""
|
|
from session import fingerprint, read_config, write_bundle
|
|
|
|
li_at, jsessionid, user_agent = read_config()
|
|
|
|
if not li_at:
|
|
log("no li_at cookie configured — set it in the connector's settings")
|
|
log("(copy it from your browser: DevTools > Application > Cookies > linkedin.com)")
|
|
return
|
|
|
|
current = fingerprint(li_at, jsessionid, user_agent)
|
|
previous = STAMP.read_text().strip() if STAMP.is_file() else ""
|
|
if current == previous:
|
|
return
|
|
|
|
write_bundle(AUTH_DIR, li_at, jsessionid, user_agent)
|
|
STAMP.write_text(current + "\n")
|
|
os.chmod(STAMP, 0o600)
|
|
log("session bundle written from the configured cookie" if not previous
|
|
else "cookie changed — session bundle rebuilt")
|
|
|
|
|
|
def main() -> None:
|
|
# Skald puts `.pydeps` on PYTHONPATH when it launches this connector; adding
|
|
# it here too keeps the script runnable by hand for debugging.
|
|
pydeps = HERE / ".pydeps"
|
|
if pydeps.is_dir() and str(pydeps) not in sys.path:
|
|
sys.path.insert(0, str(pydeps))
|
|
if str(HERE) not in sys.path:
|
|
sys.path.insert(0, str(HERE))
|
|
|
|
# setdefault throughout: an operator overriding any of these wins.
|
|
os.environ.setdefault("USER_DATA_DIR", str(AUTH_DIR / "profile"))
|
|
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", str(BROWSERS_DIR))
|
|
# No display here. Headless also keeps the download to the headless shell
|
|
# alone; full Chrome for Testing is only fetched for a headed run.
|
|
os.environ.setdefault("HEADLESS", "true")
|
|
os.environ.setdefault("TRANSPORT", "stdio")
|
|
|
|
# Replay the pasted cookie under the user agent it was minted with, when we
|
|
# know it — LinkedIn associates a session with its browser fingerprint.
|
|
_, _, user_agent = __import__("session").read_config()
|
|
if user_agent:
|
|
os.environ.setdefault("USER_AGENT", user_agent)
|
|
|
|
try:
|
|
sync_session()
|
|
except Exception as exc: # noqa: BLE001 - never block startup on this
|
|
log(f"could not write the session bundle: {exc}")
|
|
|
|
try:
|
|
from linkedin_mcp_server.cli_main import main as server_main
|
|
except ImportError as exc:
|
|
log(f"dependencies missing ({exc}); expected mcp-server-linkedin in .pydeps")
|
|
raise
|
|
|
|
server_main()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|