diff --git a/mcp/protocol.go b/mcp/protocol.go index d838067b..4ba8600e 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -2102,6 +2102,8 @@ 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" ) // UnsupportedProtocolVersionData is the SEP-2575 payload carried in the diff --git a/mcp/server.go b/mcp/server.go index ba03fbfe..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. @@ -1498,7 +1513,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{ @@ -1533,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 88c2464e..1aa42901 100644 --- a/mcp/shared.go +++ b/mcp/shared.go @@ -498,6 +498,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 @@ -518,8 +519,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 } @@ -537,11 +537,12 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) { Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientCapabilities), } } + logLevel, _ := decodeMetaValue[LoggingLevel](meta, MetaKeyLogLevel) 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/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) 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