mozak.tech Engenharia de IA Corporativa 18%

Parte I — Conectividade Padronizada entre Sistemas e Modelos

1.7 — Casos Reais: Proxy REST via Protocolo MCP (MCP REST Proxy)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar o padrão REST Proxy — MCP server que faz proxy de chamadas para APIs REST existentes

Mapear endpoints REST (GET, POST, PUT, DELETE) para tools MCP com schemas adequados

Estruturar responses de APIs REST para formato útil ao LLM (JSON formatado, contexto adicional)

Integrar um agente LLM com um server proxy para responder queries sobre dados de e-commerce

Escalar o padrão para qualquer API REST corporativa (CRM, ERP, helpdesk)

Por que isso importa

A maioria das empresas tem sistemas legados com APIs REST. Reescrevê-los não é opção. O padrão REST Proxy resolve isso: você cria um MCP server que atua como intermediário, traduzindo chamadas de tools MCP para HTTP requests para a API existente.

LLM → MCP Client → MCP Server (proxy) → HTTP → API REST existente → Banco

A beleza do padrão: a API REST não precisa mudar nada. O server MCP é uma camada fina de adaptação. E você escreve uma vez — todos os clientes LLM passam a ter acesso ao sistema existente.

Esse é o padrão usado pelos servers MCP oficiais da Anthropic para Slack, GitHub, e dezenas de outros serviços: são todos proxies que fazem fetch() para a API REST original.

Conceitos Fundamentais

O Padrão REST Proxy

// Tool MCP ↔ Endpoint REST

// ─────────────────────────────────────────────────────

// listar_produtos    ↔  GET  /produtos

// buscar_produto     ↔  GET  /produtos/:id

// listar_pedidos     ↔  GET  /pedidos

// atualizar_estoque  ↔  POST /estoque/atualizar

Cada tool MCP: 1. Recebe argumentos tipados via schema 2. Chama a API REST com os argumentos adequados (via fetch()) 3. Formata o response para LLM 4. Retorna texto/JSON

A função apiMock() no starter simula o fetch() real para evitar dependência de infraestrutura no lab. Em produção, substitua por:

const response = await fetch(`https://api.meusistema.com${endpoint}`, {

  method,

  headers: {

    'Authorization': `Bearer ${process.env.API_TOKEN}`,

    'Content-Type': 'application/json',

  },

  body: body ? JSON.stringify(body) : undefined,

})

const data = await response.json()

Mapeamento de Operações

Para um e-commerce com API REST:

Tool MCP

Método HTTP

Endpoint

Args

listar_produtos

GET

/produtos

categoria?

buscar_produto

GET

/produtos/:id

id

listar_pedidos

GET

/pedidos

atualizar_estoque

POST

/estoque/atualizar

produtoId, quantidade

Regra geral: - Operações de leitura sem modificação → GET, tool de listagem/busca - Criação → POST, tool de criação - Atualização parcial → PATCH, tool de update - Deleção → DELETE, tool de remoção (com aviso de irreversibilidade na description)

Enriquecimento de Responses

Quando você faz proxy de uma API REST, tem a oportunidade de enriquecer o response para o LLM:

// Response da API REST (dados brutos)

const produtos = await apiMock('/produtos', 'GET')



// Response enriquecido para LLM

return {

  content: [{

    type: 'text',

    text: JSON.stringify({

      total_produtos: produtos.length,

      produtos: produtos,

      alertas: {

        sem_estoque: produtos.filter(p => p.estoque === 0).map(p => p.nome),

        estoque_baixo: produtos.filter(p => p.estoque > 0 && p.estoque < 5).map(p => p.nome),

      }

    }, null, 2)

  }]

}

Isso economiza tool calls adicionais — o LLM já recebe o contexto que provavelmente vai precisar.

Aprofundamento Técnico

Tratamento de Erros de API

APIs REST retornam erros de múltiplas formas. Trate de forma robusta:

async function chamadaSegura(endpoint: string, method: string = 'GET', body?: unknown) {

  try {

    const dados = await apiMock(endpoint, method, body)



    // API retornou um objeto de erro interno

    if (typeof dados === 'object' && dados !== null && 'erro' in dados) {

      return { sucesso: false, erro: (dados as {erro: string}).erro }

    }



    return { sucesso: true, dados }

  } catch (err) {

    // Erro de rede ou exceção

    return { sucesso: false, erro: `Falha na chamada de API: ${err.message}` }

  }

}



// No tool handler:

const resultado = await chamadaSegura('/produtos')

if (!resultado.sucesso) {

  return {

    content: [{ type: 'text', text: `Erro ao buscar produtos: ${resultado.erro}` }],

    isError: true,

  }

}

Filtragem no MCP vs Filtragem na API

Você pode filtrar na API (enviando parâmetros de query) ou no MCP server (filtrando os resultados):

// Filtragem na API (preferível — menos dados na rede)

const endpoint = categoria ? `/produtos?categoria=${encodeURIComponent(categoria)}` : '/produtos'

const produtos = await apiMock(endpoint, 'GET')



// Filtragem no MCP server (fallback quando API não suporta filtros)

const todos = await apiMock('/produtos', 'GET') as Produto[]

const filtrados = categoria ? todos.filter(p => p.categoria === categoria) : todos

Para APIs com query parameters ricos, prefira a primeira abordagem. Para APIs legadas sem filtros, use a segunda.

Mutation Tools: Confirmar Antes de Executar

Tools que modificam estado (atualizar estoque, criar pedido, deletar) precisam de cuidado especial no design:

// Na description: deixe claro que é uma operação de escrita

{

  name: 'atualizar_estoque',

  description: 'MODIFICA: Atualiza a quantidade em estoque de um produto. Ação permanente. Use apenas quando o usuário confirmar explicitamente a atualização.',

  inputSchema: {

    type: 'object',

    properties: {

      produtoId: { type: 'string', description: 'ID do produto (ex: P001)' },

      quantidade: { type: 'number', description: 'Nova quantidade total em estoque (não incremento — valor absoluto)' },

    },

    required: ['produtoId', 'quantidade'],

  },

}

O aviso na description (MODIFICA:, Use apenas quando o usuário confirmar) instrui o LLM a ser mais cauteloso antes de chamar a tool.

Exemplos Anotados

Exemplo 1: Server E-Commerce Proxy 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()



// Simula API REST (em produção: fetch() real)

const PRODUTOS = [

  { id: 'P001', nome: 'Notebook Pro', preco: 5999.99, estoque: 15, categoria: 'eletrônicos' },

  { id: 'P002', nome: 'Mouse Ergonômico', preco: 299.90, estoque: 0, categoria: 'periféricos' },

  { id: 'P003', nome: 'Teclado Mecânico', preco: 499.90, estoque: 8, categoria: 'periféricos' },

]

const PEDIDOS = [

  { id: 'PED001', produtos: ['P001'], total: 5999.99, status: 'entregue' },

  { id: 'PED002', produtos: ['P002', 'P003'], total: 799.80, status: 'aguardando_pagamento' },

]



async function apiMock(endpoint: string, method = 'GET', body?: unknown): Promise<unknown> {

  await new Promise(r => setTimeout(r, 20))  // latência simulada

  if (endpoint === '/produtos') return PRODUTOS

  if (endpoint.startsWith('/produtos/')) return PRODUTOS.find(p => p.id === endpoint.split('/')[2]) ?? { erro: 'não encontrado' }

  if (endpoint === '/pedidos') return PEDIDOS

  if (endpoint === '/estoque/atualizar' && method === 'POST') {

    const { produtoId, quantidade } = body as { produtoId: string; quantidade: number }

    const p = PRODUTOS.find(p => p.id === produtoId)

    if (p) { p.estoque = quantidade; return { ok: true, estoque_atual: quantidade } }

    return { erro: 'Produto não encontrado' }

  }

  return { erro: `Endpoint não encontrado: ${method} ${endpoint}` }

}



function criarEcommerceServer(): Server {

  const server = new Server({ name: 'ecommerce', version: '1.0.0' }, { capabilities: { tools: {} } })



  server.setRequestHandler(ListToolsRequestSchema, async () => ({

    tools: [

      {

        name: 'listar_produtos',

        description: 'Lista produtos do catálogo. Inclui alertas de estoque (zero ou baixo).',

        inputSchema: {

          type: 'object',

          properties: {

            categoria: { type: 'string', description: 'Filtrar por categoria (opcional): eletrônicos, periféricos' },

          },

        },

      },

      {

        name: 'buscar_produto',

        description: 'Busca um produto específico por ID. Retorna preço, estoque e categoria.',

        inputSchema: {

          type: 'object',

          properties: { id: { type: 'string', description: 'ID do produto (ex: P001)' } },

          required: ['id'],

        },

      },

      {

        name: 'listar_pedidos',

        description: 'Lista todos os pedidos com status atual.',

        inputSchema: { type: 'object', properties: {} },

      },

      {

        name: 'atualizar_estoque',

        description: 'MODIFICA: Atualiza quantidade em estoque. Ação permanente — confirme com usuário antes.',

        inputSchema: {

          type: 'object',

          properties: {

            produtoId: { type: 'string' },

            quantidade: { type: 'number', minimum: 0 },

          },

          required: ['produtoId', 'quantidade'],

        },

      },

    ],

  }))



  server.setRequestHandler(CallToolRequestSchema, async (req) => {

    const { name, arguments: args } = req.params



    if (name === 'listar_produtos') {

      const { categoria } = (args ?? {}) as { categoria?: string }

      const todos = await apiMock('/produtos') as typeof PRODUTOS

      const filtrados = categoria ? todos.filter(p => p.categoria === categoria) : todos



      return {

        content: [{

          type: 'text',

          text: JSON.stringify({

            total: filtrados.length,

            produtos: filtrados,

            alertas: {

              sem_estoque: filtrados.filter(p => p.estoque === 0).map(p => `${p.id} (${p.nome})`),

              estoque_baixo: filtrados.filter(p => p.estoque > 0 && p.estoque < 5).map(p => p.id),

            }

          }, null, 2)

        }]

      }

    }



    if (name === 'buscar_produto') {

      const { id } = args as { id: string }

      const produto = await apiMock(`/produtos/${id}`)

      if ((produto as {erro?: string}).erro) {

        return { content: [{ type: 'text', text: `Produto ${id} não encontrado` }], isError: true }

      }

      return { content: [{ type: 'text', text: JSON.stringify(produto, null, 2) }] }

    }



    if (name === 'listar_pedidos') {

      const pedidos = await apiMock('/pedidos')

      return { content: [{ type: 'text', text: JSON.stringify(pedidos, null, 2) }] }

    }



    if (name === 'atualizar_estoque') {

      const { produtoId, quantidade } = args as { produtoId: string; quantidade: number }

      const resultado = await apiMock('/estoque/atualizar', 'POST', { produtoId, quantidade })

      return { content: [{ type: 'text', text: JSON.stringify(resultado, null, 2) }] }

    }



    throw new Error(`Tool desconhecida: ${name}`)

  })



  return server

}



