Skip to main content
Glama
roncyrthomas

Resume Matcher MCP Server

by roncyrthomas

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.

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).

Related MCP server: MCP Server

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.

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

ValueErrorisError: 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). Sequence + state-machine diagrams: 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

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

# 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

# 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

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

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to securely read, search, and analyze local file systems through the Model Context Protocol. Provides tools for listing files, pattern-based searching, and reading file contents with built-in security validation.
    6
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to interact with a sandboxed filesystem via MCP tools for reading, writing, searching, and monitoring files, including batch processing and resource discovery for resume management.
    Academic Free v1.1