Introduction

Calling tools manually is useful for testing, but the real power of MCP comes when you let an LLM decide which tools to call. This is the agentic loop: the LLM sees a list of available tools, decides which ones to call based on the user's request, your client executes the calls via MCP, and the results are fed back to the LLM to form its response. This lesson shows you how to convert MCP tool definitions to the format LLM APIs expect and implement the complete tool-use loop.

Key Concepts

  • Tool Definition Conversion: MCP tool definitions must be converted to the format your LLM API expects. For Anthropic's API, this means mapping tool.name, tool.description, and tool.inputSchema to the name, description, and input_schema fields.
  • Tool Use Loop: A multi-turn cycle where the LLM requests tool calls, your client executes them via MCP, and the results are added to the conversation as tool_result messages. The loop continues until the LLM responds without requesting more tool calls.
  • tool_use Stop Reason: When the LLM wants to call a tool, its response has a stop_reason of "tool_use" and includes tool_use content blocks specifying which tools to call.
  • tool_result Messages: After executing a tool call, you add the result back to the conversation as a message with role: "user" containing a tool_result content block.

Real World Context

You are building a coding assistant that can read files, search code, and run terminal commands through MCP servers. When a user asks "Find all TODO comments in the project and create a summary," the LLM needs to call search_files first, then maybe read_file on several results, and finally compose a summary. Each tool call is an MCP request, and each result feeds back into the conversation. Your client orchestrates this loop automatically.

Deep Dive

Converting MCP Tools to Anthropic Format

MCP tool definitions are almost identical to what the Anthropic API expects. The main difference is the field name: MCP uses inputSchema, while Anthropic uses input_schema.

typescript
const toolsResult = await client.listTools();

const anthropicTools = toolsResult.tools.map(tool => ({
  name: tool.name,
  description: tool.description || "",
  input_schema: tool.inputSchema
}));

This conversion creates an array of tool definitions that you pass to the Anthropic API's tools parameter. The LLM reads these definitions and decides when to call each tool.

The Tool Use Loop

The core loop follows this pattern:

text
1. Send messages + tools to LLM
2. If LLM response has stop_reason "tool_use":
   a. Extract tool_use blocks from the response
   b. Execute each tool call via client.callTool()
   c. Add assistant message to conversation
   d. Add tool_result messages to conversation
   e. Go to step 1
3. If LLM response has stop_reason "end_turn":
   → Return the final response

Here is a complete implementation:

typescript
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

const anthropic = new Anthropic();

async function agentLoop(
  client: Client,
  userMessage: string
): Promise<string> {
  // Get tools from MCP server
  const toolsResult = await client.listTools();
  const tools = toolsResult.tools.map(t => ({
    name: t.name,
    description: t.description || "",
    input_schema: t.inputSchema
  }));

  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage }
  ];

  // Tool use loop
  while (true) {
    const response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 4096,
      tools,
      messages
    });

    // Add assistant response to conversation
    messages.push({ role: "assistant", content: response.content });

    // If no tool use, return the text response
    if (response.stop_reason === "end_turn") {
      const textBlocks = response.content.filter(
        block => block.type === "text"
      );
      return textBlocks.map(b => b.text).join("\n");
    }

    // Execute tool calls
    const toolResults: Anthropic.ToolResultBlockParam[] = [];
    for (const block of response.content) {
      if (block.type === "tool_use") {
        const result = await client.callTool({
          name: block.name,
          arguments: block.input as Record<string, unknown>
        });

        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: result.isError
            ? `Error: ${(result.content[0] as { text: string }).text}`
            : (result.content[0] as { text: string }).text
        });
      }
    }

    // Add tool results to conversation
    messages.push({ role: "user", content: toolResults });
  }
}

Let us break down each step of this implementation.

First, we convert MCP tools to Anthropic format. This happens once before the loop starts.

Then we enter the loop. We send the full conversation history (including all previous tool results) to the LLM. The LLM either responds with text (stop_reason: "end_turn") or requests tool calls (stop_reason: "tool_use").

When the LLM requests tool calls, we iterate through the response content blocks. Each tool_use block contains the tool name, input (the arguments), and an id that links the call to its result.

We execute each tool call via client.callTool() and build a tool_result block with the matching tool_use_id. We add these results to the conversation and loop back to the LLM.

Handling Multiple Tool Calls

The LLM can request multiple tool calls in a single response. Each tool_use block in the response represents a separate call:

typescript
// The LLM might return:
// [
//   { type: "text", text: "Let me check both files..." },
//   { type: "tool_use", id: "tu_1", name: "read_file", input: { path: "a.ts" } },
//   { type: "tool_use", id: "tu_2", name: "read_file", input: { path: "b.ts" } }
// ]

// Each tool_use gets its own tool_result with matching tool_use_id

The tool_use_id is critical — it tells the LLM which result corresponds to which call. Never omit or mix up these IDs.

Multi-Server Tool Use

When you have multiple MCP servers, you need to route tool calls to the correct client:

typescript
// Map of tool name to the client that provides it
const toolClientMap = new Map<string, Client>();

for (const [serverName, client] of clients.entries()) {
  const tools = await client.listTools();
  for (const tool of tools.tools) {
    toolClientMap.set(tool.name, client);
  }
}

// When executing a tool call:
const targetClient = toolClientMap.get(toolName);
if (!targetClient) {
  throw new Error(`No server provides tool: ${toolName}`);
}
const result = await targetClient.callTool({ name: toolName, arguments: args });

Build a lookup map from tool names to clients at startup. When the LLM requests a tool call, look up which client provides that tool and route the call accordingly.

Common Pitfalls

  1. Forgetting the tool_use_id — Every tool_result must include the tool_use_id from the corresponding tool_use block. Without it, the LLM cannot match results to calls.
  2. Not handling isError in tool results — When a tool returns isError: true, pass the error message as the tool result content. The LLM can often recover by trying a different approach.
  3. Infinite loops — If the LLM keeps requesting tool calls without converging on an answer, your loop will run forever. Add a maximum iteration count as a safety measure.

Best Practices

  1. Set a maximum loop iteration count — Add a counter and break after a reasonable number of iterations (e.g., 10-20) to prevent runaway loops.
  2. Pass errors to the LLM — When a tool call fails, include the error message in the tool_result. The LLM can often self-correct by adjusting its arguments or choosing a different tool.
  3. Merge tools from multiple servers — Build a unified tool list from all connected servers and a routing map to dispatch calls to the right client.

Summary

  • Convert MCP tools to Anthropic format by mapping inputSchema to input_schema.
  • The tool use loop repeats: send messages to LLM, execute tool calls, feed results back, until the LLM responds with end_turn.
  • Each tool_use block has an id that must be matched in the corresponding tool_result.
  • Pass tool errors to the LLM as tool results so it can self-correct.
  • For multi-server setups, build a routing map from tool names to clients.

Code Examples

typescript
// Convert MCP tools to Anthropic format
const toolsResult = await client.listTools();
const anthropicTools = toolsResult.tools.map(t => ({
  name: t.name,
  description: t.description || "",
  input_schema: t.inputSchema
}));

// In the tool use loop, execute MCP calls:
for (const block of response.content) {
  if (block.type === "tool_use") {
    const result = await client.callTool({
      name: block.name,
      arguments: block.input
    });
    // Feed result back as tool_result with block.id
  }
}
✓ Completed