mozak.tech Engenharia de IA Corporativa 8%

Parte I — Conectividade Padronizada entre Sistemas e Modelos

1.3 — Protocolo MCP versus Chamada Direta de Ferramentas: Critérios de Decisão (MCP vs Function Calling)

Objetivo da Aula

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

Implementar a mesma funcionalidade via tool use direto e via MCP, comparando a estrutura de cada abordagem

Avaliar trade-offs concretos de overhead, reusabilidade, latência e manutenibilidade entre as duas abordagens

Decidir qual abordagem usar com base em critérios objetivos: escopo, maturidade, multi-cliente

Migrar código de tool use direto para MCP quando o caso justifica a mudança

Identificar os pontos onde as duas abordagens convergem (o agent loop é idêntico)

Por que isso importa

Nem todo projeto precisa de MCP. Para uma automação rápida que você vai usar uma vez, implementar um MCP server é overhead desnecessário. Para um sistema de integração que três times diferentes vão consumir via Claude Desktop, Cursor e uma API interna, MCP elimina triplicação de código.

A armadilha mais comum: usar MCP para tudo por ser “mais profissional”, sem calcular o custo real de setup. A segunda armadilha: manter tool use direto indefinidamente quando o sistema claramente evoluiu para caso de uso multi-cliente.

Esta unidade te dá a régua para medir cada caso.

Conceitos Fundamentais

Tool Use Direto: A Abordagem Acoplada

No tool use direto, a definição e execução da ferramenta vivem no mesmo código que chama o LLM:

// TUDO no mesmo lugar:



// 1. Definição da tool (schema)

const tools: Anthropic.Tool[] = [{

  name: 'buscar_acao',

  description: 'Busca cotação da B3',

  input_schema: {

    type: 'object' as const,

    properties: { ticker: { type: 'string' } },

    required: ['ticker'],

  }

}]



// 2. Execução da tool (lógica de negócio)

function executarTool(nome: string, args: Record<string, unknown>): string {

  if (nome === 'buscar_acao') {

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

    return JSON.stringify(getAcaoMock(ticker))

  }

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

}



// 3. Agent loop

async function perguntarDireto(pergunta: string) {

  const mensagens = [{ role: 'user' as const, content: pergunta }]

  while (true) {

    const r = await client.messages.create({ model, max_tokens: 300, 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

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

      for (const b of r.content) {

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

        const resultado = executarTool(b.name, b.input as Record<string, unknown>)

        mensagens.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: b.id, content: resultado }] })

      }

    }

  }

}

Vantagens: - Zero overhead de protocolo - Zero configuração de servidor ou transports - Código mais simples — lê-se de cima para baixo - Deploy junto com a aplicação — sem processo separado para gerenciar

Desvantagens: - Acoplado ao código da aplicação — para usar em outra app, copia o código - Específico para Anthropic — migrar para OpenAI exige reescrever o schema - Não é reutilizável por outros clientes (Claude Desktop, Cursor, etc.) - executarTool() cresce indefinidamente com mais tools — switch gigante

MCP: A Abordagem Desacoplada

No MCP, o server (ferramentas + lógica) é separado do client (LLM + agent loop):

// SERVER (processo separado ou in-process):

// Contém: definição de tools + lógica de execução

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

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...] }))

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

  // lógica de execução aqui

})



// CLIENT (na aplicação):

// Contém: agent loop + conversão de schema

const { tools: mcpTools } = await mcpClient.listTools()

const anthropicTools = mcpTools.map(t => ({ ...t, input_schema: t.inputSchema }))

// ... agent loop igual ao tool use direto

Vantagens: - Server reutilizável por qualquer cliente MCP - Muda de modelo LLM? Só muda o client — server continua igual - Equipe separada pode manter o server - Deploy independente — server pode ter ciclo de vida diferente do client - Scaling independente: server de ações B3 pode ter cache, rate limiting próprio

Desvantagens: - Mais arquivos para gerenciar - Setup inicial maior - Overhead de serialização JSON-RPC (mínimo, mas existe) - Debugging mais complexo — dois processos, dois pontos de falha

A Tabela de Decisão

Critério

Tool Use Direto

MCP

Uma aplicação usa a ferramenta

✓ melhor

Múltiplas aplicações usam

✓ melhor

Prototipagem / script único

✓ melhor

Sistema de produção de longa duração

✓ melhor

Equipe única

✓ ok

✓ ok

Equipes separadas

✓ melhor

Apenas Anthropic como LLM

