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 viacurl -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 separatorslist_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 tab —
extract_textandlist_pageswith 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:
- Server starts clean —
uv run python server.pyexits with no errors - Inspector runs tool —
extract_textreturns real PDF content - Hermes discovers tools —
/reload-mcpregisters both tools - Agent uses tools — A prompt like “Extract text from report.pdf” triggers the MCP tool call
- Progress reporting works — Context messages appear during extraction
Key Takeaways
FastMCPlets you turn any Python function into an LLM-accessible tool with one decoratorliteparseprovides fast spatial PDF extraction — no external PDF tooling needed- The
streamable-httptransport works for both local dev and production systemd deployment - Hermes Agent’s
tools.includefilter 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
📖 Related Reads
- 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

