mozak.tech Engenharia de IA Corporativa 5%

Parte I — Conectividade Padronizada entre Sistemas e Modelos

1.2 — Clientes MCP, Laços de Ferramentas e Transporte em Memória (MCP Clients)

Objetivo da Aula

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

Conectar um MCP client a um server usando InMemoryTransport para testes locais

Implementar o agent loop completo: listar tools → decidir → chamar → retornar resultado → Claude formata

Converter tools do formato MCP (inputSchema) para o formato Anthropic (input_schema)

Debugar problemas de conectividade e execução de tools em pipelines MCP

Entender o fluxo de decisão do LLM para chamada de tools (stop_reason === ‘tool_use’)

Por que isso importa

O MCP resolve a padronização de servers, mas o agent loop — a lógica de chamar tools repetidamente até o LLM ter informação suficiente para responder — continua sendo responsabilidade do código cliente.

Entender e implementar esse loop é a habilidade mais importante do módulo. Todo agente que usa ferramentas — seja com MCP, tool use direto, ou LangChain — implementa alguma variação desse padrão:

1. Envia pergunta ao LLM com tools disponíveis

2. LLM decide: tenho informação suficiente?

   - Sim → gera resposta final (stop_reason: 'end_turn')

   - Não → chama uma tool (stop_reason: 'tool_use')

3. Se tool_use: executa a tool, adiciona resultado ao histórico

4. Volta ao passo 1 com o histórico atualizado

Essa sequência de “pergunta → decisão → execução → nova pergunta” com o mesmo LLM é o coração de qualquer sistema agêntico. Dominar isso aqui, com o MCP adicionando uma camada de transporte, te prepara para os agentes mais complexos dos próximos módulos.

Conceitos Fundamentais

InMemoryTransport: O Transport de Desenvolvimento

Em produção, client e server são processos separados (stdio ou HTTP). Para desenvolvimento e testes, InMemoryTransport coloca client e server no mesmo processo com comunicação em memória — sem latência de rede, sem configuração de porta, sem spawn de processos.

import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'



// Cria um par de transports conectados — comunicação bidirecional em memória

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()



// Server usa serverTransport

await server.connect(serverTransport)



// Client usa clientTransport

await client.connect(clientTransport)



// Agora client e server estão conectados e podem trocar mensagens

createLinkedPair() retorna um par onde mensagens enviadas por um lado chegam no outro. É como dois pontos de uma fila de mensagens em memória.

Discovery de Tools: listTools()

Antes de poder chamar qualquer tool, o client precisa saber quais tools o server expõe. O método client.listTools() faz o request tools/list ao server e retorna a lista de tools:

const { tools } = await client.listTools()



// Cada tool tem:

// tools[0].name          → 'calcular'

// tools[0].description   → 'Calcula expressões matemáticas simples'

// tools[0].inputSchema   → { type: 'object', properties: { ... }, required: [...] }

Conversão de Formato: MCP → Anthropic

O SDK MCP usa inputSchema (camelCase). A API da Anthropic usa input_schema (snake_case). Você precisa converter:

// Formato MCP (do server):

{

  name: 'calcular',

  description: '...',

  inputSchema: {           // ← camelCase

    type: 'object',

    properties: { operacao: { type: 'string' }, a: { type: 'number' } },

  }

}



// Formato Anthropic (para o SDK):

{

  name: 'calcular',

  description: '...',

  input_schema: {          // ← snake_case

    type: 'object',

    properties: { operacao: { type: 'string' }, a: { type: 'number' } },

  }

}

A conversão é simples:

function converterParaAnthropic(tools: MCPTool[]): Anthropic.Tool[] {

  return tools.map(tool => ({

    name: tool.name,

    description: tool.description ?? '',

    input_schema: tool.inputSchema as Anthropic.Tool['input_schema'],

  }))

}

O Agent Loop Completo

Este é o pattern central. Anote cada passo:

async function rodarAgente(pergunta: string, tools: Anthropic.Tool[], mcpClient: Client): Promise<string> {

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



  while (true) {

    // 1. Envia ao Claude com as tools disponíveis

    const response = await anthropic.messages.create({

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

      max_tokens: 500,

      tools,

      messages: mensagens,

    })



    // 2. Adiciona resposta do assistente ao histórico

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



    // 3. Verifica por que o LLM parou

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

      // LLM terminou — extrai o texto da resposta final

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

      return textoBlock?.type === 'text' ? textoBlock.text : 'Sem resposta'

    }



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

      // LLM quer chamar uma ou mais tools

      const toolResults: Anthropic.ToolResultBlockParam[] = []



      // Processa cada tool_use block

      for (const block of response.content) {

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



        // 4. Chama a tool via MCP client

        const resultado = await mcpClient.callTool({

          name: block.name,

          arguments: block.input as Record<string, unknown>,

        })



        // 5. Extrai texto do resultado MCP

        const textoResultado = resultado.content

          .filter(c => c.type === 'text')

          .map(c => c.text)

          .join('\n')



        toolResults.push({

          type: 'tool_result',

          tool_use_id: block.id,

          content: textoResultado,

        })

      }



      // 6. Adiciona os resultados das tools ao histórico e continua o loop

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

    }

  }

}