// Agente e-commerce

async function consultarEcommerce(query: string): Promise<string> {

  const server = criarEcommerceServer()

  const client = new Client({ name: 'agente', 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 e-commerce. Responda em português.',

      tools, messages: mensagens,

    })

    mensagens.push({ role: 'assistant', content: r.content })

    if (r.stop_reason === 'end_turn') {

      return r.content.find(b => b.type === 'text')?.text ?? ''

    }

    const results: Anthropic.ToolResultBlockParam[] = []

    for (const b of r.content) {

      if (b.type !== 'tool_use') continue

      const resultado = await client.callTool({ name: b.name, arguments: b.input as Record<string, unknown> })

      results.push({ type: 'tool_result', tool_use_id: b.id, content: resultado.content.filter(c => c.type === 'text').map(c => c.text).join('') })

    }

    mensagens.push({ role: 'user', content: results })

  }

}



// Testes

const queries = [

  'Quais produtos estão sem estoque?',

  'Qual o valor total dos pedidos pendentes de pagamento?',

  'Preciso atualizar o estoque do Mouse Ergonômico para 25 unidades.',

]



for (const q of queries) {

  console.log(`\nQ: ${q}`)

  const r = await consultarEcommerce(q)

  console.log(`R: ${r}`)

}

Padrões e Armadilhas

Padrões

Padrão 1: Enriqueça o response com contexto derivado O LLM não precisa fazer mais tool calls se você já incluiu alertas, totais e resumos no primeiro response.

Padrão 2: Trate erros da API como isError: true no MCP

if ((resultado as {erro?: string}).erro) {

  return { content: [{ type: 'text', text: String(resultado.erro) }], isError: true }

}

Isso permite que o LLM se recupere (ex: listar IDs disponíveis) em vez de travar.

Padrão 3: Descrições de mutation tools incluem aviso explícito “MODIFICA:”, “IRREVERSÍVEL:”, “USE APENAS APÓS CONFIRMAÇÃO” — palavras-chave na description que tornam o LLM mais cauteloso.

Armadilhas

⚠️ Armadilha 1: fetch() sem timeout em produção

// PERIGO: request pode travar por tempo indeterminado

const data = await fetch(url)



// SEGURO: sempre use AbortController com timeout

const controller = new AbortController()

const timeout = setTimeout(() => controller.abort(), 10_000)  // 10s timeout

try {

  const data = await fetch(url, { signal: controller.signal })

} finally {

  clearTimeout(timeout)

}

⚠️ Armadilha 2: Não cachear responses de listagem Tools de listagem chamadas múltiplas vezes por turno desperdiçam chamadas de API. Cache em memória com TTL curto resolve:

const cache = new Map<string, {dados: unknown; expiresAt: number}>()

async function apiComCache(endpoint: string, ttlMs = 5000) {

  const cached = cache.get(endpoint)

  if (cached && Date.now() < cached.expiresAt) return cached.dados

  const dados = await apiMock(endpoint)

  cache.set(endpoint, { dados, expiresAt: Date.now() + ttlMs })

  return dados

}
⚗ 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 está completo — implementa o server e o agent loop. Não há TODOs de código.

A atividade é exploratória e de extensão:

Rode o starter com as queries de demo e observe como o agente encadeia as tool calls

Adicione um endpoint: implemente a tool buscar_pedido(pedidoId) que faz proxy para GET /pedidos/:id

Teste com queries complexas: “Quais produtos periféricos estão disponíveis e qual o valor total se eu comprar um de cada?”

Substitua apiMock por fetch() real: se você tiver acesso a alguma API REST real, conecte e veja o proxy funcionar com dados reais

Agora você está pronto para o lab.