internal/mcp: implement logging

Provide a ServerSession method that directly implements the spec
method.

Also provide a custom slog.Handler that writes a notification on each
log call. It uses a slog.JSONHandler to format the message as JSON, then
adds the additional fields of LoggingMessageParams.

I decided that the direct method should handle levels, since that is
in the spec, but that rate-limiting is an extra feature, so is only
provided in the slog handler.

Change-Id: I1190ca124e992e61fb633ac52ee3492c8e6d99c9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/674498
Auto-Submit: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Sam Thanawalla <samthanawalla@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Jonathan Amsterdam 2025-05-12 08:35:02 -04:00 • committed by Gopher Robot
commit edda031578
7 changed files with 416 additions and 42 deletions

View file

@ -823,6 +823,27 @@ Clients call the spec method `Complete` to request completions. Servers automati
### Logging
MCP specifies a notification for servers to log to clients. Server sessions implement this with the `LoggingMessage` method. It honors the minimum log level established by the client session's `SetLevel` call.
As a convenience, we also provide a `slog.Handler` that allows server authors to write logs with the `log/slog` package::
```go
// A LoggingHandler is a [slog.Handler] for MCP.
type LoggingHandler struct {...}
// LoggingHandlerOptions are options for a LoggingHandler.
type LoggingHandlerOptions struct {
// The value for the "logger" field of logging notifications.
LoggerName string
// Limits the rate at which log messages are sent.
// If zero, there is no rate limiting.
MinInterval time.Duration
}
// NewLoggingHandler creates a [LoggingHandler] that logs to the given [ServerSession] using a
// [slog.JSONHandler].
func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingHandler
```
Server-to-client logging is configured with `ServerOptions`:
```go
@ -832,24 +853,24 @@ type ServerOptions {
LoggerName string
// Log notifications to a single ClientSession will not be
// sent more frequently than this duration.
LogInterval time.Duration
LoggingInterval time.Duration
}
```
Server sessions have a field `Logger` holding a `slog.Logger` that writes to the client session. A call to a log method like `Info` is translated to a `LoggingMessageNotification` as follows:
A call to a log method like `Info` is translated to a `LoggingMessageNotification` as follows:
- The attributes and the message populate the "data" property with the output of a `slog.JSONHandler`: The result is always a JSON object, with the key "msg" for the message.
- If the `LoggerName` server option is set, it populates the "logger" property.
- The standard slog levels `Info`, `Debug`, `Warn` and `Error` map to the corresponding levels in the MCP spec. The other spec levels map to integers between the slog levels. For example, "notice" is level 2 because it is between "warning" (slog value 4) and "info" (slog value 0). The `mcp` package defines consts for these levels. To log at the "notice" level, a handler would call `session.Logger.Log(ctx, mcp.LevelNotice, "message")`.
- The standard slog levels `Info`, `Debug`, `Warn` and `Error` map to the corresponding levels in the MCP spec. The other spec levels map to integers between the slog levels. For example, "notice" is level 2 because it is between "warning" (slog value 4) and "info" (slog value 0). The `mcp` package defines consts for these levels. To log at the "notice" level, a handler would call `Log(ctx, mcp.LevelNotice, "message")`.
A client that wishes to receive log messages must provide a handler:
```go
type ClientOptions struct {
...
LogMessageHandler func(context.Context, *ClientSession, *LoggingMessageParams)
LoggingMessageHandler func(context.Context, *ClientSession, *LoggingMessageParams)
}
```

View file

