mcp: add support for logging level (SEP-2575) (#997)
## Description This PR adds the support for logging feature in meta. Fixes #966
This commit is contained in:
parent
d9714c61f1
commit
00d3642840
5 changed files with 67 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue