Aprendizados de construir um servidor MCP de catálogo de cursos — da POC à produção.
O Model Context Protocol (MCP) é um padrão aberto que permite que aplicações de IA (como o Cursor, Claude Desktop ou outros hosts) se conectem a fontes de dados e ferramentas externas de forma padronizada.
Pense no MCP como uma tomada universal: em vez de cada editor inventar sua própria integração com bancos, APIs e scripts, todos falam o mesmo protocolo — JSON-RPC 2.0 — sobre um transporte (stdio ou HTTP).
┌─────────────┐ JSON-RPC ┌─────────────┐ HTTP/SQL ┌─────────────┐
│ Cursor │ ◄──────────────► │ MCP Server │ ◄─────────────► │ Backend │
│ (cliente) │ tools/call │ (adaptador)│ │ (dados) │
│ │ resources/read │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
O MCP não substitui sua API de negócio. Ele é a camada de adaptação entre o modelo de linguagem e o mundo real.
O protocolo expõe três tipos de capacidade. Entender a diferença entre elas é o ponto central deste artigo.
| Primitiva | Metáfora | Quem controla | Protocolo |
|---|---|---|---|
| Tool | |||
| Verbo — fazer algo | |||
| Modelo (com supervisão humana) | tools/call |
||
| Resource | |||
| Substantivo — ler algo | |||
| Aplicação / usuário | resources/read |
||
| Prompt | |||
| Template — como fazer | |||
| Usuário | prompts/get |
Tools são funções que o modelo pode chamar. Cada tool tem nome, descrição, schema de entrada (JSON Schema via Zod) e retorna um resultado.
{
"name": "criar_curso",
"description": "Cria um novo curso no catálogo.",
"inputSchema": {
"type": "object",
"properties": {
"titulo": { "type": "string" },
"cargaHoraria": { "type": "number" }
},
"required": ["titulo", "cargaHoraria"]
}
}
Quando o modelo usa: o usuário pede uma ação — "crie um curso de NestJS com 12 horas" — e o modelo decide invocar criar_curso
.
Características:
isError: true
para o modelo se corrigirResources são dados identificados por URI que o host ou o modelo podem ler para obter contexto.
cursos://catalogo → catálogo completo (JSON)
cursos://f47ac10b-58cc-... → detalhes de um curso
file:///docs/guia.md → documento estático
Quando o modelo usa: o usuário quer consultar informação — "quais cursos existem?" — ou o host anexa o resource automaticamente ao contexto.
Características:
cursos://catalogo
) ou cursos://{uuid}
)Prompts são modelos de instrução parametrizados que guiam o modelo por um fluxo conhecido.
/plan-vacation destination=Barcelona duration=7 budget=3000
Quando usar: fluxos repetíveis onde você quer consistência — onboarding, checklists, workflows de revisão.
| Pergunta | Se a resposta for sim → |
|---|---|
| A operação altera algo no sistema? | |
| Tool | |
| A operação só lê dados existentes? | |
| Resource | |
| O modelo precisa decidir agir com parâmetros? | |
| Tool | |
| O dado serve como contexto de referência? | |
| Resource | |
| Há validação complexa ou efeito colateral? | |
| Tool | |
| O host pode anexar automaticamente ao chat? | |
| Resource |
Na V2 do nosso projeto, tínhamos três tools:
listar_cursos → leitura (mas exposta como Tool)
buscar_curso → leitura (mas exposta como Tool)
criar_curso → escrita ✓
Na V3, separamos corretamente:
Resources:
cursos://catalogo → leitura do catálogo
cursos://{uuid} → leitura de um curso
Tools:
criar_curso → escrita
atualizar_curso → escrita
arquivar_curso → escrita
Por que mudamos? Porque listar_cursos
e buscar_curso
não tinham efeito colateral — eram consultas disfarçadas de ações. Isso gerava redundância: o modelo podia escolher entre duas formas de fazer a mesma coisa, sem critério claro.
| Cenário | Exemplo |
|---|---|
| Criar dados | |
criar_curso({ titulo, cargaHoraria }) |
|
| Atualizar dados | |
atualizar_curso({ id, cargaHoraria: 10 }) |
|
| Ações de domínio | |
arquivar_curso({ id }) — não é DELETE, é ação de negócio |
|
| Operações com validação | |
| Rejeitar curso arquivado, validar campos obrigatórios | |
| Integrações externas | |
| Enviar email, chamar webhook, processar pagamento | |
| Cálculos ou transformações | |
| Gerar relatório, converter formato |
| Cenário | Use em vez disso |
|---|---|
| Listar dados sem efeito colateral | |
Resource (cursos://catalogo ) |
|
| Consultar um registro por ID | |
Resource (cursos://{uuid} ) |
|
| Expor documentação estática | |
Resource (docs://api-reference ) |
|
| Anexar contexto ao chat | |
| Resource (application-driven) |
| Cenário | Exemplo |
|---|---|
| Catálogo ou lista de referência | |
cursos://catalogo |
|
| Detalhe de entidade por URI | |
cursos://{uuid} |
|
| Documentação, schemas, configs | |
file:///README.md , schema://database |
|
| Dados que mudam pouco e servem de contexto | |
| Políticas, glossários, FAQs | |
| O host precisa descobrir o que existe | |
resources/list + resources/templates/list |
|
| Cenário | Use em vez disso |
|---|---|
| Operação que altera estado | |
| Tool | |
| Busca com filtros complexos | |
Tool (ex.: buscar_cursos_por_categoria ) |
|
| Ação que exige confirmação humana | |
| Tool | |
| Processamento ou cálculo | |
| Tool |
| Tipo | URI | Quando usar |
|---|---|---|
| Fixo | ||
cursos://catalogo |
||
| Dado único, endereço conhecido | ||
| Template | ||
cursos://{uuid} |
||
| Parametrizado, N instâncias |
O MCP define como cliente e server se comunicam. Isso é independente de Tools/Resources.
| Transporte | Como funciona | Quando usar |
|---|---|---|
| stdio | ||
| Cursor spawna processo; JSON-RPC via stdin/stdout | Dev local, integração com editores | |
| Streamable HTTP | ||
| Server HTTP remoto; POST/GET com JSON-RPC | Deploy remoto, múltiplos clientes, auth HTTP |
{
"mcpServers": {
"mcp-cursos": {
"command": "node",
"args": ["apps/mcp/dist/mcp.js"],
"env": {
"BACKEND_URL": "http://localhost:3000/mcp/v1",
"API_KEY": "sua-chave"
}
}
}
}
Vantagens: simples, zero config de rede, padrão do ecossistema.
Limitação: processo local — o Cursor precisa spawnar o binário.
Server expõe URL (https://api.exemplo.com/mcp
); auth via header Authorization: Bearer
. Indicado para produção compartilhada entre times.
Um erro comum é colocar toda a lógica dentro do MCP server. A arquitetura que adotamos separa responsabilidades:
┌──────────────────────────────────────────────────────────┐
│ apps/mcp Adaptador MCP (stdio) │
│ - Registra tools e resources │
│ - Traduz MCP → HTTP │
│ - Não conhece banco de dados │
└────────────────────────┬─────────────────────────────────┘
│ HTTP + API Key
▼
┌──────────────────────────────────────────────────────────┐
│ apps/backend API de negócio (NestJS) │
│ - REST /mcp/v1/cursos │
│ - Auth, validação, regras de domínio │
│ - TypeORM + SQLite │
└──────────────────────────────────────────────────────────┘
Por quê?
curl
, Postman)A spec MCP não prescreve como o server fala com sua API. REST, gRPC ou chamada in-process — escolha de implementação.
| Versão | Leitura | Escrita | Persistência | Auth |
|---|---|---|---|---|
| V1 | ||||
Tools (listar , buscar ) |
||||
Tool (criar ) |
||||
| Mock in-memory | — | |||
| V2 | ||||
Tools (listar , buscar ) |
||||
Tool (criar ) |
||||
| SQLite + backend HTTP | API Key | |||
| V3 | ||||
Resources (cursos://… ) |
||||
Tools (criar , atualizar , arquivar ) |
||||
| SQLite + soft delete | API Key |
A V3 aplicou a regra: Resource = ler, Tool = agir.
Em vez de deletar cursos, arquivamos — arquivado: true
. O curso some do catálogo ativo, mas continua consultável por URI.
| Onde | Comportamento |
|---|---|
cursos://catalogo |
|
| Lista só cursos ativos | |
cursos://{uuid} arquivado |
|
Retorna curso com arquivado: true |
|
atualizar_curso em arquivado |
|
| Bloqueado — exige reativar antes (futuro) | |
arquivar_curso |
|
POST /cursos/:id/arquivar — ação explícita |
atualizar_curso
aceita titulo
e/ou cargaHoraria
— pelo menos um obrigatório. Alinha com PATCH REST e evita reenviar dados desnecessários.
A spec MCP permite que o server notifique o cliente quando resources mudam (resources/subscribe
, resources/listChanged
).
Com subscribe: após criar_curso
, o Cursor poderia atualizar cursos://catalogo
automaticamente no contexto.
Sem subscribe (nossa V3): o modelo relê o resource quando o usuário pede — "mostra o catálogo atualizado".
Para a maioria dos casos, relê quando necessário é suficiente. Subscribe faz sentido quando o catálogo muda constantemente e o host precisa de refresh automático.
Antes de implementar, pergunte:
MCP não é magia — é protocolo. Tools são verbos, Resources são substantivos, Prompts são roteiros. Separar leitura de escrita torna seu server previsível para o modelo e mais fácil de manter para você.
Comece simples: stdio, poucas tools, mock ou API mínima. Evolua para Resources quando a leitura passiva fizer sentido, e para HTTP remoto quando precisar compartilhar o backend entre clientes.
O catálogo de cursos que construímos passou por três versões até chegar nesse modelo. Cada iteração ensinou algo — e documentar essas decisões (ADRs, glossário, este post) evita que o próximo dev reinvente a roda.
Escrito com base na implementação do projeto mcp-cursos — grill sessions, ADRs e código real.