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
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:
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 };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
Ready to learn this topic?
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:
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 };
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
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.
Ready to learn this topic?
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. After it, Structuring a Server covers how a server grows: shared state in providers, plugins, and hooks around every call.