LinkedIn Job Search MCP Server
by Tarunuppu
README.md
# LinkedIn Job Search MCP Server
A **Model Context Protocol (MCP) server** that enables AI assistants (Claude, ChatGPT, etc.) to search LinkedIn jobs with built-in rate limiting to prevent IP bans.
## Features
✅ **LinkedIn-Only Job Search** — Real-time job data from LinkedIn via JobSpy
✅ **Request Throttling** — Configurable delays (default: 2 seconds) between API calls
✅ **Rate Limit Handling** — Graceful error messages when LinkedIn limits requests
✅ **5 MCP Tools** — Search, filter, get details, company profiles, discover categories
✅ **Type-Safe** — Pydantic models for all data structures
✅ **Production Ready** — Comprehensive logging and error handling
## Project Structure
```
mcp-server/
├── src/
│ ├── index.py # MCP server with tool definitions & handlers
│ └── linkedin_client.py # JobSpy wrapper with rate limiting
├── examples/
│ └── (future usage examples)
├── requirements.txt # Python dependencies
├── pyproject.toml # Project metadata
├── README.md # This file
├── .gitignore
└── .vscode/
└── linkedin-mcp.json # VS Code MCP configuration
```
## Installation
### Prerequisites
- **Python 3.10+**
- **pip** package manager
- Virtual environment (recommended)
### Setup
1. **Clone or navigate to the project:**
```bash
cd /Users/tarunkumaruppu/AI/mcp-server
```
2. **Create and activate a virtual environment:**
```bash
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. **Install dependencies:**
```bash
pip install -r requirements.txt
```
4. **Verify installation:**
```bash
python -c "from src.linkedin_client import LinkedInJobClient; print('✅ Installation successful!')"
```
## Configuration
### Rate Limiting
Control how frequently the server calls LinkedIn to prevent IP bans.
**Default behavior:** 2 seconds between requests
**To customize:**
```bash
# Via environment variable (override default)
export LINKEDIN_REQUEST_DELAY=3.0 # 3 seconds between requests
python src/index.py
# Or set in code (src/index.py, line ~35):
linkedin_client = LinkedInJobClient(request_delay=3.0)
```
**Recommended values:**
- `2.0` — Balanced (default)
- `3.0-5.0` — Conservative (safer)
- `1.0` — Aggressive (risk of bans)
### VS Code Integration
1. Open VS Code settings (`Cmd+,` on macOS)
2. Search for `"MCP Servers"`
3. The server is pre-configured in `.vscode/linkedin-mcp.json`
4. Restart VS Code to enable
Or manually add to `.vscode/settings.json`:
```json
{
"mcpServers": {
"linkedin-job-search": {
"type": "stdio",
"command": "python",
"args": ["src/index.py"],
"cwd": "/path/to/mcp-server"
}
}
}
```
## Available Tools
### 1. `search_jobs`
Search for jobs on LinkedIn with optional filters.
**Parameters:**
- `keywords` (required) — Job search keywords (e.g., "Python Developer")
- `location` (optional) — Job location (default: "United States")
- `job_type` (optional) — Filter by type (e.g., "full-time", "part-time")
- `experience_level` (optional) — Filter by level (e.g., "entry-level", "senior")
- `hours_old` (optional) — Jobs posted within last N hours (e.g., 24, 168)
- `limit` (optional) — Max results (default: 25, max recommended: 50)
**Example:**
```
search_jobs(
keywords="Data Scientist",
location="San Francisco, CA",
job_type="full-time",
limit=20
)
```
**Response:**
```
Found 20 jobs for 'Data Scientist' in San Francisco, CA
1. Senior Data Scientist at Google (San Francisco, CA) [$150,000 - $200,000]
Job ID: a1b2c3d4e5f6
URL: https://www.linkedin.com/jobs/view/...
Type: Full-time
Posted: 2 days ago
```
---
### 2. `get_job_details`
Get detailed information about a specific job.
**Parameters:**
- `job_id` (required) — Job ID from search results
- `job_url` (required) — Job URL from search results
**Example:**
```
get_job_details(
job_id="a1b2c3d4e5f6",
job_url="https://www.linkedin.com/jobs/view/..."
)
```
**Response:**
```
Job Details (ID: a1b2c3d4e5f6)
job_id: a1b2c3d4e5f6
job_url: https://www.linkedin.com/jobs/view/...
message: Full job details are available by visiting the LinkedIn URL provided.
note: For complete job descriptions, visit the job_url directly on LinkedIn.
```
---
### 3. `filter_jobs`
Filter job results by salary, type, or keywords (local operation, no API call).
**Parameters:**
- `salary_min` (optional) — Minimum annual salary
- `salary_max` (optional) — Maximum annual salary
- `job_type` (optional) — Filter by job type
- `keywords_filter` (optional) — Filter by keywords in title/company
**Example:**
```
filter_jobs(
salary_min=120000,
salary_max=200000,
job_type="full-time",
keywords_filter="remote"
)
```
---
### 4. `fetch_company_profile`
Get company information from LinkedIn.
**Parameters:**
- `company_name` (required) — Company name (e.g., "Google")
**Example:**
```
fetch_company_profile(company_name="Google")
```
**Response:**
```
Company Profile: Google
LinkedIn URL: https://www.linkedin.com/search/results/companies/?keywords=Google
Open Jobs: 142
```
---
### 5. `list_job_categories`
Discover supported job types and experience levels (no API call).
**Example:**
```
list_job_categories()
```
**Response:**
```
Supported Job Categories
Job Types:
- Full-time
- Part-time
- Contract
- Temporary
- Internship
Experience Levels:
- Entry-level
- Mid-level
- Senior
- Executive
Note: Not all filters may be available for all searches on LinkedIn.
```
---
## Rate Limiting & Error Handling
### How Rate Limiting Works
1. **Request Throttle** — Server waits `LINKEDIN_REQUEST_DELAY` seconds between API calls
2. **Timestamp Tracking** — Each request timestamp is logged for debugging
3. **Sleep Before Request** — If requests come too fast, the server sleeps to enforce delay
### Common Errors & Solutions
#### ❌ "LinkedIn rate limit reached"
```
Error: LinkedIn rate limit reached. Please wait 5-10 minutes before retrying.
```
**Solution:** Increase `LINKEDIN_REQUEST_DELAY` to 3-5 seconds, or wait and retry.
#### ❌ "LinkedIn blocked the request (HTTP 403)"
```
Error: LinkedIn blocked the request (HTTP 403). Your IP may be temporarily banned.
Wait 24-48 hours before retrying.
```
**Solution:** Your IP is temporarily blocked by LinkedIn. Wait 24-48 hours, then retry with higher delay.
#### ❌ "Failed to search jobs"
```
Error: Failed to search jobs: [detailed error message]
```
**Solution:** Check your internet connection, keywords, and location. Review logs for details.
---
## Logging
All requests and errors are logged to `stdout` with timestamps.
**Log format:**
```
2026-06-10 14:30:45,123 - src.linkedin_client - INFO - Searching LinkedIn for: Python Developer in San Francisco
2026-06-10 14:30:47,456 - src.linkedin_client - INFO - Found 25 jobs
2026-06-10 14:30:48,789 - src.index - INFO - Tool called: search_jobs with arguments: {...}
```
**To adjust log level:**
```python
# In src/index.py, line ~20
logging.basicConfig(level=logging.DEBUG) # For verbose output
```
---
## Usage Examples
### Example 1: Simple Job Search
```
Claude: "Find me Python developer jobs in San Francisco"
→ Tool Call: search_jobs(
keywords="Python Developer",
location="San Francisco, CA",
limit=25
)
→ Result: [25 job listings with titles, companies, salaries]
```
### Example 2: Search with Filters
```
Claude: "Find senior-level data scientist jobs in NYC, full-time, posted in the last 7 days"
→ Tool Call: search_jobs(
keywords="Data Scientist",
location="New York, NY",
job_type="full-time",
experience_level="senior",
hours_old=168,
limit=20
)
→ Result: [20 filtered job listings]
```
### Example 3: Company Research
```
Claude: "How many open positions does Google have on LinkedIn?"
→ Tool Call: fetch_company_profile(company_name="Google")
→ Result:
LinkedIn URL: https://www.linkedin.com/search/results/companies/?keywords=Google
Open Jobs: 142
```
### Example 4: Multi-Step Job Hunt
```
Turn 1: Claude searches for jobs
→ search_jobs(keywords="ML Engineer", location="Remote", limit=30)
Turn 2: Claude asks for company info on top candidates
→ fetch_company_profile(company_name="Anthropic")
Turn 3: Claude filters results by salary
→ filter_jobs(salary_min=150000, salary_max=250000)
```
---
## Performance & Limitations
### Performance
| Operation | Latency | API Calls |
|-----------|---------|-----------|
| `search_jobs` | 2-10 seconds | 1 |
| `get_job_details` | 2-5 seconds | 1 |
| `filter_jobs` | <1 second | 0 (local) |
| `fetch_company_profile` | 2-10 seconds | 1 |
| `list_job_categories` | <1 second | 0 (local) |
### Limitations
⚠️ **JobSpy Limitations:**
- Limited job description text extraction (full details require visiting LinkedIn URL)
- Company profile data is minimal (name, URL, job count only)
- Salary data may be incomplete on LinkedIn
⚠️ **Rate Limiting:**
- Default 2-second throttle means max ~30 jobs per minute
- LinkedIn may impose additional limits if too many requests from same IP
- No guaranteed uptime—LinkedIn can block access at any time
⚠️ **Data Freshness:**
- Job listings are real-time from LinkedIn
- Salaries and benefits may not be up-to-date
- Closed jobs may still appear briefly in results
---
## Development
### Project Dependencies
- **mcp** — Model Context Protocol SDK
- **jobspy** — LinkedIn job scraper
- **pydantic** — Data validation & type hints
- **python-dotenv** — Environment variable support
### Code Structure
**`src/linkedin_client.py`:**
- `LinkedInJobClient` class — Main client with rate limiting
- `JobListing` Pydantic model — Job data structure
- `CompanyProfile` Pydantic model — Company data structure
**`src/index.py`:**
- MCP server setup and tool definitions
- Tool handlers (search, details, filter, company, categories)
- Error handling and formatting
### Extending the Server
To add a new tool:
1. **Define the tool schema** in `get_tools()` (src/index.py)
2. **Implement handler** in `linkedin_client.py`
3. **Add MCP handler** in `handle_call_tool()` (src/index.py)
4. **Document** in this README
Example:
```python
# 1. In linkedin_client.py
def my_new_feature(self, param: str) -> dict:
"""Implementation"""
pass
# 2. In index.py, add to get_tools():
Tool(
name="my_tool",
description="...",
inputSchema={...}
)
# 3. In handle_call_tool():
elif name == "my_tool":
return await handle_my_tool(arguments)
# 4. Create handler:
async def handle_my_tool(arguments: dict):
...
```
---
## Troubleshooting
### Issue: `ModuleNotFoundError: No module named 'mcp'`
**Solution:**
```bash
pip install -r requirements.txt
```
### Issue: Connection timeout to LinkedIn
**Solution:**
- Check internet connection
- Verify LinkedIn is accessible from your IP (not blocked)
- Increase `LINKEDIN_REQUEST_DELAY` to reduce request frequency
### Issue: Empty search results
**Solution:**
- Try different keywords (LinkedIn may have strict matching)
- Expand location (try "United States" instead of specific city)
- Check if jobs exist on LinkedIn directly
### Issue: Server won't start
**Solution:**
```bash
# Verify Python version
python --version # Should be 3.10+
# Check dependencies
python -c "import mcp; import jobspy; print('OK')"
# Run with verbose logging
PYTHONUNBUFFERED=1 python src/index.py
```
---
## Security Considerations
### Current Implementation
✅ Parameterized inputs (no SQL injection risk)
✅ Request throttling (prevents IP bans)
✅ Graceful error handling (no sensitive info leakage)
✅ Local-only operation (no cloud storage)
### For Production Use
⚠️ **Consider adding:**
- IP rotation for high-volume searches
- Request caching (SQLite) to reduce API calls
- User rate limiting (per-user request quotas)
- Audit logging (track who searches what)
- API key authentication (if exposing over network)
---
## FAQ
**Q: Will LinkedIn ban my IP?**
A: Unlikely with the default 2-second throttle. LinkedIn allows scraping via JobSpy. Higher request rates or aggressive filtering may trigger temporary blocks (24-48 hours).
**Q: Can I search all job boards at once?**
A: No, this server is LinkedIn-only. Use JobSpy directly for multi-source searches.
**Q: How many jobs can I search per day?**
A: Unlimited theoretically, but LinkedIn may rate-limit or block after 1000+ requests/hour.
**Q: Can I save job listings locally?**
A: Not in this version. Extend with SQLite caching if needed.
**Q: Does this tool post on LinkedIn?**
A: No, it's read-only. Cannot apply to jobs, message recruiters, or modify profiles.
---
## References
- [MCP Documentation](https://modelcontextprotocol.io/)
- [JobSpy GitHub](https://github.com/speedyapply/JobSpy)
- [LinkedIn Terms of Service](https://www.linkedin.com/legal/user-agreement)
- [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk)
---
## License
MIT License — See LICENSE file for details.
---
## Support
For issues, questions, or feature requests:
1. Check the **Troubleshooting** section above
2. Review **Logs** for detailed error messages
3. Verify your **Configuration** (request delay, credentials, internet)
---
**Last Updated:** June 10, 2026
**Version:** 1.0.0
**Status:** Production Ready
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues