Cada vez que um agent precisa acessar um novo data source, alguém escreve uma integração custom. GitHub? Integração custom. Jira? Integração custom. Azure Monitor? Integração custom. Banco de dados? Mais uma.
Se isso parece com o problema que REST/HTTP resolveu pra APIs web, você está pensando certo. MCP (Model Context Protocol) é a tentativa de padronizar como AI models e agents se conectam a data sources e ferramentas.
O mapa pro profissional de infra
| Conceito MCP | O que faz | Equivalente em infra |
|---|---|---|
| MCP Server | Expõe dados/ferramentas via protocolo padrão | API server, microserviço |
| MCP Client | Consome dados/ferramentas de MCP servers | API client, SDK |
| Host | Aplicação que roda o client (VS Code, Claude, etc.) | Browser, app que consome APIs |
| Tool | Função executável via MCP | Endpoint REST (POST /action) |
| Resource | Dado acessível via MCP | Endpoint REST (GET /data) |
| Prompt | Template de interação | Slash command, prompt salvo, formulário parametrizado |
| Transport | Como client e server se comunicam | stdio, Streamable HTTP (SSE legado só por compatibilidade) |
O problema N×M
Antes de MCP, cada combinação de (AI application × data source) precisava de código custom.
Com MCP:
É o mesmo valor que USB-C trouxe: um dispositivo implementa USB-C uma vez, funciona em qualquer computador.
Arquitetura do protocolo
Os três primitivos do MCP
1. Tools (ações que o model pode executar)
Tools são funções com input/output definidos. O model decide quando chamá-las.
{
"name": "restart_service",
"description": "Reinicia um serviço systemd no servidor especificado",
"inputSchema": {
"type": "object",
"properties": {
"hostname": {
"type": "string",
"description": "Hostname do servidor"
},
"service_name": {
"type": "string",
"description": "Nome do serviço systemd"
}
},
"required": ["hostname", "service_name"]
}
}
Similar a: um endpoint REST com OpenAPI spec.
2. Resources (dados que o model pode ler)
Resources são URIs que retornam dados. O model (ou host) decide quando acessá-los.
{
"uri": "server://web-prod-01/metrics",
"name": "Métricas de web-prod-01",
"description": "CPU, memória, disco e rede do servidor web-prod-01",
"mimeType": "application/json"
}
Similar a: um endpoint GET com resposta structured.
Resources podem ser estáticos (listados no resources/list) ou dinâmicos (templates com parâmetros, via resources/templates/list). A diferença prática:
| Tipo | Exemplo de URI | Quando usar |
|---|---|---|
| Estático | server://web-prod-01/metrics | Dados de um recurso fixo e conhecido |
| Template | server://{hostname}/metrics | Client preenche o hostname em runtime |
| Subscriptions | resources/subscribe → URI | Client recebe notificação quando o conteúdo muda |
Resources são read-only por design. Se o agent precisa modificar algo, isso é uma Tool. Essa separação é intencional: resources são seguros pra expor sem preocupação com side effects.
3. Prompts (templates de interação)
Prompts são templates pré-definidos que o host pode oferecer ao usuário.
{
"name": "diagnose_alert",
"description": "Template pra diagnosticar um alerta de monitoramento",
"arguments": [
{"name": "alert_name", "description": "Nome do alerta", "required": true},
{"name": "resource", "description": "Recurso afetado", "required": true}
]
}
Similar a: um form template que guia o input do usuário.
Implementando um MCP Server
Um MCP server que expõe métricas de Azure Monitor. Aqui eu usei a API atual de alto nível da SDK Python, que registra tools, resources e prompts direto com decorators.
Com Python (SDK oficial)
import os
import subprocess
from typing import Annotated
from mcp.server import MCPServer
from pydantic import Field
SUBSCRIPTION_ID = os.environ["AZURE_SUBSCRIPTION_ID"]
mcp = MCPServer("azure-monitor")
@mcp.tool()
def get_vm_metrics(
resource_group: str,
vm_name: str,
offset: Annotated[str, Field(description="Ex: 1h ou 24h")] = "1h",
) -> str:
"""Retorna métricas de CPU de uma VM Azure."""
resource_id = (
f"/subscriptions/{SUBSCRIPTION_ID}"
f"/resourceGroups/{resource_group}"
f"/providers/Microsoft.Compute/virtualMachines/{vm_name}"
)
result = subprocess.run(
[
"az", "monitor", "metrics", "list",
"--resource", resource_id,
"--metric", "Percentage CPU",
"--interval", "PT5M",
"--offset", offset,
"--output", "json",
],
capture_output=True,
text=True,
check=True,
)
return result.stdout
@mcp.tool()
def list_metric_alert_rules(resource_group: str) -> str:
"""Lista regras de alerta baseadas em métricas no resource group."""
result = subprocess.run(
[
"az", "monitor", "metrics", "alert", "list",
"--resource-group", resource_group,
"--output", "json",
],
capture_output=True,
text=True,
check=True,
)
return result.stdout
@mcp.resource("azure://monitor/metric-alert-rules")
def metric_alert_rules() -> str:
"""Regras de alertas de métricas disponíveis na subscription."""
result = subprocess.run(
[
"az", "monitor", "metrics", "alert", "list",
"--output", "json",
],
capture_output=True,
text=True,
check=True,
)
return result.stdout
@mcp.prompt()
def diagnose_alert(alert_name: str, resource: str) -> str:
"""Monta um prompt inicial pra diagnosticar um alerta."""
return (
f"Diagnostique o alerta {alert_name} no recurso {resource}. "
"Liste hipóteses, sinais pra confirmar cada uma e a ordem de checagem."
)
if __name__ == "__main__":
mcp.run()
Configurando no client (VS Code / Claude Desktop)
{
"mcpServers": {
"azure-monitor": {
"command": "python",
"args": ["./mcp-servers/azure-monitor/server.py"],
"env": {
"AZURE_SUBSCRIPTION_ID": "seu-sub-id"
}
}
}
}
Transport: como client e server se comunicam
Na prática, você vai lidar com dois transports padrão: stdio localmente e Streamable HTTP no modo remoto. SSE continua existindo por compatibilidade com clients antigos, mas não é o caminho novo.
stdio (local)
O client sobe o server como processo filho. A conversa vai por stdin e stdout.
Quando usar: tools locais (file system, CLI), integrações que rodam na mesma máquina.
Streamable HTTP (remoto)
No modo remoto, cada mensagem JSON-RPC vai por HTTP POST no endpoint MCP. O server pode responder com JSON puro ou abrir um stream SSE na mesma rota. Se precisar mandar notificações fora de uma request em andamento, o client pode abrir um GET no mesmo endpoint.
Quando usar: server compartilhado, server em cloud, vários clients apontando pro mesmo endpoint.
from server import mcp
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
streamable_http_path="/mcp",
)
Se você ainda precisa atender client antigo, a SDK Python também consegue subir SSE legado. Eu só não começaria projeto novo assim.
Security considerations
MCP servers expõem ações reais. Um MCP server mal configurado é um vetor de ataque.
Autenticação
No stdio, o comum é herdar credenciais do ambiente local. No HTTP, a especificação atual descreve um fluxo de autorização baseado em OAuth 2.1, metadata do resource server e WWW-Authenticate.
Nota: OAuth 2.1 ainda é um draft (RFC em andamento, não ratificado). Na prática, isso significa que a parte de auth do MCP spec pode mudar conforme o OAuth 2.1 avança para RFC final. Implemente seguindo a spec atual, mas esteja preparado para ajustes.
Mesmo assim, autenticar o client é só metade do trabalho. Você ainda precisa decidir o que cada identidade pode fazer.
# Adicionar auth no server HTTP
from starlette.authentication import requires
from starlette.responses import Response
@requires("authenticated")
async def handle_mcp_http_request(request):
payload = await request.json()
if payload.get("method") == "tools/call":
token = request.auth.credentials
allowed_tools = get_allowed_tools(token)
tool_name = payload["params"]["name"]
if tool_name not in allowed_tools:
return Response(status_code=403)
# Encaminhar a requisição pro handler MCP real
...
Princípio de least privilege
# Não exponha tools destrutivas por padrão
SAFE_TOOLS = ["get_metrics", "list_alerts", "get_logs"]
DANGEROUS_TOOLS = ["restart_service", "scale_resource", "delete_resource"]
def exposed_tools_for(client_level: str) -> list[str]:
tool_names = SAFE_TOOLS.copy()
if client_level == "admin":
tool_names += DANGEROUS_TOOLS
return tool_names
Input validation
def validate_get_vm_metrics(arguments: dict) -> dict:
if not validate_resource_group(arguments["resource_group"]):
raise ValueError("resource group inválido ou sem permissão")
return {
"resource_group": arguments["resource_group"],
"vm_name": sanitize_resource_name(arguments["vm_name"]),
"offset": arguments.get("offset", "1h"),
}
Ecossistema atual
| MCP Server | O que expõe | Maintained by |
|---|---|---|
| GitHub | Repos, issues, PRs, code search | GitHub/Community |
| PostgreSQL | Query, schema info | Community |
| Filesystem | Read/write files | Anthropic |
| Azure DevOps | Work items, pipelines | Community |
| Kubernetes | Pods, logs, events | Community |
| Slack | Messages, channels | Community |
A lista cresce rápido. Confira em modelcontextprotocol.io o registry atualizado.
MCP vs Function Calling: qual a diferença?
| Aspecto | Function Calling | MCP |
|---|---|---|
| Definido por | Cada provider (OpenAI, Anthropic) | Protocolo aberto |
| Escopo | Uma chamada de API | Protocolo completo (discovery, transports, auth HTTP, streaming) |
| Portabilidade | Lock-in no provider | Qualquer client, qualquer server |
| Discovery | Precisa definir tools no request | Server expõe capabilities dinamicamente |
| State | Normalmente stateless | Pode manter sessão, ou operar stateless no HTTP |
| Transport | HTTP request | stdio, Streamable HTTP |
MCP não substitui tool calling do provider no lado do modelo. Ele substitui a integração ad-hoc entre host e sistemas externos. Muitos clients traduzem tools MCP pro formato de function calling do provider, mas isso é detalhe de implementação do client.
O que pode dar errado
- MCP server com tool destrutiva exposta: expor
restart_serviceoudelete_resourcesem autenticação adequada é equivalente a deixar um endpoint de admin público. Comece com tools read-only e adicione writes com gate de aprovação. - Server crash sem retry no client: se o MCP server reinicia (deploy, OOM kill), o client precisa reconectar. A spec define retry, mas nem todo client implementa. Teste o cenário de server restart explicitamente.
- Input injection via tool arguments: o agent passa argumentos pra tool que vieram do input do usuário. Se a tool constrói queries ou comandos shell sem sanitização, isso é um vetor de injection clássico. Valide e sanitize sempre.
- Resource retornando dado stale: um resource que cacheia métricas de 5 minutos atrás. O agent decide que “CPU está baixa” baseado em dado velho enquanto o servidor está pegando fogo agora. Documente o freshness do resource e considere subscriptions.
- Discovery dinâmico expondo tools inesperadas: se o server expõe tools baseado em contexto (role do user, por exemplo) e a lógica de filtragem tem bug, o agent pode ver tools que não deveria. Trate
tools/listcomo uma superfície de ataque.
O que levar pra segunda-feira
- MCP é o “USB-C dos AI agents”. Padroniza a conversa entre hosts, clients e sistemas externos.
- Três primitivos aparecem o tempo todo: Tools (ações), Resources (dados) e Prompts (templates).
- Segurança continua sendo trabalho de engenharia. A spec ajuda no fluxo HTTP, mas autorização fina, validação e observabilidade continuam na sua mão.
- Comece expondo tools read-only. get_metrics, list_resources, get_logs. Ações de escrita entram depois.
- stdio pra local, Streamable HTTP pra remoto. SSE legado existe, mas hoje é mais compatibilidade do que recomendação.
Se você quer entender como agents usam tools e memória por dentro antes de conectar via MCP, comece pela série “AI por dentro”: como AI agents funcionam por dentro e memória, estado e consistência.
No próximo post: projetando um assistente AI pessoal de ponta a ponta.
Leitura complementar
- How MCP Works (Neo Kim, System Design Newsletter)
- Model Context Protocol specification
- MCP Servers repository