Skip to main content
Glama
roncyrthomas

Resume Matcher MCP Server

by roncyrthomas
README.md
# Resume Matcher — MCP Server & LangGraph Agent (Milestone 4)

A resume-matching system whose file-system tooling is served over the
**Model Context Protocol (MCP)**. The custom file tools from Milestone 1 are now
a standalone, JSON-RPC 2.0-compliant MCP server; the Milestone 3 LangGraph
conversational agent consumes them through an MCP client facade — same
functionality, standardized architecture. A second, optional MCP server
(DuckDuckGo, stdio) adds web search as a bonus.

## Architecture

The system is split across a protocol boundary: the agent process never touches
the filesystem directly at runtime — every read/write crosses JSON-RPC 2.0.

```mermaid
graph LR
    subgraph Host["Agent process (agent_cli.py / app.py)"]
        CLI[agent_cli.py<br/>flags: --mcp-url, --local-fs,<br/>--require-mcp, --web-search]
        AG[MatchingAgent<br/>LangGraph StateGraph]
        ENG[Engine<br/>JobMatcher + LLMClient<br/>+ fs: FileStore + web]
        FS[FileStore protocol<br/>mcp_fs_client.py]
        LOCAL[LocalFileStore<br/>delegates to fs_tools]
        MCPC[McpFileStore<br/>sync bridge over asyncio]
        DDGC[McpWebSearch<br/>stdio client]
        CLI --> AG --> ENG --> FS
        FS -.impl.-> LOCAL
        FS -.impl.-> MCPC
        ENG -.optional.-> DDGC
    end

    subgraph Server["filesystem_mcp_server.py (FastMCP)"]
        EP["/mcp — Streamable HTTP<br/>JSON-RPC 2.0"]
        HEALTH["/health — GET"]
        TOOLS["6 tools: read_file, list_files,<br/>write_file, search_in_file,<br/>watch_directory, batch_process"]
        RES["resources: resume://{filename},<br/>resume://index"]
        SANDBOX[fs_tools sandbox<br/>FS_TOOLS_BASE_DIR]
        EP --> TOOLS --> SANDBOX
        EP --> RES --> SANDBOX
    end

    DDG[duckduckgo-mcp-server<br/>subprocess]

    MCPC -- "HTTP POST /mcp" --> EP
    DDGC -- "stdio JSON-RPC" --> DDG
    LOCAL -- "direct calls (offline/test)" --> SANDBOX2[fs_tools sandbox]
```

### Components

| Component | File | Role |
|-----------|------|------|
| MCP file-system server | `filesystem_mcp_server.py` | FastMCP (official SDK), Streamable HTTP at `/mcp`, `GET /health`. Wraps the sandboxed M1 `fs_tools` primitives — it does not reimplement them. |
| M1 tool layer | `fs_tools.py` | Sandboxed file primitives: path-escape rejection, `.txt/.pdf/.docx` text extraction. |
| Client facade | `mcp_fs_client.py` | `FileStore` protocol; `LocalFileStore` (offline/tests), `McpFileStore` (sync bridge over Streamable HTTP), `McpWebSearch` (DuckDuckGo MCP over stdio). |
| Matching agent | `matching_agent.py` | LangGraph HITL agent (M3 topology unchanged); all runtime file I/O goes through the injected `FileStore` — no direct `fs_tools` import. |
| Entry points | `agent_cli.py`, `app.py` | CLI (`--mcp-url`, `--local-fs`, `--require-mcp`, `--web-search`) and Streamlit UI, both with graceful MCP fallback. |
| RAG + matcher | `resume_rag.py`, `job_matcher.py`, `reranker.py` | ChromaDB retrieval, ranking, reranking (M2). |

## The two MCP servers

### 1. Filesystem server (`filesystem_mcp_server.py`) — Streamable HTTP

**Tools** (JSON Schemas auto-generated by FastMCP from type hints, discovered
via `tools/list`):

| Tool | What it does |
|------|--------------|
| `read_file` | Read a file inside the sandbox; extracts text from `.txt/.pdf/.docx`. |
| `list_files` | List directory contents with metadata. |
| `write_file` | Write content; returns `bytes_written`. |
| `search_in_file` | Keyword search with char offsets + snippets per match. |
| `watch_directory` | **Stateful** poll cursor: first call baselines, later polls with the same `watch_id` report `new` / `modified` / `deleted` files. |
| `batch_process` | Concurrent multi-file ops with per-file failure isolation — one bad path never fails the batch. |

**Resources**: `resume://index` plus one `resume://{filename}` per file in
`resumes/` — the resource list tracks the live directory, so adding or removing
a resume changes what `resources/list` returns.

**Session lifecycle** (standard MCP over JSON-RPC 2.0):

```
initialize → notifications/initialized → tools/list → tools/call …
                                       → resources/list → resources/read …
```

### 2. Web-search server (bonus) — `duckduckgo-mcp-server` over stdio

Spawned as a per-session subprocess by `McpWebSearch`; no API key needed.
Used by the agent's deep-screen step for market context, wrapped in
try/except — if search fails, the analysis completes without it.

### Why two transports?

