Building a Robust PDF Processing Server with FastMCP: A Comprehensive Guide

The landscape of Artificial Intelligence is continually evolving, with Model Context Protocol (MCP) emerging as a pivotal standard for seamlessly integrating AI models with external tools and services. This advancement significantly enhances the capabilities of AI systems by enabling them to interact with and leverage a broader range of functionalities. This guide will walk you through the development process of constructing a powerful PDF processing server using FastMCP, emphasizing architectural best practices, robust error handling, and production-grade features.

A Quick Look at Our Toolset

Our FastMCP-powered PDF server comes equipped with a versatile array of tools designed for various PDF operations.

Server and File Management Utilities

  • server_info(): Retrieves essential server configuration and status details.
  • list_temp_resources(): Provides a list of files currently residing in the server’s temporary directory.
  • upload_file(), upload_file_base64(), upload_file_url(): Facilitate uploading files to the server from local machines or specified URLs.
  • get_resource_base64(): Enables downloading files from the server’s temporary storage.

Text and Metadata Extraction

  • get_pdf_info(): Offers quick access to a PDF’s page count, file size, and encryption status.
  • extract_text(): Extracts the entire textual content from a PDF document.
  • extract_text_by_page(): Allows for targeted text extraction from specific pages or page ranges within a PDF.
  • extract_metadata(): Reads and returns important PDF metadata, such as author, title, and creation date.

PDF Manipulation Capabilities

  • merge_pdfs(): Combines multiple PDF files into a single, cohesive document.
  • split_pdf(): Divides a larger PDF into several smaller files based on defined page ranges.
  • rotate_pages(): Rotates specified pages within a PDF document.

Conversion Functions

  • pdf_to_images(): Converts designated PDF pages into image formats (PNG, JPEG).
  • images_to_pdf(): Creates a new PDF document from a collection of image files.

The foundational code for this server can be found in the MCP PDF Server GitHub Repository.

Diving Deep: Tracing the ‘extract_text’ Tool

To illustrate the underlying mechanics, we will focus on the extract_text tool. The principles and workflow demonstrated here are consistent across all other tools, which are readily available for review in the repository.

The Architectural Blueprint: Service -> Tool -> Registration

Our design adheres to a clear separation of concerns, partitioning logic into distinct layers: “Service,” “Tool,” and “Registration.” This modular approach ensures code cleanliness, testability, and effortless extensibility. Should you wish to introduce new functionalities, you can simply replicate this established pattern.

Step 1: The Core Logic – The “Service” Layer

Before venturing into server configurations or protocol specifics, the primary requirement is a simple, reliable Python function capable of executing the core task. This forms our “Service Layer” – the operational engine.

File: src/fastmcp_pdf_server/services/pdf_processor.py

The initial step involves crafting a function that accepts a file path and returns the extracted text. For this, we utilize the pdfplumber library. It’s crucial to note that the function returns a TextExtractionResult dataclass, which guarantees a consistent and predictable data structure.

from __future__ import annotations
from dataclasses import dataclass
from typing import List
import pdfplumber
from ..utils.validators import validate_pdf

@dataclass
class TextExtractionResult:
    text: str
    page_count: int
    char_count: int

def extract_text(file_path: str, encoding: str = "utf-8") -> TextExtractionResult:
    # Validate the PDF file to ensure existence, correct format, and size limits.
    pdf_path = validate_pdf(file_path)

    # Robustly open and process the PDF using pdfplumber.
    with pdfplumber.open(str(pdf_path)) as pdf:
        texts: List[str] = []
        for page in pdf.pages:
            # Extract text, defaulting to an empty string if no text is found on a page.
            texts.append(page.extract_text() or "")

        # Concatenate text from all pages into a single string.
        text = "\n".join(texts)

        # Return a TextExtractionResult instance, adhering to the defined contract.
        return TextExtractionResult(text=text, page_count=len(texts), char_count=len(text))

This function is pure Python, decoupled from FastMCP. It can be independently unit-tested or integrated into entirely different applications, underscoring the benefits of a modular and maintainable system. Once the service logic is complete, we proceed to build the MCP “Tool.”

Step 2: The Gateway – The “Tool” Layer

Next, we must expose our service function as an MCP Tool to the external environment. This “Tool Layer” acts as an intermediary, translating raw tool calls into clean service invocations.

File: src/fastmcp_pdf_server/tools/text_extraction.py

This component is critical. It manages the tool call, resolves the input file, invokes the service, and formats the response.

# Within src/fastmcp_pdf_server/tools/text_extraction.py
from __future__ import annotations
import time
import uuid
from typing import Any
from fastmcp import FastMCP # type: ignore
from ..services import pdf_processor
from ..services.file_manager import resolve_to_path
from ..utils.logger import get_logger

logger = get_logger(__name__)

