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

> Source: <https://dev.to/higorae/mcp-na-pratica-tools-resources-e-quando-usar-cada-um-13aa>
> Published: 2026-06-17 18:03:32+00:00

*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.*
