DEV Community

Higor Morais
Higor Morais

Posted on

MCP na prática: Tools, Resources e quando usar cada um

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: true para 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 é:

  1. Cliente lista resources disponíveis (resources/list)
  2. Cliente lê o conteúdo (resources/read) passando a URI
  3. 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:

  1. Ler cursos://catalogo
  2. Verificar duplicatas
  3. Validar dados
  4. Chamar criar_curso
  5. 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
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 ✓
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 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"
      }
    }
  }
}
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/cursos removida — 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, arquivamosarquivado: 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:

  1. O que é leitura? → Resource com URI clara
  2. O que é ação? → Tool com schema validado
  3. Existe fluxo repetível? → Prompt parametrizado
  4. O MCP fala direto com o banco? → Prefira lib de domínio compartilhada; evite REST intermediária se MCP é a única interface
  5. stdio ou HTTP? → stdio para dev offline; Streamable HTTP para distribuição
  6. Preciso de notificações? → Comece sem; adicione com sessões stateful se necessário
  7. Tools de leitura coexistem com Resources? → Evite redundância — causa confusão no modelo
  8. Como distribuir? → Remote MCP (URL + auth) > npm + stdio > clone + build

Referências


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:

  1. Comece simples — stdio, poucas tools, mock ou API mínima
  2. Separe leitura de escrita — resources para consulta, tools para ação
  3. Adicione prompts — roteiros para fluxos repetíveis
  4. Vá para remote — Streamable HTTP elimina fricção de distribuição
  5. Compartilhe o domínio — uma lib, paridade entre stdio e remote

Top comments (0)