# Talking to Clients

> How a tool talks to the client while it runs. Asking the user a question mid-call, and reporting progress and logs from long-running tools.

Source: https://frontmcp.dev/learn/talking-to-clients

So far, every call has gone one way: the model calls a tool, the tool runs, and a result comes back. A tool can also talk to the client in between. It can stop and ask the person using the client a question, and it can report how far along it is while it works. This chapter covers both, and what each one looks like on the wire under MCP 2026-07-28.

**In this chapter**
- [How to ask the user a question in the middle of a call](https://frontmcp.dev/learn/asking-the-user)
- [How to report progress and send log messages from a long-running tool](https://frontmcp.dev/learn/reporting-progress)

## Asking the user mid-call

Some calls need a person, not a model: a yes before something that can't be undone, or a detail only the user knows. `this.elicit()` sends the user a question with a small form and gives your tool their answer. The call below stops to ask. Press **Accept**, **Decline** or **Cancel** in the Call tab to answer it:

```ts close-ticket.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "close_ticket",
  description: "Close a support ticket and email the customer. Asks the user to confirm first.",
  inputSchema: { id: z.string().describe("Ticket id, like T-1") },
})
export class CloseTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    const answer = await this.elicit(
      `Close ${id} and email the customer that it's resolved?`,
      z.object({ confirm: z.boolean().describe("Tick to close the ticket") }),
    );
    if (answer.status !== "accept" || answer.content?.confirm !== true) {
      return { id, closed: false, reason: `The user didn't confirm (${answer.status}).` };
    }
    return { id, closed: true };
  }
}
```

Under 2026-07-28 nothing waits on the server while the user thinks: the call returns `input_required`, and the client sends the same request again with the answer.

Read more: [Asking the User Mid-Call](https://frontmcp.dev/learn/asking-the-user)
Learn how to handle accept, decline and cancel, why `execute()` runs twice, what happens with clients that can't show a form, and how to turn elicitation on and test it.

## Reporting progress and logs

A slow tool looks just like a stuck one until it returns. `this.progress()` reports how much is done, and `this.notify()` sends log messages about what's happening. Clients ask for them per request, and get them as a stream before the result:

```ts export-tickets.tool.ts
import { Tool, ToolContext } from "@frontmcp/sdk";

const tickets = ["T-1", "T-2", "T-3", "T-4"];

@Tool({
  name: "export_tickets",
  description: "Export every support ticket to tickets.csv. Takes a few seconds.",
  inputSchema: {},
})
export class ExportTickets extends ToolContext {
  async execute() {
    await this.notify(`Exporting ${tickets.length} tickets`);
    for (const [i, id] of tickets.entries()) {
      await new Promise((resolve) => setTimeout(resolve, 100));
      await this.progress(i + 1, tickets.length, `Exported ${id}`);
    }
    return { file: "tickets.csv", rows: tickets.length };
  }
}
```

The Call tab shows the notifications above the result. Untick **Stream progress & logs** and call again: the tool works the same, it just has nobody to report to.

Read more: [Reporting Progress and Logs](https://frontmcp.dev/learn/reporting-progress)
Learn what to pass to `progress()` and `notify()`, how log levels work, what the event stream looks like, and how to test notifications.

## What's next?

Start the chapter with [Asking the User Mid-Call](https://frontmcp.dev/learn/asking-the-user). After it, [Structuring a Server](https://frontmcp.dev/learn/structuring-a-server) covers how a server grows: shared state in providers, plugins, and hooks around every call.
