Build an MCP PDF Extractor Server for Hermes Agent

The MCP (Model Context Protocol) Python SDK now supports FastMCP — a high-level API that lets you expose any Python function as an agent tool with minimal boilerplate. In this tutorial, you’ll build a production-grade PDF text extractor MCP server, test it locally, and connect it to Hermes Agent.

By the end, your Hermes Agent will be able to extract text from any PDF on your filesystem using a tool you built.

Prerequisites

  • Python 3.11+ with uv (install via curl -LsSf https://astral.sh/uv/install.sh | sh)
  • Hermes Agent installed (standard install includes MCP support)
  • Node.js 18+ with npx (for the MCP Inspector)
  • A PDF file to test against

Step 1 — Scaffold the Project

Create a new MCP server project with uv:

uv init mcp-pdf-extractor
cd mcp-pdf-extractor

Add the MCP SDK and our PDF parser:

uv add "mcp[cli]"
uv add liteparse

The SDK v1.27+ provides FastMCP — a decorator-based API that handles all protocol wiring. liteparse is a Rust-native PDF parser with spatial text extraction and layout reconstruction.

Step 2 — Write the Server

Create server.py:

from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("PDF Extractor", log_level="WARNING")

@mcp.tool()
def extract_text(path: str) -> str:
    """Extract all text from a PDF file and return it as markdown."""
    try:
        from liteparse import LiteParse
    except ImportError:
        return "Error: liteparse not installed. Run: uv add liteparse"

    p = Path(path)
    if not p.exists():
        return f"Error: file not found: {path}"
    if not p.suffix.lower() == ".pdf":
        return f"Error: not a PDF file: {path}"

    parser = LiteParse()
    result = parser.parse(str(p))
    pages = []
    for i, page in enumerate(result.pages, 1):
        text = "\n".join(item.text for item in page.text_items if item.text.strip())
        if text:
            pages.append(f"--- Page {i} ---\n{text}")
    output = "\n\n".join(pages)
    return output if output else "No text content found in PDF."

@mcp.tool()
def list_pages(path: str) -> str:
    """List page count and metadata for a PDF file."""
    try:
        from liteparse import LiteParse
    except ImportError:
        return "Error: liteparse not installed"

    p = Path(path)
    if not p.exists():
        return f"Error: file not found: {path}"

    parser = LiteParse()
    result = parser.parse(str(p))
    lines = [f"File: {p.name}", f"Pages: {len(result.pages)}"]
    for i, page in enumerate(result.pages[:5], 1):
        first_line = next(
            (item.text for item in page.text_items if item.text.strip()), ""
        )
        if first_line:
            lines.append(f"  Page {i}: \"{first_line[:80]}\"")
    if len(result.pages) > 5:
        lines.append(f"  ... ({len(result.pages) - 5} more)")
    return "\n".join(lines)

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

This server exposes two tools:

  • extract_text — Returns the full text content as markdown with page separators
  • list_pages — Shows page count and first line per page (preview without full extraction)

Both tools validate the path exists, check the file extension, and handle missing dependencies gracefully.

Step 3 — Test with MCP Inspector

Start the server in one terminal:

uv run python server.py

You’ll see output like:

INFO     Starting MCP server 'PDF Extractor' on streamable-http transport
INFO     Server running on http://0.0.0.0:8000

In another terminal, launch the MCP Inspector:

npx -y @modelcontextprotocol/inspector

Open http://localhost:5173 in your browser. The Inspector auto-discovers MCP servers on localhost:8000 via SSE. You’ll see:

  • Tools tabextract_text and list_pages with their parameter schemas
  • Resources tab — (empty, we didn’t define any resources)
  • Prompts tab — (empty)

Click extract_text, enter path as a path to a real PDF, and click Run Tool. You should see extracted text returned in real time.

If the Inspector can’t connect, verify the server is running and the transport matches. Our server uses streamable-http (default port 8000).

Step 4 — Connect to Hermes Agent

Create a config file for the MCP server:

# mcp-pdf-extractor.yaml
command: "uv"
args:
  - "run"
  - "--directory"
  - "/home/user/mcp-pdf-extractor"
  - "python"
  - "server.py"

Register it with Hermes:

hermes mcp add pdf-extractor --config mcp-pdf-extractor.yaml

Then reload MCP servers in your chat session:

/reload-mcp

Verify it loaded:

Tell me which MCP-backed tools are available right now.

Hermes should list pdf_extractor_extract_text and pdf_extractor_list_pages as available tools.

Test it:

Extract text from /home/user/sample.pdf and summarize the first page.

Hermes will call your MCP tool, receive the extracted text, and process it.

Step 5 — Use Context for Progress Reporting

The Context object lets your tools report progress and log messages. Add this enhanced version:

from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("PDF Extractor Pro", log_level="WARNING")

@mcp.tool()
async def extract_with_progress(path: str, ctx: Context) -> str:
    """Extract text from PDF with progress reporting."""
    from liteparse import LiteParse

    p = Path(path)
    if not p.exists():
        return f"Error: file not found: {path}"

    await ctx.info(f"Parsing PDF: {p.name}")
    parser = LiteParse()
    result = parser.parse(str(p))
    total = len(result.pages)

    parts = []
    for i, page in enumerate(result.pages, 1):
        text = "\n".join(
            item.text for item in page.text_items if item.text.strip()
        )
        parts.append(f"--- Page {i} ---\n{text}")
        await ctx.report_progress(
            progress=i / total, total=1.0,
            message=f"Extracted page {i}/{total}"
        )

    output = "\n\n".join(parts)
    await ctx.info(f"Done — {total} pages, {len(output)} chars")
    return output if output else "No text content found."

When Hermes calls this tool, you’ll see status updates like Extracted page 3/5 in the agent’s output.

Step 6 — Filter Tools by Name

Not every MCP server needs all its tools exposed. Hermes lets you whitelist specific tools:

# In hermes config
mcp_servers:
  pdf-extractor:
    command: "uv"
    args: ["run", "--directory", "/home/user/mcp-pdf-extractor", "python", "server.py"]
    tools:
      include: [extract_text]

This exposes only the extraction tool and hides list_pages. Good for production where you want the smallest useful surface [1].

Step 7 — Production Deployment

For long-running use, run the server as a systemd service:

# ~/.config/systemd/user/mcp-pdf-extractor.service
[Unit]
Description=MCP PDF Extractor Server
After=network.target

[Service]
ExecStart=%h/.local/bin/uv run --directory %h/mcp-pdf-extractor python server.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Enable and start:

systemctl --user daemon-reload
systemctl --user enable --now mcp-pdf-extractor.service

Update the Hermes config to point at the persistent URL:

mcp_servers:
  pdf-extractor:
    url: "http://localhost:8000"

No command or args needed — Hermes connects via HTTP instead of spawning a process each time.

Verification Checklist

After completing all steps, verify:

  1. Server starts cleanuv run python server.py exits with no errors
  2. Inspector runs toolextract_text returns real PDF content
  3. Hermes discovers tools/reload-mcp registers both tools
  4. Agent uses tools — A prompt like “Extract text from report.pdf” triggers the MCP tool call
  5. Progress reporting works — Context messages appear during extraction

Key Takeaways

  • FastMCP lets you turn any Python function into an LLM-accessible tool with one decorator
  • liteparse provides fast spatial PDF extraction — no external PDF tooling needed
  • The streamable-http transport works for both local dev and production systemd deployment
  • Hermes Agent’s tools.include filter keeps your server’s surface minimal in production
  • Context objects enable real-time progress reporting from MCP tools back to the agent

[1] Hermes Agent — Use MCP with Hermes: https://github.com/NousResearch/hermes-agent/blob/main/website/docs/guides/use-mcp-with-hermes.md [2] MCP Python SDK — GitHub: https://github.com/modelcontextprotocol/python-sdk

  • Hermes Tutorials — Hermes Agent setup, configuration, and advanced workflows
  • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
  • CodeIntel Log — code quality, debugging, and software engineering benchmarks
  • NoCode Insider — AI workflow automation with no-code tools, agents, and APIs

Cross-links automatically generated from NiteAgent.

← Back to all posts