def register(app: FastMCP) -> None:
    @app.tool()
    async def extract_text(file: Any, encoding: str | None = "utf-8") -> dict:
        """Extracts all text from a PDF.

        Accepts:
        - Full file path string
        - Short filename previously written to temporary storage
        - Bytes / file-like object / dict with base64 (will be saved to temporary)
        """
        # 1. Generate a unique operation ID for tracking purposes.
        op_id = uuid.uuid4().hex
        start = time.perf_counter()

        try:
            # 2. Resolve the flexible 'file' input into a concrete, validated absolute file path.
            resolved = resolve_to_path(file, filename_hint="uploaded.pdf")

            # 3. Invoke the clean, testable service function with the resolved path.
            res = pdf_processor.extract_text(str(resolved), encoding or "utf-8")

            # 4. Format the dataclass result into a JSON-friendly dictionary for the client.
            duration_ms = int((time.perf_counter() - start) * 1000)

            return {
                "text": res.text,
                "page_count": res.page_count,
                "char_count": res.char_count,
                "meta": {
                    "operation_id": op_id,
                    "execution_ms": duration_ms,
                    "resolved_path": str(resolved),
                },
            }
        except Exception as e: # noqa: BLE001
            # 5. Catch any exceptions, log the error for debugging, and provide a helpful hint.
            logger.error("extract_text error: %s", e)

            hint = (
                "Provide a full path, upload the file first via 'upload_file', "
                "or pass bytes/base64. Example payload:\n"
                "{\n"
                "  \"name\": \"upload_file\",\n"
                "  \"arguments\": {\n"
                "    \"file\": { \"base64\": \"<...>\", \"filename\": \"my.pdf\" }\n"
                "  }\n"
                "}"
            )
            # FastMCP will convert this ValueError into a structured error response for the LLM.
            raise ValueError(f"extract_text failed: {e}. {hint}")

The tool acts as a wrapper, orchestrating other code components. It handles diverse inputs, calls the streamlined service logic, and packages the final response. The try...except ValueError block is a crucial best practice for robust error management.

Step 3: The Final Link – The “Registration” Layer

While our tool function is defined, the server application remains unaware of its existence. The concluding step involves connecting, or registering, our tool module with the main FastMCP application instance.

File: src/fastmcp_pdf_server/main.py

This file serves as the entry point for our entire server, responsible for constructing the application object and registering all toolsets.

# Within src/fastmcp_pdf_server/main.py
from __future__ import annotations
from typing import Any
from .config import settings
from .utils.logger import get_logger

logger = get_logger(__name__)

def build_app() -> Any:
    try:
        from fastmcp import FastMCP # type: ignore
    except Exception as exc: # pragma: no cover
        raise SystemExit(
            "fastmcp is not installed. Please install dependencies first."
        ) from exc

    # Initialize the main application with name and version from configuration.
    app = FastMCP(settings.server_name, version=settings.server_version)

    # --- Tool Registration ---
    # Import modules containing tool definitions.
    from .tools import utilities, text_extraction, pdf_manipulation, conversion, uploads
    from .services.file_manager import cleanup_expired

    # Call the 'register' function of each module to attach their tools to the app.
    # This modular approach keeps the main file clean and organized.
    utilities.register(app)
    text_extraction.register(app)
    pdf_manipulation.register(app)
    conversion.register(app)
    uploads.register(app)

    # --- Startup Tasks ---
    # Perform cleanup tasks on startup, such as removing old temporary files.
    try:
        cleanup_expired()
    except Exception as exc: # noqa: BLE001
        logger.error("cleanup_expired on startup failed: %s", exc)

    return app

By importing modules and invoking a dedicated register function within each, the main file maintains its cleanliness, acting as a high-level summary of the server’s capabilities. Adding or removing an entire category of tools becomes as simple as a single line modification.

The Complete Workflow: From Request to Response

Let’s trace a request through the entire system:

  1. An LLM initiates a call to the extract_text tool.
  2. The FastMCP application, initialized in main.py, directs this call to the asynchronous extract_text function within text_extraction.py.
  3. The tool function then invokes resolve_to_path to obtain a clean, validated file path.
  4. With this path, the tool function calls the pdf_processor.extract_text service, where the actual PDF processing occurs.
  5. The service performs its task and returns a straightforward dataclass result.
  6. The tool function receives this result, enriches it with the char_count and a meta block containing operational data, and constructs the final dictionary.
  7. FastMCP transmits this comprehensive dictionary back to the LLM as a JSON response.

Experiencing the Result: Client Integration

Using a client like Claude Desktop, we can easily test our extract_text tool. The process involves registering the MCP server by adding it to the claude_desktop_config.json file.

{
  "mcpServers": {
    "pdf-processor-server": {
      "command": "D:\\Github Projects\\mcp_pdf_server\\.venv\\Scripts\\python.exe",
      "args": [
        "-m",
        "fastmcp_pdf_server"
      ],
      "env": {
        "TEMP_DIR": "D:\\Github Projects\\mcp_pdf_server\\temp_files"
      }
    }
  }
}

Once the MCP is configured, your client interface should reflect its presence, enabling interaction.

(Imagine an image showing the Claude Desktop client with the “PDF Processor Server” listed as an available MCP server.)

Typically, for such MCP clients, you’ll need to explicitly instruct your prompt to utilize the “PDF Processor Server” and, at times, specify the full file path.

(Imagine an image showing a prompt in Claude Desktop instructing the AI to use the “PDF Processor Server” and a PDF file for text extraction.)

Your Next Steps

Congratulations! You’ve successfully learned how to set up a powerful PDF processing server, connect to it, command it to extract text, and gained insight into its internal workings.

What’s next for you?

  • Explore Further Tools: Consult the README.md file in the GitHub repository for a complete listing of other available tools, such as merge_pdfs, split_pdf, and pdf_to_images.
  • Extend the Server: Challenge yourself by adding a new custom tool to the server, following the established Service -> Tool -> Registration pattern.
  • Automate Your World: Consider how you can integrate this server into your own workflows. Can it automatically extract data from invoices, or combine weekly reports into a single PDF? The possibilities are endless.

Happy Coding! 🤖

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed