Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 25 additions & 6 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}

Expand Down
7 changes: 4 additions & 3 deletions mcp/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions mcp/shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func TestValidateRequestMeta(t *testing.T) {
isNotification bool
params any
wantUsesNew bool
wantLogLevel LoggingLevel
wantErrContains string
}{
{
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion mcp/streamable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading