# this.notify

> Send log messages to the client while a tool runs, at the levels the client asked for.

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

`this.notify()` sends a log message, a `notifications/message`, to the client while a tool is still running. Clients show these in a log or a debug panel, to explain what a long call is doing. The client chooses whether to receive them, and from which level up. Without that request, `this.notify()` sends nothing.

```ts
const sent = await this.notify(message, level?)
```

---

## Reference

### `this.notify(message, level?)`

Call `this.notify()` inside a tool's `execute()`.

```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[] }) {
    await this.notify(`Closing ${ids.length} tickets`);
    for (const id of ids) {
      await closeTicket(id);
      await this.notify(`Closed ${id}`, "debug");
    }
    return { closed: ids };
  }
}
```

[See more examples below.](#usage)

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `message` | `string \| Record<string, unknown>` | What to log. A string arrives as `{ "message": "..." }`; an object arrives as it is. |
| `level` | `McpLoggingLevel` | Optional. How important the message is. Default `"info"`. |

The eight levels, from lowest to highest:

| Level | Use it for |
| --- | --- |
| `"debug"` | Detail that helps when something goes wrong. |
| `"info"` | What the tool is doing. The default. |
| `"notice"` | Something worth noticing, but normal. |
| `"warning"` | Something unexpected that the tool worked around. |
| `"error"` | Something failed. |
| `"critical"` | A part of the system is failing. |
| `"alert"` | Someone has to act now. |
| `"emergency"` | The system is unusable. |

#### Returns

A promise of a `boolean`: `true` if the message was sent, `false` if the client didn't ask for log messages, or asked only for higher levels. Nothing is thrown either way.

#### What the client receives

```json
{
  "jsonrpc": "2.0",
  "method": "notifications/message",
  "params": { "level": "info", "logger": "bulk_close", "data": { "message": "Closing 3 tickets" } }
}
```

`logger` is the tool's name, and `data` is your message.

#### Asking for log messages

Under MCP 2026-07-28, a client asks for log messages per request, with a minimum level in the request's `_meta`:

```json
"_meta": { "io.modelcontextprotocol/logLevel": "info" }
```

FrontMCP sends the messages at that level and above, and drops the rest:

| `logLevel` in the request | Levels that are sent |
| --- | --- |
| none | none |
| `"debug"` | all eight |
| `"info"` | everything except `debug` |
| `"warning"` | `warning`, `error`, `critical`, `alert` and `emergency` |
| `"error"` | `error`, `critical`, `alert` and `emergency` |

The messages arrive in the response stream, before the result, as described for [`this.progress()`](https://frontmcp.dev/reference/sdk/progress#asking-for-progress). A `progressToken` alone doesn't turn log messages on.

Before 2026-07-28, a client set one level for its whole session with `logging/setLevel`, and FrontMCP sent nothing until it did. Under 2026-07-28 that method is gone.

#### Caveats

- `this.notify()` is a protected method of `ToolContext` (and `AgentContext`). Call it from inside the class.
- **It's opt-in.** Many clients won't ask for log messages, and models usually don't see them. Anything the model needs belongs in the result.
- It isn't the server's log. For your own logs, use `this.logger` ([Logging](https://frontmcp.dev/reference/server/logging)), which writes to the server's output and never reaches the client.
- Under `frontmcp test`, `this.notify()` returns `false` until the test sends `logging/setLevel`; the test client doesn't ask for log messages by itself. The Playground's test client asks for `"debug"` with every `tools/call`.

---

## Usage

### Logging what a tool is doing

Log the steps a person watching the call would want to see. This example turns on **Stream progress & logs** in the Call tab, which asks for `"info"` and up: the `debug` lines are dropped, and the rest appear above the result.

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

const open = new Set(["T-1", "T-2", "T-3"]);

@Tool({
  name: "bulk_close",
  description: "Close several tickets at once. Skips ids that aren't open.",
  inputSchema: { ids: z.array(z.string()).min(1) },
})
export class BulkClose extends ToolContext {
  async execute({ ids }: { ids: string[] }) {
    await this.notify(`Closing ${ids.length} tickets`);
    const closed: string[] = [];
    for (const id of ids) {
      if (!open.has(id)) {
        await this.notify(`Skipped ${id}: it isn't open`, "warning");
        continue;
      }
      await this.notify(`Closing ${id}`, "debug");
      closed.push(id);
    }
    return { closed, skipped: ids.filter((id) => !closed.includes(id)) };
  }
}
```

### Sending structured data

Pass an object when a client might process the message, not only display it. It arrives as `data` without a `message` wrapper.

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

@Tool({ name: "import_tickets", description: "Import tickets from the old help desk", inputSchema: {} })
export class ImportTickets extends ToolContext {
  async execute() {
    const batches = [{ source: "legacy", rows: 120, failed: 0 }, { source: "legacy", rows: 80, failed: 2 }];
    for (const [i, batch] of batches.entries()) {
      await this.notify({ batch: i + 1, ...batch }, batch.failed ? "warning" : "info");
    }
    return { imported: 198, failed: 2 };
  }
}
```

### Filtering by level

Each test asks for a different minimum level, the way a client would, and the tool reports which of its eight messages were sent.

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

const levels = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] as const;

@Tool({ name: "log_every_level", description: "Log one message at each level", inputSchema: {} })
export class LogEveryLevel extends ToolContext {
  async execute() {
    const sent: string[] = [];
    for (const level of levels) {
      if (await this.notify(`A ${level} message`, level)) sent.push(level);
    }
    return { sent };
  }
}
```

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

const callWith = (_meta: Record<string, unknown>) =>
  ({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "log_every_level", arguments: {}, _meta } }) as const;
const callAt = (logLevel: string) => callWith({ "io.modelcontextprotocol/logLevel": logLevel });

test("no logLevel: nothing is sent", async ({ mcp }) => {
  const response = await mcp.raw.request(callWith({}));
  expect(response.result.structuredContent.sent).toEqual([]);
});

test("a progressToken alone: nothing is sent", async ({ mcp }) => {
  const response = await mcp.raw.request(callWith({ progressToken: "progress-1" }));
  expect(response.result.structuredContent.sent).toEqual([]);
});

test("\"info\": everything except debug", async ({ mcp }) => {
  const response = await mcp.raw.request(callAt("info"));
  expect(response.result.structuredContent.sent).toEqual(["info", "notice", "warning", "error", "critical", "alert", "emergency"]);
});

test("\"warning\": warning and above", async ({ mcp }) => {
  const response = await mcp.raw.request(callAt("warning"));
  expect(response.result.structuredContent.sent).toEqual(["warning", "error", "critical", "alert", "emergency"]);
});

test("\"error\": error and above", async ({ mcp }) => {
  const response = await mcp.raw.request(callAt("error"));
  expect(response.result.structuredContent.sent).toEqual(["error", "critical", "alert", "emergency"]);
});

test("\"debug\": all eight", async ({ mcp }) => {
  const response = await mcp.raw.request(callAt("debug"));
  expect(response.result.structuredContent.sent).toHaveLength(8);
});
```

### Testing log messages

In the Playground, `mcp.notifications.collect().received` lists every notification the test client received, in order, with its `method` and `params`.

```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[] }) {
    await this.notify(`Closing ${ids.length} tickets`);
    for (const id of ids) await this.notify(`Closed ${id}`, "debug");
    return { closed: ids };
  }
}
```

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

test("logs a summary, then one debug line per ticket", async ({ mcp }) => {
  await mcp.tools.call("bulk_close", { ids: ["T-1", "T-2"] });
  const logs = mcp.notifications.collect().received.filter((n) => n.method === "notifications/message");
  expect(logs.map((n) => n.params)).toEqual([
    { level: "info", logger: "bulk_close", data: { message: "Closing 2 tickets" } },
    { level: "debug", logger: "bulk_close", data: { message: "Closed T-1" } },
    { level: "debug", logger: "bulk_close", data: { message: "Closed T-2" } },
  ]);
});
```

---

## Troubleshooting

### `this.notify()` returns `false`, and the client shows nothing

Either the request had no `io.modelcontextprotocol/logLevel`, or the message's level is below it. In the Playground, tick **Stream progress & logs** in the Call tab. In your own client, add the level to the request's `_meta`.

### `debug` messages don't appear in the Call tab

The Call tab asks for `"info"` and up, like many clients. The tests ask for `"debug"`, so they receive everything. See [Filtering by level](#filtering-by-level).

### Error `-32601`: "logging/setLevel was removed in protocol 2026-07-28"

A client sent `logging/setLevel`, which 2026-07-28 removed. Send the level with each request instead:

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

@Tool({ name: "ping", description: "Log a message and reply", inputSchema: {} })
export class Ping extends ToolContext {
  async execute() {
    return { logged: await this.notify("pong") };
  }
}
```

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

test("logging/setLevel is gone", async ({ mcp }) => {
  const response = await mcp.raw.request({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } });
  expect(response).toHaveErrorCode(-32601);
  expect(response.error.message).toContain("removed in protocol 2026-07-28");
});

test("the level goes in each request instead", async ({ mcp }) => {
  const response = await mcp.raw.request({
    jsonrpc: "2.0",
    id: 2,
    method: "tools/call",
    params: { name: "ping", arguments: {}, _meta: { "io.modelcontextprotocol/logLevel": "info" } },
  });
  expect(response.result.structuredContent).toEqual({ logged: true });
});
```

### My log lines appear in the server's output, not the client

`this.logger` and `console.log` write to the server's own output. Use `this.notify()` for messages meant for the client.

### `collect().received` is empty under `frontmcp test`

The test client doesn't ask for log messages by itself, and they can arrive after `tools.call()` returns. Send `logging/setLevel` before the call, and wait with `waitFor()` before you check. See [Checking notifications](https://frontmcp.dev/reference/testing#checking-notifications).
