Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .dxtignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# IDE and editor settings
.claude/
.gemini/
.vscode/
.idea/
.cursor/
.opencode/

# CI/CD and GitHub
.github/

# Build artifacts and caches
.mypy_cache/
.ruff_cache/
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
build/
dist/
*.egg-info/

# Test and coverage
.coverage
.coveragerc
htmlcov/
.tox/
.nox/
.hypothesis/
cover/

# Docker
.docker/
.dockerignore

# Environment and secrets
.env
.env.*
.venv/
venv/
env/

# Logs
*.log

# OS files
.DS_Store
Thumbs.db

# Lock files (not needed for DXT)
uv.lock
poetry.lock
Pipfile.lock

# Git
.git/
.gitignore
.gitattributes

# Pre-commit
.pre-commit-config.yaml

# Documentation
*.md
*.svg
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ This is a **LinkedIn MCP (Model Context Protocol) Server** that enables AI assis

**Authentication Flow:**

- Uses session files stored at `~/.linkedin-mcp/session.json`
- Uses persistent browser profiles stored at `~/.linkedin-mcp/browser-profile/`
- Run with `--get-session` to create a session via browser login

**Transport Modes:**
Expand All @@ -85,7 +85,7 @@ This is a **LinkedIn MCP (Model Context Protocol) Server** that enables AI assis
**Configuration:**

- CLI arguments with comprehensive help (`--help`)
- Session stored at `~/.linkedin-mcp/session.json`
- Session stored in `~/.linkedin-mcp/browser-profile/`

**Commit Message Format:**

Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ What has Anthropic been posting about recently? https://www.linkedin.com/company
| `close_session` | Close browser session and clean up resources | Working |

> [!WARNING]
> The session file at `~/.linkedin-mcp/session.json` contains sensitive authentication data. Keep it secure and do not share it.
> The browser profile at `~/.linkedin-mcp/browser-profile/` contains sensitive authentication data. Keep it secure and do not share it.

<br/>
<br/>
Expand All @@ -64,7 +64,7 @@ What has Anthropic been posting about recently? https://www.linkedin.com/company
uvx --from git+https://github.com/stickerdaniel/linkedin-mcp-server linkedin-mcp-server --get-session
```

This opens a browser for you to log in manually (5 minute timeout for 2FA, captcha, etc.). The session is saved to `~/.linkedin-mcp/session.json`.
This opens a browser for you to log in manually (5 minute timeout for 2FA, captcha, etc.). The session is saved to `~/.linkedin-mcp/browser-profile/`.

**Step 2: Run the server**

Expand Down Expand Up @@ -104,7 +104,7 @@ uvx --from git+https://github.com/stickerdaniel/linkedin-mcp-server linkedin-mcp

**CLI Options:**

- `--get-session [PATH]` - Open browser to log in and save session (default: ~/.linkedin-mcp/session.json)
- `--get-session [PATH]` - Open browser to log in and save session (default: ~/.linkedin-mcp/browser-profile)
- `--no-headless` - Show browser window (useful for debugging scraping issues)
- `--log-level {DEBUG,INFO,WARNING,ERROR}` - Set logging level (default: WARNING)
- `--transport {stdio,streamable-http}` - Set transport mode
Expand Down Expand Up @@ -153,7 +153,7 @@ uvx --from git+https://github.com/stickerdaniel/linkedin-mcp-server linkedin-mcp

**Session issues:**

- Session is stored at `~/.linkedin-mcp/session.json`
- Session is stored in `~/.linkedin-mcp/browser-profile/`
- Make sure you have only one active LinkedIn session at a time

**Login issues:**
Expand Down Expand Up @@ -219,7 +219,7 @@ Create a session file locally, then mount it into Docker.
uvx --from git+https://github.com/stickerdaniel/linkedin-mcp-server linkedin-mcp-server --get-session
```

This opens a browser window where you log in manually (5 minute timeout for 2FA, captcha, etc.). The session is saved to `~/.linkedin-mcp/session.json`.
This opens a browser window where you log in manually (5 minute timeout for 2FA, captcha, etc.). The session is saved to `~/.linkedin-mcp/browser-profile/`.

