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 → BancoA 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/atualizarCada 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) : todosPara 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
}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.