add linkedin connector + gitignore .claude + regenerate index

This commit is contained in:
2026-08-07 13:30:44 +01:00
parent c778450812
commit 2ad3954e3a
11 changed files with 704 additions and 1 deletions
+114
View File
@@ -0,0 +1,114 @@
#!/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()