✓ ok

✓ ok

Multi-LLM (Anthropic + OpenAI + …)

✓ melhor

Claude Desktop / Cursor como cliente

✓ único

Latência crítica (<10ms overhead importa)

✓ melhor

< 5 tools simples

✓ melhor

> 10 tools complexas

✓ melhor

O Que É Idêntico nas Duas Abordagens

O agent loop fundamental — while(stop_reason === 'tool_use') { callTool() } — é idêntico. A única diferença é onde callTool() busca a lógica:

Tool Use Direto: callTool() → executarTool() local → lógica inline

MCP:             callTool() → mcpClient.callTool() → server remoto → lógica no server

Se você entende o agent loop (Unidade 02), você entende as duas abordagens. MCP apenas move a lógica de execução para um processo separado.

Aprofundamento Técnico

Abordagem MCP via InMemoryTransport

Para o lab, usamos InMemoryTransport para não precisar de processo separado. A estrutura é a mesma de produção, só o transport muda:

// Cria server de ações

function criarAcoesServer(): Server {

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



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

    tools: [{

      name: 'buscar_acao',

      description: 'Busca cotação de ação da B3 por ticker',

      inputSchema: {

        type: 'object',

        properties: {

          ticker: { type: 'string', description: 'Código da ação (PETR4, VALE3, etc.)' },

        },

        required: ['ticker'],

      },

    }],

  }))



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

    const { ticker } = req.params.arguments as { ticker: string }

    const acao = getAcaoMock(ticker.toUpperCase())



    if (!acao) {

      return { content: [{ type: 'text', text: `Ação ${ticker} não encontrada` }], isError: true }

    }



    return {

      content: [{

        type: 'text',

        text: JSON.stringify({

          ticker: ticker.toUpperCase(),

          nome: acao.nome,

          preco: acao.preco,

          variacao_dia: `${acao.variacao_dia}%`,

        })

      }]

    }

  })



  return server

}



// Configura client conectado ao server

async function criarClientMCP(): Promise<Client> {

  const server = criarAcoesServer()

  const client = new Client({ name: 'client-acoes', version: '1.0.0' }, { capabilities: {} })

  const [clientT, serverT] = InMemoryTransport.createLinkedPair()

  await server.connect(serverT)

  await client.connect(clientT)

  return client

}



// Agent loop via MCP

async function perguntarViaMCP(pergunta: string): Promise<string> {

  const mcpClient = await criarClientMCP()

  const { tools: mcpTools } = await mcpClient.listTools()



  const anthropicTools: 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: pergunta }]



  while (true) {

    const response = await anthropic.messages.create({

      model: 'claude-haiku-4-5-20251001',

      max_tokens: 300,

      tools: anthropicTools,

      messages: mensagens,

    })



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



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

      const texto = response.content.find(b => b.type === 'text')

      return texto?.type === 'text' ? texto.text : ''

    }



    if (response.stop_reason === 'tool_use') {

      const results: Anthropic.ToolResultBlockParam[] = []

      for (const b of response.content) {

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

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

        results.push({

          type: 'tool_result',

          tool_use_id: b.id,

          content: result.content.filter(c => c.type === 'text').map(c => c.text).join('\n'),

        })

      }

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

    }

  }

}

Benchmark: Medindo o Overhead do MCP

Para verificar o overhead real do InMemoryTransport:

async function benchmark(pergunta: string, iteracoes: number = 5) {

  console.log(`\nBenchmark: "${pergunta}" (${iteracoes} iterações)\n`)



  // Tool use direto

  const inicioDir = Date.now()

  for (let i = 0; i < iteracoes; i++) await perguntarDireto(pergunta)

  const tempoDir = (Date.now() - inicioDir) / iteracoes



  // MCP

  const inicioMCP = Date.now()

  for (let i = 0; i < iteracoes; i++) await perguntarViaMCP(pergunta)

  const tempoMCP = (Date.now() - inicioMCP) / iteracoes



  console.log(`Tool Use Direto: ${tempoDir.toFixed(0)}ms avg`)

  console.log(`MCP (InMemory):  ${tempoMCP.toFixed(0)}ms avg`)

  console.log(`Overhead MCP:    ${(tempoMCP - tempoDir).toFixed(0)}ms`)

  // Resultado esperado: overhead < 5ms (serialização JSON in-process)

}

O overhead do InMemoryTransport é sub-milissegundo. Para HTTP/SSE, adicione a latência de rede (~5-50ms para localhost).

