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
4 changes: 2 additions & 2 deletions blues-expert/lib/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func HandleAPIDocsTool(ctx context.Context, request *mcp.CallToolRequest, args G
TrackSession(request, "api_docs")

// Get API documentation
apiCategory, err := GetNotecardAPIs(ctx, request, args.API)
apiCategory, err := GetNotecardAPIs(args.API)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
Expand Down Expand Up @@ -207,7 +207,7 @@ func HandleDocsSearchTool(ctx context.Context, request *mcp.CallToolRequest, arg
TrackSession(request, "docs_search")

// Call the search implementation from query.go
result, err := SearchNotecardDocs(ctx, request, args.Query)
result, err := SearchNotecardDocs(ctx, args.Query)
if err != nil {
return &mcp.CallToolResult{
Content: []mcp.Content{
Expand Down
112 changes: 112 additions & 0 deletions blues-expert/lib/logger.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package lib

import (
"context"
"io"
"log/slog"
"os"
"slices"
"strings"

"github.com/rs/zerolog"
Expand Down Expand Up @@ -55,3 +58,112 @@ func parseLogLevel(level string) zerolog.Level {
return zerolog.InfoLevel
}
}

// NewSlogLogger returns an slog.Logger that writes through the global zerolog
// logger, so log output from the MCP SDK shares the server's format and level.
// Records below minLevel are dropped even when zerolog would emit them.
// Attributes inside slog groups are flattened to dotted keys.
func NewSlogLogger(minLevel slog.Level) *slog.Logger {
return slog.New(&zerologHandler{minLevel: minLevel})
}

type zerologHandler struct {
minLevel slog.Level
attrs []prefixedAttr
prefix string
}

type prefixedAttr struct {
prefix string
attr slog.Attr
}

func (h *zerologHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= h.minLevel && slogToZerologLevel(level) >= log.Logger.GetLevel()
}

func (h *zerologHandler) Handle(_ context.Context, r slog.Record) error {
event := log.WithLevel(slogToZerologLevel(r.Level)).Str("component", "mcp-sdk")
for _, pa := range h.attrs {
event = addSlogAttr(event, pa.prefix, pa.attr)
}
r.Attrs(func(a slog.Attr) bool {
event = addSlogAttr(event, h.prefix, a)
return true
})
event.Msg(r.Message)
return nil
}

func (h *zerologHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
next := *h
next.attrs = slices.Clip(h.attrs)
for _, a := range attrs {
next.attrs = append(next.attrs, prefixedAttr{prefix: h.prefix, attr: a})
}
return &next
}

func (h *zerologHandler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
next := *h
next.prefix = h.prefix + name + "."
return &next
}

func addSlogAttr(event *zerolog.Event, prefix string, a slog.Attr) *zerolog.Event {
v := a.Value.Resolve()
if v.Kind() == slog.KindGroup {
groupPrefix := prefix
if a.Key != "" {
groupPrefix += a.Key + "."
}
for _, ga := range v.Group() {
event = addSlogAttr(event, groupPrefix, ga)
}
return event
}
if a.Key == "" {
return event
}
key := prefix + a.Key
switch key {
case zerolog.LevelFieldName, zerolog.MessageFieldName, zerolog.TimestampFieldName, "component":
key = "sdk_" + key
}
switch v.Kind() {
case slog.KindString:
return event.Str(key, v.String())
case slog.KindInt64:
return event.Int64(key, v.Int64())
case slog.KindUint64:
return event.Uint64(key, v.Uint64())
case slog.KindFloat64:
return event.Float64(key, v.Float64())
case slog.KindBool:
return event.Bool(key, v.Bool())
case slog.KindDuration:
return event.Dur(key, v.Duration())
case slog.KindTime:
return event.Time(key, v.Time())
}
if err, ok := v.Any().(error); ok {
return event.AnErr(key, err)
}
return event.Interface(key, v.Any())
}

func slogToZerologLevel(level slog.Level) zerolog.Level {
switch {
case level >= slog.LevelError:
return zerolog.ErrorLevel
case level >= slog.LevelWarn:
return zerolog.WarnLevel
case level >= slog.LevelInfo:
return zerolog.InfoLevel
default:
return zerolog.DebugLevel
}
}
35 changes: 6 additions & 29 deletions blues-expert/lib/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/rs/zerolog/log"
)

const (
Expand Down Expand Up @@ -76,7 +77,7 @@ type SearchResult struct {
}

// SearchNotecardDocs performs a search against the Blues documentation API
func SearchNotecardDocs(ctx context.Context, request *mcp.CallToolRequest, query string) (*mcp.CallToolResult, error) {
func SearchNotecardDocs(ctx context.Context, query string) (*mcp.CallToolResult, error) {

// Create HTTP client with timeout
client := &http.Client{
Expand Down Expand Up @@ -117,13 +118,7 @@ func SearchNotecardDocs(ctx context.Context, request *mcp.CallToolRequest, query
if os.Getenv("BLUES_DOCS_API_KEY") != "" {
req.Header.Set("x-api-key", os.Getenv("BLUES_DOCS_API_KEY"))
} else {
// Log that we're requesting permission to access the Blues documentation API
if request != nil && request.Session != nil {
request.Session.Log(ctx, &mcp.LoggingMessageParams{
Level: "info",
Data: "Requesting access to the blues.dev documentation...",
})
}
log.Debug().Msg("Requesting access to the blues.dev documentation...")

apiKey, err := getAPIKeyFromAWS(ctx)
if err != nil {
Expand All @@ -137,24 +132,12 @@ func SearchNotecardDocs(ctx context.Context, request *mcp.CallToolRequest, query
req.Header.Set("x-api-key", apiKey)
}

// Log that we're making the search request
if request != nil && request.Session != nil {
request.Session.Log(ctx, &mcp.LoggingMessageParams{
Level: "info",
Data: fmt.Sprintf("Searching the blues.dev documentation for: %s", query),
})
}
log.Debug().Str("query", query).Msg("Searching the blues.dev documentation")

// Make the request
resp, err := client.Do(req)
if err != nil {
// Log the error for server-side debugging
if request != nil && request.Session != nil {
request.Session.Log(ctx, &mcp.LoggingMessageParams{
Level: "error",
Data: fmt.Sprintf("Ragpi request failed: %v", err),
})
}
log.Error().Err(err).Msg("Ragpi request failed")

return &mcp.CallToolResult{
Content: []mcp.Content{
Expand All @@ -174,13 +157,7 @@ func SearchNotecardDocs(ctx context.Context, request *mcp.CallToolRequest, query
errorMsg += fmt.Sprintf(": %s", string(body))
}

// Log the error for server-side debugging
if request != nil && request.Session != nil {
request.Session.Log(ctx, &mcp.LoggingMessageParams{
Level: "error",
Data: fmt.Sprintf("Ragpi API error: %s", errorMsg),
})
}
log.Error().Str("error", errorMsg).Msg("Ragpi API error")

return &mcp.CallToolResult{
Content: []mcp.Content{
Expand Down
11 changes: 9 additions & 2 deletions blues-expert/lib/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import (
"github.com/rs/zerolog/log"
)

// SessionIdleTimeout is how long a session may go without a client request
// before it is dropped, both here and by the MCP SDK's session handling.
const SessionIdleTimeout = 24 * time.Hour

var globalSessionManager *SessionManager

// RequestLog holds information about a specific request
Expand Down Expand Up @@ -164,9 +168,9 @@ func (sm *SessionManager) cleanupExpiredSessions() {
now := time.Now()
expiredSessions := make([]string, 0)

// Find sessions that haven't been accessed in the last hour
// Find sessions that have been idle longer than SessionIdleTimeout
for sessionID, session := range sm.sessions {
if now.Sub(session.LastAccessed) > time.Hour {
if now.Sub(session.LastAccessed) > SessionIdleTimeout {
expiredSessions = append(expiredSessions, sessionID)
}
}
Expand Down Expand Up @@ -232,8 +236,11 @@ func LogSessionActivityWithArgs(sessionID, toolName string, sessionData *Session
Str("arguments", argsStr).
Msg("Tool called (stateless session)")
} else {
sm := GetSessionManager()
sm.mu.RLock()
historyCount := len(sessionData.RequestLog)
totalRequests := sessionData.RequestCount
sm.mu.RUnlock()

// Show if we've truncated history
if totalRequests > int64(historyCount) && historyCount == 50 {
Expand Down
Loading
Loading