mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
Fix update-available notice: goroutine ordering, body limit, UX
Three bugs prevented the update notice from appearing: - checkForUpdate goroutine launched after blocking sessionOnce.Do, so it never ran before tool calls arrived - 4KB body limit truncated the ~15KB GitHub API response - Notice was a JSON map key with no guaranteed ordering Fixes: - Move go checkForUpdate() before sessionOnce.Do in all handlers - Increase body read limit to 64KB - Prepend notice as separate MCP TextContent block (appears first) - Link to GitHub releases page instead of go install command
This commit is contained in:
@@ -111,9 +111,10 @@ func (s *Server) handleSearchCode(_ context.Context, req *mcp.CallToolRequest) (
|
||||
"files_count": len(filePaths),
|
||||
}
|
||||
s.addIndexStatus(responseData)
|
||||
s.addUpdateNotice(responseData)
|
||||
|
||||
return jsonResult(responseData), nil
|
||||
result := jsonResult(responseData)
|
||||
s.addUpdateNotice(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectSearchFilePaths gathers indexed file paths, optionally filtered by a glob pattern.
|
||||
|
||||
@@ -36,7 +36,8 @@ func (s *Server) handleQueryGraph(_ context.Context, req *mcp.CallToolRequest) (
|
||||
"total": len(result.Rows),
|
||||
}
|
||||
s.addIndexStatus(responseData)
|
||||
s.addUpdateNotice(responseData)
|
||||
|
||||
return jsonResult(responseData), nil
|
||||
res := jsonResult(responseData)
|
||||
s.addUpdateNotice(res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -97,7 +97,8 @@ func (s *Server) handleSearchGraph(_ context.Context, req *mcp.CallToolRequest)
|
||||
"results": results,
|
||||
}
|
||||
s.addIndexStatus(responseData)
|
||||
s.addUpdateNotice(responseData)
|
||||
|
||||
return jsonResult(responseData), nil
|
||||
result := jsonResult(responseData)
|
||||
s.addUpdateNotice(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
+18
-10
@@ -108,19 +108,20 @@ func (s *Server) SessionProject() string {
|
||||
|
||||
// SetSessionRoot sets the session root path directly (for CLI mode).
|
||||
func (s *Server) SetSessionRoot(rootPath string) {
|
||||
go s.checkForUpdate()
|
||||
s.sessionOnce.Do(func() {
|
||||
s.sessionRoot = rootPath
|
||||
if rootPath != "" {
|
||||
s.sessionProject = filepath.Base(rootPath)
|
||||
}
|
||||
})
|
||||
go s.checkForUpdate()
|
||||
}
|
||||
|
||||
// --- Session detection ---
|
||||
|
||||
// onInitialized is called when the client sends notifications/initialized.
|
||||
func (s *Server) onInitialized(ctx context.Context, req *mcp.InitializedRequest) {
|
||||
go s.checkForUpdate()
|
||||
s.sessionOnce.Do(func() {
|
||||
s.sessionRoot = s.detectSessionRoot(ctx, req.Session)
|
||||
if s.sessionRoot != "" {
|
||||
@@ -128,11 +129,11 @@ func (s *Server) onInitialized(ctx context.Context, req *mcp.InitializedRequest)
|
||||
s.startAutoIndex()
|
||||
}
|
||||
})
|
||||
go s.checkForUpdate()
|
||||
}
|
||||
|
||||
// onRootsChanged re-detects session root if not yet set.
|
||||
func (s *Server) onRootsChanged(ctx context.Context, req *mcp.RootsListChangedRequest) {
|
||||
go s.checkForUpdate()
|
||||
s.sessionOnce.Do(func() {
|
||||
s.sessionRoot = s.detectSessionRoot(ctx, req.Session)
|
||||
if s.sessionRoot != "" {
|
||||
@@ -140,7 +141,6 @@ func (s *Server) onRootsChanged(ctx context.Context, req *mcp.RootsListChangedRe
|
||||
s.startAutoIndex()
|
||||
}
|
||||
})
|
||||
go s.checkForUpdate()
|
||||
}
|
||||
|
||||
// detectSessionRoot tries multiple fallback strategies to find the project root.
|
||||
@@ -255,10 +255,10 @@ func (s *Server) addIndexStatus(data map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
// addUpdateNotice injects update_available into the first tool response, then clears itself.
|
||||
func (s *Server) addUpdateNotice(data map[string]any) {
|
||||
// addUpdateNotice prepends an update notice to the first tool response, then clears itself.
|
||||
func (s *Server) addUpdateNotice(result *mcp.CallToolResult) {
|
||||
if notice, ok := s.updateNotice.Load().(string); ok && notice != "" {
|
||||
data["update_available"] = notice
|
||||
result.Content = append([]mcp.Content{&mcp.TextContent{Text: notice}}, result.Content...)
|
||||
s.updateNotice.Store("")
|
||||
}
|
||||
}
|
||||
@@ -270,22 +270,26 @@ func (s *Server) checkForUpdate() {
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", releaseURL, http.NoBody)
|
||||
if err != nil {
|
||||
slog.Warn("update check: request create failed", "err", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
slog.Warn("update check: http failed", "err", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
slog.Warn("update check: bad status", "status", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
|
||||
if err != nil {
|
||||
slog.Warn("update check: body read failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -293,17 +297,21 @@ func (s *Server) checkForUpdate() {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &release); err != nil {
|
||||
slog.Warn("update check: json parse failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
latest := strings.TrimPrefix(release.TagName, "v")
|
||||
if latest == "" || latest == Version {
|
||||
slog.Debug("update check: current", "version", Version, "latest", latest)
|
||||
return
|
||||
}
|
||||
if compareVersions(latest, Version) > 0 {
|
||||
s.updateNotice.Store(fmt.Sprintf(
|
||||
"v%s → v%s available. Update: go install github.com/DeusData/codebase-memory-mcp/cmd/codebase-memory-mcp@v%s",
|
||||
Version, latest, latest))
|
||||
notice := fmt.Sprintf(
|
||||
"⚡ Update available: v%s → v%s — download at https://github.com/DeusData/codebase-memory-mcp/releases/tag/v%s",
|
||||
Version, latest, latest)
|
||||
s.updateNotice.Store(notice)
|
||||
slog.Info("update available", "current", Version, "latest", latest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,9 +122,10 @@ func (s *Server) handleTraceCallPath(_ context.Context, req *mcp.CallToolRequest
|
||||
"total_results": len(allVisited),
|
||||
}
|
||||
s.addIndexStatus(responseData)
|
||||
s.addUpdateNotice(responseData)
|
||||
|
||||
return jsonResult(responseData), nil
|
||||
result := jsonResult(responseData)
|
||||
s.addUpdateNotice(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildNodeInfo(n *store.Node) map[string]any {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
@@ -55,8 +57,8 @@ func TestCheckForUpdate_NewerAvailable(t *testing.T) {
|
||||
if !strings.Contains(notice, "v99.0.0") {
|
||||
t.Fatalf("notice should mention v99.0.0, got: %s", notice)
|
||||
}
|
||||
if !strings.Contains(notice, "go install") {
|
||||
t.Fatalf("notice should contain install command, got: %s", notice)
|
||||
if !strings.Contains(notice, "github.com/DeusData/codebase-memory-mcp/releases/tag/v99.0.0") {
|
||||
t.Fatalf("notice should contain releases link, got: %s", notice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,15 +142,19 @@ func TestAddUpdateNotice_ShowsOnce(t *testing.T) {
|
||||
srv := &Server{}
|
||||
srv.updateNotice.Store("v0.2.0 → v0.3.0 available")
|
||||
|
||||
first := map[string]any{}
|
||||
first := &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "{}"}}}
|
||||
srv.addUpdateNotice(first)
|
||||
if _, ok := first["update_available"]; !ok {
|
||||
t.Fatal("expected update_available in first map")
|
||||
if len(first.Content) != 2 {
|
||||
t.Fatalf("expected 2 content blocks, got %d", len(first.Content))
|
||||
}
|
||||
tc, ok := first.Content[0].(*mcp.TextContent)
|
||||
if !ok || tc.Text != "v0.2.0 → v0.3.0 available" {
|
||||
t.Fatalf("expected notice as first content block, got %v", first.Content[0])
|
||||
}
|
||||
|
||||
second := map[string]any{}
|
||||
second := &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "{}"}}}
|
||||
srv.addUpdateNotice(second)
|
||||
if _, ok := second["update_available"]; ok {
|
||||
t.Fatal("expected update_available to be absent in second map (show-once)")
|
||||
if len(second.Content) != 1 {
|
||||
t.Fatal("expected update notice to be absent in second call (show-once)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user