internal/mcp: implement logging
Provide a ServerSession method that directly implements the spec method. Also provide a custom slog.Handler that writes a notification on each log call. It uses a slog.JSONHandler to format the message as JSON, then adds the additional fields of LoggingMessageParams. I decided that the direct method should handle levels, since that is in the spec, but that rate-limiting is an extra feature, so is only provided in the slog handler. Change-Id: I1190ca124e992e61fb633ac52ee3492c8e6d99c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/674498 Auto-Submit: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Sam Thanawalla <samthanawalla@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
parent
bd2af6e0b1
commit
edda031578
7 changed files with 416 additions and 42 deletions
|
|
@ -823,6 +823,27 @@ Clients call the spec method `Complete` to request completions. Servers automati
|
|||
|
||||
### Logging
|
||||
|
||||
MCP specifies a notification for servers to log to clients. Server sessions implement this with the `LoggingMessage` method. It honors the minimum log level established by the client session's `SetLevel` call.
|
||||
|
||||
As a convenience, we also provide a `slog.Handler` that allows server authors to write logs with the `log/slog` package::
|
||||
```go
|
||||
// A LoggingHandler is a [slog.Handler] for MCP.
|
||||
type LoggingHandler struct {...}
|
||||
|
||||
// LoggingHandlerOptions are options for a LoggingHandler.
|
||||
type LoggingHandlerOptions struct {
|
||||
// The value for the "logger" field of logging notifications.
|
||||
LoggerName string
|
||||
// Limits the rate at which log messages are sent.
|
||||
// If zero, there is no rate limiting.
|
||||
MinInterval time.Duration
|
||||
}
|
||||
|
||||
// NewLoggingHandler creates a [LoggingHandler] that logs to the given [ServerSession] using a
|
||||
// [slog.JSONHandler].
|
||||
func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingHandler
|
||||
```
|
||||
|
||||
Server-to-client logging is configured with `ServerOptions`:
|
||||
|
||||
```go
|
||||
|
|
@ -832,24 +853,24 @@ type ServerOptions {
|
|||
LoggerName string
|
||||
// Log notifications to a single ClientSession will not be
|
||||
// sent more frequently than this duration.
|
||||
LogInterval time.Duration
|
||||
LoggingInterval time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
Server sessions have a field `Logger` holding a `slog.Logger` that writes to the client session. A call to a log method like `Info` is translated to a `LoggingMessageNotification` as follows:
|
||||
A call to a log method like `Info` is translated to a `LoggingMessageNotification` as follows:
|
||||
|
||||
- The attributes and the message populate the "data" property with the output of a `slog.JSONHandler`: The result is always a JSON object, with the key "msg" for the message.
|
||||
|
||||
- If the `LoggerName` server option is set, it populates the "logger" property.
|
||||
|
||||
- The standard slog levels `Info`, `Debug`, `Warn` and `Error` map to the corresponding levels in the MCP spec. The other spec levels map to integers between the slog levels. For example, "notice" is level 2 because it is between "warning" (slog value 4) and "info" (slog value 0). The `mcp` package defines consts for these levels. To log at the "notice" level, a handler would call `session.Logger.Log(ctx, mcp.LevelNotice, "message")`.
|
||||
- The standard slog levels `Info`, `Debug`, `Warn` and `Error` map to the corresponding levels in the MCP spec. The other spec levels map to integers between the slog levels. For example, "notice" is level 2 because it is between "warning" (slog value 4) and "info" (slog value 0). The `mcp` package defines consts for these levels. To log at the "notice" level, a handler would call `Log(ctx, mcp.LevelNotice, "message")`.
|
||||
|
||||
A client that wishes to receive log messages must provide a handler:
|
||||
|
||||
```go
|
||||
type ClientOptions struct {
|
||||
...
|
||||
LogMessageHandler func(context.Context, *ClientSession, *LoggingMessageParams)
|
||||
LoggingMessageHandler func(context.Context, *ClientSession, *LoggingMessageParams)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ type ClientOptions struct {
|
|||
ToolListChangedHandler func(context.Context, *ClientSession, *ToolListChangedParams)
|
||||
PromptListChangedHandler func(context.Context, *ClientSession, *PromptListChangedParams)
|
||||
ResourceListChangedHandler func(context.Context, *ClientSession, *ResourceListChangedParams)
|
||||
LoggingMessageHandler func(context.Context, *ClientSession, *LoggingMessageParams)
|
||||
}
|
||||
|
||||
// bind implements the binder[*ClientSession] interface, so that Clients can
|
||||
|
|
@ -208,6 +209,7 @@ var clientMethodInfos = map[string]methodInfo[ClientSession]{
|
|||
notificationToolListChanged: newMethodInfo(clientMethod((*Client).callToolChangedHandler)),
|
||||
notificationPromptListChanged: newMethodInfo(clientMethod((*Client).callPromptChangedHandler)),
|
||||
notificationResourceListChanged: newMethodInfo(clientMethod((*Client).callResourceChangedHandler)),
|
||||
notificationLoggingMessage: newMethodInfo(clientMethod((*Client).callLoggingHandler)),
|
||||
}
|
||||
|
||||
var _ session[ClientSession] = (*ClientSession)(nil)
|
||||
|
|
@ -273,6 +275,10 @@ func (c *ClientSession) CallTool(ctx context.Context, name string, args map[stri
|
|||
return standardCall[CallToolResult](ctx, c.conn, methodCallTool, params)
|
||||
}
|
||||
|
||||
func (c *ClientSession) SetLevel(ctx context.Context, params *SetLevelParams) error {
|
||||
return call(ctx, c.conn, methodSetLevel, params, nil)
|
||||
}
|
||||
|
||||
// NOTE: the following struct should consist of all fields of callToolParams except name and arguments.
|
||||
|
||||
// CallToolOptions contains options to [ClientSession.CallTool].
|
||||
|
|
@ -302,6 +308,13 @@ func (c *Client) callResourceChangedHandler(ctx context.Context, s *ClientSessio
|
|||
return callNotificationHandler(ctx, c.opts.ResourceListChangedHandler, s, params)
|
||||
}
|
||||
|
||||
func (c *Client) callLoggingHandler(ctx context.Context, cs *ClientSession, params *LoggingMessageParams) (any, error) {
|
||||
if h := c.opts.LoggingMessageHandler; h != nil {
|
||||
h(ctx, cs, params)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func standardCall[TRes, TParams any](ctx context.Context, conn *jsonrpc2.Connection, method string, params TParams) (*TRes, error) {
|
||||
var result TRes
|
||||
if err := call(ctx, conn, method, params, &result); err != nil {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"go/format"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
|
|
@ -110,7 +111,18 @@ var declarations = config{
|
|||
Name: "-",
|
||||
Fields: config{"Params": {Name: "ListToolsParams"}},
|
||||
},
|
||||
"ListToolsResult": {},
|
||||
"ListToolsResult": {},
|
||||
"loggingCapabilities": {Substitute: "struct{}"},
|
||||
"LoggingLevel": {},
|
||||
"LoggingMessageNotification": {
|
||||
Name: "-",
|
||||
Fields: config{
|
||||
"Params": {
|
||||
Name: "LoggingMessageParams",
|
||||
Fields: config{"Data": {Substitute: "any"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ModelHint": {},
|
||||
"ModelPreferences": {},
|
||||
"PingRequest": {
|
||||
|
|
@ -153,8 +165,13 @@ var declarations = config{
|
|||
"Prompts": {Name: "promptCapabilities"},
|
||||
"Resources": {Name: "resourceCapabilities"},
|
||||
"Tools": {Name: "toolCapabilities"},
|
||||
"Logging": {Name: "loggingCapabilities"},
|
||||
},
|
||||
},
|
||||
"SetLevelRequest": {
|
||||
Name: "-",
|
||||
Fields: config{"Params": {Name: "SetLevelParams"}},
|
||||
},
|
||||
"Tool": {
|
||||
Fields: config{"InputSchema": {Substitute: "*jsonschema.Schema"}},
|
||||
},
|
||||
|
|
@ -220,7 +237,7 @@ import (
|
|||
}
|
||||
// Write out method names.
|
||||
fmt.Fprintln(buf, `const (`)
|
||||
for name, s := range schema.Definitions {
|
||||
for _, name := range slices.Sorted(maps.Keys(schema.Definitions)) {
|
||||
prefix := "method"
|
||||
method, found := strings.CutSuffix(name, "Request")
|
||||
if !found {
|
||||
|
|
@ -228,7 +245,7 @@ import (
|
|||
method, found = strings.CutSuffix(name, "Notification")
|
||||
}
|
||||
if found {
|
||||
if ms, ok := s.Properties["method"]; ok {
|
||||
if ms, ok := schema.Definitions[name].Properties["method"]; ok {
|
||||
if c := ms.Const; c != nil {
|
||||
fmt.Fprintf(buf, "%s%s = %q\n", prefix, method, *c)
|
||||
}
|
||||
|
|
@ -395,9 +412,9 @@ func writeType(w io.Writer, config *typeConfig, def *jsonschema.Schema, named ma
|
|||
fieldTypeSchema = rs
|
||||
}
|
||||
needPointer := isStruct(fieldTypeSchema)
|
||||
// Special case: there are no sampling capabilities defined, but
|
||||
// we want it to be a struct for future expansion.
|
||||
if !needPointer && name == "sampling" {
|
||||
// Special case: there are no sampling or logging capabilities defined,
|
||||
// but we want them to be structs for future expansion.
|
||||
if !needPointer && (name == "sampling" || name == "logging") {
|
||||
needPointer = true
|
||||
}
|
||||
if config != nil && config.Fields[export] != nil {
|
||||
|
|
|
|||
189
mcp/logging.go
Normal file
189
mcp/logging.go
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logging levels.
|
||||
const (
|
||||
LevelDebug = slog.LevelDebug
|
||||
LevelInfo = slog.LevelInfo
|
||||
LevelNotice = (slog.LevelInfo + slog.LevelWarn) / 2
|
||||
LevelWarning = slog.LevelWarn
|
||||
LevelError = slog.LevelError
|
||||
LevelCritical = slog.LevelError + 4
|
||||
LevelAlert = slog.LevelError + 8
|
||||
LevelEmergency = slog.LevelError + 12
|
||||
)
|
||||
|
||||
var slogToMCP = map[slog.Level]LoggingLevel{
|
||||
LevelDebug: "debug",
|
||||
LevelInfo: "info",
|
||||
LevelNotice: "notice",
|
||||
LevelWarning: "warning",
|
||||
LevelError: "error",
|
||||
LevelCritical: "critical",
|
||||
LevelAlert: "alert",
|
||||
LevelEmergency: "emergency",
|
||||
}
|
||||
|
||||
var mcpToSlog = make(map[LoggingLevel]slog.Level)
|
||||
|
||||
func init() {
|
||||
for sl, ml := range slogToMCP {
|
||||
mcpToSlog[ml] = sl
|
||||
}
|
||||
}
|
||||
|
||||
func slogLevelToMCP(sl slog.Level) LoggingLevel {
|
||||
if ml, ok := slogToMCP[sl]; ok {
|
||||
return ml
|
||||
}
|
||||
return "debug" // for lack of a better idea
|
||||
}
|
||||
|
||||
func mcpLevelToSlog(ll LoggingLevel) slog.Level {
|
||||
if sl, ok := mcpToSlog[ll]; ok {
|
||||
return sl
|
||||
}
|
||||
// TODO: is there a better default?
|
||||
return LevelDebug
|
||||
}
|
||||
|
||||
// compareLevels behaves like [cmp.Compare] for [LoggingLevel]s.
|
||||
func compareLevels(l1, l2 LoggingLevel) int {
|
||||
return cmp.Compare(mcpLevelToSlog(l1), mcpLevelToSlog(l2))
|
||||
}
|
||||
|
||||
// LoggingHandlerOptions are options for a LoggingHandler.
|
||||
type LoggingHandlerOptions struct {
|
||||
// The value for the "logger" field of logging notifications.
|
||||
LoggerName string
|
||||
// Limits the rate at which log messages are sent.
|
||||
// If zero, there is no rate limiting.
|
||||
MinInterval time.Duration
|
||||
}
|
||||
|
||||
// A LoggingHandler is a [slog.Handler] for MCP.
|
||||
type LoggingHandler struct {
|
||||
opts LoggingHandlerOptions
|
||||
ss *ServerSession
|
||||
// Ensures that the buffer reset is atomic with the write (see Handle).
|
||||
// A pointer so that clones share the mutex. See
|
||||
// https://github.com/golang/example/blob/master/slog-handler-guide/README.md#getting-the-mutex-right.
|
||||
mu *sync.Mutex
|
||||
lastMessageSent time.Time // for rate-limiting
|
||||
buf *bytes.Buffer
|
||||
handler slog.Handler
|
||||
}
|
||||
|
||||
// NewLoggingHandler creates a [LoggingHandler] that logs to the given [ServerSession] using a
|
||||
// [slog.JSONHandler].
|
||||
func NewLoggingHandler(ss *ServerSession, opts *LoggingHandlerOptions) *LoggingHandler {
|
||||
var buf bytes.Buffer
|
||||
jsonHandler := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
|
||||
// Remove level: it appears in LoggingMessageParams.
|
||||
if a.Key == slog.LevelKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
})
|
||||
lh := &LoggingHandler{
|
||||
ss: ss,
|
||||
mu: new(sync.Mutex),
|
||||
buf: &buf,
|
||||
handler: jsonHandler,
|
||||
}
|
||||
if opts != nil {
|
||||
lh.opts = *opts
|
||||
}
|
||||
return lh
|
||||
}
|
||||
|
||||
// Enabled implements [slog.Handler.Enabled] by comparing level to the [ServerSession]'s level.
|
||||
func (h *LoggingHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
// This is also checked in ServerSession.LoggingMessage, so checking it here
|
||||
// is just an optimization that skips building the JSON.
|
||||
h.ss.mu.Lock()
|
||||
mcpLevel := h.ss.logLevel
|
||||
h.ss.mu.Unlock()
|
||||
return level >= mcpLevelToSlog(mcpLevel)
|
||||
}
|
||||
|
||||
// WithAttrs implements [slog.Handler.WithAttrs].
|
||||
func (h *LoggingHandler) WithAttrs(as []slog.Attr) slog.Handler {
|
||||
h2 := *h
|
||||
h2.handler = h.handler.WithAttrs(as)
|
||||
return &h2
|
||||
}
|
||||
|
||||
// WithGroup implements [slog.Handler.WithGroup].
|
||||
func (h *LoggingHandler) WithGroup(name string) slog.Handler {
|
||||
h2 := *h
|
||||
h2.handler = h.handler.WithGroup(name)
|
||||
return &h2
|
||||
}
|
||||
|
||||
// Handle implements [slog.Handler.Handle] by writing the Record to a JSONHandler,
|
||||
// then calling [ServerSession.LoggingMesssage] with the result.
|
||||
func (h *LoggingHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
err := h.handle(ctx, r)
|
||||
// TODO(jba): find a way to surface the error.
|
||||
// The return value will probably be ignored.
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *LoggingHandler) handle(ctx context.Context, r slog.Record) error {
|
||||
// Observe the rate limit.
|
||||
// TODO(jba): use golang.org/x/time/rate. (We can't here because it would require adding
|
||||
// golang.org/x/time to the go.mod file.)
|
||||
h.mu.Lock()
|
||||
skip := time.Since(h.lastMessageSent) < h.opts.MinInterval
|
||||
h.mu.Unlock()
|
||||
if skip {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
// Make the buffer reset atomic with the record write.
|
||||
// We are careful here in the unlikely event that the handler panics.
|
||||
// We don't want to hold the lock for the entire function, because Notify is
|
||||
// an I/O operation.
|
||||
// This can result in out-of-order delivery.
|
||||
func() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.buf.Reset()
|
||||
err = h.handler.Handle(ctx, r)
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
h.lastMessageSent = time.Now()
|
||||
h.mu.Unlock()
|
||||
|
||||
params := &LoggingMessageParams{
|
||||
Logger: h.opts.LoggerName,
|
||||
Level: slogLevelToMCP(r.Level),
|
||||
Data: json.RawMessage(h.buf.Bytes()),
|
||||
}
|
||||
// We pass the argument context to Notify, even though slog.Handler.Handle's
|
||||
// documentation says not to.
|
||||
// In this case logging is a service to clients, not a means for debugging the
|
||||
// server, so we want to cancel the log message.
|
||||
return h.ss.LoggingMessage(ctx, params)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
|
@ -104,6 +105,7 @@ func TestEndToEnd(t *testing.T) {
|
|||
clientWG.Done()
|
||||
}()
|
||||
|
||||
loggingMessages := make(chan *LoggingMessageParams, 100) // big enough for all logging
|
||||
opts := &ClientOptions{
|
||||
CreateMessageHandler: func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error) {
|
||||
return &CreateMessageResult{Model: "aModel"}, nil
|
||||
|
|
@ -111,6 +113,9 @@ func TestEndToEnd(t *testing.T) {
|
|||
ToolListChangedHandler: func(context.Context, *ClientSession, *ToolListChangedParams) { notificationChans["tools"] <- 0 },
|
||||
PromptListChangedHandler: func(context.Context, *ClientSession, *PromptListChangedParams) { notificationChans["prompts"] <- 0 },
|
||||
ResourceListChangedHandler: func(context.Context, *ClientSession, *ResourceListChangedParams) { notificationChans["resources"] <- 0 },
|
||||
LoggingMessageHandler: func(_ context.Context, _ *ClientSession, lm *LoggingMessageParams) {
|
||||
loggingMessages <- lm
|
||||
},
|
||||
}
|
||||
c := NewClient("testClient", "v1.0.0", opts)
|
||||
rootAbs, err := filepath.Abs(filepath.FromSlash("testdata/files"))
|
||||
|
|
@ -319,6 +324,85 @@ func TestEndToEnd(t *testing.T) {
|
|||
t.Errorf("got %q, want %q", g, w)
|
||||
}
|
||||
})
|
||||
t.Run("logging", func(t *testing.T) {
|
||||
want := []*LoggingMessageParams{
|
||||
{
|
||||
Logger: "test",
|
||||
Level: "warning",
|
||||
Data: map[string]any{
|
||||
"msg": "first",
|
||||
"name": "Pat",
|
||||
"logtest": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Logger: "test",
|
||||
Level: "alert",
|
||||
Data: map[string]any{
|
||||
"msg": "second",
|
||||
"count": 2.0,
|
||||
"logtest": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
check := func(t *testing.T) {
|
||||
t.Helper()
|
||||
var got []*LoggingMessageParams
|
||||
// Read messages from this test until we've seen all we expect.
|
||||
for len(got) < len(want) {
|
||||
select {
|
||||
case p := <-loggingMessages:
|
||||
// Ignore logging from other tests.
|
||||
if m, ok := p.Data.(map[string]any); ok && m["logtest"] != nil {
|
||||
delete(m, "time") // remove time because it changes
|
||||
got = append(got, p)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for log messages")
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("direct", func(t *testing.T) { // Use the LoggingMessage method directly.
|
||||
|
||||
mustLog := func(level LoggingLevel, data any) {
|
||||
t.Helper()
|
||||
if err := ss.LoggingMessage(ctx, &LoggingMessageParams{
|
||||
Logger: "test",
|
||||
Level: level,
|
||||
Data: data,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing should be logged until the client sets a level.
|
||||
mustLog("info", "before")
|
||||
if err := cs.SetLevel(ctx, &SetLevelParams{Level: "warning"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mustLog("warning", want[0].Data)
|
||||
mustLog("debug", "nope") // below the level
|
||||
mustLog("info", "negative") // below the level
|
||||
mustLog("alert", want[1].Data)
|
||||
check(t)
|
||||
})
|
||||
|
||||
t.Run("handler", func(t *testing.T) { // Use the slog handler.
|
||||
// We can't check the "before SetLevel" behavior because it's already been set.
|
||||
// Not a big deal: that check is in LoggingMessage anyway.
|
||||
logger := slog.New(NewLoggingHandler(ss, &LoggingHandlerOptions{LoggerName: "test"}))
|
||||
logger.Warn("first", "name", "Pat", "logtest", true)
|
||||
logger.Debug("nope") // below the level
|
||||
logger.Info("negative") // below the level
|
||||
logger.Log(ctx, LevelAlert, "second", "count", 2, "logtest", true)
|
||||
check(t)
|
||||
})
|
||||
})
|
||||
|
||||
// Disconnect.
|
||||
cs.Close()
|
||||
|
|
|
|||
|
|
@ -241,6 +241,22 @@ type ListToolsResult struct {
|
|||
Tools []*Tool `json:"tools"`
|
||||
}
|
||||
|
||||
// The severity of a log message.
|
||||
//
|
||||
// These map to syslog message severities, as specified in RFC-5424:
|
||||
// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1
|
||||
type LoggingLevel string
|
||||
|
||||
type LoggingMessageParams struct {
|
||||
// The data to be logged, such as a string message or an object. Any JSON
|
||||
// serializable type is allowed here.
|
||||
Data any `json:"data"`
|
||||
// The severity of this log message.
|
||||
Level LoggingLevel `json:"level"`
|
||||
// An optional name of the logger issuing this message.
|
||||
Logger string `json:"logger,omitempty"`
|
||||
}
|
||||
|
||||
// Hints to use for model selection.
|
||||
//
|
||||
// Keys not declared here are currently left unspecified by the spec and are up
|
||||
|
|
@ -326,8 +342,7 @@ type PromptArgument struct {
|
|||
type PromptListChangedParams struct {
|
||||
// This parameter name is reserved by MCP to allow clients and servers to attach
|
||||
// additional metadata to their notifications.
|
||||
Meta struct {
|
||||
} `json:"_meta,omitempty"`
|
||||
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
|
||||
}
|
||||
|
||||
// Describes a message returned as part of a prompt.
|
||||
|
|
@ -381,8 +396,7 @@ type Resource struct {
|
|||
type ResourceListChangedParams struct {
|
||||
// This parameter name is reserved by MCP to allow clients and servers to attach
|
||||
// additional metadata to their notifications.
|
||||
Meta struct {
|
||||
} `json:"_meta,omitempty"`
|
||||
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
|
||||
}
|
||||
|
||||
// The sender or recipient of messages and data in a conversation.
|
||||
|
|
@ -403,8 +417,7 @@ type Root struct {
|
|||
type RootsListChangedParams struct {
|
||||
// This parameter name is reserved by MCP to allow clients and servers to attach
|
||||
// additional metadata to their notifications.
|
||||
Meta struct {
|
||||
} `json:"_meta,omitempty"`
|
||||
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
|
||||
}
|
||||
|
||||
// Present if the client supports sampling from an LLM.
|
||||
|
|
@ -417,6 +430,13 @@ type SamplingMessage struct {
|
|||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
type SetLevelParams struct {
|
||||
// The level of logging that the client wants to receive from the server. The
|
||||
// server should send all logs at this level and higher (i.e., more severe) to
|
||||
// the client as notifications/message.
|
||||
Level LoggingLevel `json:"level"`
|
||||
}
|
||||
|
||||
// Definition for a tool the client can call.
|
||||
type Tool struct {
|
||||
// Optional additional tool information.
|
||||
|
|
@ -472,8 +492,7 @@ type ToolAnnotations struct {
|
|||
type ToolListChangedParams struct {
|
||||
// This parameter name is reserved by MCP to allow clients and servers to attach
|
||||
// additional metadata to their notifications.
|
||||
Meta struct {
|
||||
} `json:"_meta,omitempty"`
|
||||
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
|
||||
}
|
||||
|
||||
// Describes the name and version of an MCP implementation.
|
||||
|
|
@ -482,6 +501,10 @@ type implementation struct {
|
|||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// Present if the server supports sending log messages to the client.
|
||||
type loggingCapabilities struct {
|
||||
}
|
||||
|
||||
// Present if the server offers any prompt templates.
|
||||
type promptCapabilities struct {
|
||||
// Whether this server supports notifications for changes to the prompt list.
|
||||
|
|
@ -507,8 +530,7 @@ type serverCapabilities struct {
|
|||
Experimental map[string]struct {
|
||||
} `json:"experimental,omitempty"`
|
||||
// Present if the server supports sending log messages to the client.
|
||||
Logging struct {
|
||||
} `json:"logging,omitempty"`
|
||||
Logging *loggingCapabilities `json:"logging,omitempty"`
|
||||
// Present if the server offers any prompt templates.
|
||||
Prompts *promptCapabilities `json:"prompts,omitempty"`
|
||||
// Present if the server offers any resources to read.
|
||||
|
|
@ -524,28 +546,28 @@ type toolCapabilities struct {
|
|||
}
|
||||
|
||||
const (
|
||||
methodCreateMessage = "sampling/createMessage"
|
||||
notificationToolListChanged = "notifications/tools/list_changed"
|
||||
notificationResourceListChanged = "notifications/resources/list_changed"
|
||||
methodListPrompts = "prompts/list"
|
||||
notificationPromptListChanged = "notifications/prompts/list_changed"
|
||||
notificationResourceUpdated = "notifications/resources/updated"
|
||||
notificationCancelled = "notifications/cancelled"
|
||||
methodSetLevel = "logging/setLevel"
|
||||
methodInitialize = "initialize"
|
||||
methodListRoots = "roots/list"
|
||||
notificationProgress = "notifications/progress"
|
||||
methodGetPrompt = "prompts/get"
|
||||
methodListResourceTemplates = "resources/templates/list"
|
||||
methodPing = "ping"
|
||||
methodComplete = "completion/complete"
|
||||
methodListResources = "resources/list"
|
||||
notificationInitialized = "notifications/initialized"
|
||||
methodCallTool = "tools/call"
|
||||
notificationLoggingMessage = "notifications/message"
|
||||
methodReadResource = "resources/read"
|
||||
notificationCancelled = "notifications/cancelled"
|
||||
methodComplete = "completion/complete"
|
||||
methodCreateMessage = "sampling/createMessage"
|
||||
methodGetPrompt = "prompts/get"
|
||||
methodInitialize = "initialize"
|
||||
notificationInitialized = "notifications/initialized"
|
||||
methodListPrompts = "prompts/list"
|
||||
methodListResourceTemplates = "resources/templates/list"
|
||||
methodListResources = "resources/list"
|
||||
methodListRoots = "roots/list"
|
||||
methodListTools = "tools/list"
|
||||
notificationLoggingMessage = "notifications/message"
|
||||
methodPing = "ping"
|
||||
notificationProgress = "notifications/progress"
|
||||
notificationPromptListChanged = "notifications/prompts/list_changed"
|
||||
methodReadResource = "resources/read"
|
||||
notificationResourceListChanged = "notifications/resources/list_changed"
|
||||
notificationResourceUpdated = "notifications/resources/updated"
|
||||
notificationRootsListChanged = "notifications/roots/list_changed"
|
||||
methodSetLevel = "logging/setLevel"
|
||||
methodSubscribe = "resources/subscribe"
|
||||
notificationToolListChanged = "notifications/tools/list_changed"
|
||||
methodUnsubscribe = "resources/unsubscribe"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -299,11 +299,11 @@ func (s *Server) Run(ctx context.Context, t Transport) error {
|
|||
// bind implements the binder[*ServerSession] interface, so that Servers can
|
||||
// be connected using [connect].
|
||||
func (s *Server) bind(conn *jsonrpc2.Connection) *ServerSession {
|
||||
cc := &ServerSession{conn: conn, server: s}
|
||||
ss := &ServerSession{conn: conn, server: s}
|
||||
s.mu.Lock()
|
||||
s.sessions = append(s.sessions, cc)
|
||||
s.sessions = append(s.sessions, ss)
|
||||
s.mu.Unlock()
|
||||
return cc
|
||||
return ss
|
||||
}
|
||||
|
||||
// disconnect implements the binder[*ServerSession] interface, so that
|
||||
|
|
@ -341,17 +341,17 @@ func (s *Server) callRootsListChangedHandler(ctx context.Context, ss *ServerSess
|
|||
// Call [ServerSession.Close] to close the connection, or await client
|
||||
// termination with [ServerSession.Wait].
|
||||
type ServerSession struct {
|
||||
server *Server
|
||||
conn *jsonrpc2.Connection
|
||||
|
||||
server *Server
|
||||
conn *jsonrpc2.Connection
|
||||
mu sync.Mutex
|
||||
logLevel LoggingLevel
|
||||
initializeParams *InitializeParams
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// Ping pings the client.
|
||||
func (ss *ServerSession) Ping(ctx context.Context, _ *PingParams) error {
|
||||
return call(ctx, ss.conn, "ping", (*PingParams)(nil), nil)
|
||||
return call(ctx, ss.conn, methodPing, (*PingParams)(nil), nil)
|
||||
}
|
||||
|
||||
// ListRoots lists the client roots.
|
||||
|
|
@ -364,6 +364,25 @@ func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessag
|
|||
return standardCall[CreateMessageResult](ctx, ss.conn, methodCreateMessage, params)
|
||||
}
|
||||
|
||||
// LoggingMessage sends a logging 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.
|
||||
func (ss *ServerSession) LoggingMessage(ctx context.Context, params *LoggingMessageParams) error {
|
||||
ss.mu.Lock()
|
||||
logLevel := ss.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.
|
||||
// TODO(jba): read other SDKs, possibly file an issue.
|
||||
return nil
|
||||
}
|
||||
if compareLevels(params.Level, logLevel) < 0 {
|
||||
return nil
|
||||
}
|
||||
return ss.conn.Notify(ctx, notificationLoggingMessage, params)
|
||||
}
|
||||
|
||||
// AddMiddleware wraps the server's current method handler using the provided
|
||||
// middleware. Middleware is applied from right to left, so that the first one
|
||||
// is executed first.
|
||||
|
|
@ -386,6 +405,7 @@ var serverMethodInfos = map[string]methodInfo[ServerSession]{
|
|||
methodCallTool: newMethodInfo(serverMethod((*Server).callTool)),
|
||||
methodListResources: newMethodInfo(serverMethod((*Server).listResources)),
|
||||
methodReadResource: newMethodInfo(serverMethod((*Server).readResource)),
|
||||
methodSetLevel: newMethodInfo(sessionMethod((*ServerSession).setLevel)),
|
||||
notificationInitialized: newMethodInfo(serverMethod((*Server).callInitializedHandler)),
|
||||
notificationRootsListChanged: newMethodInfo(serverMethod((*Server).callRootsListChangedHandler)),
|
||||
}
|
||||
|
|
@ -457,6 +477,7 @@ func (ss *ServerSession) initialize(ctx context.Context, params *InitializeParam
|
|||
Resources: &resourceCapabilities{
|
||||
ListChanged: true,
|
||||
},
|
||||
Logging: &loggingCapabilities{},
|
||||
},
|
||||
Instructions: ss.server.opts.Instructions,
|
||||
ServerInfo: &implementation{
|
||||
|
|
@ -470,6 +491,13 @@ func (ss *ServerSession) ping(context.Context, *PingParams) (struct{}, error) {
|
|||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
func (ss *ServerSession) setLevel(_ context.Context, params *SetLevelParams) (struct{}, error) {
|
||||
ss.mu.Lock()
|
||||
defer ss.mu.Unlock()
|
||||
ss.logLevel = params.Level
|
||||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
// Close performs a graceful shutdown of the connection, preventing new
|
||||
// requests from being handled, and waiting for ongoing requests to return.
|
||||
// Close then terminates the connection.
|
||||
|
|
|
|||
Loading…
Reference in a new issue