From c8904827594f1179498e508175ccf5b9358ead3d Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Thu, 18 Sep 2025 15:26:00 -0400 Subject: [PATCH 1/2] doc: document logging --- docs/client.md | 5 ++- docs/server.md | 82 ++++++++++++++++++++++++++++++++++++- internal/docs/server.src.md | 29 ++++++++++++- mcp/logging.go | 1 + mcp/server_example_test.go | 58 ++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 3 deletions(-) diff --git a/docs/client.md b/docs/client.md index cbc4db8f..2cbe082c 100644 --- a/docs/client.md +++ b/docs/client.md @@ -56,9 +56,12 @@ func Example_roots() { if _, err := s.Connect(ctx, t1, nil); err != nil { log.Fatal(err) } - if _, err := c.Connect(ctx, t2, nil); err != nil { + + clientSession, err := c.Connect(ctx, t2, nil) + if err != nil { log.Fatal(err) } + defer clientSession.Close() // ...and add a root. The server is notified about the change. c.AddRoots(&mcp.Root{URI: "file://b"}) diff --git a/docs/server.md b/docs/server.md index f59e2c7e..b683d5e4 100644 --- a/docs/server.md +++ b/docs/server.md @@ -73,6 +73,7 @@ func Example_prompts() { if err != nil { log.Fatal(err) } + defer cs.Close() // List the prompts. for p, err := range cs.Prompts(ctx, nil) { @@ -157,7 +158,86 @@ _ = mcp.NewServer(&mcp.Implementation{Name: "server"}, &mcp.ServerOptions{ ### Logging - +MCP servers can send logging messages to MCP clients so their users can keep informed of progress. +(This form of logging is distinct from server-side logging, where the +server produces logs that remain server-side, for use by server maintainers.) + +**Server-side**: +The minimum log level is part of the server state. +For stateful sessions, there is no default log level: no log messages will be sent +until the client calls `SetLevel` (see below). +For stateful sessions, the level defaults to "info". + +[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Log) is the low-level way for servers to log to clients. +It sends a logging notification to the client if the level of the message +is at least the minimum log level. + +For a simpler API, use [`NewLoggingHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#NewLoggingHandler) to obtain a [`slog.Handler`](https://pkg.go.dev/log/slog#Handler). +By setting [`LoggingHandlerOptions.MinInterval`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#LoggingHandlerOptions.MinInterval), the handler can be rate-limited +to avoid spamming clients with too many messages. + +Servers always report the logging capability. + + +**Client-side**: + +Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages. + +Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session. + +```go +func Example_logging() { + ctx := context.Background() + + // Create a server. + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + + // Create a client that displays log messages. + c := mcp.NewClient( + &mcp.Implementation{Name: "client", Version: "v0.0.1"}, + &mcp.ClientOptions{ + LoggingMessageHandler: func(_ context.Context, r *mcp.LoggingMessageRequest) { + m := r.Params.Data.(map[string]any) + fmt.Println(m["msg"], m["value"]) + }, + }) + + // Connect the server and client. + t1, t2 := mcp.NewInMemoryTransports() + ss, err := s.Connect(ctx, t1, nil) + if err != nil { + log.Fatal(err) + } + defer ss.Close() + cs, err := c.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + defer cs.Close() + + // Set the minimum log level to "info". + if err := cs.SetLoggingLevel(ctx, &mcp.SetLoggingLevelParams{Level: "info"}); err != nil { + log.Fatal(err) + } + + // Get a slog.Logger for the server session. + logger := slog.New(mcp.NewLoggingHandler(ss, nil)) + + // Log some things. + logger.Info("info shows up", "value", 1) + logger.Debug("debug doesn't show up", "value", 2) + logger.Warn("warn shows up", "value", 3) + + // Wait for them to arrive on the client. + // In a real application, the log messages would appear asynchronously + // while other work was happening. + time.Sleep(500 * time.Millisecond) + + // Output: + // info shows up 1 + // warn shows up 3 +} +``` ### Pagination diff --git a/internal/docs/server.src.md b/internal/docs/server.src.md index 50619c60..38972b9c 100644 --- a/internal/docs/server.src.md +++ b/internal/docs/server.src.md @@ -56,7 +56,34 @@ requests. ### Logging - +MCP servers can send logging messages to MCP clients so their users can keep informed of progress. +(This form of logging is distinct from server-side logging, where the +server produces logs that remain server-side, for use by server maintainers.) + +**Server-side**: +The minimum log level is part of the server state. +For stateful sessions, there is no default log level: no log messages will be sent +until the client calls `SetLevel` (see below). +For stateful sessions, the level defaults to "info". + +[`ServerSession.Log`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerSession.Log) is the low-level way for servers to log to clients. +It sends a logging notification to the client if the level of the message +is at least the minimum log level. + +For a simpler API, use [`NewLoggingHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#NewLoggingHandler) to obtain a [`slog.Handler`](https://pkg.go.dev/log/slog#Handler). +By setting [`LoggingHandlerOptions.MinInterval`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#LoggingHandlerOptions.MinInterval), the handler can be rate-limited +to avoid spamming clients with too many messages. + +Servers always report the logging capability. + + +**Client-side**: + +Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages. + +Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session. + +%include ../../mcp/server_example_test.go logging - ### Pagination diff --git a/mcp/logging.go b/mcp/logging.go index 4d33097a..b3186a96 100644 --- a/mcp/logging.go +++ b/mcp/logging.go @@ -70,6 +70,7 @@ type LoggingHandlerOptions struct { // The value for the "logger" field of logging notifications. LoggerName string // Limits the rate at which log messages are sent. + // Excess messages are dropped. // If zero, there is no rate limiting. MinInterval time.Duration } diff --git a/mcp/server_example_test.go b/mcp/server_example_test.go index 16f19e20..17ac2812 100644 --- a/mcp/server_example_test.go +++ b/mcp/server_example_test.go @@ -8,6 +8,8 @@ import ( "context" "fmt" "log" + "log/slog" + "time" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -82,3 +84,59 @@ func Example_prompts() { } // !-prompts + +// !+logging + +func Example_logging() { + ctx := context.Background() + + // Create a server. + s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) + + // Create a client that displays log messages. + c := mcp.NewClient( + &mcp.Implementation{Name: "client", Version: "v0.0.1"}, + &mcp.ClientOptions{ + LoggingMessageHandler: func(_ context.Context, r *mcp.LoggingMessageRequest) { + m := r.Params.Data.(map[string]any) + fmt.Println(m["msg"], m["value"]) + }, + }) + + // Connect the server and client. + t1, t2 := mcp.NewInMemoryTransports() + ss, err := s.Connect(ctx, t1, nil) + if err != nil { + log.Fatal(err) + } + defer ss.Close() + cs, err := c.Connect(ctx, t2, nil) + if err != nil { + log.Fatal(err) + } + defer cs.Close() + + // Set the minimum log level to "info". + if err := cs.SetLoggingLevel(ctx, &mcp.SetLoggingLevelParams{Level: "info"}); err != nil { + log.Fatal(err) + } + + // Get a slog.Logger for the server session. + logger := slog.New(mcp.NewLoggingHandler(ss, nil)) + + // Log some things. + logger.Info("info shows up", "value", 1) + logger.Debug("debug doesn't show up", "value", 2) + logger.Warn("warn shows up", "value", 3) + + // Wait for them to arrive on the client. + // In a real application, the log messages would appear asynchronously + // while other work was happening. + time.Sleep(500 * time.Millisecond) + + // Output: + // info shows up 1 + // warn shows up 3 +} + +// !-logging From fd9d5b94414dd313e769fae8b0ab67f5b06c4c8e Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Fri, 19 Sep 2025 07:47:51 -0400 Subject: [PATCH 2/2] reviewer comments --- docs/server.md | 10 +++++++--- docs/troubleshooting.md | 9 +++++++-- internal/docs/server.src.md | 3 +-- mcp/server_example_test.go | 9 +++++++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/server.md b/docs/server.md index b683d5e4..bd0de2cb 100644 --- a/docs/server.md +++ b/docs/server.md @@ -158,7 +158,7 @@ _ = mcp.NewServer(&mcp.Implementation{Name: "server"}, &mcp.ServerOptions{ ### Logging -MCP servers can send logging messages to MCP clients so their users can keep informed of progress. +MCP servers can send logging messages to MCP clients. (This form of logging is distinct from server-side logging, where the server produces logs that remain server-side, for use by server maintainers.) @@ -180,7 +180,6 @@ Servers always report the logging capability. **Client-side**: - Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages. Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session. @@ -193,12 +192,17 @@ func Example_logging() { s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) // Create a client that displays log messages. + done := make(chan struct{}) // solely for the example + var nmsgs atomic.Int32 c := mcp.NewClient( &mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{ LoggingMessageHandler: func(_ context.Context, r *mcp.LoggingMessageRequest) { m := r.Params.Data.(map[string]any) fmt.Println(m["msg"], m["value"]) + if nmsgs.Add(1) == 2 { // number depends on logger calls below + close(done) + } }, }) @@ -231,7 +235,7 @@ func Example_logging() { // Wait for them to arrive on the client. // In a real application, the log messages would appear asynchronously // while other work was happening. - time.Sleep(500 * time.Millisecond) + <-done // Output: // info shows up 1 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0f990edc..38410ad5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -29,16 +29,21 @@ func ExampleLoggingTransport() { ctx := context.Background() t1, t2 := mcp.NewInMemoryTransports() server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) - if _, err := server.Connect(ctx, t1, nil); err != nil { + serverSession, err := server.Connect(ctx, t1, nil) + if err != nil { log.Fatal(err) } + defer serverSession.Wait() client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) var b bytes.Buffer logTransport := &mcp.LoggingTransport{Transport: t2, Writer: &b} - if _, err := client.Connect(ctx, logTransport, nil); err != nil { + clientSession, err := client.Connect(ctx, logTransport, nil) + if err != nil { log.Fatal(err) } + defer clientSession.Close() + // Sort for stability: reads are concurrent to writes. for _, line := range slices.Sorted(strings.SplitSeq(b.String(), "\n")) { fmt.Println(line) diff --git a/internal/docs/server.src.md b/internal/docs/server.src.md index 38972b9c..3577ea12 100644 --- a/internal/docs/server.src.md +++ b/internal/docs/server.src.md @@ -56,7 +56,7 @@ requests. ### Logging -MCP servers can send logging messages to MCP clients so their users can keep informed of progress. +MCP servers can send logging messages to MCP clients. (This form of logging is distinct from server-side logging, where the server produces logs that remain server-side, for use by server maintainers.) @@ -78,7 +78,6 @@ Servers always report the logging capability. **Client-side**: - Set [`ClientOptions.LoggingMessageHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.LoggingMessageHandler) to receive log messages. Call [`ClientSession.SetLevel`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.SetLevel) to change the log level for a session. diff --git a/mcp/server_example_test.go b/mcp/server_example_test.go index 17ac2812..3d678c3d 100644 --- a/mcp/server_example_test.go +++ b/mcp/server_example_test.go @@ -9,7 +9,7 @@ import ( "fmt" "log" "log/slog" - "time" + "sync/atomic" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -94,12 +94,17 @@ func Example_logging() { s := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) // Create a client that displays log messages. + done := make(chan struct{}) // solely for the example + var nmsgs atomic.Int32 c := mcp.NewClient( &mcp.Implementation{Name: "client", Version: "v0.0.1"}, &mcp.ClientOptions{ LoggingMessageHandler: func(_ context.Context, r *mcp.LoggingMessageRequest) { m := r.Params.Data.(map[string]any) fmt.Println(m["msg"], m["value"]) + if nmsgs.Add(1) == 2 { // number depends on logger calls below + close(done) + } }, }) @@ -132,7 +137,7 @@ func Example_logging() { // Wait for them to arrive on the client. // In a real application, the log messages would appear asynchronously // while other work was happening. - time.Sleep(500 * time.Millisecond) + <-done // Output: // info shows up 1