MCP na prática: Tools, Resources e quando usar cada um
Aprendizados de construir um servidor MCP de catálogo de cursos — da POC ao remote MCP.
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 ┌─────────────┐ in-process ┌─────────────┐
│ Cursor │ ◄──────────────► │ MCP Server │ ◄───────────────► │ Domínio │
│ (cliente) │ tools/call │ │ CoursesService│ SQLite │
│ │ resources/read │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
O MCP não substitui sua lógica de negócio. Ele é a camada de adaptação entre o modelo de linguagem e o mundo real — mas na V4 aprendemos que essa camada pode (e deve) ser fina, compartilhando o domínio com o backend.
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
Como usar um resource na prática
Resources não são “executados” como tools. O fluxo é:
- Cliente lista resources disponíveis (
resources/list) - Cliente lê o conteúdo (
resources/read) passando a URI - O conteúdo entra no contexto do modelo
No Cursor, basta pedir em linguagem natural:
"Leia o catálogo de cursos e me diga quantos existem."
O Cursor chama resources/read com cursos://catalogo internamente. Você não monta JSON-RPC manualmente.
Para debug, use o MCP Inspector:
npx -y @modelcontextprotocol/inspector
# Conecte em http://localhost:3000/mcp com Authorization: Bearer <API_KEY>
# Aba Resources → read cursos://catalogo
Erro comum: o modelo tentar chamar listar_cursos — tool que existia na V2 e foi removida na V3. Se isso acontecer, reconecte o MCP no Cursor e peça explicitamente para ler o resource.
Prompts — templates reutilizáveis
Prompts são modelos de instrução parametrizados que guiam o modelo por um fluxo conhecido. Diferente de tools (o modelo executa) e resources (o modelo lê), prompts são invocados pelo usuário.
criar-curso titulo="Arquitetura de Software" cargaHoraria=12
Na V3.1 adicionamos o prompt criar-curso, que monta um roteiro:
- Ler
cursos://catalogo - Verificar duplicatas
- Validar dados
- Chamar
criar_curso - Confirmar resultado
O prompt não cria o curso — orienta o modelo a usar resources e tools corretamente.
Prompt MCP vs Cursor Skill: prompts vivem no server MCP e funcionam em qualquer client compatível; skills vivem em .cursor/skills/ e são específicas do Cursor.
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
Prompts (V3.1):
criar-curso → roteiro guiado de criação
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. Na prática, vimos o erro -32602: Tool listar_cursos not found quando conversas antigas ou regras desatualizadas ainda referenciavam as tools removidas.
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)
- Quer portabilidade entre clients MCP (Cursor, Claude Desktop, etc.)
❌ 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
- Precisa de comportamento contínuo do agente → use Cursor Skill
Transporte: stdio vs Streamable 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 offline |
| Streamable HTTP | Server HTTP remoto; POST com JSON-RPC | Produção, múltiplos clientes, zero install |
stdio — dev local (V4)
Mantido na V4 para desenvolvimento offline. O processo MCP acessa SQLite diretamente — sem backend HTTP intermediário.
{
"mcpServers": {
"mcp-cursos": {
"command": "node",
"args": ["${workspaceFolder}/apps/mcp/dist/mcp.js"],
"env": {
"DATABASE_PATH": "${workspaceFolder}/data/cursos.db"
}
}
}
}
Vantagens: funciona offline, debug rápido, sem auth.
Limitação: processo local — cada dev precisa do repo e build.
Streamable HTTP — remote MCP (V4)
Na V4, o backend expõe MCP diretamente em POST /mcp. O Cursor conecta por URL — sem spawnar processo, sem npx, sem build local.
{
"mcpServers": {
"mcp-cursos": {
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer dev-api-key"
}
}
}
}
Vantagens: distribuição trivial (URL + API key), um deploy serve N clientes, Docker-ready.
Modo V4: stateless — cada request cria server + transport novos. Sessões stateful ficam para V5.
Auth: Authorization: Bearer <API_KEY> no header. Mesma chave do backend/Docker.
Arquitetura: a evolução do adaptador
V2 — MCP fino + REST intermediária
Cursor → apps/mcp (stdio) → HTTP REST → apps/backend → SQLite
Funcionou para aprender, mas tinha dois contratos (REST + MCP) e latência desnecessária.
V4 — domínio compartilhado, MCP como interface única
libs/courses-domain CoursesService + TypeORM + migrations
│
libs/mcp-server createMcpServer(courses)
│
├─ apps/mcp stdio + SQLite (dev)
└─ apps/backend Streamable HTTP /mcp + SQLite (produção)
Mudanças-chave na V4:
- REST
/mcp/v1/cursosremovida — MCP é a única interface pública -
libs/courses-domain— regras de negócio compartilhadas -
libs/mcp-server— registro de resources, tools e prompts em um lugar - stdio e remote têm paridade garantida — mesma lib, mesmo comportamento
Por quê removemos a REST?
- Dois contratos = dupla manutenção
- O adaptador MCP fazia HTTP para chamar o próprio backend — hop desnecessário
- Distribuição simplifica: usuário final precisa só de URL + API key
A spec MCP não prescreve como o server fala com seus dados. REST, gRPC ou in-process — escolha de implementação. Na V4, escolhemos in-process com lib compartilhada.
Distribuir seu MCP
Com Streamable HTTP, distribuir fica simples:
| Público | O que distribuir | Config do usuário |
|---|---|---|
| Devs / open source | Backend Docker + docs |
url + headers no mcp.json |
| Times internos | Imagem GHCR + API key por time | docker run + mcp.json |
| Usuários finais | Backend hosted por você | Só URL + chave |
Próximo passo (não implementado): publicar no npm com npx -y @org/mcp-cursos para stdio, e registrar no MCP Registry para descoberta.
stdio vs remote para distribuição: remote vence — o usuário cola 10 linhas de JSON e funciona. stdio exige Node, clone e build.
Evolução do nosso projeto: V1 → V4
| Versão | Transporte | Leitura | Escrita | Prompts | Persistência |
|---|---|---|---|---|---|
| V1 | stdio | Tools (listar, buscar) |
Tool (criar) |
— | Mock in-memory |
| V2 | stdio → REST | Tools (listar, buscar) |
Tool (criar) |
— | SQLite + backend HTTP |
| V3 | stdio → REST |
Resources (cursos://…) |
Tools (criar, atualizar, arquivar) |
— | SQLite + soft delete |
| V3.1 | stdio → REST | Resources | Tools | criar-curso |
— |
| V4 | Streamable HTTP + stdio dev | Resources | Tools | criar-curso |
Domínio compartilhado, REST removida |
Cada versão ensinou algo:
- V1→V2: persistência real exige backend separado
- V2→V3: leitura ≠ tool; resources tornam o server previsível
- V3→V3.1: prompts guiam fluxos repetíveis sem duplicar tools
- V3→V4: REST intermediária era overhead; remote MCP elimina install local
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 |
arquivar_curso |
Tool — ação explícita de domínio |
Atualização parcial
atualizar_curso aceita titulo e/ou cargaHoraria — pelo menos um obrigatório. 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 V4): 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 + sessões stateful (V5) fazem 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
- Existe fluxo repetível? → Prompt parametrizado
- O MCP fala direto com o banco? → Prefira lib de domínio compartilhada; evite REST intermediária se MCP é a única interface
- stdio ou HTTP? → stdio para dev offline; Streamable HTTP para distribuição
- Preciso de notificações? → Comece sem; adicione com sessões stateful se necessário
- Tools de leitura coexistem com Resources? → Evite redundância — causa confusão no modelo
- Como distribuir? → Remote MCP (URL + auth) > npm + stdio > clone + build
Referências
- Model Context Protocol — Documentação oficial
- Understanding MCP servers — Tools, Resources, Prompts
- Specification: Tools
- Specification: Resources
- MCP Registry — catálogo oficial de servers
- Repositório de referência: projeto mcp-cursos — tags
V1,v2,v3,v3.1,v4 - ADRs:
docs/adr/— decisões de arquitetura documentadas
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ê.
A jornada do mcp-cursos resumida:
- Comece simples — stdio, poucas tools, mock ou API mínima
- Separe leitura de escrita — resources para consulta, tools para ação
- Adicione prompts — roteiros para fluxos repetíveis
- Vá para remote — Streamable HTTP elimina fricção de distribuição
- Compartilhe o domínio — uma lib, paridade entre stdio e remote
Top comments (0)