mozak.tech Engenharia de IA Corporativa 13%

Parte I — Conectividade Padronizada entre Sistemas e Modelos

1.5 — A Organização como Servidor MCP: Expondo Sistemas Internos a Modelos (Enterprise MCP)

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 correta

Padrã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) }] }
⚗ Laboratório prático — mozak.tech
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.