Servidor MCP com Python, uv e FastMCP

Tradução automática

Este artigo foi traduzido automaticamente a partir da versão original em inglês.

Este tutorial de servidor MCP constrói um pequeno servidor de feature store com FastMCP, executa-o com o uv e liga-o ao Claude Desktop. Destina-se a programadores Python que querem expor um fluxo de trabalho de base de dados local a um host MCP. No fim, terá um ambiente com o SDK v1 fixado, um servidor com três ferramentas e um recurso, e uma verificação local para executar antes de ligar o Claude Desktop.


O que é um servidor MCP?

O Model Context Protocol (MCP) é um protocolo aberto para ligar aplicações de AI a sistemas externos. Substitui uma integração personalizada para cada ferramenta por um único conjunto de convenções.

Neste tutorial vamos construir um servidor MCP FeatureStoreLite. Fica entre um LLM e uma feature store, ou seja, uma base de dados de features de ML pré-calculadas. O servidor expõe ferramentas para consultar e escrever vetores de features indexados por utilizador, produto ou documento.

Porquê construir isto?

Depurar um pipeline de features costuma obrigar a recorrer a SQL ou a escrever um script descartável para verificar um valor. Com o servidor em execução, pergunta-se ao Claude: “Qual é o vetor de features de user_123?” ou “Mostra-me os metadados de product_abc.”

Porquê usar o uv?

Vamos usar o uv para instalar pacotes, resolver dependências e gerir o ambiente virtual. A configuração do Claude Desktop vai executar este projeto a partir do seu próprio diretório com --locked, pelo que usa a dependência mcp[cli] declarada aqui e as versões registadas em uv.lock.

Ferramentas, recursos e prompts

Um servidor MCP pode oferecer três tipos de elementos. A especificação MCP distingue-os por quem decide usar cada um:

PrimitivaQuem a acionaNeste servidor
FerramentaO modelo decide chamá-laget_feature, store_feature, list_features
RecursoO host decide como o incluir no contextoschema://main
PromptO utilizador escolhe-o, como um modelo de mensagem guardadoNão é usado aqui

As ferramentas permitem ao modelo pedir uma ação. Os recursos fornecem contexto através do host; a secção 4.4 mostra por que razão expor um recurso não garante que uma resposta o use.

Visão geral da arquitetura

O diagrama mostra que partes correm na sua máquina e que dados seguem para o modelo:

