Aprendizados de construir um servidor MCP de catálogo de cursos — da POC à produção.
O que é o Model Context Protocol (MCP)?
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.
As três primitivas do MCP
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 — ações invocáveis
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:
- Pode ter efeito colateral (criar, atualizar, deletar, enviar email)
- Retorna erro estruturado com
isError: truepara o modelo se corrigir - O humano deve estar no loop (aprovação, logs, confirmação)
Resources — dados passivos legíveis
Resources 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:
- Somente leitura (por design)
- Identificados por URI (esquemas customizados são permitidos)
- Podem ser fixos (
cursos://catalogo) ou templates (cursos://{uuid}) - Application-driven: o host decide como exibir e quando incluir no contexto
Prompts — templates reutilizáveis
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.
A regra de ouro: Tool vs Resource
| 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 |
Exemplo concreto: catálogo de cursos
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.
Quando usar Tools
✅ Use Tool quando...
| 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 |
❌ Não use Tool quando...
| 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) |
Quando usar Resources
✅ Use Resource quando...
| 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
|
❌ Não use Resource quando...
| 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 |
Resource fixo vs template
| Tipo | URI | Quando usar |
|---|---|---|
| Fixo | cursos://catalogo |
Dado único, endereço conhecido |
| Template | cursos://{uuid} |
Parametrizado, N instâncias |
Quando usar Prompts
✅ Use Prompt quando...
- Existe um fluxo repetível com parâmetros conhecidos
- Você quer consistência na forma como o modelo aborda uma tarefa
- O usuário invoca explicitamente (slash command, palette)
❌ Não use Prompt quando...
- A tarefa é ad hoc e imprevisível → deixe o modelo usar tools/resources livremente
- O fluxo depende de muitas variáveis dinâmicas → tools são mais flexíveis
Transporte: stdio vs HTTP
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 |
stdio — nosso caminho na POC
{
"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.
Streamable HTTP — evolução futura
Server expõe URL (https://api.exemplo.com/mcp); auth via header Authorization: Bearer. Indicado para produção compartilhada entre times.
Arquitetura: MCP não é o backend
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ê?
- Backend testável isoladamente (
curl, Postman) - MCP fino — só adaptação de protocolo
- Mesma API serve MCP, integrações externas e mobile
- Deploy independente (MCP local, backend remoto)
A spec MCP não prescreve como o server fala com sua API. REST, gRPC ou chamada in-process — escolha de implementação.
Evolução do nosso projeto: V1 → V2 → V3
| 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.
Decisões de domínio que impactam o MCP
Arquivamento (soft delete)
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 |
Atualização parcial
atualizar_curso aceita titulo e/ou cargaHoraria — pelo menos um obrigatório. Alinha com PATCH REST e evita reenviar dados desnecessários.
Notificações: subscribe e listChanged
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.
Checklist rápido para seu próximo MCP
Antes de implementar, pergunte:
- O que é leitura? → Resource com URI clara
- O que é ação? → Tool com schema validado
- O MCP fala direto com o banco? → Prefira não; use API intermediária
- stdio ou HTTP? → stdio para local; HTTP para remoto
- Preciso de notificações? → Comece sem; adicione se o refresh manual incomodar
- Tools de leitura coexistem com Resources? → Evite redundância
Referências
- Model Context Protocol — Documentação oficial
- Understanding MCP servers — Tools, Resources, Prompts
- Specification: Tools
- Specification: Resources
- Repositório de referência: projeto mcp-cursos — POC com V1 (mock), V2 (backend), V3 (resources)
Conclusão
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.























