MCP Server Local em Python: Como Criar Seu Próprio Servidor de Contexto para IA Sem Enviar Dados para a Nuvem
Você já parou para pensar quantas vezes seus dados pessoais viajam pela internet antes mesmo de você terminar de digitar um prompt?
Toda vez que você usa uma IA com contexto externo — documentos, arquivos locais, banco de dados — esses dados saem da sua máquina, passam por servidores de terceiros, e voltam transformados em resposta. É prático. É rápido. Mas é também uma violação de privacidade que acontece milhões de vezes por dia sem ninguém questionar.
Hoje eu vou te mostrar como criar um servidor MCP (Model Context Protocol) 100% local em Python puro, que permite suas IAs acessarem seus arquivos, bancos e ferramentas sem que nenhum byte saia da sua rede. Zero dependência de APIs externas. Zero vazamento de dados. Controle total.
O Problema: Seu Contexto Local Não Tem Ponte Segura
Modelos de linguagem são ótimos em gerar texto, mas péssimos em acessar informação específica da sua máquina. Quer que a IA leia seus arquivos .md? Quer que ela consulte seu banco SQLite local? Quer que ela execute uma automação no seu sistema de arquivos?
Até recentemente, você tinha duas opções:
1. Copiar e colar o conteúdo manualmente (tedioso e propenso a erro)
2. Usar extensões que enviam seus dados para serviços terceiros (rápido, mas inseguro)
O MCP resolve isso criando um protocolo padronizado onde ferramentas externas — seus arquivos, seu banco, suas APIs locais — se conectam ao modelo de forma controlada e segura. E a melhor parte: você pode rodar tudo localmente.
Box Perrengue: Quando o Contexto Vazou Antes do Deploy
Em março deste ano, eu estava construindo um assistente para analisar logs de produção. A solução óbvia era usar uma extensão que lia os arquivos de log diretamente e passava para o modelo. Funcionou perfeitamente no meu notebook.
Até que eu percebi, olhando o tráfego de rede com tcpdump, que cada requisição ao modelo incluía os últimos 50KB de logs brutos. Logs que continham IPs internos, mensagens de erro com stack traces completos, e — pior — alguns tokens de autenticação que não deveriam estar ali.
Eu tinha acabado de enviar dados sensíveis de produção para servidores de uma empresa que eu nem sabia onde ficavam.
Foi aí que eu decidi: se eu queria contexto local, eu ia construir minha própria ponte. Uma que eu pudesse auditar, controlar, e que nunca deixasse minha rede sem minha autorização explícita.
A Solução: MCP Server Local em 4 Passos
Vamos construir um servidor MCP que expõe três ferramentas:
1. read_file — lê arquivos locais
2. query_sqlite — executa queries em bancos SQLite locais
3. list_directory — lista diretórios
Tudo em Python puro, sem frameworks pesados, sem dependências externas além do que já vem no Python.
Passo 1: A Estrutura Base do Servidor MCP
O MCP usa JSON-RPC sobre stdio (entrada/saída padrão). Isso significa que o servidor é um script Python que lê comandos da entrada padrão e escreve respostas na saída padrão. Simples e direto.
#!/usr/bin/env python3
"""
MCP Server Local - Servidor de Contexto para IA
100% local, sem dependências externas
"""
import json
import sys
import os
import sqlite3
from pathlib import Path
# Configuração de segurança: diretórios permitidos
ALLOWED_PATHS = [
Path.home() / "Documents",
Path.home() / "Projects",
]
def validate_path(path_str: str) -> Path:
"""Valida se o caminho está dentro dos diretórios permitidos"""
path = Path(path_str).resolve()
for allowed in ALLOWED_PATHS:
try:
path.relative_to(allowed)
return path
except ValueError:
continue
raise PermissionError(f"Caminho não permitido: {path}")
def send_response(response: dict):
"""Envia resposta JSON-RPC via stdout"""
print(json.dumps(response), flush=True)
def send_error(request_id, code: int, message: str):
"""Envia erro JSON-RPC"""
send_response({
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": code, "message": message}
})
Esse código define a base: validação de caminhos (segurança primeiro!) e funções para enviar respostas no formato JSON-RPC que o MCP espera.
Passo 2: Implementando as Ferramentas
Agora vamos implementar as três ferramentas que o servidor vai expor:
def tool_read_file(path: str) -> str:
"""Lê o conteúdo de um arquivo de texto"""
validated = validate_path(path)
if not validated.exists():
raise FileNotFoundError(f"Arquivo não encontrado: {path}")
if not validated.is_file():
raise ValueError(f"Não é um arquivo: {path}")
# Limita tamanho para evitar estourar contexto
if validated.stat().st_size > 100_000: # 100KB
raise ValueError(f"Arquivo muito grande: {validated.stat().st_size} bytes")
return validated.read_text(encoding='utf-8')
def tool_query_sqlite(db_path: str, query: str) -> str:
"""Executa uma query SELECT em banco SQLite local"""
validated = validate_path(db_path)
if not validated.exists():
raise FileNotFoundError(f"Banco não encontrado: {db_path}")
# Segurança: apenas SELECT, nunca INSERT/UPDATE/DELETE
query_lower = query.strip().lower()
if not query_lower.startswith('select'):
raise PermissionError("Apenas queries SELECT são permitidas")
conn = sqlite3.connect(str(validated))
cursor = conn.cursor()
try:
cursor.execute(query)
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description]
# Formata resultado como texto tabular
result = []
result.append(" | ".join(columns))
result.append("-" * 50)
for row in rows[:100]: # Limita a 100 linhas
result.append(" | ".join(str(cell) for cell in row))
return "\n".join(result)
finally:
conn.close()
def tool_list_directory(path: str) -> str:
"""Lista arquivos e diretórios"""
validated = validate_path(path)
if not validated.exists():
raise FileNotFoundError(f"Diretório não encontrado: {path}")
if not validated.is_dir():
raise ValueError(f"Não é um diretório: {path}")
entries = []
for entry in sorted(validated.iterdir()):
entry_type = "📁" if entry.is_dir() else "📄"
size = ""
if entry.is_file():
size = f" ({entry.stat().st_size:,} bytes)"
entries.append(f"{entry_type} {entry.name}{size}")
return "\n".join(entries)
Cada ferramenta valida o caminho, aplica restrições de segurança, e retorna o resultado como texto. Note como a query SQLite só permite SELECT — isso impede que a IA acidentalmente (ou por prompt injection) modifique seu banco.
Passo 3: O Loop Principal do Servidor
Agora precisamos do loop que lê requisições JSON-RPC e despacha para as ferramentas:
def handle_request(request: dict):
"""Processa uma requisição JSON-RPC"""
request_id = request.get("id")
method = request.get("method")
params = request.get("params", {})
# Responde ao handshake inicial do MCP
if method == "initialize":
send_response({
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {
"name": "mcp-local-server",
"version": "1.0.0"
}
}
})
return
# Lista ferramentas disponíveis
if method == "tools/list":
send_response({
"jsonrpc": "2.0",
"id": request_id,
"result": {
"tools": [
{
"name": "read_file",
"description": "Lê o conteúdo de um arquivo de texto local",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Caminho absoluto do arquivo"
}
},
"required": ["path"]
}
},
{
"name": "query_sqlite",
"description": "Executa query SELECT em banco SQLite local",
"inputSchema": {
"type": "object",
"properties": {
"db_path": {"type": "string"},
"query": {"type": "string"}
},
"required": ["db_path", "query"]
}
},
{
"name": "list_directory",
"description": "Lista arquivos e diretórios",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
}
]
}
})
return
# Executa ferramenta
if method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
try:
if tool_name == "read_file":
result = tool_read_file(arguments["path"])
elif tool_name == "query_sqlite":
result = tool_query_sqlite(arguments["db_path"], arguments["query"])
elif tool_name == "list_directory":
result = tool_list_directory(arguments["path"])
else:
send_error(request_id, -32601, f"Ferramenta desconhecida: {tool_name}")
return
send_response({
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": result}]
}
})
except Exception as e:
send_error(request_id, -32000, str(e))
return
# Método desconhecido
send_error(request_id, -32601, f"Método desconhecido: {method}")
def main():
"""Loop principal do servidor"""
print("MCP Local Server iniciado. Aguardando requisições...", file=sys.stderr)
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
handle_request(request)
except json.JSONDecodeError:
send_error(None, -32700, "JSON inválido")
except Exception as e:
print(f"Erro inesperado: {e}", file=sys.stderr)
if __name__ == "__main__":
main()
Pronto. Esse é o servidor completo. Salve como mcp_server.py e torne executável com chmod +x mcp_server.py.
Passo 4: Conectando ao Cliente (Claude Desktop, Continue, etc.)
Agora você precisa configurar seu cliente de IA para usar esse servidor. No Claude Desktop, por exemplo, você edita o arquivo claude_desktop_config.json:
{
"mcpServers": {
"local-server": {
"command": "python3",
"args": ["/caminho/para/mcp_server.py"]
}
}
}
Reinicie o Claude Desktop e pronto. Agora quando você pedir “leia o arquivo /home/user/Documents/notes.md”, ele vai usar seu servidor local, não uma extensão que envia dados para fora.
Por Que Isso Importa (Muito Mais do Que Parece)
Você pode estar pensando: “Ok, legal, mas qual a diferença prática?”
A diferença é controle. Quando você usa extensões de terceiros, você está confiando que:
1. Eles não armazenam seus dados
2. Eles não vazam seus dados
3. Eles não mudam a política de privacidade amanhã
Com um servidor MCP local, você:
- **Audita** todo o tráfego (é só adicionar logs no `handle_request`)
- **Controla** quais caminhos são acessíveis
- **Restringe** operações perigosas (como eu fiz com o SQLite)
- **Evolui** o servidor conforme suas necessidades
O Próximo Passo: Expandindo o Servidor
Esse servidor básico já funciona, mas você pode expandir de várias formas:
Adicione mais ferramentas:
- Busca full-text em arquivos `.md`
- Execução de scripts shell (com whitelist de comandos)
- Consulta a APIs locais (Home Assistant, por exemplo)
Melhore a segurança:
- Adicione autenticação por token
- Implemente rate limiting
- Crie perfis de acesso diferentes por cliente
Otimize performance:
- Cache de arquivos frequentemente acessados
- Indexação prévia de diretórios grandes
- Compressão de respostas grandes
A Pergunta Que Fica
Agora eu te pergunto: quantas vezes por semana você cola dados sensíveis no prompt sem pensar duas vezes?
Senhas, tokens, dados de clientes, código proprietárico. Tudo isso viaja pela rede toda vez que você pede ajuda para uma IA.
Se você já passou por isso — ou pior, se você já descobriu depois que o dado vazou — me conta nos comentários. O que você fez? Como resolveu? E mais importante: o que você vai fazer diferente a partir de agora?
Porque privacidade não é sobre ter algo a esconder. É sobre ter o direito de escolher o que compartilhar. E quando o assunto é IA, essa escolha começa com quem controla o contexto.
