# Tool Call Execution

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Execution and Context
Source: https://www.devtutorials.ai/learn/mcp/execution-context/tool-call-execution

> The steps of an MCP tool call: catalog, inspection, selection, approval, tools/call and results, with content blocks, outputSchema and structuredContent, isError, progress and cancellation.

Think about ordering at a busy café with a menu board. You read the menu and choose. The cashier checks that the item exists and, if needed, asks "Are you sure? It's spicy." The kitchen makes it. Then you get a tray: a drink, a sandwich, maybe a receipt with a link to the café's app.

A tool call follows a similar path. The host reads the menu (`tools/list`). The model requests a tool. The host checks the request and, when its policy requires it, asks the user. The server does the work. The result comes back as a "tray" of **content blocks**.

**The main idea:** a typical tool call follows these steps: **list**, **inspect**, **select**, **approve when needed**, **call**, and **read the result**. Each step has a job, and the host decides which tools to show and what checks or approval its policy requires.

## A Real-World Example

Maya asks the assistant: "When will A-1002 arrive?"

1. When it started, the host got the tool catalog with `tools/list`.
2. It gave the model the read-only tools that suit this chat.
3. The model asked for `get_order_status` with `{"order_id": "A-1002"}`.
4. The host checked the tool exists, the arguments fit the schema, and the tool is read-only. No approval needed.
5. The client sent `tools/call`.
6. The server returned text for the model, **structured data** for the app, and a **link** to the full order record.
7. The model answered: "Your air purifier is being prepared and should arrive on 18 October."

**In simple words:** the host runs a careful checklist around every tool call.

## What You Will Learn

In this lesson, you will learn:

1. How a host reads and prepares the tool catalog.
2. How the host selects, checks and approves a call.
3. The kinds of content blocks a result can hold.
4. How `outputSchema` and `structuredContent` give programs reliable data.
5. How `isError` results feed back to the model.
6. Where progress and cancellation fit in.

The **MCP Lab** has a new tool, `get_order_status`, with an output schema. When you call it, the Explorer shows its `structuredContent` and a button to read the linked resource.

Before a model can use a tool, the host must know which tools are available. The tool catalog is the host's source of information about what a server can do and what inputs each tool expects.

## The Catalog

### tools/list

Before a model can request a tool, it needs to know which tools exist and what inputs they accept. The host gets this information from the tool catalog.

The client asks the server for its tools once, keeps the list, and asks again when it hears `notifications/tools/list_changed`. Long lists may come in **pages**: the result then has a `nextCursor`, and the client asks again with that cursor until there is none.

Getting a list of tools is only the first step. Before the host gives those tools to a model, it often reviews, filters, and adapts them for its own environment and policies.

A raw MCP catalog is not always the same format expected by a model provider. Hosts often transform, filter, and review tools before exposing them to the model.

### Catalog Parsing and Schema Inspection

Before showing tools to the model, the host prepares them:

- **Convert:** model APIs want tools in their own format, usually a name, a description and a parameters schema. The host copies them from the MCP catalog.
- **Inspect:** the host reads each schema and annotation. It can refuse tools with broken schemas and plan which ones need approval under its policy. Annotations are hints, not proof that a tool is safe.
- **Select:** the host doesn't have to show every tool. It can show only tools that suit this chat or this user, like read-only tools for a reporting assistant.

The example below filters on the `readOnlyHint` annotation to demonstrate selection. In a real host, do not treat an annotation as a permission grant; apply the host's own policy and trust decisions.

```js runnable title="Prepare the catalog for the model"
// Step 1: turn the tools/list catalog into the "tools" list a model API expects.
const catalog = [
  {
    name: "lookup_order",
    description: "Get one order.",
    inputSchema: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
    annotations: { readOnlyHint: true },
  },
  {
    name: "cancel_order",
    description: "Cancel one order.",
    inputSchema: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
    annotations: { destructiveHint: true },
  },
  {
    name: "export_report",
    description: "Build a report.",
    inputSchema: {
      type: "object",
      properties: { rows: { type: "integer", minimum: 1 } },
      required: ["rows"],
    },
    annotations: { readOnlyHint: true },
  },
];

// The host can also leave tools out. Here: show only read-only tools in this chat.
const forModel = catalog
  .filter((t) => t.annotations?.readOnlyHint === true)
  .map((t) => ({ name: t.name, description: t.description, parameters: t.inputSchema }));

console.log(
  forModel
    .map((t) => `${t.name}(${Object.keys(t.parameters.properties).join(", ")})`)
    .join("\n"),
);
console.log("hidden:", catalog.length - forModel.length);
```

