Introduction

Discovering tools is only the first step — the real power comes from calling them. The client.callTool() method sends a tool invocation request to the server, which executes the tool and returns the result. Understanding how to pass arguments, interpret different result types, and handle errors is fundamental to building any MCP-powered application.

Key Concepts

  • client.callTool(): The method that invokes a tool on the server. It takes an object with name (the tool identifier) and arguments (the parameters as a key-value object).
  • Tool Result Content: Tool results contain a content array. Each item has a type field — "text" for plain text, "image" for base64-encoded images, or "resource" for embedded resource references.
  • isError Flag: Tool results include an isError boolean. When true, the content describes the error rather than a successful result. The LLM can use this information to retry or adjust its approach.
  • Progress Notifications: For long-running tools, the server can send progress notifications during execution. The client can listen for these to display progress indicators.

Real World Context

Your AI assistant needs to query a database to answer a user's question about sales data. The MCP server exposes a run_query tool that accepts an SQL string. Your client calls client.callTool({ name: "run_query", arguments: { sql: "SELECT * FROM sales WHERE date > '2025-01-01'" } }). The server executes the query and returns the results as text content. If the query fails (bad SQL, permission denied), the server returns the result with isError: true and an error message, which the LLM can use to correct the query and try again.

Deep Dive

Basic Tool Call

The simplest tool call passes a name and arguments:

typescript
const result = await client.callTool({
  name: "get_weather",
  arguments: {
    city: "Paris",
    units: "celsius"
  }
});

The name must match a tool that the server exposed via listTools(). The arguments object must conform to the tool's inputSchema. The method returns a result object with a content array.

Handling Text Content

The most common result type is text content:

typescript
const result = await client.callTool({
  name: "get_weather",
  arguments: { city: "Paris" }
});

for (const item of result.content) {
  if (item.type === "text") {
    console.error(item.text);
    // "Current weather in Paris: 18°C, partly cloudy"
  }
}

Text content is the simplest to handle — just read the text field. Most tools return their results as text, whether it is a formatted string, JSON data, or a status message.

Handling Image Content

Some tools return images (screenshots, charts, generated diagrams):

typescript
const result = await client.callTool({
  name: "take_screenshot",
  arguments: { url: "https://example.com" }
});

for (const item of result.content) {
  if (item.type === "image") {
    // item.data is a base64-encoded string
    // item.mimeType is e.g. "image/png"
    const buffer = Buffer.from(item.data, "base64");
    // Save, display, or pass to the LLM as an image
  }
}

Image content includes a data field with the base64-encoded image and a mimeType field indicating the format.

Handling Embedded Resources

Tools can also return embedded resource references:

typescript
const result = await client.callTool({
  name: "search_files",
  arguments: { query: "TODO" }
});

for (const item of result.content) {
  if (item.type === "resource") {
    console.error(`Found resource: ${item.resource.uri}`);
    console.error(`Content: ${item.resource.text}`);
  }
}

Embedded resources include the full resource data inline, so you do not need a separate readResource() call.

Checking the isError Flag

Always check the isError flag to determine if the tool call succeeded:

typescript
const result = await client.callTool({
  name: "run_query",
  arguments: { sql: "SELECT * FROM nonexistent_table" }
});

if (result.isError) {
  // The content describes the error
  const errorMsg = result.content
    .filter(c => c.type === "text")
    .map(c => c.text)
    .join("\n");
  console.error("Tool error:", errorMsg);
  // Feed this error back to the LLM so it can adjust
} else {
  // Process successful result
}

The isError flag is distinct from a transport-level error. A transport error means the message did not reach the server. An isError: true result means the server received and processed the request, but the tool itself failed (e.g., bad SQL, file not found, API error).

Structured Content

In addition to the content array, tool results may include a structuredContent field. When a tool defines an output schema, the server can return typed, structured data that your application can use programmatically without parsing text:

typescript
const result = await client.callTool({
  name: "calculate_bmi",
  arguments: { weightKg: 70, heightM: 1.75 }
});

// Access text content for LLM consumption
for (const item of result.content) {
  if (item.type === "text") console.error(item.text);
}

// Access structured content for programmatic use
if (result.structuredContent) {
  console.error("BMI value:", result.structuredContent.bmi);
}

The structuredContent field is optional and only present when the tool defines an output schema and returns structured data. Always check for its existence before accessing it.

Progress Notifications

For long-running tools, you can pass a progress callback and timeout options:

typescript
const result = await client.callTool(
  {
    name: "analyze_codebase",
    arguments: { path: "/project" }
  },
  {
    timeout: 120_000,
    resetTimeoutOnProgress: true,
    maxTotalTimeout: 600_000,
    onprogress: ({ progress, total }) => {
      console.error(
        `Progress: ${progress}/${total ?? "?"}`
      );
    }
  }
);

The options object supports timeout (per-message timeout in ms), resetTimeoutOnProgress (reset timeout when progress is received), maxTotalTimeout (absolute maximum wait time), and onprogress (callback for progress updates). Progress notifications are optional — the server must support them and the tool must send them.

Common Pitfalls

  1. Not validating arguments against the input schema — Sending arguments that do not match the tool's inputSchema will cause the server to return an error. Validate before calling.
  2. Ignoring the isError flag — If you treat every result as successful, error messages will be processed as if they were real data, leading to nonsensical LLM responses.
  3. Assuming all content is text — Tools can return text, images, and embedded resources. Always check item.type before accessing content-specific fields.

Best Practices

  1. Always check isError before processing results — This lets you handle errors gracefully, either by retrying, asking the LLM to adjust, or showing an error to the user.
  2. Handle all content types — Even if you expect text, code defensively for images and resources. A server update might change what a tool returns.
  3. Set timeouts for tool calls — Some tools may hang or take unexpectedly long. Set a reasonable timeout to prevent your application from blocking indefinitely.

Summary

  • client.callTool({ name, arguments }) invokes a tool on the server and returns a result with a content array.
  • Content items can be text (type: "text"), images (type: "image" with base64 data), or embedded resources (type: "resource").
  • Tool results may also include structuredContent for typed programmatic access when the tool defines an output schema.
  • The isError flag indicates whether the tool execution failed — always check it before processing results.
  • Progress notifications and timeout options let you track and control long-running tool operations.
  • Validate arguments against the tool's inputSchema before calling to avoid server-side errors.

Code Examples

typescript
const result = await client.callTool({
  name: "get_weather",
  arguments: { city: "Paris" }
});

if (result.isError) {
  console.error("Error:", result.content[0].text);
} else {
  for (const item of result.content) {
    if (item.type === "text") {
      console.error(item.text);
    } else if (item.type === "image") {
      console.error(`Image: ${item.mimeType}`);
    }
  }
}
✓ Completed