From 6faee51330ec41a0aa3f68f0e1ad5b8d6b37bd6d Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Sat, 6 Jun 2026 11:02:28 +0000 Subject: [PATCH 1/2] feat: support passing log levels through protocol request metadata --- mcp/client.go | 11 +++++++++-- mcp/protocol.go | 2 ++ mcp/server.go | 7 ++++++- mcp/shared.go | 7 ++++--- mcp/streamable.go | 4 +++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/mcp/client.go b/mcp/client.go index 5f142fb1..df184b17 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -174,6 +174,8 @@ type ClientOptions struct { // reset" guidance, letting a transient miss pass without tearing down an // otherwise live session. Has no effect unless KeepAlive is non-zero. KeepAliveFailureThreshold int + // If set, requests the server to set its log level to the given value. + LogLevel LoggingLevel } // toolContextKeyType is the context key type for passing tool definitions @@ -354,6 +356,7 @@ func (c *Client) discover(ctx context.Context, cs *ClientSession) (*InitializeRe MetaKeyProtocolVersion: protocolVersion, MetaKeyClientInfo: c.impl, MetaKeyClientCapabilities: caps, + MetaKeyLogLevel: c.opts.LogLevel, }, } req := &DiscoverRequest{Session: cs, Params: params} @@ -445,8 +448,9 @@ func (cs *ClientSession) usesNewProtocol() bool { } // injectRequestMeta populates the SEP-2575 per-request `_meta` triple -// (protocolVersion, clientInfo, clientCapabilities) on the given outgoing -// request params. Keys already present in params.Meta are not overwritten. +// (protocolVersion, clientInfo, clientCapabilities) and optional LoggingLevel +// on the given outgoing request params. Keys already present in params.Meta +// are not overwritten. func injectRequestMeta[T any, P interface { *T Params @@ -468,6 +472,9 @@ func injectRequestMeta[T any, P interface { if _, ok := m[MetaKeyClientCapabilities]; !ok { m[MetaKeyClientCapabilities] = cs.client.capabilities(res.ProtocolVersion) } + if _, ok := m[MetaKeyLogLevel]; !ok && cs.client.opts.LogLevel != "" { + m[MetaKeyLogLevel] = cs.client.opts.LogLevel + } params.SetMeta(m) return params } diff --git a/mcp/protocol.go b/mcp/protocol.go index f5b79aeb..2e69e0c7 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -2102,4 +2102,6 @@ const ( MetaKeyClientInfo = "io.modelcontextprotocol/clientInfo" // MetaKeyClientCapabilities carries the client's [ClientCapabilities]. MetaKeyClientCapabilities = "io.modelcontextprotocol/clientCapabilities" + // MetaKeyLogLevel identifies the desired log level for the request. + MetaKeyLogLevel = "io.modelcontextprotocol/logLevel" ) diff --git a/mcp/server.go b/mcp/server.go index ba03fbfe..01a59f7c 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -1498,7 +1498,7 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, } switch req.Method { - case methodInitialize, methodPing, notificationInitialized: + case methodInitialize, methodPing, notificationInitialized, methodSetLevel: if validatedMeta.usesNewProtocol { ss.server.opts.Logger.Error("method removed in the new protocol", "method", req.Method) return nil, &jsonrpc.Error{ @@ -1520,6 +1520,11 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, state.InitializeParams = validatedMeta.initializeParams }) } + if validatedMeta.usesNewProtocol && validatedMeta.logLevel != "" { + ss.updateState(func(state *ServerSessionState) { + state.LogLevel = validatedMeta.logLevel + }) + } } // modelcontextprotocol/go-sdk#26: handle calls asynchronously, and diff --git a/mcp/shared.go b/mcp/shared.go index a0232d37..66c57e15 100644 --- a/mcp/shared.go +++ b/mcp/shared.go @@ -487,6 +487,7 @@ func extractRequestMeta(rawParams json.RawMessage) Meta { type validatedMeta struct { usesNewProtocol bool initializeParams *InitializeParams + logLevel LoggingLevel } // validateRequestMeta inspects a JSON-RPC request to detect whether it follows @@ -507,8 +508,7 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) { if !ok { return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil } - // Notifications do not carry full client identity. In new protocol, only cancel notification - // is allowed in STDIO. + // Notifications do not carry full client identity. if !req.IsCall() { return &validatedMeta{usesNewProtocol: true, initializeParams: nil}, nil } @@ -526,11 +526,12 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) { Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientCapabilities), } } + logLevel, _ := meta[MetaKeyLogLevel].(LoggingLevel) return &validatedMeta{usesNewProtocol: true, initializeParams: &InitializeParams{ ProtocolVersion: protocolVersion, Capabilities: capabilities, ClientInfo: clientInfo, - }}, nil + }, logLevel: logLevel}, nil } // A Request is a method request with parameters and additional information, such as the session. diff --git a/mcp/streamable.go b/mcp/streamable.go index e6a9bfe5..a46c984c 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -441,7 +441,9 @@ func (h *StreamableHTTPHandler) ephemeralConnectOpts(req *http.Request) (opts *S if !hasInitialized && !usesNewProtocol { state.InitializedParams = new(InitializedParams) } - state.LogLevel = "info" + if !usesNewProtocol { + state.LogLevel = "info" + } return &ServerSessionOptions{ State: state, }, usesNewProtocol, nil From 75c4219ee4fd702e0a837c7fc9af7e0e02d02b21 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Mon, 8 Jun 2026 09:52:03 +0000 Subject: [PATCH 2/2] refactor: scope logging level to individual requests instead of session state --- mcp/client.go | 11 ++--------- mcp/server.go | 34 ++++++++++++++++++++++++---------- mcp/shared.go | 2 +- mcp/shared_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/mcp/client.go b/mcp/client.go index df184b17..5f142fb1 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -174,8 +174,6 @@ type ClientOptions struct { // reset" guidance, letting a transient miss pass without tearing down an // otherwise live session. Has no effect unless KeepAlive is non-zero. KeepAliveFailureThreshold int - // If set, requests the server to set its log level to the given value. - LogLevel LoggingLevel } // toolContextKeyType is the context key type for passing tool definitions @@ -356,7 +354,6 @@ func (c *Client) discover(ctx context.Context, cs *ClientSession) (*InitializeRe MetaKeyProtocolVersion: protocolVersion, MetaKeyClientInfo: c.impl, MetaKeyClientCapabilities: caps, - MetaKeyLogLevel: c.opts.LogLevel, }, } req := &DiscoverRequest{Session: cs, Params: params} @@ -448,9 +445,8 @@ func (cs *ClientSession) usesNewProtocol() bool { } // injectRequestMeta populates the SEP-2575 per-request `_meta` triple -// (protocolVersion, clientInfo, clientCapabilities) and optional LoggingLevel -// on the given outgoing request params. Keys already present in params.Meta -// are not overwritten. +// (protocolVersion, clientInfo, clientCapabilities) on the given outgoing +// request params. Keys already present in params.Meta are not overwritten. func injectRequestMeta[T any, P interface { *T Params @@ -472,9 +468,6 @@ func injectRequestMeta[T any, P interface { if _, ok := m[MetaKeyClientCapabilities]; !ok { m[MetaKeyClientCapabilities] = cs.client.capabilities(res.ProtocolVersion) } - if _, ok := m[MetaKeyLogLevel]; !ok && cs.client.opts.LogLevel != "" { - m[MetaKeyLogLevel] = cs.client.opts.LogLevel - } params.SetMeta(m) return params } diff --git a/mcp/server.go b/mcp/server.go index 01a59f7c..5f3cbc00 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -1367,13 +1367,28 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli return res, nil } +// logLevelContextKey carries the per-request log level from +// [ServerSession.handle] to [ServerSession.Log] for new-protocol +// (>= 2026-06-30) requests. The level is scoped to a single in-flight request +// — including handler goroutines that call [ServerSession.Log] concurrently — +// rather than to the session, which avoids races between concurrent requests +// and aligns with SEP-2575's per-request opt-in model. The value type is +// [LoggingLevel]; an empty string means the request opted out of log messages. +type logLevelContextKey struct{} + // Log sends a log 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. +// +// For new-protocol (>= 2026-06-30) requests, the level is taken from the +// originating request's `_meta` field (SEP-2575); an absent or empty value +// suppresses the message per spec. For old-protocol requests, the level is +// taken from the session state set via `logging/setLevel`. func (ss *ServerSession) Log(ctx context.Context, params *LoggingMessageParams) error { - ss.mu.Lock() - logLevel := ss.state.LogLevel - ss.mu.Unlock() + logLevel, ok := ctx.Value(logLevelContextKey{}).(LoggingLevel) + if !ok { + ss.mu.Lock() + logLevel = ss.state.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. @@ -1520,11 +1535,6 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, state.InitializeParams = validatedMeta.initializeParams }) } - if validatedMeta.usesNewProtocol && validatedMeta.logLevel != "" { - ss.updateState(func(state *ServerSessionState) { - state.LogLevel = validatedMeta.logLevel - }) - } } // modelcontextprotocol/go-sdk#26: handle calls asynchronously, and @@ -1538,6 +1548,10 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any, // server->client calls and notifications to the incoming request from which // they originated. See [idContextKey] for details. ctx = context.WithValue(ctx, idContextKey{}, req.ID) + // For new-protocol requests, propagate the per-request log level. + if validatedMeta.usesNewProtocol { + ctx = context.WithValue(ctx, logLevelContextKey{}, validatedMeta.logLevel) + } return handleReceive(ctx, ss, req) } diff --git a/mcp/shared.go b/mcp/shared.go index 66c57e15..daa5c71a 100644 --- a/mcp/shared.go +++ b/mcp/shared.go @@ -526,7 +526,7 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) { Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientCapabilities), } } - logLevel, _ := meta[MetaKeyLogLevel].(LoggingLevel) + logLevel, _ := decodeMetaValue[LoggingLevel](meta, MetaKeyLogLevel) return &validatedMeta{usesNewProtocol: true, initializeParams: &InitializeParams{ ProtocolVersion: protocolVersion, Capabilities: capabilities, diff --git a/mcp/shared_test.go b/mcp/shared_test.go index 065d00b0..62f65832 100644 --- a/mcp/shared_test.go +++ b/mcp/shared_test.go @@ -21,6 +21,7 @@ func TestValidateRequestMeta(t *testing.T) { isNotification bool params any wantUsesNew bool + wantLogLevel LoggingLevel wantErrContains string }{ { @@ -101,6 +102,35 @@ func TestValidateRequestMeta(t *testing.T) { params: json.RawMessage(`{"_meta": "not an object", "name": "x"}`), wantUsesNew: false, }, + { + name: "new protocol with logLevel", + method: methodCallTool, + params: map[string]any{ + "_meta": map[string]any{ + MetaKeyProtocolVersion: protocolVersion20260630, + MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"}, + MetaKeyClientCapabilities: map[string]any{}, + MetaKeyLogLevel: "warning", + }, + "name": "x", + }, + wantUsesNew: true, + wantLogLevel: "warning", + }, + { + name: "new protocol without logLevel", + method: methodCallTool, + params: map[string]any{ + "_meta": map[string]any{ + MetaKeyProtocolVersion: protocolVersion20260630, + MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"}, + MetaKeyClientCapabilities: map[string]any{}, + }, + "name": "x", + }, + wantUsesNew: true, + wantLogLevel: "", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -127,6 +157,9 @@ func TestValidateRequestMeta(t *testing.T) { if usesNew != tc.wantUsesNew { t.Errorf("usesNewProtocol = %v, want %v", usesNew, tc.wantUsesNew) } + if vmeta != nil && vmeta.logLevel != tc.wantLogLevel { + t.Errorf("logLevel = %q, want %q", vmeta.logLevel, tc.wantLogLevel) + } if tc.wantErrContains == "" { if err != nil { t.Errorf("unexpected error: %v", err)