From f9212197058bda00025349ebdd239b1ba1db4de3 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 5 Oct 2026 13:39:58 -0500 Subject: [PATCH 1/6] refactor: replace deprecated Session.Log calls with server-side logging MCP's logging feature is deprecated (SEP-2577). Ragpi status and errors now go to zerolog instead of only to clients that requested log messages, and parameters that only fed those calls are removed. Co-Authored-By: Claude Opus 5.5 --- blues-expert/lib/handlers.go | 4 +-- blues-expert/lib/query.go | 35 ++++--------------- blues-expert/lib/validate.go | 67 +++--------------------------------- 3 files changed, 13 insertions(+), 93 deletions(-) diff --git a/blues-expert/lib/handlers.go b/blues-expert/lib/handlers.go index 0d69dfc..59de889 100644 --- a/blues-expert/lib/handlers.go +++ b/blues-expert/lib/handlers.go @@ -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{ @@ -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{ diff --git a/blues-expert/lib/query.go b/blues-expert/lib/query.go index 35bd0e2..6d5e6ed 100644 --- a/blues-expert/lib/query.go +++ b/blues-expert/lib/query.go @@ -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 ( @@ -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{ @@ -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 { @@ -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{ @@ -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{ diff --git a/blues-expert/lib/validate.go b/blues-expert/lib/validate.go index c25d9d9..99f432a 100644 --- a/blues-expert/lib/validate.go +++ b/blues-expert/lib/validate.go @@ -2,7 +2,6 @@ package lib import ( "bytes" - "context" "encoding/json" "fmt" "io" @@ -13,7 +12,6 @@ import ( "sync" "time" - "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/rs/zerolog/log" "github.com/santhosh-tekuri/jsonschema/v5" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" // Enable HTTP/HTTPS loading @@ -73,15 +71,7 @@ func extractRefs(schemaMap map[string]interface{}, baseURL string) []string { } // fetchAndCacheSchema fetches a schema from the URL and caches it -// If request is provided, it will create or retrieve a session for logging -func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url string) (io.Reader, error) { - // Log that we're fetching the schema - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: fmt.Sprintf("Fetching Notecard API schema from %s...", url), - }) - } +func fetchAndCacheSchema(url string) (io.Reader, error) { log.Debug().Str("url", url).Msg("Fetching Notecard API schema") resp, err := http.Get(url) @@ -90,13 +80,6 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url } defer resp.Body.Close() - // Log schema download progress - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: "Schema download in progress, please wait...", - }) - } log.Info().Msg("Schema download in progress, please wait...") if resp.StatusCode != http.StatusOK { @@ -107,13 +90,6 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url return nil, fmt.Errorf("failed to read schema %s: %v", url, err) } - // Log processing status - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: "Processing and validating schema...", - }) - } log.Info().Msg("Processing and validating schema...") // Verify it's valid JSON before caching and extract version @@ -128,13 +104,6 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url schemaVersion = version } - // Log caching status - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: "Caching schema for future use...", - }) - } log.Debug().Msg("Caching schema for future use...") // Save to cache @@ -150,23 +119,11 @@ func fetchAndCacheSchema(ctx context.Context, request *mcp.CallToolRequest, url } } - // Log completion - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: "Schema fetch and cache completed successfully", - }) - } log.Info().Msg("Schema fetch and cache completed successfully") return bytes.NewReader(data), nil } -// fetchAndCacheSchemaBackground fetches a schema without MCP logging (for background operations) -func fetchAndCacheSchemaBackground(url string) (io.Reader, error) { - return fetchAndCacheSchema(context.Background(), nil, url) -} - // formatErrorMessage formats jsonschema validation errors into user-friendly messages func formatErrorMessage(reqType string, errUnformatted error) (err error) { if errUnformatted == nil { @@ -396,7 +353,7 @@ func loadOrFetchSchema(url string) (io.Reader, error) { // Check if cache has expired if isCacheExpired(url) { // Cache expired: fetch fresh copy - return fetchAndCacheSchemaBackground(url) + return fetchAndCacheSchema(url) } data, err := io.ReadAll(file) @@ -407,12 +364,12 @@ func loadOrFetchSchema(url string) (io.Reader, error) { var v interface{} if err := json.Unmarshal(data, &v); err != nil { // Invalid cache: proceed to fetch - return fetchAndCacheSchemaBackground(url) + return fetchAndCacheSchema(url) } return bytes.NewReader(data), nil } // Cache miss: fetch from URL - return fetchAndCacheSchemaBackground(url) + return fetchAndCacheSchema(url) } // resolveSchemaError attempts to validate against specific request schemas for better error messages @@ -520,7 +477,7 @@ type PropertySubDescription struct { } // GetNotecardAPIs returns API documentation for a specific API or lists available APIs -func GetNotecardAPIs(ctx context.Context, request *mcp.CallToolRequest, apiName string) (*APICategory, error) { +func GetNotecardAPIs(apiName string) (*APICategory, error) { // Ensure schema is initialized if err := initSchema(defaultSchemaURL); err != nil { return nil, fmt.Errorf("failed to initialize schema: %v", err) @@ -534,13 +491,6 @@ func GetNotecardAPIs(ctx context.Context, request *mcp.CallToolRequest, apiName // If no cache files found, force schema initialization to populate cache if len(cacheFiles) == 0 { - // Log to client that we're fetching fresh schema - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: "No cached API schema found, fetching fresh schema from remote...", - }) - } log.Debug().Msg("No cached API schema found, fetching fresh schema from remote...") // Force a fresh fetch by safely resetting the schema cache @@ -570,13 +520,6 @@ func GetNotecardAPIs(ctx context.Context, request *mcp.CallToolRequest, apiName // Check if the specific API schema file exists if _, err := os.Stat(schemaFile); os.IsNotExist(err) { - // Log to client that we're refreshing schema - if request != nil && request.Session != nil { - request.Session.Log(ctx, &mcp.LoggingMessageParams{ - Level: "info", - Data: fmt.Sprintf("API '%s' not found in cache, refreshing schema...", apiName), - }) - } log.Debug().Str("api", apiName).Msg("API not found in cache, refreshing schema...") // Try to refresh the cache in case the API was recently added From bb986d64cf3865e6ce93c311909c78ad523a64b8 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 5 Oct 2026 13:39:58 -0500 Subject: [PATCH 2/6] fix: guard session counter reads when logging tool calls Parallel tool calls in one session raced on RequestCount and RequestLog (reported by the race detector). Co-Authored-By: Claude Opus 5.5 --- blues-expert/lib/session.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blues-expert/lib/session.go b/blues-expert/lib/session.go index 8bc8183..2c6b74c 100644 --- a/blues-expert/lib/session.go +++ b/blues-expert/lib/session.go @@ -232,8 +232,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 { From 5758b467a9dbbb667a1fae62d983c649d3dc92f1 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 5 Oct 2026 13:39:59 -0500 Subject: [PATCH 3/6] chore: drop unused sessionManager variable Keep the NewSessionManager call; it sets the global used by TrackSession and starts its cleanup loop. Co-Authored-By: Claude Opus 5.5 --- blues-expert/main.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blues-expert/main.go b/blues-expert/main.go index 45e1069..df05895 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -19,9 +19,8 @@ import ( const mcpEndpointPath = "/expert/mcp" var ( - envFilePath string - logLevel string - sessionManager *lib.SessionManager + envFilePath string + logLevel string ) func init() { @@ -114,8 +113,9 @@ func main() { } } - // Initialize session manager - sessionManager = lib.NewSessionManager() + // Initialize the session manager used by lib.TrackSession (this also starts + // its cleanup loop). Tool handlers panic if it isn't initialized. + lib.NewSessionManager() // Create a new MCP server impl := &mcp.Implementation{Name: "Blues Expert MCP", Version: serverVersion()} From 984c8744c302ade4cd78c9baf1354300e6f467f7 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 5 Oct 2026 13:40:00 -0500 Subject: [PATCH 4/6] feat: route MCP SDK warnings and errors through zerolog Adds an slog-to-zerolog adapter. SDK output was previously discarded; Info lifecycle lines stay filtered out. Co-Authored-By: Claude Opus 5.5 --- blues-expert/lib/logger.go | 112 +++++++++++++++++++++++++++++++++++++ blues-expert/main.go | 7 +++ 2 files changed, 119 insertions(+) diff --git a/blues-expert/lib/logger.go b/blues-expert/lib/logger.go index 89cf43a..11f9b12 100644 --- a/blues-expert/lib/logger.go +++ b/blues-expert/lib/logger.go @@ -1,8 +1,11 @@ package lib import ( + "context" "io" + "log/slog" "os" + "slices" "strings" "github.com/rs/zerolog" @@ -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 + } +} diff --git a/blues-expert/main.go b/blues-expert/main.go index df05895..494984a 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "flag" "io" + "log/slog" "net/http" "os" "time" @@ -117,10 +118,15 @@ func main() { // its cleanup loop). Tool handlers panic if it isn't initialized. lib.NewSessionManager() + // Route the MCP SDK's warnings and errors through zerolog (its Info lines + // are per-session lifecycle chatter) + sdkLogger := lib.NewSlogLogger(slog.LevelWarn) + // Create a new MCP server impl := &mcp.Implementation{Name: "Blues Expert MCP", Version: serverVersion()} opts := &mcp.ServerOptions{ Instructions: "This MCP server provides expert guidance on using the Blues Notecard & Notehub. When using this tool for developing firmware, use the 'firmware_entrypoint' tool to get started. Otherwise, use the 'docs_search' tool to search the Blues documentation.", + Logger: sdkLogger, } s := mcp.NewServer(impl, opts) @@ -170,6 +176,7 @@ func main() { return s }, &mcp.StreamableHTTPOptions{ DisableLocalhostProtection: disableLocalhostProtection, + Logger: sdkLogger, }) // Route MCP server requests to /expert/ path with panic recovery. From 6b908add92ddd0c106d11a12d1d76185d35e05cd Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 5 Oct 2026 13:40:00 -0500 Subject: [PATCH 5/6] feat: log MCP requests rejected with 4xx/5xx responses Makes rejections inside the SDK (415 Content-Type, 403 Host, 404 session) visible in server logs. Co-Authored-By: Claude Opus 5.5 --- blues-expert/main.go | 60 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/blues-expert/main.go b/blues-expert/main.go index 494984a..1eb7fae 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -13,6 +13,7 @@ import ( "github.com/joho/godotenv" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -56,6 +57,61 @@ func panicRecoveryMiddleware(next http.HandlerFunc) http.HandlerFunc { } } +// statusRecorder captures the response status for logRejectedRequests. It +// forwards Flush and Unwrap so SSE streaming keeps working through it. +type statusRecorder struct { + http.ResponseWriter + status int +} + +func (r *statusRecorder) WriteHeader(code int) { + if r.status == 0 && code >= http.StatusOK { + r.status = code + } + r.ResponseWriter.WriteHeader(code) +} + +func (r *statusRecorder) Write(b []byte) (int, error) { + if r.status == 0 { + r.status = http.StatusOK + } + return r.ResponseWriter.Write(b) +} + +func (r *statusRecorder) Flush() { + _ = http.NewResponseController(r.ResponseWriter).Flush() +} + +func (r *statusRecorder) Unwrap() http.ResponseWriter { + return r.ResponseWriter +} + +// logRejectedRequests logs MCP requests that end in a 4xx or 5xx response, +// including rejections made inside the SDK (e.g. 415 for a missing +// Content-Type, 403 for a foreign Host header, 404 for an expired session). +func logRejectedRequests(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rec := &statusRecorder{ResponseWriter: w} + next(rec, r) + if rec.status < http.StatusBadRequest { + return + } + level := zerolog.WarnLevel + if rec.status >= http.StatusInternalServerError { + level = zerolog.ErrorLevel + } + log.WithLevel(level). + Str("method", r.Method). + Str("path", r.URL.Path). + Int("status", rec.status). + Str("host", r.Host). + Str("content_type", r.Header.Get("Content-Type")). + Str("user_agent", r.UserAgent()). + Bool("has_session", r.Header.Get("Mcp-Session-Id") != ""). + Msg("MCP request rejected") + } +} + // servePublicSSEStream answers a session-less GET to the MCP endpoint with an // open (keep-alive only) SSE stream, returning 200 instead of the go-sdk's // default 405 "GET requires an active session". @@ -186,13 +242,13 @@ func main() { // servePublicSSEStream for why). POST, DELETE, and session-scoped GET — the // transport paths real MCP clients and Claude Code use — are untouched and // delegated to the SDK. - mux.HandleFunc("/expert/", panicRecoveryMiddleware(func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/expert/", panicRecoveryMiddleware(logRejectedRequests(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && r.URL.Path == mcpEndpointPath && r.Header.Get("Mcp-Session-Id") == "" { servePublicSSEStream(w, r) return } httpHandler.ServeHTTP(w, r) - })) + }))) log.Info().Str("port", port).Msg("Starting HTTP server") log.Info().Msg("MCP server available at /expert/") From e3035e4496861d47d77c527731de44e27644e572 Mon Sep 17 00:00:00 2001 From: Rob Lauer Date: Mon, 5 Oct 2026 13:40:01 -0500 Subject: [PATCH 6/6] feat: expire idle MCP sessions after 24 hours Abandoned sessions previously stayed in memory until the next deploy. The app's own session tracking now uses the same timeout instead of 1 hour. Co-Authored-By: Claude Opus 5.5 --- blues-expert/lib/session.go | 8 ++++++-- blues-expert/main.go | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/blues-expert/lib/session.go b/blues-expert/lib/session.go index 2c6b74c..5c921a6 100644 --- a/blues-expert/lib/session.go +++ b/blues-expert/lib/session.go @@ -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 @@ -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) } } diff --git a/blues-expert/main.go b/blues-expert/main.go index 1eb7fae..8a08ff5 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -227,11 +227,15 @@ func main() { log.Warn().Msg("DNS rebinding (localhost) protection is disabled") } - // Create StreamableHTTPHandler for MCP requests + // Create StreamableHTTPHandler for MCP requests. The session timeout keeps + // abandoned sessions from accumulating in memory; only POSTs count as + // activity, and a client returning after it expires gets a 404 and must + // re-initialize. httpHandler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return s }, &mcp.StreamableHTTPOptions{ DisableLocalhostProtection: disableLocalhostProtection, + SessionTimeout: lib.SessionIdleTimeout, Logger: sdkLogger, })