This pull request addresses multiple minor documentation and code
comment fixes to correct typos and improve clarity. These changes do not
introduce functional modifications but enhance readability and
professionalism.
### Documentation Fixes:
* Corrected a typo in the "Security Considerations" section header in
`design/design.md`. ("Securty" → "Security")
* Fixed spelling errors in the "Governance and Community" section of
`design/design.md`. ("accomodating" → "accommodating")
* Updated the "Proposals" section in `design/design.md` to fix a typo.
("accomodate" → "accommodate")
### Code Comment Fixes:
* Corrected a typo in a comment in `mcp/logging.go`. ("LoggingMesssage"
→ "LoggingMessage")
* Fixed a typo in a comment in `mcp/streamable.go`. ("priviledged" →
"privileged")
<!-- Provide a brief summary of your changes -->
## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->
This PR is for fixing some typos in the repo
## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->
by code diff
## Breaking Changes
<!-- Will users need to update their code or configurations? -->
N/A
## Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->
- [ ] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [ ] My code follows the repository's style guidelines
- [ ] New and existing tests pass locally
- [ ] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
## Additional context
<!-- Add any other context, implementation notes, or design decisions
-->
189 lines
5.2 KiB
Go
189 lines
5.2 KiB
Go
// 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.LoggingMessage] 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.Log(ctx, params)
|
|
}
|