Skip to main content
Glama
Pranali0809

ATS MCP Server

by Pranali0809
README.md
# ats-mcp-server

An MCP server over job boards. Ask it for a company's open roles and it answers
from Greenhouse, Workday, or LinkedIn, normalized into one shape.

Greenhouse and Workday are public JSON feeds — no browser, no login, no API key.
LinkedIn is different: it needs an authenticated browser session, so this server
delegates to a running `linkedin-mcp-server` rather than reimplementing it.

## Run it

```bash
pip install -e .

ats-mcp-server                      # stdio, for MCP clients that spawn a subprocess
ats-mcp-server --transport http     # streamable HTTP on 127.0.0.1:8100/mcp
```

## Testing it

**1. Without any setup** — the public sources need nothing running:

```bash
python -m venv .venv && .venv/Scripts/python -m pip install -e .

.venv/Scripts/python -c "
import asyncio, json
from fastmcp import Client
from ats_mcp.server import mcp

async def main():
    async with Client(mcp) as c:
        r = json.loads((await c.call_tool('search_jobs',
            {'company': 'Stripe', 'ai_ml_only': True, 'limit': 5})).content[0].text)
        print(r['found_per_source'], '->', r['returned'], 'kept')
        for j in r['data']:
            print(' ', j['source'], j['title'])

asyncio.run(main())
"
```

**2. With the MCP Inspector** — a UI to click through every tool:

```bash
npx @modelcontextprotocol/inspector .venv/Scripts/ats-mcp-server
```

**3. Including LinkedIn** — start the delegated server first, in its own window:

```bash
uvx --with "fastmcp<4" "mcp-server-linkedin==4.23.1" \
    --transport streamable-http --host 127.0.0.1 --port 8000 --path /mcp
```

Then call `source_status` to confirm `reachable: true`, and pass
`sources: ["greenhouse", "workday", "linkedin"]` to any search. LinkedIn is
never searched unless you name it — see below.

**4. From an MCP client** (Claude Desktop, Claude Code) — point it at the stdio
entry point:

```json
{ "mcpServers": { "ats": { "command": "C:/path/to/.venv/Scripts/ats-mcp-server" } } }
```

## Tools

| Tool | What it does |
|---|---|
| `find_board` | Locate a company's board on Greenhouse or Workday. Cached for the process lifetime. |
| `register_board` | Pin a board explicitly when discovery can't guess it. Verified before it's accepted. |
| `search_jobs` | Search one company's roles, with optional AI/ML-only and junior filters. |
| `search_many` | Search several companies, merged into one list. Unresolvable names are reported, not fatal. |
| `get_job` | Full posting, including the description. |
| `classify_title` | Run the title heuristics without touching the network. |
| `source_status` | Which sources are configured, whether LinkedIn is reachable, quota left. |

## Why LinkedIn is opt-in

Greenhouse and Workday are searched by default. LinkedIn is not, and has to be
named explicitly in `sources`.

Public feeds cost nothing to query. LinkedIn runs through a real logged-in
account, where volume and machine-like regularity are what get accounts
restricted. So it carries a throttle the public sources don't: a randomized
4-10s gap between calls and a self-imposed ceiling of 60 calls/day, both
enforced in `throttle.py`. Hitting the ceiling returns a readable error rather
than a platform-imposed restriction.

A source failing never fails the search. Ask for LinkedIn while its server is
down and you still get the Greenhouse and Workday results, with the LinkedIn
problem reported in `errors`.

## How the sources differ

**Greenhouse** — one `GET` returns a company's entire board (Stripe's is ~630
postings). There's no search parameter, so the board is fetched once, cached
for 15 minutes, and matched locally.

**Workday** — a `POST` with real server-side `searchText`, so a targeted query
never pulls the whole board. Results are relevance-ranked by Workday, and how
strictly it interprets a query varies by tenant: a small career site may return
loosely-related postings. Titles are worth checking before trusting the set.

## Board discovery

Greenhouse boards are addressed by a single token that's usually the company
name (`figma`, `stripe`), so those resolve on the first guess.

Workday needs three coordinates — tenant, numbered host, and career-site name —
and the site name is often company-specific (`NvidiaExternalCareerSite`).
Discovery tries name-derived combinations first and gives up after a bounded
number of probes rather than hammering a careers endpoint to establish that a
company isn't on Workday. When it can't find one, read the coordinates off the
careers URL and call `register_board`:

```
https://nvidia.wd5.myworkdayjobs.com/NvidiaExternalCareerSite/...
        └tenant┘ └───── host ─────┘ └────── site ──────┘
```

## Filters

`ai_ml_only` keeps AI/ML/MLOps/GenAI/Data-Science titles and drops management
and Data Engineer roles. Junior/intern/new-grad titles are excluded by default;
`include_junior` keeps them. These are heuristics over titles, which is all a
board search result usually gives you — `classify_title` shows how any title
will be treated.

## Notes

`fastmcp` is pinned `>=4.0,<5` on purpose. It has removed decorator keyword
arguments across majors before (4.0 dropped `exclude_args`), so an unbounded
upper bound is a future crash on a fresh install.

TDQS

A3.9/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a clearly distinct role: discovery (find_board), manual pinning (register_board), single-company search (search_jobs), multi-company search (search_many), detail retrieval (get_job), local classification (classify_title), and source diagnostics (source_status). There is minimal overlap, and the descriptions clarify boundaries like one vs. many companies.

Naming Consistency4/5

Almost all tools use a consistent snake_case verb_noun pattern: find_board, register_board, search_jobs, search_many, get_job, classify_title. source_status breaks the pattern by starting with a noun, a minor inconsistency. Overall readable and predictable.

Tool Count5/5

Seven tools is well-scoped for an ATS/job-board search server; each tool covers a distinct operation without redundancy. The count supports core discovery, search, detail, classification, and status workflows.

Completeness4/5

The surface covers discovery, source registration, single/multi-company search, job detail, title classification, and source status, which are the main lifecycle operations for this domain. Minor gaps exist, such as no explicit board-removal or broader source-listing operation, but agents can work around these.

Maintenance

ActivityMaintained
ResponsivenessNo issues