this.notify

MCP 2026-07-28

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.

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

Reference

this.notify(message, level?)

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

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.

Parameters

ParameterTypeDescription
messagestring | Record<string, unknown>What to log. A string arrives as { "message": "..." }; an object arrives as it is.
levelMcpLoggingLevelOptional. How important the message is. Default "info".

The eight levels, from lowest to highest:

LevelUse 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

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

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

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

logLevel in the requestLevels that are sent
nonenone
"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(). 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), 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.

Open
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)) };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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.

Open
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 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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.

Open
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 };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Testing log messages

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

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

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.


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.

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:

Open
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") };
  }
}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

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.