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:
| Primitiva | Quem a aciona | Neste servidor |
|---|---|---|
| Ferramenta | O modelo decide chamá-la | get_feature, store_feature, list_features |
| Recurso | O host decide como o incluir no contexto | schema://main |
| Prompt | O utilizador escolhe-o, como um modelo de mensagem guardado | Nã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:
- O utilizador faz uma pergunta em linguagem natural.
- O Claude Desktop é o host MCP. Envia a conversa ao modelo Claude, cria um cliente MCP para este servidor e gere a ligação.
- O nosso servidor
FastMCPexpõe três ferramentas (get_feature,store_feature,list_features) e um recurso (schema://main). - 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:
database.pytrata das operações SQLite.featurestore_server.pydefine 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:
vectortipado. 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. Oallow_inf_nan=Falserejeita depoisNaNe infinitos, que o analisador JSON do Python aceita por omissão.metadatatipado. O esquema aceita um objeto JSON,nullou um valor omitido. Oallow_nan=Falsetambém impedeNaNnos metadados guardados.- Erros lançados. O
get_featurelançaValueErrorpara uma chave inexistente. Uma exceção lançada chega ao cliente como erro de ferramenta comisError: true; uma string devolvida parece um resultado bem-sucedido. - Substituições. O
store_featureusaINSERT 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:
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.
- O Claude Desktop inicia um cliente MCP dedicado para o servidor e descobre as suas ferramentas.
- O host disponibiliza as descrições das ferramentas ao Claude.
- O Claude pode pedir um tool call quando a pergunta precisa de dados da feature store.
- O cliente MCP envia esse pedido ao servidor.
- O servidor executa a função Python e devolve o resultado ao host.
- 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:
-
“Liste todas as features disponíveis.” O resultado determinístico contém as duas chaves iniciais:
user_123eproduct_abc. -
“Obtenha o vetor de features de user_123.” A resposta contém o vetor e os metadados
premiumdedatabase.py. -
“Guarde uma nova feature para
new_itemcom o vetor[0.5, 0.5]e os metadados{"type": "test"}. Depois obtenhanew_iteme 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,nullou ser omitidos. A leitura seguinte verifica o ciclo completo de escrita e leitura. -
“Obtenha o vetor de features de user_999.” O
get_featurelança um erro para a chave inexistente. O Claude deve dizer que a chave não existe e pode chamarlist_featurespara 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:
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:
- Claude Code:
claude mcp add featurestore -- uv run --directory /ABSOLUTE/PATH/TO/mcp-featurestore --locked mcp run /ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py. Tudo o que vem depois de--é o comando que inicia o servidor. - O Cursor lê o mesmo formato JSON
mcpServersa partir de~/.cursor/mcp.json, ou.cursor/mcp.jsonnum projeto.
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\logsno Windows. Omcp.logregista os eventos de ligação. Omcp-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
uvestá noPATHdo Claude Desktop. Se não estiver, indique o caminho completo do binário (owhich uvdiz 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.
- Verifique os logs em
-
“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.dbestá a ser criado junto dodatabase.py. O caminho vem deget_db_path(), que o resolve relativamente ao script, por isso uma mudança do diretório de trabalho não deve mover o ficheiro.
- Reproduza-o no Inspector com
-
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_featuresubstitui 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.