Exemplos Anotados

Exemplo 1: Mesma Query, Duas Abordagens

const PERGUNTA = 'Como está a VALE3 hoje? Ela está acima ou abaixo de R$ 65?'



// Abordagem 1: Tool Use Direto

// Pros: código mais curto, zero setup

// Cons: lógica de ação misturada com lógica de agente

async function abordagem1() {

  const tools: Anthropic.Tool[] = [{

    name: 'buscar_acao',

    description: 'Busca cotação da B3',

    input_schema: {

      type: 'object' as const,

      properties: { ticker: { type: 'string' } },

      required: ['ticker'],

    }

  }]



  const mensagens: Anthropic.MessageParam[] = [{ role: 'user', content: PERGUNTA }]



  while (true) {

    const r = await anthropic.messages.create({

      model: 'claude-haiku-4-5-20251001', max_tokens: 300, tools, messages: mensagens

    })

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



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

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

        ? r.content.find(b => b.type === 'text')!.text  // as string — verificado

        : ''

    }



    for (const b of r.content) {

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

      const { ticker } = b.input as { ticker: string }

      const acao = getAcaoMock(ticker)

      mensagens.push({

        role: 'user',

        content: [{

          type: 'tool_result',

          tool_use_id: b.id,

          content: JSON.stringify(acao ?? { erro: 'não encontrado' })

        }]

      })

    }

  }

}



// Abordagem 2: MCP

// Pros: server reutilizável, agnóstico de LLM

// Cons: mais setup, dois componentes

async function abordagem2() {

  const client = await criarClientMCP()  // server criado internamente

  return perguntarViaMCP(PERGUNTA)  // usa o client MCP

}



// Resultado das duas abordagens deve ser idêntico

const [r1, r2] = await Promise.all([abordagem1(), abordagem2()])

console.log('Direto:', r1)

console.log('MCP:   ', r2)

// Texto diferente (LLM não é determinístico), mas semanticamente equivalente

Padrões e Armadilhas

Padrões

Padrão 1: Comece com tool use direto, migre para MCP quando o sistema crescer Para < 5 tools em uma aplicação: tool use direto. Quando você se pega copiando código de tools entre projetos ou quando outro time pede acesso à mesma ferramenta: migre para MCP. A migração é estrutural, não conceptual — o agent loop não muda.

Padrão 2: Extraia executarTool() para arquivo separado antes de migrar para MCP

// tools/acoes.ts — extrai antes de migrar

export function executarBuscarAcao(ticker: string): string { ... }



// app.ts

import { executarBuscarAcao } from './tools/acoes'

function executarTool(nome: string, args: ...) {

  if (nome === 'buscar_acao') return executarBuscarAcao(args.ticker)

}



// Quando migrar para MCP: move executarBuscarAcao para o server

// O server.setRequestHandler(CallToolRequestSchema) chama a mesma função

Armadilhas

⚠️ Armadilha 1: MCP para script de uso único

// OVERKILL: MCP para um script que roda uma vez

// 50 linhas de setup para salvar 10 linhas de código



// CERTO: tool use direto é suficiente

⚠️ Armadilha 2: Tool use direto quando há 3+ consumers

// PROBLEMA: mesmo código de ferramenta copiado em 3 projetos

// Quando o endpoint da API mudar, você atualiza em 3 lugares



// CORRETO: MCP server centralizado, 3 clients apontam para o mesmo server

⚠️ Armadilha 3: Comparar latência de InMemoryTransport com produção

InMemoryTransport: <1ms overhead

stdio local: 1-5ms overhead

HTTP localhost: 5-20ms overhead

HTTP remoto: 50-200ms overhead

Se latência importa, teste com o transport que você usará em produção.

⚗ 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 implementa a Abordagem 1 (tool use direto) completamente e deixa um TODO para a Abordagem 2 (MCP):

TODO — Em abordagemMCP(): implemente o mesmo agent loop usando MCP client + server. A função criarAcoesServer() já existe no starter — você precisa: (1) criar o client, (2) conectar via InMemoryTransport, (3) listar tools e converter para formato Anthropic, (4) rodar o agent loop. O loop é idêntico ao da Abordagem 1 — só muda como você executa a tool: mcpClient.callTool() em vez de executarTool() local. Seção de referência: Aprofundamento Técnico → “Abordagem MCP via InMemoryTransport”.

Após implementar, o main() roda a mesma query nas duas abordagens e compara respostas e latência.

Agora você está pronto para o lab.