Skip to main content
Glama
Tarunuppu

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