resume-ats-mcp
by jppoamaral
README.md
# resume-ats-mcp
A local [Model Context Protocol](https://modelcontextprotocol.io) server that turns Claude Desktop into an ATS (Applicant Tracking System) resume checker. It plugs in as a **connector**: Claude calls it as a tool mid-conversation, the server does the parsing/scoring, and Claude narrates the result.
## What it does
Two tools, exposed over MCP:
| Tool | Input | Output |
|---|---|---|
| `list_resume_files` | a directory (optional) | paths to `.pdf`/`.docx`/`.md`/`.txt` files found there |
| `evaluate_resume` | a resume (path or pasted text) + an optional job description | a Markdown report: formatting audit + keyword-match score |
**Formatting audit** — parses the file and flags things that break real ATS parsers: tables, text in headers/footers, embedded images, non-extractable ("scanned image") PDFs, page count.
**Keyword match** — when a job description is supplied, extracts candidate keywords from it (capitalized phrases, tech tokens like `CI/CD` or `.NET`, and frequently-repeated terms) and checks which ones appear in the resume, word-boundary-safe (so `CI` won't false-match inside "effi**ci**ent"). Returns a `matched/total` percentage plus the explicit missing-keyword list.
This is a heuristic, not a certified ATS engine — it's regex/frequency-based, with no LLM call inside the tool itself. The value is in feeding *structured, deterministic* signal to Claude, which then reasons over it in the conversation.
## Architecture
```mermaid
flowchart LR
subgraph Claude Desktop
UI[Chat UI] --> Model[Claude]
end
Model -- "MCP stdio\n(JSON-RPC over stdin/stdout)" --> Server[server.py\nMCPServer instance]
Server --> Parse[pypdf / python-docx\nfile parsing]
Server --> Score[keyword extraction\n+ formatting audit]
Server -- reads --> FS[(Resume files\non disk)]
```
Claude Desktop launches `server.py` as a **child process** and talks to it over **stdio** using JSON-RPC — this is the "local connector" pattern in MCP, as opposed to a remote HTTP/SSE connector. No network port, no auth: the process only exists while Claude Desktop is running, and only your local machine can reach it.
## How the connector is registered
Claude Desktop reads `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) on startup. Adding a `mcpServers` entry tells it what command to spawn:
```json
{
"mcpServers": {
"resume-ats": {
"command": "/absolute/path/to/mcp-server/.venv/bin/python",
"args": ["/absolute/path/to/mcp-server/server.py"],
"env": {
"RESUME_ATS_DIR": "/absolute/path/to/your/resumes"
}
}
}
}
```
- `command`/`args` — point at the venv's Python interpreter directly (not a bare `python3`), so the server always runs with its own installed dependencies regardless of what's active in your shell.
- `env.RESUME_ATS_DIR` — the only machine-specific configuration. It sets the default directory `list_resume_files` browses, without hardcoding a personal path into the source code.
After editing the config, fully quit (Cmd+Q) and reopen Claude Desktop — it only reads this file at launch.
## Implementation notes
- Built on `mcp[cli]` — the official Python MCP SDK. `@mcp.tool()` decorates a plain function; its type hints and docstring become the tool's schema and description, which is what the model sees when deciding whether/how to call it.
- `stdio` is the default transport (`mcp.run()`), matching what Claude Desktop's local-connector launcher expects.
- File parsing is dispatched by extension: `pypdf` for `.pdf`, `python-docx` for `.docx`, plain read for `.md`/`.txt`.
- Keyword matching uses a lookaround-based regex (`(?<![A-Za-z0-9])keyword(?![A-Za-z0-9])`) rather than `str.count()`, to avoid substring false-positives on short tokens.
## Setup
```bash
git clone <this-repo>
cd mcp-server
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txt
```
Then add the `mcpServers` entry above to `claude_desktop_config.json`, pointing `command`/`args` at this checkout and `RESUME_ATS_DIR` at wherever your resumes live. Restart Claude Desktop.
## Testing without Claude Desktop
The MCP SDK ships a client you can drive directly, which is how this was verified during development:
```python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="./.venv/bin/python", args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
print(await session.list_tools())
print(await session.call_tool("list_resume_files", {}))
asyncio.run(main())
```
## Limitations
- Keyword extraction is heuristic (regex + frequency), not semantic — it won't recognize "led a team" as matching a JD's "leadership," for example.
- No OCR: image-based/scanned PDFs will correctly be flagged as low-text but can't be scored.
- Single-machine, single-user: this is a local stdio connector, not a hosted service.
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues