Construindo Ferramentas MCP: Um Servidor de Processamento de PDF

Gabriel Melendez

Publicado em 18 de Set

(Versão em Espanhol) Construindo Ferramentas MCP: Um Servidor de Processamento de PDF

Model Context Protocol (MCP) emergiu como um padrão revolucionário para conectar modelos de IA com ferramentas e serviços externos para melhorar suas capacidades. Vou guiá-lo através de uma visão geral de alto nível do processo de desenvolvimento para construir um servidor integral de processamento de PDF usando FastMCP, com arquitetura adequada, tratamento de erros e recursos de produção.

Ferramentas Disponíveis de um Vistazo

Servidor e Utilidades de Arquivos

  • server_info(): Obter a configuração e estado do servidor.
  • list_temp_resources(): Listar arquivos atualmente no diretório temporário do servidor.
  • upload_file(), upload_file_base64(), upload_file_url(): Enviar arquivos para o servidor a partir da sua máquina local ou de uma URL.
  • get_resource_base64(): Baixar um arquivo do diretório temporário do servidor.

Texto e Metadados

  • get_pdf_info(): Obter rapidamente a contagem de páginas, tamanho do arquivo e estado de criptografia.
  • extract_text(): Extrair o conteúdo de texto completo de um PDF.
  • extract_text_by_page(): Extrair texto de páginas específicas ou intervalos de páginas.
  • extract_metadata(): Ler os metadados do PDF (autor, título, data de criação, etc.).

Manipulação de PDF

  • merge_pdfs(): Combinar vários arquivos PDF em um único documento.
  • split_pdf(): Dividir um PDF em vários arquivos menores com base em intervalos de páginas.
  • rotate_pages(): Rotacionar páginas específicas dentro de um PDF.

Conversão

  • pdf_to_images(): Converter páginas específicas do PDF em arquivos de imagem (PNG, JPEG).
  • images_to_pdf(): Criar um novo PDF a partir de uma lista de arquivos de imagem.

Você pode encontrar o código base no Repositório do GitHub :file_folder: Servidor MCP PDF

Nosso Caso de Estudo: Rastreando a Ferramenta “extract_text”

Vamos explorar ‘extract_text’; todas as outras ferramentas compartilham um fluxo de trabalho consistente e são facilmente acessíveis no repositório, se você quiser revisá-lo.

Padrão

Ao separar a lógica em “Serviço” → “Ferramenta” → “Registro”, mantemos o código limpo, testável e fácil de estender. Você pode adicionar sua própria ferramenta seguindo exatamente este padrão.

Passo 1: A Lógica Central - o “Serviço”

Antes de pensar em servidores, ferramentas ou protocolos, precisamos de uma função simples e confiável em Python que possa realizar nossa tarefa central. Esta é então a “Camada de Serviço”, o motor.

Arquivo: src/fastmcp_pdf_server/services/pdf_processor.py

Nosso primeiro passo é escrever uma função que receba um caminho de arquivo e retorne o texto. Usamos a biblioteca “pdfplumber” para isso. Observe que a função retorna uma classe de dados “TextExtractionResult”, que ajuda a garantir uma estrutura de dados consistente.

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

# Uma classe de dados fornece um tipo de retorno estruturado e previsível para nosso serviço.
# É como uma classe leve e auto-documentada.
@dataclass
class TextExtractionResult:
    text: str
    page_count: int
    char_count: int

def extract_text(file_path: str, encoding: str = "utf-8") -> TextExtractionResult:
    # Primeiro, executar o arquivo através de um validador para garantir que existe, é um PDF,
    # e está dentro dos limites de tamanho permitidos. Isso falha cedo se a entrada for ruim.
    pdf_path = validate_pdf(file_path)

    # Usar pdfplumber para abrir e processar robustamente o PDF.
    with pdfplumber.open(str(pdf_path)) as pdf:
        texts: List[str] = []
        for page in pdf.pages:
            # Extrair texto, padrão uma string vazia se uma página não tiver texto.
            texts.append(page.extract_text() or "")

        # Unir o texto de todas as páginas em uma única string.
        text = "\n".join(texts)

        # Retornar uma instância de nossa classe de dados, garantindo que o contrato seja cumprido.
        return TextExtractionResult(text=text, page_count=len(texts), char_count=len(text))
```Esta função é Python puro. Não sabe nada sobre FastMCP. Pode ser testada unitariamente com "pytest" ou usada em uma aplicação completamente diferente. Esta separação é a base de um sistema mantível. Uma vez que fizemos nossa lógica de serviço, continuamos com a "Ferramenta" MCP.

## Passo 2: A Ponte - A "Ferramenta"

Agora precisamos expor nossa função de serviço para o mundo exterior como uma Ferramenta MCP. Esta "Camada de Ferramenta" age como uma ponte. Lida com a realidade desordenada de uma chamada de ferramenta e a traduz em uma chamada limpa para nosso serviço.

**Arquivo:** `src/fastmcp_pdf_server/tools/text_extraction.py`

Esta é a peça mais crítica do quebra-cabeça. Lidará com a chamada da ferramenta, resolverá o arquivo, chamará o serviço e formatará a resposta.

Dentro de 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)

A função ‘register’ é uma convenção para agrupar registros de ferramentas.

A aplicação principal chamará esta função, passando a si mesma como argumento.

def register(app: FastMCP) → None:
# O decorador @app.tool() é o que oficialmente registra esta função como uma ferramenta MCP.
@app.tool()
async def extract_text(file: Any, encoding: str | None = “utf-8”) → dict:
“”"Extrair todo o texto de um PDF.

    Aceita:
    - String de caminho completo
    - Nome de arquivo curto previamente escrito no armazenamento temporário
    - Bytes / tipo arquivo / dict com base64 (será salvo no temporário)
    """
    # 1. Gerar um ID único para esta operação específica. Isso é crucial para
    # rastrear um único pedido através dos logs.
    op_id = uuid.uuid4().hex
    start = time.perf_counter()

    try:
        # 2. Resolver a entrada flexível 'file' (que poderia ser um caminho, nome de arquivo, ou
        # objeto base64) em um caminho de arquivo absoluto concreto e validado.
        resolved = resolve_to_path(file, filename_hint="uploaded.pdf")

        # 3. Chamar a função de serviço limpa e testável com o caminho resolvido.
        # Aqui é onde acontece o processamento real do PDF.
        res = pdf_processor.extract_text(str(resolved), encoding or "utf-8")

        # 4. O serviço retorna uma classe de dados. Agora formatamos isso no
        # dicionário final amigável para JSON para o cliente.
        duration_ms = int((time.perf_counter() - start) * 1000)

        return {
            "text": res.text,
            "page_count": res.page_count,
            "char_count": res.char_count,
            # O bloco 'meta' fornece dados operacionais valiosos ao cliente.
            "meta": {
                "operation_id": op_id,
                "execution_ms": duration_ms,
                "resolved_path": str(resolved),
            },
        }
    except Exception as e: # noqa: BLE001
        # 5. Esta é a rede de segurança. Se qualquer parte do processo falhar,
        # registrar o erro completo para depuração...
        logger.error("extract_text error: %s", e)

        hint = (
            "Forneça um caminho completo, faça upload do arquivo primeiro via 'upload_file', "
            "ou passe bytes/base64. Exemplo de payload:\n"
            "{\n"
            "  \"name\": \"upload_file\",\n"
            "  \"arguments\": {\n"
            "    \"file\": { \"base64\": \"<...>\", \"filename\": \"my.pdf\" }\n"
            "  }\n"
            "}"
        )
        # ...e lançar um ValueError simples. FastMCP converterá isso em uma
        # resposta de erro limpa e estruturada para o LLM, prevenindo um crash.
        raise ValueError(f"extract_text failed: {e}. {hint}")

A ferramenta é apenas um wrapper. É um gerente que coordena outras partes do código. Lida com entradas desordenadas, chama a lógica de serviço limpa e embala a resposta final. O padrão 'try...except ValueError' é uma melhor prática crítica.

## Passo 3: A Conexão Final - O "Registro"

Nossa função de ferramenta está definida, mas a aplicação do servidor ainda não sabe que existe. O passo final é conectar, ou registrar, nosso módulo de ferramenta com a instância principal da aplicação "FastMCP".

**Arquivo:** `src/fastmcp_pdf_server/main.py`

Este arquivo é o ponto de entrada de todo nosso servidor. Seu trabalho é construir o objeto aplicação e registrar todos os conjuntos de ferramentas.

Dentro de 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:
# Este bloco try/except fornece um erro amigável se o usuário
# esqueceu de instalar as dependências de requirements.txt.
try:
from fastmcp import FastMCP # type: ignore
except Exception as exc: # pragma: no cover
raise SystemExit(
“fastmcp não está instalado. Por favor instale as dependências primeiro.”
) from exc

# Inicializar a aplicação principal, obtendo nome e versão de config.
app = FastMCP(settings.server_name, version=settings.server_version)

# --- Registro de Ferramentas ---
# Importar os módulos que contêm nossas definições de ferramentas.
from .tools import utilities, text_extraction, pdf_manipulation, conversion, uploads
from .services.file_manager import cleanup_expired

# Chamar a função 'register' de cada módulo para anexar suas ferramentas à app.
# Esta abordagem modular mantém o arquivo principal limpo.
utilities.register(app)
text_extraction.register(app)
pdf_manipulation.register(app)
conversion.register(app)
uploads.register(app)

# --- Tarefas de Início ---
# É uma boa prática executar tarefas de limpeza no início.
# Aqui, removemos qualquer arquivo antigo do diretório temporário.
try:
    cleanup_expired()
except Exception as exc: # noqa: BLE001
    logger.error("cleanup_expired no início falhou: %s", exc)

return app

Ao importar módulos e chamar uma função "register" de cada um. O arquivo principal se mantém limpo e age como um resumo de alto nível das capacidades do servidor. Adicionar ou remover toda uma categoria de ferramentas é tão simples quanto adicionar ou remover uma linha aqui.

## O Panorama Completo

Agora, rastreemos um pedido de princípio a fim:

1. Um LLM chama a ferramenta `extract_text`.
2. A aplicação `FastMCP`, construída em `main.py`, roteia a chamada para a função async `extract_text` dentro de `text_tools.py`.
3. A função da ferramenta chama `resolve_to_path` para obter um caminho de arquivo limpo.
4. A função da ferramenta então chama o serviço `pdf_processor.extract_text` com esse caminho limpo.
5. O serviço faz o trabalho pesado e retorna um dicionário simples: `{'text': ..., 'page_count': ...}`.
6. A função da ferramenta recebe este dicionário, adiciona o `char_count` e o bloco `meta`, e retorna o dicionário final enriquecido.
7. `FastMCP` envia este dicionário final de volta ao LLM como uma resposta JSON.

## O Resultado Final

Usando Claude Desktop como Cliente MCP podemos testar nossa ferramenta "extract_text" de nosso servidor, simplesmente registrando o MCP, adicionando-o ao arquivo de configuração "claude_desktop_config.json"

{
“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”
}
}
}
}


[![ |800x587](upload://3E89VeaCPCl8I7dgmYB16ToyTEH.webp)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdiesxdd65n7z43bc4xyy.png)

Normalmente, para este tipo de Clientes MCP, você deve adicionar ao seu prompt o uso do Servidor MCP, neste caso, nosso "PDF Processor Server"; às vezes, também é necessário especificar o caminho completo do arquivo.

[![ |791x934](upload://oN74jIq6Pj1oBvIbcXYYgVe9N6q.webp)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzei96rk0aw55mcbkfde5.png)

## Para Onde Ir a Partir Daqui?

Você conseguiu! Você configurou um servidor, aprendeu como se conectar a ele, ordenou que extraísse texto e até deu uma olhada sob o capô para ver como tudo funciona.

O que vem a seguir?

* **Explore Outras Ferramentas**: Dê uma olhada no arquivo `README.md`. Você encontrará uma lista completa de outras ferramentas que você pode chamar, como `merge_pdfs`, `split_pdf` e `pdf_to_images`.
* **Estenda o Servidor**: Tente adicionar sua própria ferramenta! Siga o padrão.
* **Automatize sua vida**: Pense em seus próprios fluxos de trabalho. Você poderia usar este servidor para extrair automaticamente texto de faturas? Ou para combinar seus relatórios semanais em um único PDF? O poder é seu.

Happy Coding! 🤖