Skip to main content
Glama
VishweshMurali

SkillMapper MCP Server

SkillMapper

An agentic RAG system that compares resumes against job descriptions, retrieves supporting evidence via hybrid search, and produces structured evaluation results with human-in-the-loop escalation for low-confidence cases. Exposed two ways -- a single consolidated MCP server (5 tools, in-process LangGraph agent) and an alternate 5-server MCP architecture (one server per pipeline stage, driven by an orchestrator client) -- plus a shared Streamlit reviewer UI.

Runs entirely locally against Ollama (llama3.1:8b for generation, nomic-embed-text for embeddings) — no paid API keys required.

Architecture

                         ┌─────────────────────────┐
                         │      MCP Server          │
                         │  (5 tools, stdio)         │
                         └────────────┬─────────────┘
                                      │
              ┌───────────────────────┼────────────────────────┐
              │                       │                        │
   ┌──────────▼──────────┐  ┌─────────▼─────────┐   ┌──────────▼──────────┐
   │ search_documents      │  │ compare_resume_to_job│   │ lookup_evaluation_  │
   │ retrieve_evidence     │  │  → LangGraph agent   │   │ result / capture_   │
   │  → hybrid retriever   │  │                        │   │ reviewer_feedback   │
   └──────────┬────────────┘  └─────────┬──────────────┘   │  → SQLite repo      │
              │                          │                  └──────────┬───────────┘
              │              ┌───────────▼────────────┐               │
              │              │ retrieve → compare →     │               │
              │              │ confidence_check          │◄──────────────┘
              │              │  ├─ retry (widen k) ───┐  │
              │              │  ├─ escalate ──────────┼──┼──► Streamlit reviewer app
              │              │  └─ finalize            │  │   (approve/override/reject,
              │              └────────────┬────────────┘  │    feeds back into SQLite)
              │                            │               │
   ┌──────────▼────────────────────────────▼───────────────▼──────┐
   │      Hybrid retriever: BM25 (rank_bm25) + Chroma (dense)       │
   │      fused with Reciprocal Rank Fusion                          │
   └──────────┬─────────────────────────────────────────────────────┘
              │
   ┌──────────▼──────────────────────────────────────────┐
   │  Ingestion: LlamaIndex SentenceSplitter run per-section  │
   │  (section boundaries detected first) → embedded via     │
   │  Ollama (nomic-embed-text, content-hash cached, async)  │
   └───────────────────────────────────────────────────────┘

Alternate: 5-server MCP architecture

mcp_servers/ ports the same retry/escalate semantics to five standalone MCP servers -- one per pipeline stage -- driven by an orchestrator client (scripts/run_orchestrator.py) instead of in-process LangGraph nodes. It's a parallel implementation, not a replacement: both architectures share the same hybrid retriever, SQLite repo, and Streamlit app, and both are actively tested (see Repo layout).

    ┌────────────────────────────────────────────────────────────────┐
    │ Orchestrator client (scripts/run_orchestrator.py)              │
    │ spawns + drives 5 servers over stdio, per session               │
    └────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
    ┌────────────────────────────────────────────────────────────────┐
    │ jd-parser: parse_jd(job_id)                                     │
    │ -> requirement_blocks                                           │
    └────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
    ┌────────────────────────────────────────────────────────────────┐
    │ for each requirement block:                                     │
    │ rag-router: decide_retrieval(retry_count)                       │
    │ -> hybrid retriever (BM25 + Chroma, RRF) -> evidence             │
    └────────────────────────────────────────────────────────────────┘
                                     │
                              (all evidence)
                                     ▼
    ┌────────────────────────────────────────────────────────────────┐
    │ resume-comparator: compare_resume_jd                            │
    │ -> verdict, matched/missing skills, self-reported confidence    │
    └────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
    ┌────────────────────────────────────────────────────────────────┐
    │ scorer: score_comparison(evidence, self_reported_confidence)    │
    │ -> confidence                                                   │
    └────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
    confidence >= threshold?
      ├─ yes ─────────────────────────────────────────────────► completed
      ├─ no, retries left ──► retry_count++, loop back to rag-router
      └─ no, retries exhausted ─────────────────────────────────► escalated
                                     │
                                     ▼
    ┌────────────────────────────────────────────────────────────────┐
    │ SQLite repo (same repo as the LangGraph path above)             │
    │ + data/sessions/<session_id>.jsonl event log                    │
    │ -> feeds the same Streamlit reviewer app                        │
    └────────────────────────────────────────────────────────────────┘
                                     │
                         (optional --eval flag)
                                     ▼
    ┌────────────────────────────────────────────────────────────────┐
    │ ragas-eval: run_ragas_eval(session_id)                          │
    │ re-judges the session's JSONL log after the fact                │
    └────────────────────────────────────────────────────────────────┘

Run it the same way as the LangGraph path:

uv run scripts/run_orchestrator.py resume-01-senior-swe-backend job-01-swe-backend
uv run scripts/run_orchestrator.py resume-01-senior-swe-backend job-01-swe-backend --eval  # + RAGAS-score the session

Why these choices

  • Custom LangGraph StateGraph, not create_react_agent. The agent needs an explicit retry counter (widening k on each attempt, not repeating the same call) and a terminal escalate-to-human path. The prebuilt ReAct agent's fixed internal routing doesn't expose hooks for either.

  • Hybrid retrieval (BM25 + dense), fused with Reciprocal Rank Fusion. Semantic similarity alone can miss an exact certification name or tool acronym (e.g. "CKA") buried in a long skills list; BM25 catches the exact string, dense catches paraphrases. Both signals are fused rather than picking one.

  • Raw chromadb + rank_bm25 instead of llama-index-vector-stores-chroma / QueryFusionRetriever. LlamaIndex is used only for its chunking primitives. Retrieval and fusion are hand-written so the RRF math and metadata filtering are unit-testable and debuggable directly, instead of living behind a framework abstraction — the same "don't treat the pipeline as a black box" principle that drove the chunking fix below.

  • Metadata-aware, section-first chunking. See the debugging story.

Related MCP server: ResumePilot MCP

The debugging story

The pipeline was fully orchestrated and technically functional, but wasn't producing good evaluations. Rather than guessing at the LLM prompt or the agent's routing logic, each stage was isolated and tested independently. Running RAGAS against the RAG layer alone (not the full comparison agent) pointed at the retrieval/chunking layer rather than orchestration. (On the final corpus and judges both chunkers score high on faithfulness and recall; see the measured results below, which do not show a decisive end-to-end gain.)

Inspecting the chunker: naive whole-document sentence-splitting was cutting across resume section boundaries — merging the tail of an "Experience" entry into the start of a "Skills" list, or vice versa, whenever the two were short enough to fit in one chunk window. The fix was metadata-aware chunking: split the document into sections first (via heading detection), then run the sentence splitter independently within each section, so a chunk can never straddle two sections by construction.

This is reproduced as a real, re-runnable comparison in this repo — not just asserted. scripts/run_chunking_comparison.py ingests the same corpus into two Chroma collections (naive vs. metadata-aware), runs the identical 12-question test set through both, and scores each with RAGAS under the same local judge:

Measured results (12-question test set, 4 RAGAS metrics, identical retrieval and answer pipeline for both collections; local Ollama judge; 2 independent runs, 2026-08-10 and 2026-10-16):

Metric

Run A (Aug 10): naive

Run A: metadata-aware

Run A Δ

Run B (Oct 16, qwen2.5:7b): naive

Run B: metadata-aware

Run B Δ

faithfulness

0.905

0.868

-0.036

0.958

0.992

+0.034

answer_relevancy

0.857

0.827

-0.029

0.841

0.865

+0.024

context_precision

0.757

0.549

-0.208

0.785

0.662

-0.122

context_recall

1.000

1.000

+0.000

0.993

1.000

+0.007

How to read this honestly: the two runs disagree on the sign of the faithfulness and answer-relevancy deltas, and both differences are small relative to the noise of an 8B local judge on 12 questions, so I do not claim a measured end-to-end quality win from section-aware chunking. Two things are consistent across both runs: context_recall is saturated (~1.0) for both chunkers on this small corpus, and context_precision is lower for metadata-aware chunking (more, smaller chunks put more marginal chunks into the retrieved set). What the change does guarantee is structural: a chunk can no longer straddle two resume sections, which is proven by unit test (below), not by the RAGAS deltas. Run A's judge model was not recorded; Run B used qwen2.5:7b. Reproduce with uv run scripts/ingest.py then uv run scripts/run_chunking_comparison.py (tens of minutes). See Limitations for why an 8B local judge's absolute scores aren't comparable to GPT-4-judged benchmarks.

A synthetic unit test also encodes the bug directly: tests/unit/test_naive_chunking.py::test_naive_chunker_straddles_section_boundary constructs a fixture where naive chunking demonstrably merges two sections into one chunk, and tests/unit/test_chunking.py::test_chunks_never_straddle_a_section_boundary proves the metadata-aware chunker cannot do this.

