handshake-mcp-server
by sudhxnva
README.md
# Handshake MCP Server
<p align="left">
<a href="https://pypi.org/project/handshake-mcp-server/" target="_blank"><img src="https://img.shields.io/pypi/v/handshake-mcp-server?color=blue" alt="PyPI"></a>
<a href="https://github.com/sudhxnva/handshake-mcp-server/actions/workflows/ci.yml" target="_blank"><img src="https://github.com/sudhxnva/handshake-mcp-server/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI Status"></a>
<a href="https://github.com/sudhxnva/handshake-mcp-server/actions/workflows/publish.yml" target="_blank"><img src="https://github.com/sudhxnva/handshake-mcp-server/actions/workflows/publish.yml/badge.svg" alt="Publish"></a>
<a href="https://github.com/sudhxnva/handshake-mcp-server/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/License-Apache%202.0-brightgreen?labelColor=32383f" alt="License"></a>
</p>
Through this Handshake MCP server, AI assistants like Claude can connect to your [Handshake](https://app.joinhandshake.com) account — the leading job and internship platform for students. Search jobs, browse employers, explore events, and pull student or employer profiles, all from your AI chat.
## Installation Methods
[](#-uvx-setup-recommended)
[](#-docker-setup)
[](#-local-setup-develop--contribute)
<!-- TODO: Add demo video here -->
## Usage Examples
```
Find software engineering internships at companies in San Francisco
```
```
What events are coming up on Handshake this week?
```
```
Get details for job posting 12345678 — does it sponsor visas?
```
```
Show me the employer profile for Google on Handshake
```
## Features & Tools
| Tool | Description | Status |
|------|-------------|--------|
| `get_student_profile` | Get a student's profile by user ID (education, experience, skills, etc.) | Working |
| `get_employer_profile` | Get employer overview, open jobs, and reviews | Working |
| `search_employers` | Search for employers by keyword | Working |
| `get_job_details` | Get full job/internship posting with metadata (salary, visa, dates) | Working |
| `search_jobs` | Search jobs with keyword, location, type, and sort filters | Working |
| `get_event_details` | Get event details for career fairs and info sessions | Working |
| `search_events` | Search for upcoming events | Working |
| `close_session` | Close the browser session and free resources | Working |
> [!IMPORTANT]
> Handshake is a **student-gated platform** — you must have a valid student account and log in before tools will work. Run `uvx handshake-mcp-server --login` (or the setup wizard) to authenticate.
<br/>
## 🚀 uvx Setup (Recommended)
**Prerequisites:** [Install uv](https://docs.astral.sh/uv/getting-started/installation/) and a Handshake student account.
### 1. Install the Patchright Chromium browser (one-time)
```bash
uvx --from handshake-mcp-server patchright install chromium
```
### 2. Run the setup wizard
```bash
uvx handshake-mcp-server setup
```
The wizard asks whether you want Docker or local mode, handles login, and prints the exact `claude mcp add-json` command to register the server with your MCP client.
### 3. Register with your MCP client
After setup, paste the command the wizard printed. For example:
```bash
# Local mode (opens a browser window on your machine)
claude mcp add-json handshake '{"command":"uvx","args":["handshake-mcp-server"]}'
# Docker mode (runs headless in a container)
claude mcp add-json handshake '{"command":"uvx","args":["handshake-mcp-server","docker"]}'
```
Or add manually to your MCP client config (`claude_desktop_config.json` or equivalent):
```json
{
"mcpServers": {
"handshake": {
"command": "uvx",
"args": ["handshake-mcp-server"]
}
}
}
```
Restart your MCP client. Done.
> [!NOTE]
> The server keeps a single Chromium browser open for its entire lifetime — this avoids re-authentication overhead and makes subsequent tool calls much faster. With `--no-headless`, a browser window stays visible for the duration of the session. This is normal.
### uvx Setup Help
<details>
<summary><b>🔧 CLI Options</b></summary>
| Flag | Description |
|------|-------------|
| `--login` | Open browser for manual login, save profile, then exit |
| `--logout` | Clear saved authentication profile and exit |
| `--status` | Check current session status and exit |
| `--no-headless` | Run browser in headed (visible) mode |
| `--virtual-display` | Run via Xvfb virtual framebuffer (Linux only) |
| `--vnc-login` | Start a noVNC web server for browser-based login |
| `--vnc-port` | Port for noVNC server (default: 6080) |
| `--transport` | MCP transport: `stdio` (default) or `streamable-http` |
| `--host` | HTTP host (default: 127.0.0.1) |
| `--port` | HTTP port (default: 8000) |
| `--log-level` | `DEBUG`, `INFO`, `WARNING`, or `ERROR` (default: WARNING) |
**HTTP mode example:**
```bash
uvx handshake-mcp-server --transport streamable-http --host 127.0.0.1 --port 8000
```
</details>
<details>
<summary><b>❗ Troubleshooting</b></summary>
**Login issues:**
- Make sure you have a valid Handshake student account
- Handshake may show a CAPTCHA on first login — `--login` opens a browser so you can solve it manually
- If your session expires, re-run `uvx handshake-mcp-server --login`
**Cloudflare detection:**
- Handshake uses Cloudflare bot protection. On any desktop with a GUI, always use `--no-headless` (the setup wizard does this automatically)
- On headless Linux servers, use `--virtual-display` to avoid the headless fingerprint
- See [Cloudflare Bot Detection](#cloudflare-bot-detection) for details
**Session issues:**
- Browser profile is stored at `~/.handshake-mcp/profile/`
- Run `uvx handshake-mcp-server --status` to check if your session is still valid
- Run `uvx handshake-mcp-server --logout` to clear the profile and start fresh
**Installation issues:**
- Ensure uv is installed: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- Install the Chromium browser: `uvx handshake-mcp-server` will install it automatically on first run
</details>
<br/>
## 🐳 Docker Setup
**Prerequisites:** [Docker](https://www.docker.com/get-started/) installed and running. No other dependencies needed.
Docker runs the server headless inside a container using an Xvfb virtual display — no browser window on your machine. The browser profile (cookies/session) is stored in a named Docker volume (`handshake-profile`) and persists across container restarts.
### 1. One-time login
```bash
docker compose build
docker compose run --rm -p 6080:6080 handshake-mcp --vnc-login
```
Open `http://localhost:6080/vnc.html` in your browser and log into Handshake manually.
### 2. Start the server
```bash
docker compose up -d
```
The MCP server is now available at `http://127.0.0.1:8000/mcp` (streamable-http transport).
### 3. Configure your MCP client
```json
{
"mcpServers": {
"handshake": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "handshake-profile:/home/pwuser/.handshake-mcp",
"handshake-mcp-server",
"--transport", "stdio",
"--virtual-display"
]
}
}
}
```
### Docker Setup Help
<details>
<summary><b>🔧 Useful Commands</b></summary>
```bash
docker compose logs -f # tail server logs
docker compose run --rm handshake-mcp --status # check session validity
docker compose down # stop the server
docker rmi -f handshake-mcp-server && docker volume rm -f handshake-profile # full reset
```
</details>
<details>
<summary><b>❗ Troubleshooting</b></summary>
**Login issues:**
- Open `http://localhost:6080/vnc.html` within 5 minutes of starting `--vnc-login`
- If the session expires, repeat the one-time login step
- Chromium may leave lock files (`SingletonLock`) in the profile dir after a container kill — the server cleans these up automatically on next start
**Docker issues:**
- Check Docker is running: `docker ps`
- Rebuild after pulling new code: `docker compose build`
- Full reset: `docker rmi -f handshake-mcp-server && docker volume rm -f handshake-profile`
</details>
<br/>
## 🐍 Local Setup (Develop & Contribute)
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture guidelines and the PR checklist. Please [open an issue](https://github.com/sudhxnva/handshake-mcp-server/issues) first to discuss your change before submitting a PR.
**Prerequisites:** [Git](https://git-scm.com/downloads) and [uv](https://docs.astral.sh/uv/) installed.
```bash
# 1. Clone the repository
git clone https://github.com/sudhxnva/handshake-mcp-server
cd handshake-mcp-server
# 2. Install dependencies
uv sync --group dev
# 3. Install the Chromium browser
uv run patchright install chromium
# 4. Log in (opens a browser window)
uv run -m handshake_mcp_server --login --no-headless
# 5. Start the server
uv run -m handshake_mcp_server --no-headless
```
### Local Setup Help
<details>
<summary><b>🔧 Development Commands</b></summary>
```bash
uv run ruff check . # lint
uv run ruff check . --fix # lint + auto-fix
uv run ruff format . # format
uv run ty check # type check
uv run pytest # tests
uv run pytest --cov # tests with coverage
```
**Claude Desktop config for local dev:**
```json
{
"mcpServers": {
"handshake": {
"command": "uv",
"args": ["--directory", "/path/to/handshake-mcp-server", "run", "-m", "handshake_mcp_server"]
}
}
}
```
</details>
<details>
<summary><b>❗ Troubleshooting</b></summary>
**Scraping issues:**
- Use `--no-headless` to watch browser actions and debug scraping problems
- Add `--log-level DEBUG` for verbose logging
**Python/Patchright issues:**
- Requires Python 3.12+: `python --version`
- Reinstall Patchright: `uv run patchright install chromium`
- Reinstall dependencies: `uv sync --reinstall`
</details>
<br/>
## Cloudflare Bot Detection
Patchright does **not** bypass Cloudflare's headless fingerprint when `headless=True` — that mode uses the `chromium-headless-shell` binary, which is trivially detected.
| Environment | Solution |
|---|---|
| Desktop with GUI (macOS, Windows, Linux) | Use `--no-headless` — opens a real browser window |
| Headless Linux server | Use `--virtual-display` — Chrome runs non-headless against an Xvfb virtual display |
| Docker | Default CMD already uses `--virtual-display` |
Cloudflare challenge pages are detected automatically and raise a `RateLimitError` if unresolved.
<br/>
## Tool Return Format
All scraping tools return:
```json
{
"url": "string",
"sections": { "section_name": "raw text content" },
"references": { "section_name": [{ "kind": "job", "url": "/jobs/123", "text": "..." }] },
"section_errors": { "section_name": { "error_type": "...", "error_message": "..." } },
"unknown_sections": ["invalid_section_name"]
}
```
`get_job_details` also returns a `metadata` key with structured fields:
```json
{
"metadata": {
"id": "123", "title": "Software Engineer Intern", "company": "Acme Corp",
"salary": 3000, "salary_type": "hourly",
"work_type": "hybrid", "locations": ["San Francisco, CA"],
"job_type": "internship", "employment_type": "part_time",
"start_date": "2025-06-01", "end_date": "2025-08-31",
"deadline": "2025-03-01", "posted_at": "2025-01-15",
"work_auth_required": false, "accepts_opt": true, "accepts_cpt": false,
"will_sponsor": true, "apply_url": "https://..."
}
}
```
> [!NOTE]
> Salary values from the GraphQL API are in **cents** — divide by 100 to get dollars.
`search_jobs` also returns `jobs` (card-level metadata list) and `job_ids`. `search_employers` returns `employer_ids`. `search_events` returns `event_ids`.
<br/>
## Acknowledgements
Built with [FastMCP](https://gofastmcp.com/) and [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python). Inspired by the architecture of [linkedin-mcp-server](https://github.com/stickerdaniel/linkedin-mcp-server).
Use in accordance with [Handshake's Terms of Service](https://joinhandshake.com/tos/). Web scraping may violate platform terms. This tool is intended for personal use only.
## License
Apache-2.0 — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues