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 │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
Enter fullscreen mode Exit fullscreen mode
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"]
}
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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? ResourceExemplo 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 ✓
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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 dadoscriar_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ênciacursos://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 Fixocursos://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 installstdio — 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"
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
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"
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
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 + docsurl + 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.
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
답글 남기기