The MCP Handbook

Chapter 18: Extensions - Notifications (Push Messages)

While standard MCP is based on a request-response model, Notifications allow one side (server or client) to send information to the other without waiting for a response. This is the "push" principle of MCP.

1. What Are Notifications?

In MCP, notifications are asynchronous messages. They have no request_id because no response is expected or possible. They serve to synchronize state or report events.

Standard Notification Types (Server -> Client)

  • notifications/message: Sends log data to the client.
  • notifications/progress: Reports progress on a running operation.
  • notifications/resources/list_changed: Signals that the list of available resources has changed.
  • notifications/prompts/list_changed: Signals changes to the prompts.

2. Implementation in the Server (Go)

A server sends notifications via the Session object, which is included in the tool request.

Example: Reporting Progress

func handleLongRun(ctx context.Context, req *mcp.CallToolRequest, args any) (*mcp.CallToolResult, any, error) {
    token := req.Params.GetProgressToken()
    if token != nil {
        req.Session.NotifyProgress(ctx, &mcp.ProgressNotificationParams{
            Progress:      25.0,
            Total:         100.0,
            Message:       "A quarter done...",
            ProgressToken: token,
        })
    }
    return mcp.NewToolResultText("Done!"), nil, nil
}

3. Implementation in the Client (our mcp-tester)

For a client to be able to process notifications, it must register corresponding handlers when starting the session. Our mcp-tester uses ClientOptions for that.

Here is an excerpt from transport.go:

func getClient(verbose bool) *mcp.Client {
    opts := &mcp.ClientOptions{
        // Handler for log messages
        LoggingMessageHandler: func(ctx context.Context, req *mcp.LoggingMessageRequest) {
            fmt.Printf("[SERVER LOG] [%s] %s: %v\n", 
                req.Params.Level, req.Params.Logger, req.Params.Data)
        },
        // Handler for progress updates
        ProgressNotificationHandler: func(ctx context.Context, req *mcp.ProgressNotificationClientRequest) {
            fmt.Printf("[PROGRESS] Token: %v, %.2f%%: %s\n", 
                req.Params.ProgressToken, (req.Params.Progress/req.Params.Total)*100, req.Params.Message)
        },
    }
    // ... create the client ...
}

Why Is This Important?

Without these handlers, the client would simply ignore incoming JSON-RPC notifications. With the handlers, we can give the user real-time feedback while they wait for the result of a long-running tool call.


4. Detecting Support

Whether a server or client supports notifications is negotiated during the initialize step via capabilities:

  • Server capability: logging: {} means the server will send logs.
  • Client capability: roots: { listChanged: true } means the client wants to be informed about changes to the workspace roots.

In mcp-tester you can observe this negotiation with the inspect command and the verbose flag (-v).


5. Request Cancellation: Aborting Tasks

A particularly powerful aspect of notifications is request cancellation. It allows the client to abort a request that has already been sent but not yet completed (e.g. a long tool call).

The Mechanism: `$/cancelRequest`

When a client wants to cancel a request, it sends a notification of type $/cancelRequest with the original requestId.

Implementation in Go (Server Side)

Our Go library makes handling cancellations extremely simple, since it is based directly on the standard Go pattern context.Context. When a client sends a cancellation, the ctx passed to the tool handler is automatically "cancelled".

Example of clean cancellation:

func handleCalculations(ctx context.Context, req *mcp.CallToolRequest, args any) (*mcp.CallToolResult, any, error) {
    for i := 0; i < 100; i++ {
        // IMPORTANT: check whether the chat/agent has been cancelled
        select {
        case <-ctx.Done():
            // Cleanup and exit quickly
            return nil, nil, ctx.Err()
        default:
            // Continue working...
            doHeavyWork()
        }
    }
    return mcp.NewToolResultText("Done"), nil, nil
}

Why Is This Important?

Without cancellation, long-running tools would consume valuable server resources (CPU, memory), even when the agent no longer needs the response (e.g. because the user closed the chat or asked a different question). In a scaled environment this is essential for performance and cost efficiency.


Conclusion

Notifications make MCP come alive. From simple logging to progress bars to hard task cancellation (cancelRequest) - they turn a static API into an interactive interface that shows the user (and the model) what is happening "behind the scenes".

← Chapter 17: Security & Authentication | Table of Contents | Next Chapter: Extensions - Tasks →


Copyright Michael Lechner - 2026-03-01

Licence: CC BY-NC 4.0