Expected output:

```text
lookup_order(order_id)
export_report(rows)
hidden: 1
```

Showing a focused set of tools can make the available choices easier to understand. Give the model the tools that fit the task instead of including unrelated ones.

Once the model can see available tools, it may decide that a tool is needed to answer a question. However, choosing a tool does not mean the tool gets executed immediately. The host still needs to validate the request and apply its approval rules.

## Selection and Approval

### The Model Selects

The model reads the question and the tool list, and asks for a tool by name with arguments. It may also answer without a tool. Only the model's **request** comes from the model. Everything after that is the host's decision.

### The Host Checks

A model can make mistakes. It may choose the wrong tool, omit required arguments, or request an operation that should not be allowed in the current conversation. For that reason, hosts validate requests before sending them to the server.

Before sending a request, a host can check:

- Does a tool with this name exist?
- Do the arguments fit the input schema?
- Is the tool allowed in this chat?
- Does it need the user's approval?

If the arguments are wrong, the host can send the problem back to the model without calling the server. The server must still validate every request it receives; it cannot rely on the host's check.

```js runnable title="Check a tool request"
// Step 2: the model asked for a tool. The host checks it before anything runs.
const catalog = {
  lookup_order: { annotations: { readOnlyHint: true }, required: ["order_id"] },
  create_ticket: {
    annotations: { readOnlyHint: false, destructiveHint: false },
    required: ["summary", "priority"],
  },
};

function decide(call) {
  const tool = catalog[call.name];
  if (!tool) {
    return "reject: no such tool";
  }
  const missing = tool.required.filter((k) => !(k in call.arguments));
  if (missing.length) {
    return "send back to the model: missing " + missing.join(", ");
  }
  if (tool.annotations.readOnlyHint) {
    return "run now";
  }
  return "ask the user: " + call.name + " " + JSON.stringify(call.arguments);
}

console.log(decide({ name: "lookup_order", arguments: { order_id: "A-1002" } }));
console.log(
  decide({
    name: "create_ticket",
    arguments: { summary: "Lamp broken", priority: "high" },
  }),
);
console.log(decide({ name: "create_ticket", arguments: { summary: "Lamp broken" } }));
console.log(decide({ name: "refund_all", arguments: {} }));
```

Expected output:

```text
run now
ask the user: create_ticket {"summary":"Lamp broken","priority":"high"}
send back to the model: missing priority
reject: no such tool
```

### User Approval

Approval is most commonly used for actions that create, update, delete, send, purchase, or otherwise affect real data. Read-only operations often run immediately, while actions with consequences usually require confirmation.

When its policy calls for confirmation, the host should show the user what is about to happen, including the tool name and arguments. The user can approve, edit or refuse. Approval rules belong to the host; MCP does not itself require a confirmation step for every tool that changes data.

After the host has accepted a tool request, it can send that request to the server. The server performs the work and returns a result that the host and model can use.

## tools/call and Its Result

### The Request

`tools/call` sends the tool `name` and the `arguments`. Over HTTP, the client also adds the `Mcp-Name` header with the tool name.

Not every tool returns the same kind of data. Some return text, others return images, audio, or references to resources. MCP represents all of these using content blocks.

### Content Blocks

The result's `content` is a list of **blocks**. One result can mix several kinds:

| Block type | What it holds |
| --- | --- |
| `text` | Plain text for the model |
| `image` | Base64 image `data` with a `mimeType` |
| `audio` | Base64 audio `data` with a `mimeType` |
| `resource_link` | A pointer to a resource: a `uri`, a `name`, a `mimeType`. The client reads it only if needed |
| `resource` | An **embedded** resource: the resource's contents, right inside the result |