Por que o loop? Um LLM pode decidir chamar múltiplas tools sequencialmente. Por exemplo: “Qual a variação da PETR4 em relação à VALE3?” pode gerar: 1. buscar_acao('PETR4') → resultado 2. buscar_acao('VALE3') → resultado 3. Claude calcula e responde com o texto final

Cada tool call é um turno no loop. O loop termina quando stop_reason !== 'tool_use'.

Aprofundamento Técnico

Por Que Adicionar response.content Ao Histórico

Um detalhe crucial do agent loop que confunde iniciantes:

// Quando Claude quer chamar uma tool, response.content tem DOIS blocos:

// [0]: { type: 'text', text: 'Deixa eu verificar o câmbio...' }  (pensamento do Claude)

// [1]: { type: 'tool_use', id: '...', name: 'historico_cambio', input: {...} }



// Você DEVE adicionar TODOS os blocos ao histórico:

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



// Se você adicionar só o tool_use e omitir o texto:

mensagens.push({ role: 'assistant', content: [response.content[1]] })  // ERRADO!

// → Erro da API: mensagem inválida

A API valida que o histórico é consistente. Se Claude disse “Deixa eu verificar…” (texto) e depois chamou uma tool, você precisa incluir o texto E a tool_use no histórico — senão parece que a tool_use veio do nada.

Extraindo Texto de Resultado MCP

O resultado de client.callTool() é:

{

  content: [

    { type: 'text', text: '...' },   // texto normal

    // ou

    { type: 'image', data: '...', mimeType: 'image/png' },  // imagem (menos comum)

  ],

  isError: false  // true se a tool retornou erro

}

Para extrair texto de forma robusta:

const textoResultado = resultado.content

  .filter((c): c is { type: 'text'; text: string } => c.type === 'text')

  .map(c => c.text)

  .join('\n')



// Verifica erro

if (resultado.isError) {

  console.error('Tool retornou erro:', textoResultado)

}

Tratando max_tokens no Loop

Se Claude atingir max_tokens no meio de uma chamada de tool, stop_reason será 'max_tokens', não 'tool_use'. O loop deve tratar isso:

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

  // Truncado antes de terminar

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

  return `[RESPOSTA TRUNCADA] ${parcial?.type === 'text' ? parcial.text : ''}`

}

Para evitar truncamento, use max_tokens adequado. Para respostas com tool calls, 500-1000 tokens é geralmente suficiente. Para respostas finais longas, 2000+.

Exemplos Anotados

Exemplo 1: Setup Completo Client-Server

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()



// === 1. CRIAR O SERVER ===

function criarServer(): Server {

  const server = new Server(

    { name: 'calculadora', version: '1.0.0' },

    { capabilities: { tools: {} } }

  )



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

    tools: [{

      name: 'calcular',

      description: 'Calcula operações matemáticas: soma, subtração, multiplicação, divisão',

      inputSchema: {

        type: 'object',

        properties: {

          operacao: { type: 'string', enum: ['soma', 'subtracao', 'multiplicacao', 'divisao'] },

          a: { type: 'number', description: 'Primeiro número' },

          b: { type: 'number', description: 'Segundo número' },

        },

        required: ['operacao', 'a', 'b'],

      },

    }],

  }))



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

    const { operacao, a, b } = req.params.arguments as { operacao: string; a: number; b: number }



    let resultado: number

    switch (operacao) {

      case 'soma': resultado = a + b; break

      case 'subtracao': resultado = a - b; break

      case 'multiplicacao': resultado = a * b; break

      case 'divisao':

        if (b === 0) return { content: [{ type: 'text', text: 'Erro: divisão por zero' }], isError: true }

        resultado = a / b

        break

      default:

        return { content: [{ type: 'text', text: `Operação desconhecida: ${operacao}` }], isError: true }

    }



    return {

      content: [{ type: 'text', text: `${a} ${operacao} ${b} = ${resultado}` }],

    }

  })



  return server

}



// === 2. SETUP DO CLIENT ===

async function setup(): Promise<Client> {

  const server = criarServer()

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



  // TODO 1: Conectar via InMemoryTransport

  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()

  await server.connect(serverTransport)

  await client.connect(clientTransport)



  return client

}



