MCP na prática: Tools, Resources e quando usar cada um A developer building a Model Context Protocol (MCP) server for a course catalog learned to distinguish between Tools, Resources, and Prompts. The team refactored their V2 implementation, which incorrectly exposed read operations as Tools, into a V3 design where read-only data access uses Resources and write operations use Tools. This change eliminated redundancy and clarified the model's decision criteria. 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.