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 atualizadoEssa 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álidaA 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' }],
})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.