Arquitetura do FeatureStoreLite: o modelo Claude, no fornecedor do modelo, recebe a sua pergunta, a lista de ferramentas e os tool results, que são linhas de features.db; na sua máquina, o Claude Desktop e o seu cliente MCP comunicam por stdin e stdout com o processo do servidor FeatureStoreLite, que expõe três ferramentas e o recurso schema://main e lê e escreve na tabela SQLite com as linhas user_123 e product_abcArquitetura do FeatureStoreLite: o modelo Claude, no fornecedor do modelo, recebe a sua pergunta, a lista de ferramentas e os tool results, que são linhas de features.db; na sua máquina, o Claude Desktop e o seu cliente MCP comunicam por stdin e stdout com o processo do servidor FeatureStoreLite, que expõe três ferramentas e o recurso schema://main e lê e escreve na tabela SQLite com as linhas user_123 e product_abc

  1. O utilizador faz uma pergunta em linguagem natural.
  2. O Claude Desktop é o host MCP. Envia a conversa ao modelo Claude, cria um cliente MCP para este servidor e gere a ligação.
  3. O nosso servidor FastMCP expõe três ferramentas (get_feature, store_feature, list_features) e um recurso (schema://main).
  4. O SQLite é o armazenamento de apoio dos vetores de features.

O host passa a lista de ferramentas ao Claude. O Claude pode pedir um tool call, mas é o cliente MCP de cada servidor que envia as mensagens do protocolo. O servidor e o SQLite correm localmente, mas os tool results seguem para o modelo como parte da conversa. Exponha apenas dados que também colaria na conversa.


1. Configuração e instalação

1.1. Instalar o uv

Se ainda não tem o uv, instale-o. O resto do tutorial pressupõe que está no seu PATH.

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or via Homebrew
brew install uv

1.2. Inicializar o projeto

Crie um novo diretório e inicialize um projeto Python. O uv init cria um pyproject.toml por si.

# Create project directory
mkdir mcp-featurestore
cd mcp-featurestore

# Initialize Python project
uv init

# Add the MCP SDK with CLI tools
uv add "mcp[cli]>=1.28,<2"

Este tutorial usa a API v1 do SDK Python do MCP. A documentação oficial do SDK v1 indica aos utilizadores da v1 que fixem mcp>=1.28,<2, por isso mantenha o limite superior <2 até migrar o código. Depois deste comando, o uv.lock regista o ambiente completo já resolvido.

O código usa o FastMCP incluído no SDK oficial (mcp.server.fastmcp). O pacote autónomo fastmcp é um projeto distinto, com uma API diferente, por isso não misture a sua documentação com este código.


2. Construir o servidor

Dois ficheiros, separados por responsabilidade:

  1. database.py trata das operações SQLite.
  2. featurestore_server.py define o servidor MCP.

2.1. A camada de base de dados (database.py)

Este módulo gere a ligação SQLite e alguns auxiliares. Inicializamo-lo com duas linhas de exemplo, para que o servidor tenha algo para devolver na primeira consulta.

Crie o database.py:

# database.py
import json
import os
import sqlite3

def get_db_path() -> str:
    """Get the database path - always in the script's directory"""
    script_dir = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(script_dir, "features.db")

def init_db() -> None:
    """Initialize the feature store database with table and sample data"""
    conn = sqlite3.connect(get_db_path())
    conn.execute("""
        CREATE TABLE IF NOT EXISTS features (
            key TEXT PRIMARY KEY,
            vector TEXT NOT NULL,
            metadata TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    # Sample data for experimentation
    example_features = [
        (
            "user_123",
            "[0.1, 0.2, -0.5, 0.8, 0.3, -0.1, 0.9, -0.4]",
            json.dumps({"type": "user", "id": 123, "segment": "premium"}),
        ),
        (
            "product_abc",
            "[0.7, -0.3, 0.4, 0.1, -0.8, 0.6, 0.2, -0.5]",
            json.dumps({"type": "product", "id": "abc", "category": "electronics"}),
        ),
    ]

    # Insert if not exists
    for key, vector, metadata in example_features:
        try:
            conn.execute(
                "INSERT INTO features (key, vector, metadata) VALUES (?, ?, ?)",
                (key, vector, metadata),
            )
        except sqlite3.IntegrityError:
            pass  # Already exists

    conn.commit()
    conn.close()

def get_db_connection() -> sqlite3.Connection:
    """Get a database connection"""
    return sqlite3.connect(get_db_path())

if __name__ == "__main__":
    init_db()
    print("✅ Database initialized successfully!")

Inicialize a base de dados:

uv run python database.py

2.2. O servidor MCP (featurestore_server.py)

O FastMCP faz a maior parte do trabalho. Decore uma função Python simples e ela fica registada como ferramenta ou recurso MCP. O FastMCP constrói o esquema de entrada de cada ferramenta a partir das anotações de tipo e usa a docstring como descrição da ferramenta. O modelo vê ambos, por isso escreva a docstring para o modelo: diga o que a ferramenta faz e o aspeto de cada argumento.

Crie o featurestore_server.py:

# featurestore_server.py
import json
from typing import Annotated, Any

from mcp.server.fastmcp import FastMCP
from pydantic import Field

from database import get_db_connection, init_db

# Initialize the MCP Server
mcp = FastMCP("FeatureStoreLite")

# Ensure DB is ready when server starts
init_db()

# A float that rejects NaN and infinities
FiniteFloat = Annotated[float, Field(allow_inf_nan=False)]

@mcp.resource("schema://main")
def get_schema() -> str:
    """The CREATE TABLE statements of the feature store database."""
    conn = get_db_connection()
    try:
        schema = conn.execute(
            "SELECT sql FROM sqlite_master WHERE type='table'"
        ).fetchall()
        return "\n".join(sql[0] for sql in schema if sql[0]) or "No tables found."
    finally:
        conn.close()

@mcp.tool()
def store_feature(
    key: str,
    vector: Annotated[list[FiniteFloat], Field(min_length=1)],
    metadata: dict[str, Any] | None = None,
) -> str:
    """Store a feature vector under key. Overwrites an existing key.

    vector: non-empty list of finite numbers, e.g. [0.1, 0.2].
    metadata: optional JSON object, e.g. {"type": "user"}.
    """
    metadata_json = None if metadata is None else json.dumps(metadata, allow_nan=False)
    conn = get_db_connection()
    try:
        conn.execute(
            "INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
            (key, json.dumps(vector), metadata_json),
        )
        conn.commit()
    finally:
        conn.close()
    return f"Stored feature '{key}'"

@mcp.tool()
def get_feature(key: str) -> str:
    """Return the vector and metadata stored under key, as JSON.

    Call list_features first if you do not know the key.
    """
    conn = get_db_connection()
    try:
        row = conn.execute(
            "SELECT vector, metadata FROM features WHERE key = ?", (key,)
        ).fetchone()
    finally:
        conn.close()

    if row is None:
        raise ValueError(f"Feature '{key}' not found. Call list_features for valid keys.")
    return json.dumps(
        {
            "key": key,
            "vector": json.loads(row[0]),
            "metadata": json.loads(row[1]) if row[1] else None,
        },
        indent=2,
    )

@mcp.tool()
def list_features() -> str:
    """Return a JSON array of every stored feature key."""
    conn = get_db_connection()
    try:
        rows = conn.execute("SELECT key FROM features").fetchall()
        return json.dumps([row[0] for row in rows])
    finally:
        conn.close()

if __name__ == "__main__":
    mcp.run()

Quatro detalhes deste código decidem o que o modelo envia e o que recebe de volta:

  • vector tipado. O tool schema pede um array não vazio de números. Com o SDK 1.30, o Pydantic também converte booleanos e strings numéricas em floats: [true, false] passa a [1.0, 0.0], e ["1.5", "2"] passa a [1.5, 2.0]. Depois verifica se a lista não está vazia e se cada valor é finito antes de executar a função. Se um modelo enviar o vetor como uma string JSON, como "[1, NaN]", o FastMCP analisa primeiro a string. O allow_inf_nan=False rejeita depois NaN e infinitos, que o analisador JSON do Python aceita por omissão.
  • metadata tipado. O esquema aceita um objeto JSON, null ou um valor omitido. O allow_nan=False também impede NaN nos metadados guardados.
  • Erros lançados. O get_feature lança ValueError para uma chave inexistente. Uma exceção lançada chega ao cliente como erro de ferramenta com isError: true; uma string devolvida parece um resultado bem-sucedido.
  • Substituições. O store_feature usa INSERT OR REPLACE, por isso substitui uma chave existente. A docstring di-lo, para que o modelo o saiba antes de chamar a ferramenta.

3. Testar com o MCP Inspector

Antes de o ligar ao Claude, verifique o servidor com o MCP Inspector. É uma pequena interface web que se liga ao servidor da mesma forma que um host. Nela pode explorar os recursos do servidor, chamar cada ferramenta com os seus próprios argumentos e ver cada pedido e resposta.

O SDK inicia o Inspector através de npx, pelo que este passo também precisa de Node.js e npm com npx no seu PATH. O Inspector 2.10.1 requer Node.js 22.19.0 ou posterior. Verifique os comandos instalados e depois inicie o Inspector:

node --version
npx --version
uv run mcp dev featurestore_server.py

O comando inicia o servidor no MCP Inspector através de stdio. Use o URL do navegador que o comando imprime. A porta da interface do Inspector depende da implementação, por isso não assuma uma porta fixa.

Chame o get_feature com key="user_123". Se devolver o JSON da linha inicial, o servidor está a funcionar. Depois chame-o com uma chave que não existe: o resultado é um erro de ferramenta com isError: true. Isto verifica o caminho local do protocolo, o registo das ferramentas, a consulta à base de dados e o caminho de erro. Não testa como o Claude escolhe ferramentas; isso é verificado na secção 4.

O Inspector lê o esquema SQL a partir do recurso schema://main:

MCP Inspector a mostrar o recurso get_schema e a instrução CREATE TABLE do SQLite devolvida


4. Ligar ao Claude Desktop

Quando o Inspector confirmar que o servidor funciona, registe-o no Claude Desktop.

4.1. Configurar o Claude

Edite o ficheiro de configuração do Claude Desktop:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%/Claude/claude_desktop_config.json

Adicione o seu servidor ao objeto mcpServers:

{
    "mcpServers": {
        "featurestore": {
            "command": "uv",
            "args": [
                "run",
                "--directory",
                "/ABSOLUTE/PATH/TO/mcp-featurestore",
                "--locked",
                "mcp",
                "run",
                "/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
            ]
        }
    }
}

Importante: use caminhos absolutos, tanto para o diretório do projeto como para featurestore_server.py. O Claude Desktop inicia o servidor como um processo separado. Para um comando como mcp, o uv run descobre o projeto a partir do diretório de trabalho, pelo que --directory seleciona o pyproject.toml e o uv.lock deste projeto; o --locked falha em vez de alterar esse ficheiro de bloqueio. O comando uv add do passo 1.2 já declarou mcp[cli] no projeto.

O SDK tem também um atalho: uv run mcp install featurestore_server.py escreve por si a entrada do Claude Desktop. No mcp 1.30, essa entrada executa uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. Não tem --directory, por isso não usa o uv.lock deste projeto, e o --with mcp[cli] não tem limite <2. Use-o para um teste rápido; a configuração manual acima é a reprodutível.

4.2. Como funciona a interação

Eis o que corre de ponta a ponta quando o Claude precisa de consultar uma feature. Os passos 1 e 2 correm uma vez, quando o Claude Desktop inicia o servidor. Os passos 3 a 6 correm para cada pergunta que precise de dados de features.

Diagrama de sequência de uma consulta de user_123 entre o modelo Claude, o Claude Desktop, o servidor FeatureStoreLite e features.db: tools/list e as descrições das ferramentas no arranque; depois, a cada pergunta, o pedido get_feature do modelo, uma mensagem tools/call, o SELECT no SQLite, o resultado JSON e a resposta final, numerados de 1 a 6 como na lista abaixoDiagrama de sequência de uma consulta de user_123 entre o modelo Claude, o Claude Desktop, o servidor FeatureStoreLite e features.db: tools/list e as descrições das ferramentas no arranque; depois, a cada pergunta, o pedido get_feature do modelo, uma mensagem tools/call, o SELECT no SQLite, o resultado JSON e a resposta final, numerados de 1 a 6 como na lista abaixo

  1. O Claude Desktop inicia um cliente MCP dedicado para o servidor e descobre as suas ferramentas.
  2. O host disponibiliza as descrições das ferramentas ao Claude.
  3. O Claude pode pedir um tool call quando a pergunta precisa de dados da feature store.
  4. O cliente MCP envia esse pedido ao servidor.
  5. O servidor executa a função Python e devolve o resultado ao host.
  6. O host pode fornecer o resultado ao Claude para a resposta final.

Esta separação entre host, cliente e servidor segue a especificação da arquitetura MCP.

4.3. Exemplos de consultas

Reinicie o Claude Desktop e experimente alguns prompts:

  1. “Liste todas as features disponíveis.” O resultado determinístico contém as duas chaves iniciais: user_123 e product_abc.

  2. “Obtenha o vetor de features de user_123.” A resposta contém o vetor e os metadados premium de database.py.

  3. “Guarde uma nova feature para new_item com o vetor [0.5, 0.5] e os metadados {"type": "test"}. Depois obtenha new_item e mostre o vetor e os metadados guardados.” O vetor validado não pode estar vazio e só pode conter floats finitos após a conversão. Os metadados podem ser um objeto JSON, null ou ser omitidos. A leitura seguinte verifica o ciclo completo de escrita e leitura.

  4. “Obtenha o vetor de features de user_999.” O get_feature lança um erro para a chave inexistente. O Claude deve dizer que a chave não existe e pode chamar list_features para mostrar as chaves válidas.

4.4. Ler as respostas do Claude

O servidor devolve dados de ferramentas e recursos. O Claude escolhe a ferramenta e escreve a explicação em torno do resultado.

Esta captura regista uma execução de junho de 2025 com uma versão anterior do exemplo. Pedi ao Claude que mostrasse o esquema da base de dados:

Claude Desktop apresenta um esquema conceptual incorreto do FeatureStoreLite na execução de junho de 2025

O Claude descreveu o sistema como um armazenamento NoSQL ou de documentos e apresentou um esquema conceptual, mas o database.py cria uma tabela relacional SQLite. A resposta apresentada não é a instrução CREATE TABLE devolvida por schema://main. A captura não regista se o Claude Desktop leu esse recurso nem que contexto o Claude recebeu, pelo que não demonstra a causa do erro.

Os recursos são controlados pela aplicação: o host decide como os incluir no contexto do modelo, através da seleção pelo utilizador ou da inclusão automática. O MCP não exige que o host passe todos os recursos ao modelo. Se o modelo deve pedir o esquema através de um tool call, exponha-o também como ferramenta.

Uma explicação plausível do modelo não é o mesmo que o resultado devolvido pela ferramenta. Quando o pormenor importa, verifique o tool result ou o código-fonte.

4.5. Outros hosts MCP

O servidor não depende do Claude Desktop. Qualquer host que inicie servidores stdio na mesma máquina pode executar o mesmo comando uv. O Claude Desktop usa o JSON da secção 4.1; os outros dois são:

Um host noutra máquina não consegue iniciar um processo stdio na sua. Para um servidor partilhado por várias pessoas, execute-o sobre Streamable HTTP com mcp.run(transport="streamable-http") e ponha autenticação à frente.


5. Resolução de problemas

Descubra que parte falhou antes de mudar o código: os erros de arranque estão nos logs, os erros de ferramenta repetem-se no Inspector sem um modelo, e uma resposta errada exige comparar o tool result com o texto do Claude.

  • “Server connection failed”:

    • Verifique os logs em ~/Library/Logs/Claude/ no macOS ou em %APPDATA%\Claude\logs no Windows. O mcp.log regista os eventos de ligação. O mcp-server-featurestore.log, na mesma pasta, contém o stderr do próprio servidor, por isso um traceback de arranque aparece aí (logs do Claude Desktop).
    • Confirme que a configuração usa um caminho absoluto, não relativo.
    • Confirme que o uv está no PATH do Claude Desktop. Se não estiver, indique o caminho completo do binário (o which uv diz onde está).
    • Em stdio, o stdout transporta as mensagens do protocolo. Registe no stderr; um print() no servidor corrompe esse fluxo e pode interromper a ligação.
  • “Tool execution error”:

    • Reproduza-o no Inspector com uv run mcp dev featurestore_server.py. O Inspector mostra a mensagem de erro e os argumentos que a causaram.
    • Verifique se o features.db está a ser criado junto do database.py. O caminho vem de get_db_path(), que o resolve relativamente ao script, por isso uma mudança do diretório de trabalho não deve mover o ficheiro.
  • A chamada tem êxito, mas a resposta está errada:

    • Abra o tool result na conversa e compare-o com o texto do Claude. A secção 4.4 mostra uma resposta do modelo que descreveu incorretamente a base de dados.

6. Antes de um agente o usar a sério

  • O store_feature substitui qualquer chave, incluindo as linhas iniciais. Mantenha a aprovação por chamada ativa para as ferramentas de escrita no host, ou mova-as para um servidor separado que só ative quando precisar.
  • Os tool results chegam ao fornecedor do modelo como parte da conversa. Não devolva dados que têm de ficar na máquina.
  • Um servidor partilhado por várias pessoas precisa de Streamable HTTP e autenticação em vez de stdio (secção 4.5).

7. Conclusão

É tudo: um servidor FastMCP, um armazenamento SQLite e uma configuração do Claude Desktop que aponta para um comando uv run. O mesmo formato serve para quase tudo o que se consegue envolver numa função Python. Troque as chamadas SQLite por uma feature store real, uma API interna ou um registo de modelos, e o servidor continua pequeno.

Referências