mcp/server: enable server logging (#501)

mcp/server.go: implement server‑side logging

- Thread a *slog.Logger through server paths to improve debuggability
- Introduce no‑op discard handler + ensureLogger so loggers are never
nil
- Use ensured loggers in Server
- address todo comments related to adding logging support

All tests are passing and no functionality should change with this PR

Fixes https://github.com/modelcontextprotocol/go-sdk/issues/170
This commit is contained in:
Suraj Bobade 2025-10-03 00:14:32 +05:30 • committed by GitHub
commit 1dcbf62661
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -12,6 +12,7 @@ import (
"encoding/json"
"fmt"
"iter"
"log/slog"
"maps"
"net/url"
"path/filepath"
@ -54,6 +55,8 @@ type Server struct {
type ServerOptions struct {
// Optional instructions for connected clients.
Instructions string
// If non-nil, log server activity.
Logger *slog.Logger
// If non-nil, called when "notifications/initialized" is received.
InitializedHandler func(context.Context, *InitializedRequest)
// PageSize is the maximum number of items to return in a single page for
@ -130,6 +133,10 @@ func NewServer(impl *Implementation, options *ServerOptions) *Server {
opts.GetSessionID = randText
}
if opts.Logger == nil { // ensure we have a logger
opts.Logger = ensureLogger(nil)
}
return &Server{
impl: impl,
opts: opts,
@ -695,6 +702,7 @@ func (s *Server) ResourceUpdated(ctx context.Context, params *ResourceUpdatedNot
sessions := slices.Collect(maps.Keys(subscribedSessions))
s.mu.Unlock()
notifySessions(sessions, notificationResourceUpdated, params)
s.opts.Logger.Info("resource updated notification sent", "uri", params.URI, "subscriber_count", len(sessions))
return nil
}
@ -712,6 +720,7 @@ func (s *Server) subscribe(ctx context.Context, req *SubscribeRequest) (*emptyRe
s.resourceSubscriptions[req.Params.URI] = make(map[*ServerSession]bool)
}
s.resourceSubscriptions[req.Params.URI][req.Session] = true
s.opts.Logger.Info("resource subscribed", "uri", req.Params.URI, "session_id", req.Session.ID())
return &emptyResult{}, nil
}
@ -733,6 +742,7 @@ func (s *Server) unsubscribe(ctx context.Context, req *UnsubscribeRequest) (*emp
delete(s.resourceSubscriptions, req.Params.URI)
}
}
s.opts.Logger.Info("resource unsubscribed", "uri", req.Params.URI, "session_id", req.Session.ID())
return &emptyResult{}, nil
}
@ -751,8 +761,10 @@ func (s *Server) unsubscribe(ctx context.Context, req *UnsubscribeRequest) (*emp
// It need not be called on servers that are used for multiple concurrent connections,
// as with [StreamableHTTPHandler].
func (s *Server) Run(ctx context.Context, t Transport) error {
s.opts.Logger.Info("server run start")
ss, err := s.Connect(ctx, t, nil)
if err != nil {
s.opts.Logger.Error("server connect failed", "error", err)
return err
}
@ -764,8 +776,14 @@ func (s *Server) Run(ctx context.Context, t Transport) error {
select {
case <-ctx.Done():
ss.Close()
s.opts.Logger.Error("server run cancelled", "error", ctx.Err())
return ctx.Err()
case err := <-ssClosed:
if err != nil {
s.opts.Logger.Error("server session ended with error", "error", err)
} else {
s.opts.Logger.Info("server session ended")
}
return err
}
}
@ -781,6 +799,7 @@ func (s *Server) bind(mcpConn Connection, conn *jsonrpc2.Connection, state *Serv
s.mu.Lock()
s.sessions = append(s.sessions, ss)
s.mu.Unlock()
s.opts.Logger.Info("server session connected", "session_id", ss.ID())
return ss
}
@ -796,6 +815,7 @@ func (s *Server) disconnect(cc *ServerSession) {
for _, subscribedSessions := range s.resourceSubscriptions {
delete(subscribedSessions, cc)
}
s.opts.Logger.Info("server session disconnected", "session_id", cc.ID())
}
// ServerSessionOptions configures the server session.
@ -820,7 +840,14 @@ func (s *Server) Connect(ctx context.Context, t Transport, opts *ServerSessionOp
state = opts.State
onClose = opts.onClose
}
return connect(ctx, t, s, state, onClose)
s.opts.Logger.Info("server connecting")
ss, err := connect(ctx, t, s, state, onClose)
if err != nil {
s.opts.Logger.Error("server connect error", "error", err)
return nil, err
}
return ss, nil
}
// TODO: (nit) move all ServerSession methods below the ServerSession declaration.
@ -840,9 +867,11 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar
})
if !wasInit {
ss.server.opts.Logger.Error("initialized before initialize")
return nil, fmt.Errorf("%q before %q", notificationInitialized, methodInitialize)
}
if wasInitd {
ss.server.opts.Logger.Error("duplicate initialized notification")
return nil, fmt.Errorf("duplicate %q received", notificationInitialized)
}
if ss.server.opts.KeepAlive > 0 {
@ -851,6 +880,7 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar
if h := ss.server.opts.InitializedHandler; h != nil {
h(ctx, serverRequestFor(ss, params))
}
ss.server.opts.Logger.Info("session initialized")
return nil, nil
}
@ -1088,6 +1118,7 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
case methodInitialize, methodPing, notificationInitialized:
default:
if !initialized {
ss.server.opts.Logger.Error("method invalid during initialization", "method", req.Method)
return nil, fmt.Errorf("method %q is invalid during session initialization", req.Method)
}
}
@ -1144,6 +1175,7 @@ func (ss *ServerSession) setLevel(_ context.Context, params *SetLoggingLevelPara
ss.updateState(func(state *ServerSessionState) {
state.LogLevel = params.Level
})
ss.server.opts.Logger.Info("client log level set", "level", params.Level)
return &emptyResult{}, nil
}