Skip to content

feat: add configurable user agent support #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 14, 2025
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Copy the cookie from the output and set it as `LINKEDIN_COOKIE` in your client c
- `--path PATH` - HTTP server path (default: /mcp)
- `--get-cookie` - Attempt to login with email and password and extract the LinkedIn cookie
- `--cookie {cookie}` - Pass a specific LinkedIn cookie for login
- `--user-agent {user_agent}` - Specify custom user agent string to prevent anti-scraping detection

**HTTP Mode Example (for web-based MCP clients):**
```bash
Expand All @@ -117,6 +118,7 @@ docker run -it --rm \
stickerdaniel/linkedin-mcp-server:latest \
--transport streamable-http --host 0.0.0.0 --port 8080 --path /mcp
```

**Test with mcp inspector:**
1. Install and run mcp inspector ```bunx @modelcontextprotocol/inspector```
2. Click pre-filled token url to open the inspector in your browser
Expand Down Expand Up @@ -245,6 +247,7 @@ uv run main.py --no-headless --no-lazy-init
- `--get-cookie` - Login with email and password and extract the LinkedIn cookie
- `--clear-keychain` - Clear all stored LinkedIn credentials and cookies from system keychain
- `--cookie {cookie}` - Pass a specific LinkedIn cookie for login
- `--user-agent {user_agent}` - Specify custom user agent string to prevent anti-scraping detection
- `--transport {stdio,streamable-http}` - Set transport mode
- `--host HOST` - HTTP server host (default: 127.0.0.1)
- `--port PORT` - HTTP server port (default: 8000)
Expand Down
13 changes: 13 additions & 0 deletions linkedin_mcp_server/config/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class EnvironmentKeys:
# Chrome configuration
CHROMEDRIVER = "CHROMEDRIVER"
HEADLESS = "HEADLESS"
USER_AGENT = "USER_AGENT"

# Server configuration
LOG_LEVEL = "LOG_LEVEL"
Expand Down Expand Up @@ -120,6 +121,9 @@ def load_from_env(config: AppConfig) -> AppConfig:
if chromedriver := os.environ.get(EnvironmentKeys.CHROMEDRIVER):
config.chrome.chromedriver_path = chromedriver

if user_agent := os.environ.get(EnvironmentKeys.USER_AGENT):
config.chrome.user_agent = user_agent

# Log level
if log_level_env := os.environ.get(EnvironmentKeys.LOG_LEVEL):
log_level_upper = log_level_env.upper()
Expand Down Expand Up @@ -225,6 +229,12 @@ def load_from_args(config: AppConfig) -> AppConfig:
help="Specify LinkedIn cookie directly",
)

parser.add_argument(
"--user-agent",
type=str,
help="Specify custom user agent string to prevent anti-scraping detection",
)

args = parser.parse_args()

# Update configuration with parsed arguments
Expand Down Expand Up @@ -261,6 +271,9 @@ def load_from_args(config: AppConfig) -> AppConfig:
if args.cookie:
config.linkedin.cookie = args.cookie

if args.user_agent:
config.chrome.user_agent = args.user_agent

return config


Expand Down
1 change: 1 addition & 0 deletions linkedin_mcp_server/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class ChromeConfig:
headless: bool = True
chromedriver_path: Optional[str] = None
browser_args: List[str] = field(default_factory=list)
user_agent: Optional[str] = None


@dataclass
Expand Down
18 changes: 15 additions & 3 deletions linkedin_mcp_server/drivers/chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import logging
import os
import platform
from typing import Dict, Optional

from linkedin_scraper.exceptions import (
Expand All @@ -27,8 +28,19 @@
from linkedin_mcp_server.config import get_config
from linkedin_mcp_server.exceptions import DriverInitializationError


# Constants
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
def get_default_user_agent() -> str:
"""Get platform-specific default user agent to reduce fingerprinting."""
system = platform.system()

if system == "Windows":
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
elif system == "Darwin": # macOS
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
else: # Linux and others
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"


# Global driver storage to reuse sessions
active_drivers: Dict[str, webdriver.Chrome] = {}
Expand Down Expand Up @@ -72,8 +84,8 @@ def create_chrome_options(config) -> Options:
chrome_options.add_argument("--aggressive-cache-discard")
chrome_options.add_argument("--disable-ipc-flooding-protection")

# Set user agent (configurable with sensible default)
user_agent = getattr(config.chrome, "user_agent", DEFAULT_USER_AGENT)
# Set user agent (configurable with platform-specific default)
user_agent = config.chrome.user_agent or get_default_user_agent()
chrome_options.add_argument(f"--user-agent={user_agent}")

# Add any custom browser arguments from config
Expand Down