In practice, text is the most common block type. Images and audio are used for media results, while `resource_link` and `resource` are useful when a tool needs to expose additional data.

```js runnable title="Handle each block type"
// Step 3: read a tool result. It can hold several kinds of content blocks.
const result = {
  content: [
    { type: "text", text: "Order A-1001 is shipped." },
    { type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" },
    { type: "audio", data: "UklGRg==", mimeType: "audio/wav" },
    {
      type: "resource_link",
      uri: "orders://A-1001",
      name: "Order A-1001",
      mimeType: "application/json",
    },
    {
      type: "resource",
      resource: { uri: "kb://shipping", mimeType: "text/plain", text: "3 to 6 days." },
    },
  ],
  resultType: "complete",
};

for (const block of result.content) {
  switch (block.type) {
    case "text":
      console.log("text     ->", block.text);
      break;
    case "image":
    case "audio":
      console.log(block.type.padEnd(8), "->", block.mimeType, "as base64");
      break;
    case "resource_link":
      console.log("link     ->", block.uri, "(read it later if needed)");
      break;
    case "resource":
      console.log("embedded ->", block.resource.uri + ":", block.resource.text);
      break;
  }
}
```

Expected output:

```text
text     -> Order A-1001 is shipped.
image    -> image/png as base64
audio    -> audio/wav as base64
link     -> orders://A-1001 (read it later if needed)
embedded -> kb://shipping: 3 to 6 days.
```

A `resource_link` can keep results small. Instead of including a huge record, the tool points to where it can be read. The host can decide whether that resource is needed.

Think of a `resource_link` as receiving a file path rather than the entire file. The tool tells the host where the data lives, and the host can decide whether retrieving it is worth the extra cost.

The content blocks above are useful for presenting information. However, applications sometimes need data in a predictable format that code can process without guessing.

### Structured Results

Text is easy for a model to read, but harder for a program to use reliably. For example, a program can reliably read `structuredContent.status` to check whether an order is shipped. Extracting the same value from a paragraph of text requires guessing and may fail.

If a tool declares an **`outputSchema`**, its result must include **`structuredContent`**, a JSON object that matches that schema. A text block with the same data is useful for compatibility with clients that primarily consume text, but it does not replace `structuredContent`.

The official SDK used in this course checks `structuredContent` against the declared `outputSchema` before sending the result. If the data is missing or does not match, that SDK reports a tool error instead. This validation behavior is an SDK implementation detail, not a check performed automatically by every client.

```js runnable title="Check structured content"
// A small teaching example for the fields below, not a full JSON Schema validator.
const outputSchema = {
  type: "object",
  properties: {
    order_id: { type: "string" },
    status: { type: "string", enum: ["processing", "shipped", "delivered"] },
    eta: { type: "string" },
  },
  required: ["order_id", "status", "eta"],
};

function matches(schema, value) {
  for (const key of schema.required) {
    if (!(key in value)) {
      return `missing ${key}`;
    }
  }
  for (const [key, rule] of Object.entries(schema.properties)) {
    if (key in value && typeof value[key] !== rule.type) {
      return `${key} is not a ${rule.type}`;
    }
    if (rule.enum && !rule.enum.includes(value[key])) {
      return `${key} is not one of ${rule.enum.join("/")}`;
    }
  }
  return "ok";
}

const result = {
  structuredContent: { order_id: "A-1002", status: "processing", eta: "2026-10-18" },
  content: [
    {
      type: "text",
      text: '{"order_id":"A-1002","status":"processing","eta":"2026-10-18"}',
    },
  ],
};
console.log("check:", matches(outputSchema, result.structuredContent));
console.log(
  "a program can now read:",
  result.structuredContent.status,
  "until",
  result.structuredContent.eta,
);
console.log(
  "bad one:",
  matches(outputSchema, { order_id: "A-1", status: "lost", eta: "?" }),
);
```

Expected output:

```text
check: ok
a program can now read: processing until 2026-10-18
bad one: status is not one of processing/shipped/delivered
```

Not every failure is the same. Sometimes the request itself is invalid, which is a protocol error. Other times the request is valid but the requested work cannot be completed. That second case is a tool error.

Not every tool execution succeeds. Some requests are valid but still cannot be completed because of missing data, unavailable records, or business rules.

### isError

When a tool runs but cannot complete its task, such as when an order does not exist, its result can set **`isError: true`** and include a helpful message in `content`. The host can pass that result back to the model, which may correct the call, ask the user for more information, or explain the problem. A protocol-level error is different: the request itself could not be processed as a valid call.

```js runnable title="A tool error, then a retry"
// The whole execution, with a stand-in server and a stand-in model.
const server = {
  call(name, args) {
    if (name !== "lookup_order") {
      return { error: { code: -32602, message: `Tool ${name} not found` } };
    }
    if (args.order_id !== "A-1002") {
      return {
        result: {
          content: [{ type: "text", text: "No order " + args.order_id }],
          isError: true,
        },
      };
    }
    return {
      result: {
        content: [
          { type: "text", text: "A-1002: Air purifier, processing, arrives 2026-10-18" },
        ],
      },
    };
  },
};
const modelWants = [
  { name: "lookup_order", arguments: { order_id: "A-1020" } },
  { name: "lookup_order", arguments: { order_id: "A-1002" } },
];

for (let turn = 0; turn < modelWants.length; turn++) {
  const call = modelWants[turn];
  const reply = server.call(call.name, call.arguments);
  if (reply.error) {
    console.log("protocol error, stop:", reply.error.message);
    break;
  }
  const text = reply.result.content.map((c) => c.text).join(" ");
  if (reply.result.isError) {
    console.log(`turn ${turn + 1}: tool error -> back to the model: "${text}"`);
    continue;
  }
  console.log(`turn ${turn + 1}: success -> model writes the answer from: "${text}"`);
  break;
}
```

Expected output:

```text
turn 1: tool error -> back to the model: "No order A-1020"
turn 2: success -> model writes the answer from: "A-1002: Air purifier, processing, arrives 2026-10-18"
```

A tool result becomes part of the model's context. Text from outside, such as an email or web page, may contain instructions aimed at the model. Treat that text as untrusted data, not as instructions to follow. Merely labeling it as data does not make it safe; keep approval and access rules in host code.

Many tool calls finish almost instantly, but some take longer. Large reports, long-running searches, or external operations may need a way to report progress and allow cancellation.

Some tools respond in seconds, while others may generate reports, search large data sets, or wait for external systems. Long-running work benefits from progress updates and a way to stop execution when it is no longer needed.

## Progress and Cancellation

Long tool calls can use the progress and cancellation features from Chapter 2:

- The client adds a `progressToken`, and the server streams `notifications/progress` before the result.
- To cancel, the client closes that request's stream over Streamable HTTP. With stdio, it sends `notifications/cancelled` with the request ID.

A host should show progress to the user, offer a Stop button for slow tools, and set a time limit, so a stuck tool can't freeze the chat.

## Common mistakes

- **Treating the model's tool request as approved.** The model only asks. The host decides.
- **Throwing protocol errors for tool failures.** A missing order is `isError: true`, so the model can recover.
- **Ignoring `nextCursor`.** Large catalogs come in pages. Keep asking until there is no cursor.
- **Ignoring non-text blocks.** A host that only reads `text` loses images, links and embedded resources.
- **No time limit.** A slow tool without progress or a timeout looks like a frozen app.

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- A typical tool call goes: **list**, **inspect**, **select**, **approve when needed**, **call**, **read the result**.
- The host converts and filters the catalog, applies its checks, and asks the user when its policy requires approval.
- Results hold **content blocks**: `text`, `image`, `audio`, `resource_link` and embedded `resource`.
- With an **`outputSchema`**, return matching **`structuredContent`**; a text copy can help text-focused clients.
- **`isError: true`** goes back to the model so it can recover.
- Long calls may use **progress** and **cancellation**; hosts should also set a **time limit**. A cancellation request does not guarantee the work stopped.

Next: **Tool Schema Validation**, checking arguments and results on both sides.
