this.progress

MCP 2026-07-28

this.progress() sends a notifications/progress message while a tool is still running, so a client can show a progress bar instead of a spinner. The client has to ask for progress by sending a progressToken with the request. Without one, this.progress() sends nothing.

const sent = await this.progress(progress, total?, message?)

Reference

this.progress(progress, total?, message?)

Call this.progress() inside a tool's execute(), each time a meaningful part of the work is done.

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

@Tool({ name: "bulk_close", description: "Close several tickets", inputSchema: { ids: z.array(z.string()) } })
class BulkClose extends ToolContext {
  async execute({ ids }: { ids: string[] }) {
    for (const [i, id] of ids.entries()) {
      await closeTicket(id);
      await this.progress(i + 1, ids.length, `Closed ${id}`);
    }
    return { closed: ids };
  }
}

See more examples below.

Parameters

ParameterTypeDescription
progressnumberHow much is done so far. MCP requires each value to be higher than the last.
totalnumberOptional. How much there is in all, so clients can show a fraction. Leave it out when you don't know.
messagestringOptional. What's happening now, like "Closed T-2".

Returns

A promise of a boolean: true if the notification was sent, false if the request had no progressToken. Nothing is thrown either way.

What the client receives

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": { "progressToken": "progress-1", "progress": 2, "total": 3, "message": "Closed T-2" }
}

progressToken is the token from the request. total and message are left out when you don't pass them.

Asking for progress

A client asks for progress per request, with a progressToken (a string or a number) in the request's _meta:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "bulk_close",
    "arguments": { "ids": ["T-1", "T-2", "T-3"] },
    "_meta": { "progressToken": "progress-1" }
  }
}

Under MCP 2026-07-28, when a request carries a progressToken or an io.modelcontextprotocol/logLevel, and its Accept header includes text/event-stream, FrontMCP answers with a stream (Content-Type: text/event-stream). Each notification is an event, in the order the tool sent them, and the last event is the JSON-RPC response. A request without either gets a plain JSON response.

Protocol versions before 2026-07-28 use the same progressToken, and FrontMCP sends the notifications on the client's session.

An in-process client from connect() asks per call too: pass { onProgress } as callTool()'s third argument, and it gets each update as { progress, total, message }. See Checking whether the client is listening. (Changed in 1.9.3: before, callTool() sent no progress token, so this.progress() always returned false under connect().)

Caveats

  • this.progress() is a protected method of ToolContext (and AgentContext). Call it from inside the class.
  • It's opt-in. Many clients never send a progressToken, so don't put anything the model needs only in a progress message. Put it in the result.
  • FrontMCP doesn't check that progress increases. That's up to you.
  • A tool's progress on a session ignores the token 0. For a request on a session, before 2026-07-28, that sends progressToken: 0, a tool's this.progress() sends nothing and returns false; a token like 7 works. An agent's this.progress() sends on 0 (since 1.9.4), and under 2026-07-28 a tool's does too. This was checked on a Node server, since the Playground's client speaks 2026-07-28.
  • Under frontmcp test, the test client sends a progressToken only while a collectProgress() collector is active, so otherwise this.progress() returns false. The Playground's test client sends one with every tools/call.

Usage

Reporting progress through a list

Report after each item, with the item count as total. This example turns on Stream progress & logs in the Call tab, which sends a progressToken. The notifications appear above the result, and the Wire tab shows the stream.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "bulk_close",
  description: "Close several tickets at once. Reports progress after each ticket.",
  inputSchema: { ids: z.array(z.string()).min(1).describe("Ticket ids, like T-1") },
})
export class BulkClose extends ToolContext {
  async execute({ ids }: { ids: string[] }) {
    for (const [i, id] of ids.entries()) {
      await new Promise((resolve) => setTimeout(resolve, 200)); // stands in for the real work
      await this.progress(i + 1, ids.length, `Closed ${id}`);
    }
    return { closed: ids };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Untick Stream progress & logs and call again: the result is the same, and nothing is streamed.

Choosing what to report

Progress values

Example 1 of 2

Steps with a total

When the work has known steps, send the step number and the number of steps. Clients can show a percentage.

Open
import { Tool, ToolContext } from "@frontmcp/sdk";

const steps = ["Loading tickets", "Formatting CSV", "Uploading"];

@Tool({ name: "export_tickets", description: "Export all tickets to CSV and upload the file", inputSchema: {} })
export class ExportTickets extends ToolContext {
  async execute() {
    for (const [i, step] of steps.entries()) {
      await this.progress(i, steps.length, step);
    }
    await this.progress(steps.length, steps.length, "Done");
    return { url: "https://files.example.com/tickets.csv" };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Checking whether the client is listening

The return value says whether a notification went out. You rarely need it, but it shows the opt-in at work. The first test sends a request without a progressToken; the second uses the test client, which sends one; the third calls through connect(), without and then with onProgress.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "bulk_close", description: "Close several tickets", inputSchema: { ids: z.array(z.string()) } })
export class BulkClose extends ToolContext {
  async execute({ ids }: { ids: string[] }) {
    let sent = 0;
    for (const [i, id] of ids.entries()) {
      if (await this.progress(i + 1, ids.length, `Closed ${id}`)) sent++;
    }
    return { closed: ids, progressSent: sent };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Testing progress

In the Playground, mcp.notifications.collectProgress().all lists the progress notifications the test client received, in order.

Open
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "bulk_close", description: "Close several tickets", inputSchema: { ids: z.array(z.string()) } })
export class BulkClose extends ToolContext {
  async execute({ ids }: { ids: string[] }) {
    for (const [i, id] of ids.entries()) {
      await this.progress(i + 1, ids.length, `Closed ${id}`);
    }
    return { closed: ids };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


Troubleshooting

this.progress() returns false, and the client shows nothing

The request didn't include a progressToken, so there was nowhere to send progress. The client decides this: in the Playground, tick Stream progress & logs; in your own client, set _meta.progressToken on the request; with connect(), pass { onProgress } to callTool(). The tool still runs and returns its result.

Progress goes backwards, or never reaches the end

MCP requires every progress value to be higher than the one before, and FrontMCP sends what you pass without checking. Count up, and when you pass total, finish with progress equal to total.

TypeScript says progress is protected

this.progress() can only be called from inside the tool class. To report progress from a helper, pass it a callback:

await importTickets(rows, (done) => this.progress(done, rows.length));

collectProgress().all is empty under frontmcp test

The call had no progress token, or the test checked before the updates arrived. Call mcp.notifications.collectProgress() before the call, so the test client sends a token, and wait with waitForComplete() before you check. See Checking notifications.