How Our AI Hiring Platform Gets Smarter Without Code Changes

Published: (December 24, 2025 at 10:38 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Introduction

Last month, our AI hiring platform could only process PDF resumes.

This week, it handles PDFs, Word documents, and scanned images with OCR.

We didn’t change a single line of code in our resume‑processing agent. We just deployed new extractors to the cluster — and the LLM‑powered agent started using them automatically.

This is what building on MCP Mesh feels like.


The Platform

We built a multi‑agent hiring platform on Kubernetes:

┌─────────────────────────────────────────────────────────────────────────┐
│                         MCP Mesh Registry                               │
│                  (Discovery + Topology + Health)                        │
└─────────────────────────────────────────────────────────────────────────┘
       ▲              ▲                ▲              ▲              ▲
       │              │                │              │              │
  ┌────┴────┐   ┌─────┴─────┐   ┌──────┴──────┐  ┌────┴────┐   ┌─────┴─────┐
  │  Resume │   │    Job    │   │  Interview  │  │ Scoring │   │    LLM    │
  │  Agent  │   │  Matcher  │   │    Agent    │  │  Agent  │   │ Providers │
  │ (LLM)   │   │           │   │   (LLM)     │  │ (LLM)   │   │           │
  └────┬────┘   └───────────┘   └─────────────┘  └─────────┘   └───────────┘

       │ dynamically discovers extractors

  ┌───────────────────────────────────────────────────────────────────┐
  │                   Extractor Tools                                 │
  │  ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐            │
  │  │   PDF   │   │   DOC   │   │  Image  │   │ Future  │            │
  │  │Extractor│   │Extractor│   │  (OCR)  │   │   ...   │            │
  │  └─────────┘   └─────────┘   └─────────┘   └─────────┘            │
  │       tags: [extractor, pdf]  [extractor, doc]  [extractor, ocr] │
  └───────────────────────────────────────────────────────────────────┘

Key insight: agents powered by @mesh.llm don’t have hard‑coded dependencies. They discover tools at runtime and intelligently choose which ones to use.


The Resume Agent

Here’s the core of our resume‑processing logic:

@mesh.llm(
    provider={"capability": "llm", "tags": ["+claude"]},
    filter=[{"tags": ["extractor"]}],          # Discover all extractors
    system_prompt="""You process uploaded resumes.

    Available tools let you extract text from different file formats.
    Choose the appropriate extractor based on the file type.
    Then analyze the extracted text and return structured candidate data.""",
    max_iterations=3,
)
@mesh.tool(
    capability="process_resume",
    tags=["resume", "ai"],
)
async def process_resume(
    file_path: str,
    file_type: str,
    llm: MeshLlmAgent = None
) -> CandidateProfile:
    return await llm(f"Process this {file_type} resume: {file_path}")

The magic is in filter=[{"tags": ["extractor"]}]. The LLM sees every tool tagged with extractor and decides which one to call based on the file type.


Day 1: PDF Only

When we launched, we had a single extractor:

# pdf_extractor.py
@mesh.tool(
    capability="extract_pdf",
    tags=["extractor", "pdf"],
    description="Extract text content from PDF files"
)
async def extract_pdf(file_path: str) -> ExtractedText:
    # PDF extraction logic
    return ExtractedText(content=text, metadata={...})

The Resume Agent’s LLM sees: Available tools: [extract_pdf]

User uploads resume.pdf → LLM reasons: “This is a PDF, I’ll use extract_pdf” → extracts text → returns a structured profile.


Day 30: Adding Word Support

Product requested Word document support, so we added a new extractor:

# doc_extractor.py
@mesh.tool(
    capability="extract_doc",
    tags=["extractor", "doc", "docx"],
    description="Extract text content from Word documents (.doc, .docx)"
)
async def extract_doc(file_path: str) -> ExtractedText:
    # Word extraction logic
    return ExtractedText(content=text, metadata={...})

Deploy to Kubernetes:

helm install doc-extractor oci://ghcr.io/dhyansraj/mcp-mesh/mcp-mesh-agent \
  --version 0.7.12 \
  -n mcp-mesh \
  -f doc-extractor/helm-values.yaml

Within 10 seconds, the Resume Agent’s LLM sees: Available tools: [extract_pdf, extract_doc]

User uploads resume.docx → LLM reasons: “This is a Word document, I’ll use extract_doc” → works.

No code change to the Resume Agent. No restart. No config update.


Day 60: Image OCR for Scanned Resumes

HR reported that candidates sometimes upload scanned PDFs or photos. We added an OCR extractor:

# image_extractor.py
@mesh.tool(
    capability="extract_image_ocr",
    tags=["extractor", "image", "ocr", "scan"],
    description="Extract text from images or scanned documents using OCR"
)
async def extract_image_ocr(file_path: str) -> ExtractedText:
    # OCR logic (Tesseract, Cloud Vision, etc.)
    return ExtractedText(content=text, confidence=0.92, metadata={...})

Deploy:

helm install image-extractor oci://ghcr.io/dhyansraj/mcp-mesh/mcp-mesh-agent \
  --version 0.7.12 \
  -n mcp-mesh \
  -f image-extractor/helm-values.yaml

Now the Resume Agent’s LLM sees: Available tools: [extract_pdf, extract_doc, extract_image_ocr]

User uploads resume_scan.jpg → LLM reasons: “This is an image, I’ll use extract_image_ocr” → works.

Handling Scanned PDFs

When a user uploads a PDF that is actually a scanned image (no selectable text), the LLM:

  1. Tries extract_pdf → gets empty/garbled text.
  2. Recognises the failure and falls back to extract_image_ocr.
  3. Successfully extracts the text via OCR and proceeds with candidate analysis.

Takeaway

By tag‑driven discovery and runtime tool selection, our platform can evolve its capabilities instantly—without touching the core agent code. This is the power of building on MCP Mesh.

The Agent Got Smarter

It learned a new recovery strategy without anyone writing that logic.

We didn’t tell the Resume Agent about OCR. We didn’t update its prompts.
We just deployed an extractor with good tags and a clear description — and the LLM figured out when to use it.


Why This Works

Traditional micro‑services require explicit wiring:

# Traditional approach – hard‑coded routing
def process_resume(file_path: str, file_type: str):
    if file_type == "pdf":
        text = call_pdf_service(file_path)
    elif file_type in ["doc", "docx"]:
        text = call_doc_service(file_path)
    elif file_type in ["jpg", "png"]:
        text = call_ocr_service(file_path)
    else:
        raise UnsupportedFormatError(file_type)
    # …

Every new format requires code changes, redeployment, and testing.

MCP Mesh with @mesh.llm inverts this:

FeatureDescription
Tools self‑describeEach extractor has tags and a description
LLM discovers toolsfilter=[{"tags": ["extractor"]}] broadcasts intent
LLM reasons about toolsChooses based on context, not hard‑coded rules
Mesh handles routingTool calls go to the right agent automatically

The Resume Agent’s code stays frozen. The platform’s capabilities expand with each Helm install.


The Enterprise Reality

AspectTraditionalMCP Mesh
Adding new file formatCode change + deploy + testhelm install
Config files for routingPer‑service0
Recovery logic for edge casesManual if/elseLLM figures it out
Time to add capabilityHours/daysMinutes

Infrastructure

# One‑time cluster setup
helm install mcp-core oci://ghcr.io/dhyansraj/mcp-mesh/mcp-mesh-core \
  --version 0.7.12 \
  -n mcp-mesh --create-namespace

# Deploys: Registry + PostgreSQL + Redis + Tempo + Grafana

The same agent code runs locally (meshctl start), in Docker Compose, and in Kubernetes—only the infrastructure changes.


What the LLM Sees

When the Resume Agent’s LLM runs, it receives a tool list like:

{
  "tools": [
    {
      "name": "extract_pdf",
      "description": "Extract text content from PDF files",
      "tags": ["extractor", "pdf"],
      "input_schema": {"file_path": "string"}
    },
    {
      "name": "extract_doc",
      "description": "Extract text content from Word documents (.doc, .docx)",
      "tags": ["extractor", "doc", "docx"],
      "input_schema": {"file_path": "string"}
    },
    {
      "name": "extract_image_ocr",
      "description": "Extract text from images or scanned documents using OCR",
      "tags": ["extractor", "image", "ocr", "scan"],
      "input_schema": {"file_path": "string"}
    }
  ]
}

The LLM reads the descriptions, understands capabilities, and makes intelligent choices. Add a new tool? It appears in this list within seconds.


LLM Failover (Bonus)

We run two LLM providers:

helm install claude-provider oci://ghcr.io/dhyansraj/mcp-mesh/mcp-mesh-agent \
  -f claude-provider/helm-values.yaml -n mcp-mesh

helm install openai-provider oci://ghcr.io/dhyansraj/mcp-mesh/mcp-mesh-agent \
  -f openai-provider/helm-values.yaml -n mcp-mesh

The Resume Agent’s provider={"capability": "llm", "tags": ["+claude"]} prefers Claude.

When Claude’s API goes down:

  1. Mesh detects unhealthy (missed heartbeats)
  2. Topology updates
  3. Next request routes to OpenAI
  4. When Claude recovers, traffic returns

Zero failover code—this is how the mesh works.


What’s Next

This article showed what we built—an AI platform that genuinely gets smarter as you add capabilities.

The next article explains why we chose MCP over REST as the foundation—and why that choice matters more than you might think.

👉 [Next: MCP vs REST — Why MCP is the better microservice protocol]


Resources

Back to Blog

Related posts

Read more »