**Step 2: Configure Claude Desktop with Docker**

Expand Down Expand Up @@ -409,7 +409,7 @@ uv run -m linkedin_mcp_server

**CLI Options:**

- `--get-session [PATH]` - Open browser to log in and save session (default: ~/.linkedin-mcp/session.json)
- `--get-session [PATH]` - Open browser to log in and save session (default: ~/.linkedin-mcp/browser-profile)
- `--no-headless` - Show browser window (useful for debugging scraping issues)
- `--log-level {DEBUG,INFO,WARNING,ERROR}` - Set logging level (default: WARNING)
- `--transport {stdio,streamable-http}` - Set transport mode
Expand Down Expand Up @@ -465,7 +465,7 @@ uv run -m linkedin_mcp_server --transport streamable-http --host 127.0.0.1 --por

**Session issues:**

- Session is stored in `~/.linkedin-mcp/session.json`
- Session is stored in `~/.linkedin-mcp/browser-profile/`
- Use `--clear-session` to clear the session and start fresh

**Python/Playwright issues:**
Expand Down
45 changes: 25 additions & 20 deletions linkedin_mcp_server/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,35 @@
from typing import Literal

from linkedin_mcp_server.drivers.browser import (
DEFAULT_SESSION_PATH,
session_exists,
DEFAULT_USER_DATA_DIR,
profile_exists,
)
from linkedin_mcp_server.exceptions import CredentialsNotFoundError
from linkedin_mcp_server.utils import get_linkedin_cookie

logger = logging.getLogger(__name__)

AuthSource = Literal["session", "cookie"]
AuthSource = Literal["profile", "cookie"]


def get_authentication_source() -> AuthSource:
"""
Check available authentication methods in priority order.

Priority:
1. Session file (most reliable)
1. Persistent browser profile (most reliable)
2. LINKEDIN_COOKIE env var (Docker headless)

Returns:
String indicating auth source: "session" or "cookie"
String indicating auth source: "profile" or "cookie"

Raises:
CredentialsNotFoundError: If no authentication method available
"""
# Priority 1: Session file
if session_exists():
logger.info(f"Using session from {DEFAULT_SESSION_PATH}")
return "session"
# Priority 1: Browser profile
if profile_exists():
logger.info(f"Using browser profile from {DEFAULT_USER_DATA_DIR}")
return "profile"

# Priority 2: Cookie from environment
if get_linkedin_cookie():
Expand All @@ -48,35 +48,40 @@ def get_authentication_source() -> AuthSource:
raise CredentialsNotFoundError(
"No LinkedIn authentication found.\n\n"
"Options:\n"
" 1. Run with --get-session to create a session file (recommended)\n"
" 1. Run with --get-session to create a browser profile (recommended)\n"
" 2. Set LINKEDIN_COOKIE environment variable with your li_at cookie\n"
" 3. Run with --no-headless to login interactively\n\n"
"For Docker users:\n"
" Create session on host first: uvx linkedin-mcp-server --get-session\n"
" Create profile on host first: uvx linkedin-mcp-server --get-session\n"
" Then mount into Docker: -v ~/.linkedin-mcp:/home/pwuser/.linkedin-mcp\n"
" Or set LINKEDIN_COOKIE environment variable: -e LINKEDIN_COOKIE=your_li_at"
)


def clear_session(session_path: Path | None = None) -> bool:
def clear_session(user_data_dir: Path | None = None) -> bool:
"""
Clear stored session file.
Clear browser profile directory.

Args:
session_path: Path to session file
user_data_dir: Browser profile directory

Returns:
True if clearing was successful
"""
if session_path is None:
session_path = DEFAULT_SESSION_PATH
if user_data_dir is None:
from linkedin_mcp_server.config import get_config

if session_path.exists():
config = get_config()
user_data_dir = Path(config.browser.user_data_dir).expanduser()

if user_data_dir.exists():
try:
session_path.unlink()
logger.info(f"Session cleared from {session_path}")
import shutil

