Branch: 001-update-version-display | Date: 2025-12-15
This feature adds centralized version display and update notifications to MCPProxy. The core server checks GitHub releases every 4 hours and exposes update info via REST API. Tray, WebUI, and CLI consume this API.
-
Create
internal/updatecheck/packageinternal/updatecheck/ ├── checker.go # Background service ├── github.go # GitHub API client └── types.go # VersionInfo, GitHubRelease types -
Key files to create/modify:
internal/updatecheck/checker.go- Background ticker serviceinternal/updatecheck/github.go- Refactor frominternal/tray/tray.go:1062-1106internal/httpapi/server.go- ExtendhandleGetInfo()with update field
- Modify
internal/tray/tray.go:- Add version menu item at top
- Replace
checkForUpdates()with API polling - Show "New version available" conditionally
- Create
frontend/src/components/UpdateBanner.vue - Modify
frontend/src/App.vueto show version in footer/sidebar
- Modify
cmd/mcpproxy/doctor.goto show version + update status
- Create
docs/features/version-updates.md - Add tests: Unit tests + E2E API tests
// internal/updatecheck/checker.go
package updatecheck
import (
"context"
"sync"
"time"
"go.uber.org/zap"
"golang.org/x/mod/semver"
)
const (
DefaultCheckInterval = 4 * time.Hour
GitHubRepo = "smart-mcp-proxy/mcpproxy-go"
)
type Checker struct {
logger *zap.Logger
version string
checkInterval time.Duration
mu sync.RWMutex
versionInfo *VersionInfo
}
func New(logger *zap.Logger, version string) *Checker {
return &Checker{
logger: logger,
version: version,
checkInterval: DefaultCheckInterval,
}
}
func (c *Checker) Start(ctx context.Context) {
// Check if disabled
if os.Getenv("MCPPROXY_DISABLE_AUTO_UPDATE") == "true" {
c.logger.Info("Update checker disabled by environment variable")
return
}
// Initial check
c.check()
ticker := time.NewTicker(c.checkInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.check()
}
}
}
func (c *Checker) GetVersionInfo() *VersionInfo {
c.mu.RLock()
defer c.mu.RUnlock()
return c.versionInfo
}// internal/httpapi/server.go - modify handleGetInfo()
func (s *Server) handleGetInfo(w http.ResponseWriter, r *http.Request) {
// ... existing code ...
// Add update info
var updateInfo interface{}
if s.updateChecker != nil {
updateInfo = s.updateChecker.GetVersionInfo().ToAPIResponse()
}
response := map[string]interface{}{
"version": version,
"web_ui_url": webUIURL,
"listen_addr": listenAddr,
"endpoints": endpoints,
"update": updateInfo, // NEW
}
s.writeSuccess(w, response)
}// internal/tray/tray.go - in setupMenu()
// Add version at top of menu
versionItem := systray.AddMenuItem("MCPProxy "+a.version, "Current version")
versionItem.Disable()
systray.AddSeparator()
// ... rest of menu setup ...
// Add update menu item (shown conditionally)
a.updateMenuItem = systray.AddMenuItem("", "")
a.updateMenuItem.Hide() // Hidden by default, shown when update available<!-- frontend/src/components/UpdateBanner.vue -->
<template>
<div v-if="showBanner && updateInfo?.available"
class="alert alert-info shadow-lg mb-4">
<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="stroke-current flex-shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>
Update available: <strong>{{ updateInfo.latest_version }}</strong>
</span>
</div>
<div class="flex-none">
<a :href="updateInfo.release_url" target="_blank"
class="btn btn-sm btn-primary">
View Release
</a>
<button @click="dismiss" class="btn btn-sm btn-ghost">
Dismiss
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import type { UpdateInfo } from '@/types/contracts';
const props = defineProps<{ updateInfo: UpdateInfo | null }>();
const dismissed = ref(false);
const showBanner = computed(() => !dismissed.value);
function dismiss() {
dismissed.value = true;
sessionStorage.setItem('update-banner-dismissed', 'true');
}
onMounted(() => {
dismissed.value = sessionStorage.getItem('update-banner-dismissed') === 'true';
});
</script>// cmd/mcpproxy/doctor.go
func runDoctor(cmd *cobra.Command, args []string) error {
fmt.Println("MCPProxy Doctor")
fmt.Printf("Version: %s", version)
// Get update info from API if server is running
if updateInfo := getUpdateInfo(); updateInfo != nil {
if updateInfo.UpdateAvailable {
fmt.Printf(" (update available: %s)", *updateInfo.LatestVersion)
fmt.Printf("\n Download: %s", *updateInfo.ReleaseURL)
} else {
fmt.Print(" (latest)")
}
}
fmt.Println()
fmt.Println("───────────────────────────")
// ... rest of doctor checks ...
}-
internal/updatecheck/checker_test.go- Test ticker, version comparison -
internal/updatecheck/github_test.go- Test API response parsing
- API endpoint returns update field
- Null update when disabled via env var
- Add to
scripts/test-api-e2e.sh:# Test version endpoint response=$(curl -s -H "X-API-Key: $API_KEY" "$BASE_URL/api/v1/info") assert_json_field "$response" ".data.version" "v"
- Tray shows version in menu
- "New version available" appears when update exists
- Clicking opens GitHub releases
- WebUI banner shows/dismisses correctly
-
mcpproxy doctorshows version
| Variable | Effect |
|---|---|
MCPPROXY_DISABLE_AUTO_UPDATE=true |
Disables background checks entirely |
MCPPROXY_ALLOW_PRERELEASE_UPDATES=true |
Includes prereleases in comparison |
| File | Purpose |
|---|---|
internal/updatecheck/checker.go |
Background update service |
internal/updatecheck/github.go |
GitHub API client |
internal/updatecheck/types.go |
Shared types |
internal/updatecheck/checker_test.go |
Unit tests |
frontend/src/components/UpdateBanner.vue |
WebUI notification |
docs/features/version-updates.md |
User documentation |
| File | Changes |
|---|---|
internal/httpapi/server.go |
Add update field to /api/v1/info |
internal/tray/tray.go |
Add version menu item, update notification |
cmd/mcpproxy/doctor.go |
Add version output |
frontend/src/App.vue |
Show version, include UpdateBanner |
oas/swagger.yaml |
Document API extension |
scripts/test-api-e2e.sh |
Add version endpoint tests |