Tutorial de um servidor MCP: criar com Python, uv e FastMCP

Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.

Os developers de Python podem transformar um fluxo de trabalho local de feature store num servidor FastMCP com versões fixadas para o Claude Desktop, usando uv para criar o ambiente, disponibilizar ferramentas e um recurso, e verificar o servidor localmente antes de estabelecer a ligação ao desktop.

Em resumo: Um pequeno servidor FastMCP é suficiente para disponibilizar uma ferramenta local útil a um agent. Use uv para uma configuração reprodutível, mantenha a fronteira do servidor restrita, valide o formato do vector na fronteira da ferramenta e teste a ferramenta MCP antes de a ligar a um modelo.


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 separada para cada ferramenta por um único conjunto de convenções.

Neste tutorial, vamos criar um servidor MCP FeatureStoreLite. Este fica entre um LLM e uma feature store, ou seja, uma base de dados de features de ML pré-calculadas. O servidor disponibiliza ferramentas para consultar e gravar feature vectors indexados por utilizador, produto ou documento.

Porquê criar isto?

Depurar um pipeline de features normalmente implica recorrer a SQL ou escrever um script descartável para verificar um valor. Com o servidor em execução, pode perguntar ao Claude: “Qual é o feature vector de user_123?” ou “Mostra-me os metadados de product_abc.”

Porquê usar uv?

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

Visão geral da arquitetura

As quatro componentes e a forma como se integram:

Arquitetura do FeatureStoreLite, desde um pedido do utilizador, passando pelo host MCP do Claude Desktop e pelo client por servidor, até ao servidor FastMCP e ao armazenamento SQLiteArquitetura do FeatureStoreLite, desde um pedido do utilizador, passando pelo host MCP do Claude Desktop e pelo client por servidor, até ao servidor FastMCP e ao armazenamento SQLite

  1. O utilizador faz uma pergunta em linguagem natural.
  2. O Claude Desktop é o host MCP. Cria um client MCP para este servidor e gere a ligação.
  3. O nosso servidor FastMCP disponibiliza get_feature e store_feature como ferramentas MCP.
  4. O SQLite é o armazenamento subjacente dos feature vectors.

O host pode disponibilizar ao Claude as ferramentas descobertas. O Claude pode solicitar um tool call, mas o client MCP por servidor envia as mensagens do protocolo.


1. Configuração e instalação

1.1. Instalar uv

Se ainda não tiver uv, instale-o. O resto do tutorial assume que está disponível 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 diretório novo e inicialize um projeto Python. uv init cria automaticamente um pyproject.toml.

# 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 MCP Python SDK. A documentação oficial do SDK v1 indica aos utilizadores da v1 que fixem mcp>=1.28,<2; por isso, mantenha o limite superior de <2 até migrar o código. uv.lock regista o ambiente completo, com as dependências resolvidas, depois deste comando.

O código inline neste artigo é canónico. O repositório complementar indicado nas referências é uma versão histórica. Não reproduz a dependência atualmente fixada nem a validação atual dos vectors.


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 da base de dados (database.py)

Este módulo é responsável pela ligação SQLite e por alguns auxiliares. Vamos inserir duas linhas de exemplo para que o servidor tenha algo para devolver na primeira consulta.

Crie 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)

FastMCP faz a maior parte do trabalho. Decore uma função Python simples e esta é registada como uma ferramenta ou recurso MCP. A docstring torna-se a descrição que o LLM vê. Escreva-a a pensar nesse leitor.

Crie featurestore_server.py:

# featurestore_server.py
import json
import math
from mcp.server.fastmcp import FastMCP
from database import get_db_connection, init_db

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

# Ensure DB is ready when server starts
init_db()

def reject_non_finite_json_number(value: str) -> None:
    """Reject NaN and infinities, which are not valid JSON numbers."""
    raise ValueError(f"Non-finite JSON number: {value}")

@mcp.resource("schema://main")
def get_schema() -> str:
    """
    Resource: Provide the database schema.
    Resources provide context to the host application.
    The host decides whether to pass that context to the model.
    """
    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: str, metadata: str | None = None) -> str:
    """
    Tool: Store a feature vector.
    Tools are executable functions that LLMs can call to perform actions.
    """
    try:
        parsed_vector = json.loads(
            vector, parse_constant=reject_non_finite_json_number
        )
    except (json.JSONDecodeError, ValueError):
        return "Error: Vector must be valid JSON with finite numbers"

    try:
        valid_vector = (
            isinstance(parsed_vector, list)
            and bool(parsed_vector)
            and all(
                isinstance(value, (int, float))
                and not isinstance(value, bool)
                and math.isfinite(value)
                for value in parsed_vector
            )
        )
    except OverflowError:
        valid_vector = False

    if not valid_vector:
        return "Error: Vector must be a non-empty JSON array of finite numbers (e.g., '[0.1, 0.2]')"

    metadata_json = None
    if metadata is not None:
        try:
            parsed_metadata = json.loads(
                metadata, parse_constant=reject_non_finite_json_number
            )
        except (json.JSONDecodeError, ValueError):
            return "Error: Metadata must be valid JSON with finite numbers"
        if not isinstance(parsed_metadata, dict):
            return "Error: Metadata must be a JSON object (e.g., '{\"type\": \"test\"}')"
        metadata_json = json.dumps(parsed_metadata, allow_nan=False)

    conn = get_db_connection()
    try:
        conn.execute(
            "INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
            (key, json.dumps(parsed_vector, allow_nan=False), metadata_json),
        )
        conn.commit()
        return f"Successfully stored feature '{key}'"
    except Exception as e:
        return f"Error: {str(e)}"
    finally:
        conn.close()

@mcp.tool()
def get_feature(key: str) -> str:
    """
    Tool: Retrieve a feature vector by key.
    """
    conn = get_db_connection()
    try:
        row = conn.execute(
            "SELECT vector, metadata FROM features WHERE key = ?", (key,)
        ).fetchone()

        if row:
            return json.dumps(
                {
                    "key": key,
                    "vector": json.loads(row[0]),
                    "metadata": json.loads(row[1]) if row[1] else None,
                },
                indent=2,
            )
        return f"Feature '{key}' not found."
    finally:
        conn.close()

@mcp.tool()
def list_features() -> str:
    """
    Tool: List all available feature keys.
    """
    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()

O parser JSON do Python aceita NaN e infinitos por predefinição, embora estejam fora da gramática dos números JSON. O callback parse_constant rejeita essas representações e math.isfinite verifica cada elemento do vector antes de o SQLite receber a linha. Os metadados seguem um único contrato: têm de ser uma string que contenha um objecto JSON, e o servidor faz o parsing e a normalização antes da inserção. Consulte as notas de interoperabilidade de JSON do Python.


3. Testar com o MCP Inspector

Antes de ligar o servidor ao Claude, faça uma verificação rápida com o MCP Inspector. É uma pequena interface web para chamar ferramentas e ler recursos diretamente.

uv run mcp dev featurestore_server.py

O comando inicia o servidor sob o MCP Inspector através de stdio. Use o URL do browser apresentado pelo comando. A porta da interface do Inspector depende da implementação; não assuma uma porta fixa.

Foi este o aspeto da minha execução original de junho de 2025 no MCP Inspector v0.14.0. A captura de ecrã regista a configuração stdio do Inspector e uma ligação bem-sucedida. O argumento --with mcp, sem versão fixada, é histórico; ao reproduzir agora o tutorial, use o comando v1 fixado apresentado neste artigo.

MCP Inspector v0.14.0 ligado ao FeatureStoreLite através de stdio com uv

Chame get_feature com key="user_123". Se devolver o JSON correspondente à linha inicial, o servidor está a funcionar. Isto verifica o caminho do protocolo local, o registo da ferramenta, a consulta à base de dados e o output JSON. Não valida a qualidade da pesquisa vectorial. Para retrieval em produção, adicione testes com queries representativas, resultados de vizinhos mais próximos esperados, limiares de distância, filtros de metadados e um conjunto de avaliação alinhado com a sua carga de trabalho.

A mesma execução expôs as três ferramentas e o recurso schema://main. Estas duas capturas de ecrã são verificações úteis da descoberta: o Inspector encontrou as funções e leu o schema SQL devolvido pelo servidor.

MCP Inspector a mostrar store_feature, get_feature, list_features e uma chamada bem-sucedida a list_features

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

A captura de ecrã da ferramenta contém quatro linhas: user_123, product_abc, doc_guide_001 e recommendation_engine. Essa era a minha base de dados local mais completa em 10 de junho de 2025. A implementação de list_features devolvia objectos com os campos key e created_at. A função atual devolve apenas um array JSON de strings de chaves e insere apenas as duas primeiras linhas. Não compare a saída histórica linha a linha com o fixture atual.


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 objecto 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. uv run descobre um projeto a partir do directório de trabalho para um comando como mcp; por isso, --directory selecciona o pyproject.toml e o uv.lock deste projeto; --locked falha em vez de alterar esse lockfile. O comando uv add do passo 1.2 já declarou mcp[cli] no projeto.

4.2. Como funciona a interação

Eis o que acontece de ponta a ponta quando o Claude precisa de consultar uma feature:

Consulta de uma feature em seis passos no FeatureStoreLite, desde a descoberta da ferramenta MCP, passando por tools/call e pelo acesso ao SQLite, até à resposta final do ClaudeConsulta de uma feature em seis passos no FeatureStoreLite, desde a descoberta da ferramenta MCP, passando por tools/call e pelo acesso ao SQLite, até à resposta final do Claude

  1. O Claude Desktop inicia um client MCP dedicado para o servidor e descobre as respetivas ferramentas.
  2. O host disponibiliza as descrições das ferramentas ao Claude.
  3. O Claude pode solicitar um tool call quando a pergunta requer dados da feature store.
  4. O client 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, client e servidor segue a especificação da arquitetura MCP. Os recursos também fornecem contexto à aplicação host. O MCP não exige que o host passe todos os recursos ao modelo.

4.3. Exemplos de queries

Reinicie o Claude Desktop e experimente alguns prompts:

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

  2. “Obtém o feature vector de user_123.” A resposta contém o vector e os metadados premium de database.py.

  3. “Guarda uma nova feature para new_item com a string JSON do vector [0.5, 0.5] e a string JSON dos metadados {"type": "test"}. Depois, recupera new_item e mostra o vector e os metadados guardados.” Ambos os argumentos têm de conter JSON válido. A escrita só é bem-sucedida depois de o servidor validar o vector e os metadados. A leitura seguinte verifica o ciclo completo de escrita/leitura.

4.4. O que mostrou a minha execução no Claude Desktop

As capturas de ecrã seguintes são da mesma execução de junho de 2025, antes de eu reduzir o fixture do artigo a duas linhas. Mostram o que o Claude Desktop apresentou depois de se ligar ao meu servidor. São observações dessa execução, não uma saída MCP garantida. O servidor devolve dados de ferramentas e recursos; o Claude escolhe uma ferramenta e redige a explicação em torno do resultado.

Primeiro pedi ao Claude que mostrasse o schema da base de dados:

Claude Desktop a responder a um pedido para mostrar o schema da base de dados do FeatureStoreLite

O Claude errou num detalhe importante. Descreveu o sistema como um armazenamento NoSQL ou de documentos, embora database.py use SQLite e crie uma tabela relacional. A coluna metadata contém texto JSON, mas isso não altera o motor da base de dados. A captura de ecrã é um bom lembrete de que uma explicação plausível do modelo não constitui o contrato da ferramenta. Verifique a instrução CREATE TABLE devolvida ou o código-fonte quando essa distinção for relevante.

Também pedi ao Claude que listasse as features disponíveis:

Claude Desktop a chamar list_features e a apresentar as quatro linhas da base de dados do autor de junho de 2025

As quatro chaves correspondem à base de dados local mais completa mostrada no Inspector. Rótulos como “user embedding” e “model embedding” são interpretações do Claude baseadas nos nomes e metadados. A própria list_features apenas garante as linhas que a sua implementação devolve.

Por fim, recuperei product_abc:

Claude Desktop a chamar get_feature para product_abc e a apresentar o seu vector e os seus metadados

Aqui, o vector e os metadados vieram do resultado da ferramenta. O texto sobre similaridade, recomendações e clustering veio do Claude. São utilizações possíveis de um embedding, mas o servidor deste tutorial apenas armazena e recupera vectors. Não implementa pesquisa de vizinhos mais próximos.


5. Resolução de problemas

Alguns modos de falha que convém conhecer:

  • “Server connection failed”:

    • Verifique os logs em ~/Library/Logs/Claude/mcp.log no macOS.
    • Confirme que a configuração usa um caminho absoluto, não um caminho relativo.
    • Confirme que uv está no PATH do Claude Desktop. Se não estiver, indique o caminho completo para o binário (which uv dir-lhe-á onde se encontra).
  • “Tool execution error”:

    • Reproduza o erro no Inspector com uv run mcp dev featurestore_server.py. O Inspector mostra o erro bruto, que o Claude Desktop normalmente oculta.
    • Verifique se features.db está a ser criado junto de database.py. O caminho vem de get_db_path(), que o resolve relativamente ao script; assim, uma alteração do directório de trabalho não deverá mover o ficheiro.

6. Conclusão

É tudo: um servidor FastMCP, um armazenamento SQLite e uma configuração do Claude Desktop que aponta para um comando uv run. A mesma estrutura funciona para a maioria das coisas que possam ser encapsuladas numa função Python. Substitua as chamadas SQLite por uma feature store real, uma API interna ou um model registry, e o servidor continua pequeno.

Principais conclusões

  1. O MCP é uma fronteira de ferramentas, não uma razão para expor todas as funções internas.
  2. Mantenha as ferramentas do servidor pequenas, tipadas e fáceis de testar sem um LLM.
  3. Use uv para que o ambiente do tutorial possa ser reconstruído de raiz.
  4. Trate o servidor MCP como código de produção assim que um agent lhe puder fazer chamadas.

Referências