# this.progress

> Tell the client how far a long-running tool call has got, when the client asked for progress.

Source: https://frontmcp.dev/reference/sdk/progress

`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](https://frontmcp.dev/learn/reporting-progress) by sending a `progressToken` with the request. Without one, `this.progress()` sends nothing.

```ts
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.

```ts 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.](#usage)

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `progress` | `number` | How much is done so far. MCP requires each value to be higher than the last. |
| `total` | `number` | Optional. How much there is in all, so clients can show a fraction. Leave it out when you don't know. |
| `message` | `string` | Optional. 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

```json
{
  "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`:

```json
{
  "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`](https://frontmcp.dev/reference/sdk/notify#asking-for-log-messages), 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()`](https://frontmcp.dev/reference/sdk/connect#hearing-a-tools-progress) 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](#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.

```ts bulk-close.tool.ts
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 };
  }
}
```

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

### Choosing what to report

<Examples title="Progress values">

#### Example: Steps with a total
When the work has known steps, send the step number and the number of steps. Clients can show a percentage.

```ts export.tool.ts
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" };
  }
}
```

#### Example: No total
When you can't know the total, leave it out and count up. Clients show activity without a percentage.

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

const pages = [["mail-1", "mail-2"], ["mail-3"], []];

@Tool({ name: "scan_inbox", description: "Turn unread support emails into tickets", inputSchema: {} })
export class ScanInbox extends ToolContext {
  async execute() {
    let seen = 0;
    for (const page of pages) {
      if (!page.length) break;
      seen += page.length;
      await this.progress(seen, undefined, `${seen} emails read`);
    }
    return { created: seen };
  }
}
```

### 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`.

```ts bulk-close.tool.ts active
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 };
  }
}
```

```ts listening.test.ts
import { test, expect } from "@frontmcp/testing";
import { App, connect } from "@frontmcp/sdk";
import { BulkClose } from "./bulk-close.tool";

@App({ id: "desk", name: "Desk", tools: [BulkClose] })
class Desk {}

test("without a progressToken, nothing is sent", async ({ mcp }) => {
  const response = await mcp.raw.request({
    jsonrpc: "2.0",
    id: 1,
    method: "tools/call",
    params: { name: "bulk_close", arguments: { ids: ["T-1", "T-2"] } },
  });
  expect(response.result.structuredContent).toEqual({ closed: ["T-1", "T-2"], progressSent: 0 });
});

test("with a progressToken, every call is sent", async ({ mcp }) => {
  const result = await mcp.tools.call("bulk_close", { ids: ["T-1", "T-2"] });
  expect(result.json()).toEqual({ closed: ["T-1", "T-2"], progressSent: 2 });
});

test("a `connect()` client asks with `onProgress`", async () => {
  const client = await connect({ info: { name: "help-desk", version: "1.0.0" }, apps: [Desk] });
  const updates: unknown[] = [];
  try {
    expect((await client.callTool("bulk_close", { ids: ["T-1"] })).structuredContent).toEqual({ closed: ["T-1"], progressSent: 0 });
    const result = await client.callTool("bulk_close", { ids: ["T-1", "T-2"] }, { onProgress: (update) => updates.push(update) });
    expect(result.structuredContent).toEqual({ closed: ["T-1", "T-2"], progressSent: 2 });
    expect(updates).toEqual([
      { progress: 1, total: 2, message: "Closed T-1" },
      { progress: 2, total: 2, message: "Closed T-2" },
    ]);
  } finally {
    await client.close();
  }
});
```

### Testing progress

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

```ts bulk-close.tool.ts active
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 };
  }
}
```

```ts progress.test.ts
import { test, expect } from "@frontmcp/testing";

test("reports one step per ticket, ending at the total", async ({ mcp }) => {
  await mcp.tools.call("bulk_close", { ids: ["T-1", "T-2", "T-3"] });
  const updates = mcp.notifications.collectProgress().all;
  expect(updates.map((u) => [u.progress, u.total])).toEqual([[1, 3], [2, 3], [3, 3]]);
  expect(updates[2]).toMatchObject({ message: "Closed T-3" });
});
```

---

## 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()`](https://frontmcp.dev/reference/sdk/connect#hearing-a-tools-progress), 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:

```ts
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](https://frontmcp.dev/reference/testing#checking-notifications).