@ -53,6 +53,7 @@ type ClientOptions struct {
ToolListChangedHandler func(context.Context, *ClientSession, *ToolListChangedParams)
PromptListChangedHandler func(context.Context, *ClientSession, *PromptListChangedParams)
ResourceListChangedHandler func(context.Context, *ClientSession, *ResourceListChangedParams)
LoggingMessageHandler func(context.Context, *ClientSession, *LoggingMessageParams)
}
// bind implements the binder[*ClientSession] interface, so that Clients can
@ -208,6 +209,7 @@ var clientMethodInfos = map[string]methodInfo[ClientSession]{
notificationToolListChanged: newMethodInfo(clientMethod((*Client).callToolChangedHandler)),
notificationPromptListChanged: newMethodInfo(clientMethod((*Client).callPromptChangedHandler)),
notificationResourceListChanged: newMethodInfo(clientMethod((*Client).callResourceChangedHandler)),
notificationLoggingMessage: newMethodInfo(clientMethod((*Client).callLoggingHandler)),
}
var _ session[ClientSession] = (*ClientSession)(nil)
@ -273,6 +275,10 @@ func (c *ClientSession) CallTool(ctx context.Context, name string, args map[stri
return standardCall[CallToolResult](ctx, c.conn, methodCallTool, params)
}
func (c *ClientSession) SetLevel(ctx context.Context, params *SetLevelParams) error {
return call(ctx, c.conn, methodSetLevel, params, nil)
}
// NOTE: the following struct should consist of all fields of callToolParams except name and arguments.
// CallToolOptions contains options to [ClientSession.CallTool].
@ -302,6 +308,13 @@ func (c *Client) callResourceChangedHandler(ctx context.Context, s *ClientSessio
return callNotificationHandler(ctx, c.opts.ResourceListChangedHandler, s, params)
}
func (c *Client) callLoggingHandler(ctx context.Context, cs *ClientSession, params *LoggingMessageParams) (any, error) {
if h := c.opts.LoggingMessageHandler; h != nil {
h(ctx, cs, params)
}
return nil, nil
}
func standardCall[TRes, TParams any](ctx context.Context, conn *jsonrpc2.Connection, method string, params TParams) (*TRes, error) {
var result TRes
if err := call(ctx, conn, method, params, &result); err != nil {

View file

@ -19,6 +19,7 @@ import (
"go/format"
"io"
"log"
"maps"
"net/http"
"os"
"reflect"
@ -110,7 +111,18 @@ var declarations = config{
Name: "-",
Fields: config{"Params": {Name: "ListToolsParams"}},
},
"ListToolsResult": {},
"ListToolsResult": {},
"loggingCapabilities": {Substitute: "struct{}"},
"LoggingLevel": {},
"LoggingMessageNotification": {
Name: "-",
Fields: config{
"Params": {
Name: "LoggingMessageParams",
Fields: config{"Data": {Substitute: "any"}},
},
},
},
"ModelHint": {},
"ModelPreferences": {},
"PingRequest": {
@ -153,8 +165,13 @@ var declarations = config{
"Prompts": {Name: "promptCapabilities"},
"Resources": {Name: "resourceCapabilities"},
"Tools": {Name: "toolCapabilities"},
"Logging": {Name: "loggingCapabilities"},
},
},
"SetLevelRequest": {
Name: "-",
Fields: config{"Params": {Name: "SetLevelParams"}},
},
"Tool": {
Fields: config{"InputSchema": {Substitute: "*jsonschema.Schema"}},
},
@ -220,7 +237,7 @@ import (
}
// Write out method names.
fmt.Fprintln(buf, `const (`)
for name, s := range schema.Definitions {
for _, name := range slices.Sorted(maps.Keys(schema.Definitions)) {
prefix := "method"
method, found := strings.CutSuffix(name, "Request")
if !found {
@ -228,7 +245,7 @@ import (
method, found = strings.CutSuffix(name, "Notification")
}
if found {
if ms, ok := s.Properties["method"]; ok {
if ms, ok := schema.Definitions[name].Properties["method"]; ok {
if c := ms.Const; c != nil {
fmt.Fprintf(buf, "%s%s = %q\n", prefix, method, *c)
}
@ -395,9 +412,9 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
fieldTypeSchema = rs
}
needPointer := isStruct(fieldTypeSchema)
// Special case: there are no sampling capabilities defined, but
// we want it to be a struct for future expansion.
if !needPointer && name == "sampling" {
// Special case: there are no sampling or logging capabilities defined,
// but we want them to be structs for future expansion.
if !needPointer && (name == "sampling" || name == "logging") {
needPointer = true
}
if config != nil && config.Fields[export] != nil {

189
mcp/logging.go Normal file
View file

@ -0,0 +1,189 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"bytes"
"cmp"
"context"
"encoding/json"
"log/slog"
"sync"
"time"
)
// Logging levels.
const (
LevelDebug = slog.LevelDebug
LevelInfo = slog.LevelInfo
LevelNotice = (slog.LevelInfo + slog.LevelWarn) / 2
LevelWarning = slog.LevelWarn
LevelError = slog.LevelError
LevelCritical = slog.LevelError + 4
LevelAlert = slog.LevelError + 8
LevelEmergency = slog.LevelError + 12
)
var slogToMCP = map[slog.Level]LoggingLevel{
LevelDebug: "debug",
LevelInfo: "info",
LevelNotice: "notice",
LevelWarning: "warning",
LevelError: "error",
LevelCritical: "critical",
LevelAlert: "alert",
LevelEmergency: "emergency",
}
var mcpToSlog = make(map[LoggingLevel]slog.Level)
func init() {
for sl, ml := range slogToMCP {
mcpToSlog[ml] = sl
}
}
func slogLevelToMCP(sl slog.Level) LoggingLevel {
if ml, ok := slogToMCP[sl]; ok {
return ml
}
return "debug" // for lack of a better idea
}
func mcpLevelToSlog(ll LoggingLevel) slog.Level {
if sl, ok := mcpToSlog[ll]; ok {
return sl
}
// TODO: is there a better default?
return LevelDebug
}
// compareLevels behaves like [cmp.Compare] for [LoggingLevel]s.
func compareLevels(l1, l2 LoggingLevel) int {
return cmp.Compare(mcpLevelToSlog(l1), mcpLevelToSlog(l2))
}
// LoggingHandlerOptions are options for a LoggingHandler.
type LoggingHandlerOptions struct {
// The value for the "logger" field of logging notifications.
LoggerName string
// Limits the rate at which log messages are sent.
// If zero, there is no rate limiting.
MinInterval time.Duration
}
// A LoggingHandler is a [slog.Handler] for MCP.
type LoggingHandler struct {
opts LoggingHandlerOptions
ss *ServerSession
// Ensures that the buffer reset is atomic with the write (see Handle).
// A pointer so that clones share the mutex. See
// https://github.com/golang/example/blob/master/slog-handler-guide/README.md#getting-the-mutex-right.
mu *sync.Mutex
lastMessageSent time.Time // for rate-limiting
buf *bytes.Buffer
handler slog.Handler
}
// NewLoggingHandler creates a [LoggingHandler] that logs to the given [ServerSession] using a
// [slog.JSONHandler].
func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingHandler {
var buf bytes.Buffer
jsonHandler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
// Remove level: it appears in LoggingMessageParams.
if a.Key == slog.LevelKey {
return slog.Attr{}
}
return a
},
})
lh := &LoggingHandler{
ss: ss,
mu: new(sync.Mutex),
buf: &buf,
handler: jsonHandler,
}
if opts != nil {
lh.opts = *opts
}
return lh
}
// Enabled implements [slog.Handler.Enabled] by comparing level to the [ServerSession]'s level.
func (h *LoggingHandler) Enabled(ctx context.Context, level slog.Level) bool {
// This is also checked in ServerSession.LoggingMessage, so checking it here
// is just an optimization that skips building the JSON.
h.ss.mu.Lock()
mcpLevel := h.ss.logLevel
h.ss.mu.Unlock()
return level >= mcpLevelToSlog(mcpLevel)
}
// WithAttrs implements [slog.Handler.WithAttrs].
func (h *LoggingHandler) WithAttrs(as []slog.Attr) slog.Handler {
h2 := *h
h2.handler = h.handler.WithAttrs(as)
return &h2
}
// WithGroup implements [slog.Handler.WithGroup].
func (h *LoggingHandler) WithGroup(name string) slog.Handler {
h2 := *h
h2.handler = h.handler.WithGroup(name)
return &h2
}
// Handle implements [slog.Handler.Handle] by writing the Record to a JSONHandler,
// then calling [ServerSession.LoggingMesssage] with the result.
func (h *LoggingHandler) Handle(ctx context.Context, r slog.Record) error {
err := h.handle(ctx, r)
// TODO(jba): find a way to surface the error.
// The return value will probably be ignored.
return err
}
func (h *LoggingHandler) handle(ctx context.Context, r slog.Record) error {
// Observe the rate limit.
// TODO(jba): use golang.org/x/time/rate. (We can't here because it would require adding
// golang.org/x/time to the go.mod file.)
h.mu.Lock()
skip := time.Since(h.lastMessageSent) < h.opts.MinInterval
h.mu.Unlock()
if skip {
return nil
}
var err error
// Make the buffer reset atomic with the record write.
// We are careful here in the unlikely event that the handler panics.
// We don't want to hold the lock for the entire function, because Notify is
// an I/O operation.
// This can result in out-of-order delivery.
func() {
h.mu.Lock()
defer h.mu.Unlock()
h.buf.Reset()
err = h.handler.Handle(ctx, r)
}()
if err != nil {
return err
}
h.mu.Lock()
h.lastMessageSent = time.Now()
h.mu.Unlock()
params := &LoggingMessageParams{
Logger: h.opts.LoggerName,
Level: slogLevelToMCP(r.Level),
Data: json.RawMessage(h.buf.Bytes()),
}
// We pass the argument context to Notify, even though slog.Handler.Handle's
// documentation says not to.
// In this case logging is a service to clients, not a means for debugging the
// server, so we want to cancel the log message.
return h.ss.LoggingMessage(ctx, params)
}

View file

@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"path/filepath"
"runtime"
"slices"
@ -104,6 +105,7 @@ func TestEndToEnd(t *testing.T) {
clientWG.Done()
}()
loggingMessages := make(chan *LoggingMessageParams, 100) // big enough for all logging
opts := &ClientOptions{
CreateMessageHandler: func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error) {
return &CreateMessageResult{Model: "aModel"}, nil
@ -111,6 +113,9 @@ func TestEndToEnd(t *testing.T) {
ToolListChangedHandler: func(context.Context, *ClientSession, *ToolListChangedParams) { notificationChans["tools"] <- 0 },
PromptListChangedHandler: func(context.Context, *ClientSession, *PromptListChangedParams) { notificationChans["prompts"] <- 0 },
ResourceListChangedHandler: func(context.Context, *ClientSession, *ResourceListChangedParams) { notificationChans["resources"] <- 0 },
LoggingMessageHandler: func(_ context.Context, _ *ClientSession, lm *LoggingMessageParams) {
loggingMessages <- lm
},
}
c := NewClient("testClient", "v1.0.0", opts)
rootAbs, err := filepath.Abs(filepath.FromSlash("testdata/files"))
@ -319,6 +324,85 @@ func TestEndToEnd(t *testing.T) {
t.Errorf("got %q, want %q", g, w)
}
})
t.Run("logging", func(t *testing.T) {
want := []*LoggingMessageParams{
{
Logger: "test",
Level: "warning",
Data: map[string]any{
"msg": "first",
"name": "Pat",
"logtest": true,
},
},
{
Logger: "test",
Level: "alert",
Data: map[string]any{
"msg": "second",
"count": 2.0,
"logtest": true,
},
},
}
check := func(t *testing.T) {
t.Helper()
var got []*LoggingMessageParams
// Read messages from this test until we've seen all we expect.
for len(got) < len(want) {
select {
case p := <-loggingMessages:
// Ignore logging from other tests.
if m, ok := p.Data.(map[string]any); ok && m["logtest"] != nil {
delete(m, "time") // remove time because it changes
got = append(got, p)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for log messages")
}
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
}
t.Run("direct", func(t *testing.T) { // Use the LoggingMessage method directly.
mustLog := func(level LoggingLevel, data any) {
t.Helper()
if err := ss.LoggingMessage(ctx, &LoggingMessageParams{
Logger: "test",
Level: level,
Data: data,
}); err != nil {
t.Fatal(err)
}
}
// Nothing should be logged until the client sets a level.
mustLog("info", "before")
if err := cs.SetLevel(ctx, &SetLevelParams{Level: "warning"}); err != nil {
t.Fatal(err)
}
mustLog("warning", want[0].Data)
mustLog("debug", "nope") // below the level
mustLog("info", "negative") // below the level
mustLog("alert", want[1].Data)
check(t)
})
t.Run("handler", func(t *testing.T) { // Use the slog handler.
// We can't check the "before SetLevel" behavior because it's already been set.
// Not a big deal: that check is in LoggingMessage anyway.
logger := slog.New(NewLoggingHandler(ss, &LoggingHandlerOptions{LoggerName: "test"}))
logger.Warn("first", "name", "Pat", "logtest", true)
logger.Debug("nope") // below the level
logger.Info("negative") // below the level
logger.Log(ctx, LevelAlert, "second", "count", 2, "logtest", true)
check(t)
})
})
// Disconnect.
cs.Close()

View file

@ -241,6 +241,22 @@ type ListToolsResult struct {
Tools []*Tool `json:"tools"`
}
// The severity of a log message.
//
// These map to syslog message severities, as specified in RFC-5424:
// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
type LoggingLevel string
type LoggingMessageParams struct {
// The data to be logged, such as a string message or an object. Any JSON
// serializable type is allowed here.
Data any `json:"data"`
// The severity of this log message.
Level LoggingLevel `json:"level"`
// An optional name of the logger issuing this message.
Logger string `json:"logger,omitempty"`
}
// Hints to use for model selection.
//
// Keys not declared here are currently left unspecified by the spec and are up
@ -326,8 +342,7 @@ type PromptArgument struct {
type PromptListChangedParams struct {
// This parameter name is reserved by MCP to allow clients and servers to attach
// additional metadata to their notifications.
Meta struct {
} `json:"_meta,omitempty"`
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
// Describes a message returned as part of a prompt.
@ -381,8 +396,7 @@ type Resource struct {
type ResourceListChangedParams struct {
// This parameter name is reserved by MCP to allow clients and servers to attach
// additional metadata to their notifications.
Meta struct {
} `json:"_meta,omitempty"`
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
// The sender or recipient of messages and data in a conversation.
@ -403,8 +417,7 @@ type Root struct {
type RootsListChangedParams struct {
// This parameter name is reserved by MCP to allow clients and servers to attach
// additional metadata to their notifications.
Meta struct {
} `json:"_meta,omitempty"`
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
// Present if the client supports sampling from an LLM.
@ -417,6 +430,13 @@ type SamplingMessage struct {
Role Role `json:"role"`
}
type SetLevelParams struct {
// The level of logging that the client wants to receive from the server. The
// server should send all logs at this level and higher (i.e., more severe) to
// the client as notifications/message.
Level LoggingLevel `json:"level"`
}
// Definition for a tool the client can call.
type Tool struct {
// Optional additional tool information.
@ -472,8 +492,7 @@ type ToolAnnotations struct {
type ToolListChangedParams struct {
// This parameter name is reserved by MCP to allow clients and servers to attach
// additional metadata to their notifications.
Meta struct {
} `json:"_meta,omitempty"`
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}
// Describes the name and version of an MCP implementation.
@ -482,6 +501,10 @@ type implementation struct {
Version string `json:"version"`
}
// Present if the server supports sending log messages to the client.
type loggingCapabilities struct {
}
// Present if the server offers any prompt templates.
type promptCapabilities struct {
// Whether this server supports notifications for changes to the prompt list.
@ -507,8 +530,7 @@ type serverCapabilities struct {
Experimental map[string]struct {
} `json:"experimental,omitempty"`
// Present if the server supports sending log messages to the client.
Logging struct {
} `json:"logging,omitempty"`
Logging *loggingCapabilities `json:"logging,omitempty"`
// Present if the server offers any prompt templates.
Prompts *promptCapabilities `json:"prompts,omitempty"`
// Present if the server offers any resources to read.
@ -524,28 +546,28 @@ type toolCapabilities struct {
}
const (
methodCreateMessage = "sampling/createMessage"
notificationToolListChanged = "notifications/tools/list_changed"
notificationResourceListChanged = "notifications/resources/list_changed"
methodListPrompts = "prompts/list"
notificationPromptListChanged = "notifications/prompts/list_changed"
notificationResourceUpdated = "notifications/resources/updated"
notificationCancelled = "notifications/cancelled"
methodSetLevel = "logging/setLevel"
methodInitialize = "initialize"
methodListRoots = "roots/list"
notificationProgress = "notifications/progress"
methodGetPrompt = "prompts/get"
methodListResourceTemplates = "resources/templates/list"
methodPing = "ping"
methodComplete = "completion/complete"
methodListResources = "resources/list"
notificationInitialized = "notifications/initialized"
methodCallTool = "tools/call"
notificationLoggingMessage = "notifications/message"
methodReadResource = "resources/read"
notificationCancelled = "notifications/cancelled"
methodComplete = "completion/complete"
methodCreateMessage = "sampling/createMessage"
methodGetPrompt = "prompts/get"
methodInitialize = "initialize"
notificationInitialized = "notifications/initialized"
methodListPrompts = "prompts/list"
methodListResourceTemplates = "resources/templates/list"
methodListResources = "resources/list"
methodListRoots = "roots/list"
methodListTools = "tools/list"
notificationLoggingMessage = "notifications/message"
methodPing = "ping"
notificationProgress = "notifications/progress"
notificationPromptListChanged = "notifications/prompts/list_changed"
methodReadResource = "resources/read"
notificationResourceListChanged = "notifications/resources/list_changed"
notificationResourceUpdated = "notifications/resources/updated"
notificationRootsListChanged = "notifications/roots/list_changed"
methodSetLevel = "logging/setLevel"
methodSubscribe = "resources/subscribe"
notificationToolListChanged = "notifications/tools/list_changed"
methodUnsubscribe = "resources/unsubscribe"
)

View file

@ -299,11 +299,11 @@ func (s *Server) Run(ctx context.Context, t Transport) error {
// bind implements the binder[*ServerSession] interface, so that Servers can
// be connected using [connect].
func (s *Server) bind(conn *jsonrpc2.Connection) *ServerSession {
cc := &ServerSession{conn: conn, server: s}
ss := &ServerSession{conn: conn, server: s}
s.mu.Lock()
s.sessions = append(s.sessions, cc)
s.sessions = append(s.sessions, ss)
s.mu.Unlock()
return cc
return ss
}
// disconnect implements the binder[*ServerSession] interface, so that
@ -341,17 +341,17 @@ func (s *Server) callRootsListChangedHandler(ctx context.Context, ss *ServerSess
// Call [ServerSession.Close] to close the connection, or await client
// termination with [ServerSession.Wait].
type ServerSession struct {
server *Server
conn *jsonrpc2.Connection
server *Server
conn *jsonrpc2.Connection
mu sync.Mutex
logLevel LoggingLevel
initializeParams *InitializeParams
initialized bool
}
// Ping pings the client.
func (ss *ServerSession) Ping(ctx context.Context, _ *PingParams) error {
return call(ctx, ss.conn, "ping", (*PingParams)(nil), nil)
return call(ctx, ss.conn, methodPing, (*PingParams)(nil), nil)
}
// ListRoots lists the client roots.
@ -364,6 +364,25 @@ func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessag
return standardCall[CreateMessageResult](ctx, ss.conn, methodCreateMessage, params)
}
// LoggingMessage sends a logging message to the client.
// The message is not sent if the client has not called SetLevel, or if its level
// is below that of the last SetLevel.
func (ss *ServerSession) LoggingMessage(ctx context.Context, params *LoggingMessageParams) error {
ss.mu.Lock()
logLevel := ss.logLevel
ss.mu.Unlock()
if logLevel == "" {
// The spec is unclear, but seems to imply that no log messages are sent until the client
// sets the level.
// TODO(jba): read other SDKs, possibly file an issue.
return nil
}
if compareLevels(params.Level, logLevel) < 0 {
return nil
}
return ss.conn.Notify(ctx, notificationLoggingMessage, params)
}
// AddMiddleware wraps the server's current method handler using the provided
// middleware. Middleware is applied from right to left, so that the first one
// is executed first.
@ -386,6 +405,7 @@ var serverMethodInfos = map[string]methodInfo[ServerSession]{
methodCallTool: newMethodInfo(serverMethod((*Server).callTool)),
methodListResources: newMethodInfo(serverMethod((*Server).listResources)),
methodReadResource: newMethodInfo(serverMethod((*Server).readResource)),
methodSetLevel: newMethodInfo(sessionMethod((*ServerSession).setLevel)),
notificationInitialized: newMethodInfo(serverMethod((*Server).callInitializedHandler)),
notificationRootsListChanged: newMethodInfo(serverMethod((*Server).callRootsListChangedHandler)),
}
@ -457,6 +477,7 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam
Resources: &resourceCapabilities{
ListChanged: true,
},
Logging: &loggingCapabilities{},
},
Instructions: ss.server.opts.Instructions,
ServerInfo: &implementation{
@ -470,6 +491,13 @@ func (ss *ServerSession) ping(context.Context, *PingParams) (struct{}, error) {
return struct{}{}, nil
}
func (ss *ServerSession) setLevel(_ context.Context, params *SetLevelParams) (struct{}, error) {
ss.mu.Lock()
defer ss.mu.Unlock()
ss.logLevel = params.Level
return struct{}{}, nil
}
// Close performs a graceful shutdown of the connection, preventing new
// requests from being handled, and waiting for ongoing requests to return.
// Close then terminates the connection.