Streamable HTTP for the file server: it's a long-lived, independently
addressable service — multiple clients can share it, and it gets a real ops
surface (`GET /health`, curl-able). stdio for the search server: a per-session
helper with no reuse requirement, so spawning it as a child process is the
simpler fit. Details in [`docs/architecture.md`](docs/architecture.md).

### Error model (three tiers)

| Tier | Example | Surface |
|------|---------|---------|
| Domain outcome | File not found, path escapes sandbox | `{"success": false, "error": ...}` in a normal result — the agent branches on it as data, never an exception |
| Programmer error | Invalid `operation` enum | `ValueError` → `isError: true` — loud, signals a caller bug |
| Protocol error | Malformed JSON-RPC | JSON-RPC error object, handled by the MCP SDK |

### Sync bridge

LangGraph nodes are synchronous; the MCP SDK is async. `McpFileStore` runs a
daemon thread hosting a private asyncio loop, and a single coroutine owns the
open/close of the HTTP session (anyio cancel scopes must stay on one task).
Sync callers submit via `run_coroutine_threadsafe` with a 30 s timeout, so a
hung server can never deadlock a graph node.

## Agent workflow — where MCP is called

The LangGraph pipeline (`parse_jd → extract_requirements → search_resumes →
rank_candidates → summarize_shortlist → generate_report → human_feedback`)
loops on a HITL interrupt (`refine` / `compare` / `interview` / `screen` /
`explain` / `chat` / `done`). Two states cross the MCP boundary:

- **`deep_analyze`** (fan-out per shortlisted candidate on `screen`) —
  `tools/call read_file` to fetch the full resume body, plus optional DDG
  search for market context.
- **`write_decision_log`** (on `done`) — `tools/call write_file` to persist the
  decision log JSON.

Retrieval/ranking use the local ChromaDB index directly: RAG indexing is an
offline batch pipeline, and routing thousands of embedding reads through a
network protocol adds latency for zero interoperability benefit
(see the refactor boundary in [`docs/architecture.md`](docs/architecture.md)).
Sequence + state-machine diagrams:
[`docs/diagrams/agent_mcp_interaction.md`](docs/diagrams/agent_mcp_interaction.md).

## Resilience

- **Graceful fallback**: the CLI and Streamlit UI probe the server at startup;
  if unreachable they warn and fall back to `LocalFileStore` (identical
  interface, no network).
- **`--require-mcp`**: hard-fails with a start hint when the server is down —
  for demos that must prove MCP is in the loop.
- **Sandbox**: every tool and resource resolves paths against
  `FS_TOOLS_BASE_DIR`; escape attempts get a structured error. Server binds to
  loopback by default.
- **Known gaps** (out of scope): no auth/TLS on `/mcp`, single-process server,
  watch cursors are in-memory.

## Setup

```powershell
python -m venv --system-site-packages .venv
.venv\Scripts\pip install -r requirements.txt
copy .env.example .env   # add ANTHROPIC_API_KEY for live LLM runs
```

## Run

```powershell
# Terminal 1 — MCP server
.venv\Scripts\python filesystem_mcp_server.py
# health check: http://127.0.0.1:8765/health

# Terminal 2 — agent through MCP
.venv\Scripts\python agent_cli.py "senior python backend engineer" --require-mcp
# optional bonus: add --web-search (DuckDuckGo MCP, no API key)

# Streamlit UI — opens on the MCP Dashboard: live server status, tool
# playground, resume:// resources browser, and a JSON-RPC traffic log fed by
# every agent/playground call. Web search is a sidebar toggle.
.venv\Scripts\python -m streamlit run app.py
```

Configuration precedence is **CLI > env > default** for base dir
(`FS_TOOLS_BASE_DIR`), host/port (`MCP_FS_HOST`/`MCP_FS_PORT`), and the
client URL (`MCP_FS_URL`, default `http://127.0.0.1:8765/mcp`).

## Demo & tests

```powershell
# Narrated end-to-end demo (server + discovery + all 6 tools + watch + batch + agent).
# Asserts every step's expectation — exits 0 only if all hold.
.venv\Scripts\python scripts\demo_m4.py            # full
.venv\Scripts\python scripts\demo_m4.py --skip-agent  # tools only, no API key needed

# Test suite (fully offline — StubLLM, no API calls). Integration tests start a
# real server subprocess and speak real Streamable HTTP — no mocked transport.
.venv\Scripts\python -m pytest -q
```

Scenario-by-scenario coverage map (each deliverable → the test or demo step
that proves it): [`docs/test_scenarios.md`](docs/test_scenarios.md).

## Docs

- [`docs/problem_statement.md`](docs/problem_statement.md) — assignment framing
- [`docs/architecture.md`](docs/architecture.md) — full design: transport choice, JSON-RPC flow, error model, config, security
- [`docs/features.md`](docs/features.md) — feature inventory
- [`docs/diagrams/agent_mcp_interaction.md`](docs/diagrams/agent_mcp_interaction.md) — sequence + LangGraph state machine with MCP call sites
- [`docs/test_scenarios.md`](docs/test_scenarios.md) — S1–S12 scenario table mapped to tests/demo steps

Earlier milestones (M1 file tools, M2 RAG + job matcher, M3 LangGraph agent)
are included as the working baseline this milestone builds on.