The optimization story

Two mechanisms were implemented for real and benchmarked honestly — including where the numbers didn't match initial assumptions:

Embedding cache (content-hash + query/document-role keyed, via diskcache): a clear, dramatic win. Re-embedding the same 8 texts with a warm cache took essentially 0s vs. 2.7s cold (measured with scripts/run_benchmark.py) — a >6000x speedup, since a cache hit skips the Ollama round-trip entirely. (Re-run on a later date it was ~25s cold vs. 0.00s warm: the cold figure varies with whether Ollama had the embedding model loaded, but the warm path is always effectively free.)

Async parallel embedding (asyncio.gather + asyncio.Semaphore fan-out, used by the agent's retrieve node when it queries once per JD requirement): measured no speedup in this environment (serial and concurrency=4 both took ~2.4s for 8 texts). This local Ollama install processes embedding requests serially by default (OLLAMA_NUM_PARALLEL is typically 1 unless explicitly reconfigured), so concurrent requests queue rather than overlap. The code path is real and correctly bounded (verified in tests/unit/test_embedding_cache.py), but on this machine the cache — not the parallelism — is what actually reduces latency. This is reported as measured, not adjusted to match a target number.

Storage: comparing the two Chroma collections on this repo's demo corpus (8 resumes + 3 job descriptions):

Collection

Chunks

Text bytes

Embedding bytes

Total

naive

25

16.4KB

75.0KB

91.4KB

metadata-aware

54

14.7KB

162.0KB

176.7KB

Metadata-aware chunking here uses more storage than naive, not less — because respecting section boundaries prevents the splitter from merging short adjacent sections into one chunk, producing more (smaller) chunks and therefore more embeddings. This is the real, honest tradeoff: the structural guarantee of section-aware chunking (no chunk spans two sections) is paid for partly in embedding storage, not free; the RAGAS table above does not show a decisive retrieval-quality gain to offset it on this corpus. (Regenerate with uv run scripts/run_benchmark.py; requires scripts/ingest.py to have populated both collections first.)

Quickstart

# 1. Install Ollama (https://ollama.com) and pull the two models used
ollama pull llama3.1:8b
ollama pull nomic-embed-text

# 2. Install dependencies
uv sync

# 3. Ingest the synthetic resume/JD corpus (both chunking variants)
uv run scripts/ingest.py

# 4. Run the agent once, end to end
uv run scripts/run_agent.py resume-01-senior-swe-backend job-01-swe-backend

# 5. Run the MCP server (stdio transport)
uv run scripts/run_mcp_server.py

# 6. Run the Streamlit reviewer app
uv run streamlit run src/skillmapper/review_app/app.py

MCP tools

Tool

Purpose

search_documents

Hybrid (BM25 + dense) search over ingested resume/JD chunks

retrieve_evidence

Resume evidence supporting one specific job requirement

compare_resume_to_job

Full agent run: retrieve → compare → retry/escalate → structured EvaluationResult

capture_reviewer_feedback

Record a human reviewer's decision (same path the Streamlit app uses)

lookup_evaluation_result

Query stored evaluations by id, resume, job, or status

compare_resume_to_job can take 30 seconds to a few minutes on a local 8B model (retrieval + generation, plus up to 2 retries). It short-circuits to a cached completed result on repeat calls unless force_rerun=True. The MCP Inspector is a good way to exercise all 5 tools manually:

npx @modelcontextprotocol/inspector uv run scripts/run_mcp_server.py

Streamlit app

The Streamlit app (uv run streamlit run src/skillmapper/review_app/app.py) is more than a reviewer queue -- it's a working front end onto the whole pipeline:

Tab

What it does

Documents

Lists every ingested resume/JD with its chunk/section breakdown; upload a new one (file or pasted markdown) or edit an existing one in place. Saving deletes that document's stale chunks, re-ingests it into both Chroma collections, and reloads the in-memory BM25 index -- no restart needed.

Retrieval

Runs the same hybrid (BM25 + dense, RRF-fused) retriever the agent uses, with live query/doc-type/doc-id/section filters, so you can see exactly what evidence a given query surfaces.

Score

Pick a resume + job description and run the full LangGraph agent on demand; the result renders immediately and is persisted (it also appears in History).

Escalation Queue / History

The original reviewer workflow -- approve/override/reject escalated evaluations, browse past runs.

Observability (optional): LangSmith

The agent's LLM calls (langchain_ollama.ChatOllama) and the hybrid retriever (@traceable-wrapped) are LangSmith-instrumented but inert by default. Set in .env:

LANGSMITH_TRACING=true
LANGSMITH_API_KEY=<your key from smith.langchain.com>
LANGSMITH_PROJECT=skillmapper

and every compare_resume_to_job run produces a nested trace tree in LangSmith: retrieve (with the underlying hybrid_retrieve call and its RRF-fused hits) → compare (the exact prompt and structured-output parse) → confidence_check → the retry/escalate routing decision. With LANGSMITH_TRACING unset or false, none of this code path runs and there is no dependency on the LangSmith service.

Testing

uv run pytest                 # fast unit suite, no Ollama required (default)
uv run pytest -m ollama       # + integration tests against a real local Ollama

Unit tests cover: section splitting, chunking boundary-safety (including a test that documents the naive-chunking bug directly), Reciprocal Rank Fusion math and metadata post-filtering, embedding cache hit/miss + query/document-role discrimination, LangGraph retry/escalation routing (stubbed LLM, real k-widening-per-retry assertions), SQLite repository round-trips, MCP tool functions, and RAGAS report formatting.

Repo layout

src/skillmapper/
  models.py              Resume, JobDescription, Chunk, EvaluationResult, ReviewerFeedback
  config.py               pydantic-settings (Ollama host/models, thresholds, paths, optional LangSmith)
  ingestion/               section splitting, metadata-aware + naive chunkers, ingest/delete pipeline
  retrieval/               embeddings (cached), Chroma, BM25, hybrid RRF fusion (LangSmith-traceable)
  agent/                   LangGraph state/nodes/graph, LLM wrapper, runtime wiring
  storage/                 SQLite repository (evaluations, reviewer feedback)
  mcp_server/              FastMCP-style server (mcp.server.mcpserver.MCPServer) + 5 tools
  review_app/              Streamlit UI: Documents (browse/upload/edit), Retrieval (search tester),
                             Score (on-demand agent run), Escalation Queue, History
  eval/                    RAGAS harness, testset, chunking comparison, benchmark, report formatting
  orchestration/           shared schemas + JSONL session-event log used by the 5-server architecture
mcp_servers/                the alternate 5-server MCP architecture (see above), one dir per server:
  jd-parser/                 parse_jd
  rag-router/                 decide_retrieval (wraps the same hybrid retriever)
  resume-comparator/          compare_resume_jd
  scorer/                     score_comparison
  ragas-eval/                 run_ragas_eval (scores a session's JSONL log)
  orchestrator/               client that spawns + drives the 5 servers over stdio
scripts/                   CLI entry points for each of the above, incl. run_orchestrator.py
tests/unit/                fast, fully mocked (incl. tests/unit/mcp_servers/ for the 5-server tools)
tests/integration/         marked slow/ollama, require a real local Ollama (incl. the 5-server e2e path)
data/synthetic/            8 synthetic resumes + 3 job descriptions (author-generated, fictitious)

Limitations

  • The agent's "confidence" measures retrieval coverage, not correctness. It is 0.3 x the LLM's self-reported confidence plus 0.7 x the fraction of job requirements for which retrieval returned at least one chunk above a score threshold. A clearly weak match can therefore still score high confidence (a data-analyst resume against an ML-engineer job came back weak_match with confidence 0.94) because evidence was found for every requirement, even though it showed the candidate lacks them. Treat verdict / overall_score as the assessment and confidence as "was there evidence to reason over"; the human-escalation path mainly catches retrieval gaps, not wrong verdicts.

  • Model choice affects results. The documented setup is llama3.1:8b. The full suite (unit + all 11 Ollama integration tests) was also verified end to end with qwen2.5:7b by setting OLLAMA_LLM_MODEL=qwen2.5:7b in .env; scores and wording differ between models.

  • RAGAS judge is a local 8B model, not GPT-4. Scores are noisier and not directly comparable to published GPT-4-judged benchmarks. The chunking-comparison story is built around the delta between naive and metadata-aware chunking under the same fixed judge, which is judge-independent in a way absolute scores aren't.

  • compare_resume_to_job is slow (30s-minutes) on local hardware — there's no async job-polling API, just a SQLite short-circuit on repeat calls.

  • Single-user SQLite, no auth on the Streamlit app — fine for a local portfolio demo, not for multi-tenant use.

  • Synthetic corpus is small (8 resumes, 3 JDs) — enough to demonstrate the retrieval/chunking/agent mechanics honestly, not a claim about performance at scale.

License

MIT -- see LICENSE.

Available Tools

5 tools
capture_reviewer_feedbackC

Record a human reviewer's decision on an evaluation.

ParametersJSON Schema
NameRequiredDescriptionDefault
commentsNo
decisionYes
evaluation_idYes
reviewer_nameYes
corrected_verdictNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
statusYes
feedback_idYes

TDQS

C2.6/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, so the description carries full behavioral burden. It implies a write/persist operation but doesn't say whether entries are immutable, whether duplicates are allowed, whether the reviewer must be authorized, or what happens on conflicting decisions. An output schema exists, so return values needn't be explained, but the mutation semantics are undisclosed.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single sentence, front-loaded with the verb and resource, no filler. Efficient but so terse that it omits necessary detail rather than being genuinely well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

A mutation tool with 5 params, 0% schema coverage, no annotations. The output schema covers returns, but the description doesn't compensate for the parameter and behavioral gaps. An agent cannot reliably determine what 'decision' values are valid or what 'corrected_verdict' means without guessing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% across 5 parameters. The description mentions none of them specifically — not evaluation_id, decision format, reviewer_name, comments, or corrected_verdict. With zero coverage and no compensating text, meaning is left entirely to bare param titles.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb 'Record' plus resource 'human reviewer's decision on an evaluation', which is clearly distinguishable from siblings like lookup_evaluation_result or search_documents. It doesn't name siblings explicitly but the purpose is unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No indication of when this should be used versus alternatives like lookup_evaluation_result, no prerequisites, no mention of whether the evaluation must exist already. Usage is only implied by the verb 'Record'.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

compare_resume_to_jobA

Run the full resume-vs-job comparison agent (retrieval + LLM compare + retry/escalation). Expect 30s-several minutes on a local 8B model; returns a cached completed result unless force_rerun=True.

ParametersJSON Schema
NameRequiredDescriptionDefault
job_idYes
resume_idYes
force_rerunNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
idNo
job_idYes
statusYes
verdictYes
evidenceNo
reasoningYes
resume_idYes
confidenceYes
created_atNo
retries_usedNo
overall_scoreYes
matched_skillsNo
missing_skillsNo
escalation_reasonNo

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it discloses two high-value traits: long and variable latency (30s-several minutes on a local 8B model) and default cache-return behavior. It omits cost/auth and whether results are deterministic, but covers the operationally critical facts well.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences, zero waste, with the pipeline scope front-loaded and the latency/caching caveat following. Every clause earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be described. The description supplies the latency, caching, and force_rerun context an agent needs to call it correctly; only auth/permission prerequisites are unaddressed, which is minor for this operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must compensate. It explains force_rerun=True semantics (bypasses the cached result), which is the only non-obvious parameter; resume_id and job_id are self-explanatory by name. It adds some meaning but not full documentation for the coverage gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb+resource ('Run the full resume-vs-job comparison agent') and enumerates the pipeline stages (retrieval + LLM compare + retry/escalation). It distinguishes the tool from siblings like retrieve_evidence by signaling it is the complete end-to-end operation, though it does not explicitly name a sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied: this is the tool for a full comparison, and the caching note hints you need not rerun needlessly. However, it never states when to prefer it over retrieve_evidence or lookup_evaluation_result, nor any exclusions or prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

lookup_evaluation_resultB

Look up stored evaluation results by id, or filter by resume/job/status.

ParametersJSON Schema
NameRequiredDescriptionDefault
job_idNo
statusNo
resume_idNo
evaluation_idNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultsYes

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. "Look up stored ... results" implies a read-only retrieval over persisted data and discloses that both id-based and filter-based access exist, but permissions, result limits, pagination, and how multiple filters combine are all unstated.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler. It is efficient, though it is terse enough that a little more structure around the two lookup modes would have cost nothing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained. However, for a four-optional-parameter query tool with no annotations, the description omits filter-combination rules, empty-result behavior, and any cap on returned records.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It maps concepts to all four parameters (evaluation_id via "id", resume_id, job_id, status), but adds no format details, does not note that every parameter is optional, and does not explain filter combination semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a specific verb+resource ("Look up stored evaluation results") and states the two access modes: by id or by filter. The sibling tools (search_documents, retrieve_evidence, compare_resume_to_job, capture_reviewer_feedback) are clearly different resources, so no explicit differentiation is needed, but it is also not named.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It implies two usage modes (id lookup vs filtering by resume/job/status) but never states when to prefer one, whether filters can be combined, or what to do when no match exists. There is no mention of alternatives or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

retrieve_evidenceC

Retrieve resume evidence supporting a specific job requirement.

ParametersJSON Schema
NameRequiredDescriptionDefault
top_kNo
job_idYes
resume_idYes
requirementYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
evidenceYes
requirementYes

TDQS

C2.7/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. Beyond implying a read operation with 'Retrieve,' it does not state permissions, rate limits, or any other behavioral trait.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The single sentence is front-loaded and contains no filler, earning its place. However, it may be too terse for a tool with four parameters and no schema descriptions, so it is not maximally well-sized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be explained. But with no annotations and 0% schema description coverage, the description is insufficiently complete: it omits usage context and parameter semantics needed to invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain the parameters, but it only vaguely connects to 'requirement' and 'resume.' It does not clarify resume_id, job_id, or top_k, leaving most parameter semantics undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a specific verb ('Retrieve') and resource ('resume evidence') scoped to a job requirement. It is clear but does not differentiate itself from sibling tools like compare_resume_to_job or search_documents.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool versus alternatives, nor any prerequisites or exclusions. The description only states what it does, leaving the agent to infer its role among the sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_documentsC

Hybrid (BM25 + dense) search over ingested resume/job-description chunks.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryYes
top_kNo
sectionNo
doc_typeNoany

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultsYes

TDQS

C2.9/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, so the description carries the full burden. It usefully discloses the hybrid BM25+dense ranking approach, which is real behavioral context beyond the schema, but says nothing about ranking behavior, result volume, or whether filtering happens before or after retrieval.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with no filler, and the retrieval mechanism is stated up front. It is efficient, though arguably too terse given the undocumented parameters.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists so return values need not be explained, but with 0% parameter coverage and no routing guidance versus four siblings, the description is not complete enough for an agent to call this correctly in a multi-tool retrieval set.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate and does not. Only 'query' is self-evident; 'section' and 'doc_type' are entirely undefined in both schema and description, and 'top_k' semantics (default 5, ranking cut) are unstated.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (search) and resource (ingested resume/job-description chunks) plus the retrieval technique (hybrid BM25 + dense). It does not distinguish itself from the sibling retrieve_evidence, which sounds like an overlapping retrieval tool, so the agent cannot tell them apart from the text alone.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No when-to-use guidance, no conditions, no mention of alternatives such as retrieve_evidence or compare_resume_to_job. The agent is left to guess which retrieval tool fits a given task.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 5 tool updatesv0.1.0
    • First observedcapture_reviewer_feedback
    • First observedcompare_resume_to_job
    • First observedlookup_evaluation_result
    • First observedretrieve_evidence
    • First observedsearch_documents

TDQS

B3.2/5.0

Scored across 5 tools

Disambiguation4/5

search_documents and retrieve_evidence both retrieve content, which could cause confusion, but the descriptions differentiate them (broad hybrid search vs. targeted evidence for a specific requirement). The other three tools (compare, capture_feedback, lookup) are clearly distinct.

Naming Consistency5/5

All five tools follow a consistent verb_noun snake_case pattern: search_documents, retrieve_evidence, compare_resume_to_job, capture_reviewer_feedback, lookup_evaluation_result. Verbs are varied but appropriate and readable.

Tool Count4/5

Five tools is reasonably scoped for a resume-vs-job evaluation domain, covering search, targeted retrieval, the full comparison agent, feedback, and result lookup. It is slightly lean but each tool earns its place.

Completeness3/5

The lifecycle is mostly covered (search, retrieve, compare, capture feedback, lookup results), but there is no ingestion tool despite descriptions referencing 'ingested' chunks, and no update/delete or reviewer-triage operations. These gaps may force agents to look outside the server.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables resume review against job descriptions through a three-stage pipeline: match scoring, experience rewriting, and ATS optimization. Supports text or document uploads.
    0
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables parsing a master resume, analyzing job descriptions for ATS/match scores, generating truthful tailored resumes, tracking applications, and producing PDF reports through Claude Desktop.
    17
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to upload resumes and receive structured ATS scores with parseability, section coverage, contact-info, and keyword analysis, along with qualitative improvement suggestions via an LLM.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables autonomous, evidence-driven screening of technical candidates by evaluating resumes against job descriptions and rubrics, producing ranked scores, gap analyses, and audit-ready PDF, HTML, and Markdown reports.
    -