// === 3. AGENT LOOP ===

async function perguntar(pergunta: string, client: Client): Promise<string> {

  // TODO 2: Listar tools e converter para formato Anthropic

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

  const anthropicTools: Anthropic.Tool[] = mcpTools.map(tool => ({

    name: tool.name,

    description: tool.description ?? '',

    input_schema: tool.inputSchema as Anthropic.Tool['input_schema'],

  }))



  // TODO 3: Agent loop

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



  while (true) {

    const response = await anthropic.messages.create({

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

      max_tokens: 500,

      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 toolResults: Anthropic.ToolResultBlockParam[] = []



      for (const block of response.content) {

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



        console.log(`  → Chamando tool: ${block.name}(${JSON.stringify(block.input)})`)



        const resultado = await client.callTool({

          name: block.name,

          arguments: block.input as Record<string, unknown>,

        })



        const texto = resultado.content

          .filter(c => c.type === 'text')

          .map(c => c.text)

          .join('\n')



        console.log(`  ← Resultado: ${texto}`)



        toolResults.push({

          type: 'tool_result',

          tool_use_id: block.id,

          content: texto,

        })

      }



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

    }

  }

}



// === 4. MAIN ===

async function main() {

  const client = await setup()



  const perguntas = [

    'Quanto é 15 * 7?',

    'Calcule 100 dividido por 4 e depois some 25',

  ]



  for (const p of perguntas) {

    console.log(`\nPergunta: ${p}`)

    const resposta = await perguntar(p, client)

    console.log(`Resposta: ${resposta}`)

  }

}



main().catch(console.error)

Padrões e Armadilhas

Padrões

Padrão 1: Sempre incluir response.content completo no histórico Mesmo quando stop_reason === 'tool_use', o Claude pode ter gerado texto antes de chamar a tool. Adicionar só o tool_use block ao histórico causará erro da API.

Padrão 2: Processar múltiplos tool_use blocks num único turno Claude pode decidir chamar múltiplas tools simultaneamente (parallelismo):

// ERRADO: processa só a primeira tool_use

const primeiraToolUse = response.content.find(b => b.type === 'tool_use')

if (primeiraToolUse) { ... }



// CORRETO: processa todas

for (const block of response.content) {

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

  // processa este block

}

Padrão 3: Timeout no agent loop para prevenir loops infinitos

let iteracoes = 0

while (true) {

  if (++iteracoes > 10) throw new Error('Loop de agente excedeu 10 iterações')

  // ... resto do loop

}

Armadilhas

⚠️ Armadilha 1: Esquecer o await em client.connect()

client.connect(clientTransport)  // sem await — conexão não estabelecida antes de usar

await client.listTools()         // falha: não conectado



// CORRETO:

await client.connect(clientTransport)

await client.listTools()  // agora funciona

⚠️ Armadilha 2: inputSchema vs input_schema

// SDK MCP retorna inputSchema (camelCase)

const tool = (await client.listTools()).tools[0]

tool.inputSchema  // ✓ existe



// Anthropic espera input_schema (snake_case)

const anthropicTool: Anthropic.Tool = {

  name: tool.name,

  description: tool.description ?? '',

  input_schema: tool.inputSchema  // ← conversão necessária

}

⚠️ Armadilha 3: Tool result com formato errado

// ERRADO: content como string simples

toolResults.push({

  type: 'tool_result',

  tool_use_id: block.id,

  content: 'resultado aqui',  // string funciona mas pode ser ambíguo

})



// MELHOR: content como array de content blocks (mais explícito)

toolResults.push({

  type: 'tool_result',

  tool_use_id: block.id,

  content: [{ type: 'text', text: 'resultado aqui' }],

})
⚗ 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 tem 3 TODOs que completam a integração MCP-Anthropic:

TODO 1 — Em integrarComClaude() (ou função equivalente): conecte o MCP client ao server via InMemoryTransport. Crie o par com InMemoryTransport.createLinkedPair(), conecte o server com server.connect(serverTransport) e o client com await client.connect(clientTransport). Seção de referência: Conceitos Fundamentais → “InMemoryTransport”.

TODO 2 — Após conectar: liste as tools com await client.listTools() e converta para o formato Anthropic (renomear inputSchema → input_schema). Seção de referência: Conceitos Fundamentais → “Conversão de Formato: MCP → Anthropic”.

TODO 3 — Implemente o agent loop completo: loop while(true), chame anthropic.messages.create() com as tools convertidas, adicione response.content ao histórico, processe end_turn retornando o texto, e para tool_use chame client.callTool() e adicione os tool_results. Seção de referência: Conceitos Fundamentais → “O Agent Loop Completo” e Exemplos Anotados → Exemplo 1.

Agora você está pronto para o lab.