Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Projetar a exposição de sistemas corporativos (CRM, ERP, suporte) como MCP servers com tools bem definidas
Implementar um agent loop que usa múltiplas tools de negócio para responder queries complexas
Estruturar os dados de negócio retornados por tools para máxima utilidade ao LLM
Mapear operações de sistema (CRUD) para primitivas MCP (tools, resources)
Identificar quais operações devem ser tools vs resources em sistemas corporativos reais
Por que isso importa
Decisão de Arquitetura: Empresa como Plataforma MCP
Expor sistemas internos via MCP cria uma nova camada de integração que precisa de governo. Quatro decisões de arquitetura antes de implementar: (1) Catálogo de servers: onde estão registrados os MCP servers disponíveis? Quem pode descobri-los? (2) Contrato de schema: como versionar as tools (v1/v2)? O que acontece quando o schema muda e o agente foi treinado com o schema anterior? (3) Autorização por tool: quem pode chamar deletar_cliente? A autorização deve ser no nível da tool, não só do server. (4) Coexistência com API gateway: o MCP server é uma fachada sobre o mesmo backend que o API gateway já expõe? Se sim, não duplique a lógica de autenticação — delegue ao backend existente. Trate MCP servers com o mesmo rigor de versionamento e governança que APIs internas.
A premissa desta unidade é transformadora: cada sistema interno da sua empresa pode se tornar um servidor MCP, e com isso um agente LLM pode operar sobre dados reais de negócio.
Hoje em empresas, queries como “crie um ticket de suporte para o cliente ACME sobre o pedido #12345 e escale para vendas” exigem abrir 3 sistemas diferentes (CRM, helpdesk, Slack), copiar e colar informações entre eles, e gastar 10-15 minutos. Com um agente que tem acesso a MCPs do CRM, helpdesk e Slack, essa mesma query leva 30 segundos — o agente executa as três operações automaticamente.
Este é o modelo “empresa como plataforma MCP”: você expõe seus sistemas via MCP uma vez, e qualquer agente — hoje e no futuro — pode usar esses sistemas sem integrações adicionais. O investimento em escrever o MCP server é feito uma vez; o benefício se acumula indefinidamente.
Conceitos Fundamentais
O Mapeamento Sistema → MCP
Para cada sistema interno, você mapeia operações para tools MCP:
CRM (Customer Relationship Management):
GET /clientes/:id → tool 'get_cliente'
GET /clientes?busca=... → tool 'buscar_clientes'
PUT /clientes/:id → tool 'atualizar_cliente'
POST /clientes → tool 'criar_cliente'
GET /clientes/:id/historico → resource 'clientes://{id}/historico'Sistema de Suporte (Helpdesk):
GET /tickets?clienteId=... → tool 'listar_tickets'
POST /tickets → tool 'criar_ticket'
PUT /tickets/:id/status → tool 'atualizar_ticket'
POST /tickets/:id/escalar → tool 'escalar_ticket'ERP/Pedidos:
GET /pedidos/:id → tool 'buscar_pedido'
GET /pedidos?clienteId=... → tool 'listar_pedidos_cliente'
PUT /pedidos/:id/status → tool 'atualizar_status_pedido'A regra geral: - Operações que buscam dados com parâmetros variáveis: tools - Operações que modificam estado: tools (com isError em caso de falha) - Dados estáticos ou raramente modificados: resources (documentação, schemas, configurações)
Descrições de Tools para Agentes de Negócio
O LLM decide qual tool chamar baseado na description. Para sistemas corporativos, descriptions precisam ser ricas com contexto de negócio:
// POBRE — não ajuda o LLM a decidir quando usar
{ name: 'criar_ticket', description: 'Cria um ticket' }
// RICA — LLM sabe exatamente quando usar e quais campos esperar
{
name: 'criar_ticket',
description: `Abre um ticket de suporte para um cliente.
Use quando: cliente reportar problema, solicitar suporte técnico, ou precisar de escalonamento.
Campos: clienteId (obrigatório, ex: C001), assunto, prioridade ('baixa'|'media'|'alta'|'critica').
Retorna: ticket criado com ID gerado automaticamente.`,
}Agent Loop para Queries de Negócio
Queries de negócio geralmente requerem múltiplas tools em sequência. Por exemplo, “crie um ticket para o cliente ACME sobre o pedido #12345”:
1. get_cliente('ACME') → { id: 'C001', nome: 'ACME Corp', ... }
2. listar_pedidos('C001') → verifica que pedido #12345 existe
3. criar_ticket({ clienteId: 'C001', assunto: 'Pedido #12345', prioridade: 'alta' })
4. Claude formula resposta: "Ticket T002 criado para ACME Corp sobre pedido #12345"O LLM encadeia as tools automaticamente. Você não precisa hardcodar essa sequência — o agent loop while(stop_reason === 'tool_use') cuida disso.
Aprofundamento Técnico
Estruturando Respostas para LLMs
O LLM vai processar o texto que você retorna como content. Estruture para facilitar o processamento:
// MENOS ÚTIL: dados brutos que o LLM precisa parsear
return {
content: [{ type: 'text', text: '{"id":"C001","nome":"ACME","cnpj":"12.345.678/0001-99","plano":"Pro","status":"ativo","contato":"maria@acme.com"}' }]
}
// MAIS ÚTIL: JSON formatado com labels claros
return {
content: [{
type: 'text',
text: JSON.stringify({
cliente: {
id: 'C001',
nome: 'ACME Corp',
plano: 'Pro',
status: 'ativo',
contato_principal: 'maria@acme.com',
},
alertas: ['Tem ticket aberto de alta prioridade'],
}, null, 2)
}]
}O JSON formatado com 2 espaços de indentação é mais fácil para o LLM processar e para você debugar.
Adicionando Contexto de Negócio ao Tool Result
Em sistemas corporativos, o LLM beneficia de contexto além do dado puro:
// Ao retornar um cliente, inclua informações adicionais relevantes
if (name === 'get_cliente') {
const cliente = clientes.find(c => c.id === busca || c.nome.toLowerCase().includes(busca.toLowerCase()))
if (!cliente) {
return {
content: [{ type: 'text', text: `Cliente '${busca}' não encontrado. Clientes disponíveis: ${clientes.map(c => `${c.id} (${c.nome})`).join(', ')}` }],
isError: true,
}
}
// Enriquece o resultado com dados relacionados relevantes
const pedidosDoCliente = pedidos.filter(p => p.clienteId === cliente.id)
const ticketsAbertos = tickets.filter(t => t.clienteId === cliente.id && t.status === 'aberto')
return {
content: [{
type: 'text',
text: JSON.stringify({
...cliente,
resumo: {
total_pedidos: pedidosDoCliente.length,
valor_total_pedidos: pedidosDoCliente.reduce((sum, p) => sum + p.valor, 0),
tickets_abertos: ticketsAbertos.length,
}
}, null, 2)
}]
}
}Isso evita que o LLM precise fazer chamadas adicionais para obter contexto básico.
System Prompt para Agentes de Negócio
O system prompt define o contexto do agente. Para agentes corporativos:
const systemPrompt = `Você é um assistente de operações para a equipe de Customer Success.
Você tem acesso às seguintes ferramentas de negócio:
- get_cliente: buscar informações de clientes
- listar_pedidos: ver pedidos de um cliente
- criar_ticket: abrir tickets de suporte
- listar_tickets: ver tickets existentes
REGRAS:
1. Sempre confirme com o usuário antes de criar ou modificar dados
2. Quando criar um ticket, sempre inclua contexto suficiente no assunto
3. Prioridade 'critica' apenas para clientes Enterprise com SLA afetado
4. Para clientes 'churned', não crie tickets — redirecione para o time de Retenção
Sempre responda em português.`Exemplos Anotados
Exemplo 1: CRM MCP Server Completo
import Anthropic from '@anthropic-ai/sdk'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
const anthropic = new Anthropic()
// Dados mock de negócio
const clientes = [
{ id: 'C001', nome: 'ACME Corp', plano: 'Pro', status: 'ativo', contato: 'maria@acme.com' },
{ id: 'C002', nome: 'TechStart Ltda', plano: 'Starter', status: 'ativo', contato: 'joao@tech.com' },
]
const pedidos = [
{ id: 'P12345', clienteId: 'C001', produto: 'Plano Pro Anual', valor: 3588, status: 'ativo' },
]
let proximoTicketId = 1
const tickets: Array<{id: string; clienteId: string; assunto: string; status: string; prioridade: string}> = []
function criarCRMServer(): Server {
const server = new Server({ name: 'crm', version: '1.0.0' }, { capabilities: { tools: {} } })
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_cliente',
description: 'Busca cliente por ID (ex: C001) ou nome parcial. Retorna dados + resumo de pedidos e tickets.',
inputSchema: {
type: 'object',
properties: { busca: { type: 'string', description: 'ID ou nome do cliente' } },
required: ['busca'],
},
},
{
name: 'listar_pedidos',
description: 'Lista todos os pedidos de um cliente pelo clienteId',
inputSchema: {
type: 'object',
properties: { clienteId: { type: 'string' } },
required: ['clienteId'],
},
},
{
name: 'criar_ticket',
description: 'Cria ticket de suporte. Use para problemas técnicos, dúvidas, ou escalamentos.',
inputSchema: {
type: 'object',
properties: {
clienteId: { type: 'string' },
assunto: { type: 'string' },
prioridade: { type: 'string', enum: ['baixa', 'media', 'alta', 'critica'] },
},
required: ['clienteId', 'assunto', 'prioridade'],
},
},
],
}))
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params
if (name === 'get_cliente') {
const { busca } = args as { busca: string }
const cliente = clientes.find(c =>
c.id === busca || c.nome.toLowerCase().includes(busca.toLowerCase())
)
if (!cliente) {
return {
content: [{ type: 'text', text: `Cliente '${busca}' não encontrado. IDs disponíveis: ${clientes.map(c => c.id).join(', ')}` }],
isError: true,
}
}
const pedidosCliente = pedidos.filter(p => p.clienteId === cliente.id)
const ticketsCliente = tickets.filter(t => t.clienteId === cliente.id)
return {
content: [{
type: 'text',
text: JSON.stringify({
...cliente,
resumo: {
total_pedidos: pedidosCliente.length,
valor_total: pedidosCliente.reduce((s, p) => s + p.valor, 0),
tickets_ativos: ticketsCliente.filter(t => t.status === 'aberto').length,
}
}, null, 2)
}]
}
}
if (name === 'listar_pedidos') {
const { clienteId } = args as { clienteId: string }
const resultado = pedidos.filter(p => p.clienteId === clienteId)
return {
content: [{
type: 'text',
text: JSON.stringify({ clienteId, total: resultado.length, pedidos: resultado }, null, 2)
}]
}
}
if (name === 'criar_ticket') {
const { clienteId, assunto, prioridade } = args as { clienteId: string; assunto: string; prioridade: string }
const cliente = clientes.find(c => c.id === clienteId)
if (!cliente) return { content: [{ type: 'text', text: `Cliente ${clienteId} não encontrado` }], isError: true }
const ticket = {
id: `T${String(proximoTicketId++).padStart(3, '0')}`,
clienteId, assunto, prioridade, status: 'aberto',
}
tickets.push(ticket)
return { content: [{ type: 'text', text: JSON.stringify(ticket, null, 2) }] }
}
throw new Error(`Tool desconhecida: ${name}`)
})
return server
}
// Agent para queries de negócio
async function agenteCRM(query: string): Promise<string> {
const server = criarCRMServer()
const client = new Client({ name: 'agent', version: '1.0.0' }, { capabilities: {} })
const [ct, st] = InMemoryTransport.createLinkedPair()
await server.connect(st)
await client.connect(ct)
const { tools: mcpTools } = await client.listTools()
const tools: Anthropic.Tool[] = mcpTools.map(t => ({
name: t.name,
description: t.description ?? '',
input_schema: t.inputSchema as Anthropic.Tool['input_schema'],
}))
const mensagens: Anthropic.MessageParam[] = [{ role: 'user', content: query }]
while (true) {
const r = await anthropic.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 500,
system: 'Você é um assistente de Customer Success. Use as tools disponíveis para responder perguntas sobre clientes e pedidos.',
tools,
messages: mensagens,
})
mensagens.push({ role: 'assistant', content: r.content })
if (r.stop_reason === 'end_turn') {
const texto = r.content.find(b => b.type === 'text')
return texto?.type === 'text' ? texto.text : ''
}
const results: Anthropic.ToolResultBlockParam[] = []
for (const b of r.content) {
if (b.type !== 'tool_use') continue
console.log(` → ${b.name}(${JSON.stringify(b.input)})`)
const resultado = await client.callTool({ name: b.name, arguments: b.input as Record<string, unknown> })
const texto = resultado.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
console.log(` ← ${texto.slice(0, 100)}...`)
results.push({ type: 'tool_result', tool_use_id: b.id, content: texto })
}
mensagens.push({ role: 'user', content: results })
}
}
// Teste
agenteCRM('Busque o cliente ACME Corp e crie um ticket de alta prioridade sobre o pedido P12345 que está com problema de acesso.')
.then(console.log)
.catch(console.error)Padrões e Armadilhas
Padrões
Padrão 1: Incluir contexto de erro útil — sugira alternativas
// RUIM: erro genérico
return { content: [{ type: 'text', text: 'Cliente não encontrado' }], isError: true }
// BOM: inclui o que está disponível para ajudar o LLM a se recuperar
return {
content: [{
type: 'text',
text: `Cliente 'ACME' não encontrado. Clientes disponíveis: C001 (ACME Corp), C002 (TechStart)`
}],
isError: true,
}
// O LLM vê 'ACME Corp' e chama novamente com a busca corretaPadrão 2: Enriqueça resultados com resumo de dados relacionados Quando buscar um cliente, inclua automaticamente contagem de pedidos e tickets. Evita que o LLM precise fazer 2-3 chamadas adicionais para contexto básico.
Padrão 3: System prompt define as regras de negócio do agente
const system = `Regras:
- Cliente 'churned': não crie tickets, redirecione para Retenção
- Prioridade 'critica': apenas Enterprise com SLA afetado
- Confirme antes de criar/modificar dados`Armadilhas
⚠️ Armadilha 1: Tools que fazem muito
// ERRADO: tool única que busca cliente + pedidos + tickets
{ name: 'get_tudo_do_cliente', ... }
// CORRETO: tools separadas e composíveis
{ name: 'get_cliente', ... }
{ name: 'listar_pedidos', ... }
{ name: 'listar_tickets', ... }
// O LLM compõe as chamadas conforme necessário⚠️ Armadilha 2: Retornar dados sensíveis desnecessariamente
// PERIGO: retorna senha hash, token de API, dados PII completos
return { content: [{ type: 'text', text: JSON.stringify(clienteCompleto) }] }
// SEGURO: projete apenas os campos necessários
const { senha, api_key, ...dadosSeguroS } = cliente
return { content: [{ type: 'text', text: JSON.stringify(dadosSeguroS) }] }Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
O starter desta unidade é completo — o server CRM está implementado, assim como o agent loop. Não há TODOs de código.
A atividade é exploratória:
Rode o starter e observe o agente executar as tool calls em sequência para uma query complexa
Adicione uma nova tool: atualizar_status_ticket(ticketId, novoStatus) — siga o padrão da Unidade 04
Teste queries encadeadas: “Busque o cliente C001, liste seus pedidos, e crie um ticket de alta prioridade sobre o pedido mais recente”
Observe o raciocínio do Claude: adicione console.log(response.content) no loop para ver o pensamento intermediário do modelo
O objetivo é internalizar como o agente encadeia múltiplas tools de negócio automaticamente — sem que você defina a sequência explicitamente.
Agora você está pronto para o lab.