this.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.
const sent = await this.notify(message, level?)
Reference
this.notify(message, level?)
Call this.notify() inside a tool's execute().
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 };
}
}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
{
"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 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(). 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 ofToolContext(andAgentContext). 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()returnsfalseuntil the test sendslogging/setLevel; the test client doesn't ask for log messages by itself. The Playground's test client asks for"debug"with everytools/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.
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.
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.
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.
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:
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.