shutil.rmtree(user_data_dir)
logger.info(f"Browser profile cleared from {user_data_dir}")
return True
except OSError as e:
logger.warning(f"Could not clear session: {e}")
logger.warning(f"Could not clear browser profile: {e}")
return False
return True
53 changes: 37 additions & 16 deletions linkedin_mcp_server/cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@
from linkedin_mcp_server.cli import print_claude_config
from linkedin_mcp_server.config import get_config
from linkedin_mcp_server.drivers.browser import (
DEFAULT_SESSION_PATH,
DEFAULT_USER_DATA_DIR,
close_browser,
get_or_create_browser,
session_exists,
migrate_from_legacy_session,
needs_migration,
profile_exists,
set_headless,
)
from linkedin_mcp_server.exceptions import CredentialsNotFoundError
Expand Down Expand Up @@ -62,7 +64,7 @@ def choose_transport_interactive() -> Literal["stdio", "streamable-http"]:


def clear_session_and_exit() -> None:
"""Clear LinkedIn session and exit."""
"""Clear LinkedIn browser profile and exit."""
config = get_config()

configure_logging(
Expand All @@ -73,16 +75,16 @@ def clear_session_and_exit() -> None:
version = get_version()
logger.info(f"LinkedIn MCP Server v{version} - Session Clear mode")

if not session_exists():
print("ℹ️ No session file found")
if not profile_exists():
print("ℹ️ No browser profile found")
print("Nothing to clear.")
sys.exit(0)

print(f"🔑 Clear LinkedIn session from {DEFAULT_SESSION_PATH}?")
print(f"🔑 Clear LinkedIn browser profile from {DEFAULT_USER_DATA_DIR}?")

try:
confirmation = (
input("Are you sure you want to clear the session? (y/N): ").strip().lower()
input("Are you sure you want to clear the profile? (y/N): ").strip().lower()
)
if confirmation not in ("y", "yes"):
print("❌ Operation cancelled")
Expand All @@ -92,9 +94,9 @@ def clear_session_and_exit() -> None:
sys.exit(0)

if clear_session():
print("✅ LinkedIn session cleared successfully!")
print("✅ LinkedIn browser profile cleared successfully!")
else:
print("❌ Failed to clear session")
print("❌ Failed to clear browser profile")
sys.exit(1)

sys.exit(0)
Expand Down Expand Up @@ -130,10 +132,10 @@ def session_info_and_exit() -> None:
version = get_version()
logger.info(f"LinkedIn MCP Server v{version} - Session Info mode")

# Check if session file exists first
if not session_exists():
print(f"❌ No session file found at {DEFAULT_SESSION_PATH}")
print(" Run with --get-session to create a session")
# Check if browser profile exists first
if not profile_exists():
print(f"❌ No browser profile found at {DEFAULT_USER_DATA_DIR}")
print(" Run with --get-session to create a profile")
sys.exit(1)

# Check if session is valid by testing login status
Expand All @@ -151,10 +153,10 @@ async def check_session() -> bool:
valid = asyncio.run(check_session())

if valid:
print(f"✅ Session is valid: {DEFAULT_SESSION_PATH}")
print(f"✅ Session is valid: {DEFAULT_USER_DATA_DIR}")
sys.exit(0)
else:
print(f"❌ Session expired or invalid: {DEFAULT_SESSION_PATH}")
print(f"❌ Session expired or invalid: {DEFAULT_USER_DATA_DIR}")
print(" Run with --get-session to re-authenticate")
sys.exit(1)

Expand All @@ -163,14 +165,33 @@ def ensure_authentication_ready() -> None:
"""
Phase 1: Ensure authentication is ready.

Checks for existing session file.
Checks for existing browser profile or attempts migration from legacy session.json.
If not found, runs interactive setup in interactive mode.

Raises:
CredentialsNotFoundError: If authentication setup fails
"""
config = get_config()

# Check for migration need
if needs_migration():
if config.is_interactive:
print(
"📦 Legacy session.json detected. Migrating to new persistent profile..."
)
success = asyncio.run(migrate_from_legacy_session())
if success:
print("✅ Migration successful!")
return
else:
print(
"⚠️ Migration failed. Please run --get-session to re-authenticate."
)
else:
logger.warning(
"Legacy session.json found. Run interactively or use --get-session to upgrade."
)

# Check for existing session
try:
get_authentication_source()
Expand Down
Loading