Skip to content

Commit d5016a6

Browse files
committed
Refactor configuration defaults and improve error handling
- Introduced a constant for the default port in config.go to enhance maintainability. - Updated DefaultConfig to use the new constant for setting the Listen address. - Improved error handling in loader.go by changing file write permissions for security. - Added a testServerConnection method in e2e_test.go to verify server readiness more effectively. - Refactored function signatures in various files for clarity and consistency.
1 parent 816e539 commit d5016a6

11 files changed

Lines changed: 208 additions & 82 deletions

File tree

internal/config/config.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import (
55
"time"
66
)
77

8+
const (
9+
defaultPort = ":8080"
10+
)
11+
812
// Config represents the main configuration structure
913
type Config struct {
1014
Listen string `json:"listen" mapstructure:"listen"`
@@ -140,7 +144,7 @@ type ToolStatEntry struct {
140144
// DefaultConfig returns a default configuration
141145
func DefaultConfig() *Config {
142146
return &Config{
143-
Listen: ":8080",
147+
Listen: defaultPort,
144148
DataDir: "", // Will be set to ~/.mcpproxy by loader
145149
EnableTray: true,
146150
DebugSearch: false,
@@ -176,7 +180,7 @@ func DefaultConfig() *Config {
176180
// Validate validates the configuration
177181
func (c *Config) Validate() error {
178182
if c.Listen == "" {
179-
c.Listen = ":8080"
183+
c.Listen = defaultPort
180184
}
181185
if c.TopK <= 0 {
182186
c.TopK = 5

internal/config/loader.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func setupViper() {
153153
}
154154

155155
// findAndLoadConfigFile tries to find config file in common locations
156-
func findAndLoadConfigFile(cfg *Config) (bool, string, error) {
156+
func findAndLoadConfigFile(cfg *Config) (found bool, path string, err error) {
157157
// Common config file locations
158158
locations := []string{
159159
ConfigFileName,
@@ -247,7 +247,7 @@ func SaveConfig(cfg *Config, path string) error {
247247
}
248248

249249
fmt.Printf("[DEBUG] SaveConfig - about to write file: %s\n", path)
250-
if err := os.WriteFile(path, data, 0644); err != nil {
250+
if err := os.WriteFile(path, data, 0600); err != nil {
251251
fmt.Printf("[DEBUG] SaveConfig - WriteFile failed: %v\n", err)
252252
return fmt.Errorf("failed to write config file: %w", err)
253253
}
@@ -325,9 +325,7 @@ func CreateSampleConfig(path string) error {
325325
}
326326

327327
// Helper function to get current time (useful for testing)
328-
var now = func() time.Time {
329-
return time.Now()
330-
}
328+
var now = time.Now
331329

332330
// createDefaultConfigFile creates a default configuration file with default settings
333331
func createDefaultConfigFile(path string, cfg *Config) error {

internal/server/e2e_test.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,41 @@ func (env *TestEnvironment) waitForServerReady() {
136136
env.t.Fatalf("Timeout waiting for server to be ready. Status: %+v", status)
137137
case <-ticker.C:
138138
if env.proxyServer.IsRunning() {
139-
// Give it a bit more time to fully initialize
140-
time.Sleep(2 * time.Second)
141-
return
139+
// Actually test if the HTTP server is accepting connections
140+
if env.testServerConnection() {
141+
// Give it a bit more time to fully initialize
142+
time.Sleep(500 * time.Millisecond)
143+
return
144+
}
142145
}
143146
}
144147
}
145148
}
146149

150+
// testServerConnection tests if the server is actually accepting HTTP connections
151+
func (env *TestEnvironment) testServerConnection() bool {
152+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
153+
defer cancel()
154+
155+
req, err := http.NewRequestWithContext(ctx, "GET", env.proxyAddr, http.NoBody)
156+
if err != nil {
157+
return false
158+
}
159+
160+
client := &http.Client{
161+
Timeout: 1 * time.Second,
162+
}
163+
164+
resp, err := client.Do(req)
165+
if err != nil {
166+
return false
167+
}
168+
resp.Body.Close()
169+
170+
// Any response (even an error response) means the server is accepting connections
171+
return true
172+
}
173+
147174
// CreateMockUpstreamServer creates and starts a mock upstream MCP server
148175
func (env *TestEnvironment) CreateMockUpstreamServer(name string, tools []mcp.Tool) *MockUpstreamServer {
149176
// Create MCP server

internal/server/mcp.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ func (p *MCPProxyServer) handleInspectQuarantinedTools(ctx context.Context, requ
722722
toolAnalysis := map[string]interface{}{
723723
"name": tool.Name,
724724
"full_name": fmt.Sprintf("%s:%s", serverName, tool.Name),
725-
"description": fmt.Sprintf("\"%s\"", tool.Description), // Quote the description for LLM analysis
725+
"description": fmt.Sprintf("%q", tool.Description), // Quote the description for LLM analysis
726726
"input_schema": inputSchema,
727727
"server_name": serverName,
728728
"quarantine_status": "QUARANTINED",
@@ -1213,7 +1213,7 @@ func (p *MCPProxyServer) analyzeQuery(query string) map[string]interface{} {
12131213
}
12141214

12151215
// explainToolRanking explains why a specific tool was ranked as it was
1216-
func (p *MCPProxyServer) explainToolRanking(query string, targetTool string, results []*config.SearchResult) map[string]interface{} {
1216+
func (p *MCPProxyServer) explainToolRanking(query, targetTool string, results []*config.SearchResult) map[string]interface{} {
12171217
explanation := map[string]interface{}{
12181218
"target_tool": targetTool,
12191219
"query": query,
@@ -1223,18 +1223,19 @@ func (p *MCPProxyServer) explainToolRanking(query string, targetTool string, res
12231223

12241224
// Find the tool in results
12251225
for i, result := range results {
1226-
if result.Tool.Name == targetTool {
1227-
explanation["found_in_results"] = true
1228-
explanation["rank"] = i + 1
1229-
explanation["score"] = result.Score
1230-
explanation["tool_details"] = map[string]interface{}{
1231-
"name": result.Tool.Name,
1232-
"server": result.Tool.ServerName,
1233-
"description": result.Tool.Description,
1234-
"has_params": len(result.Tool.ParamsJSON) > 0,
1235-
}
1236-
break
1226+
if result.Tool.Name != targetTool {
1227+
continue
1228+
}
1229+
explanation["found_in_results"] = true
1230+
explanation["rank"] = i + 1
1231+
explanation["score"] = result.Score
1232+
explanation["tool_details"] = map[string]interface{}{
1233+
"name": result.Tool.Name,
1234+
"server": result.Tool.ServerName,
1235+
"description": result.Tool.Description,
1236+
"has_params": result.Tool.ParamsJSON != "",
12371237
}
1238+
break
12381239
}
12391240

12401241
// Analyze why tool might not rank well
@@ -1256,8 +1257,9 @@ func (p *MCPProxyServer) explainToolRanking(query string, targetTool string, res
12561257
if strings.Contains(targetTool, ":") {
12571258
parts := strings.SplitN(targetTool, ":", 2)
12581259
if len(parts) == 2 {
1259-
suggestions = append(suggestions, fmt.Sprintf("Try searching for server name: '%s'", parts[0]))
1260-
suggestions = append(suggestions, fmt.Sprintf("Try searching for tool name: '%s'", parts[1]))
1260+
suggestions = append(suggestions,
1261+
fmt.Sprintf("Try searching for server name: '%s'", parts[0]),
1262+
fmt.Sprintf("Try searching for tool name: '%s'", parts[1]))
12611263
if strings.Contains(parts[1], "_") {
12621264
words := strings.Split(parts[1], "_")
12631265
suggestions = append(suggestions, fmt.Sprintf("Try searching for individual words: '%s'", strings.Join(words, " ")))

internal/server/server.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ func (s *Server) backgroundToolIndexing(ctx context.Context) {
292292

293293
// loadConfiguredServers synchronizes the storage and upstream manager from the current config.
294294
// This is the source of truth when configuration is loaded from disk.
295+
//
296+
//nolint:unparam // function designed to be best-effort, always returns nil by design
295297
func (s *Server) loadConfiguredServers() error {
296298
s.logger.Info("Synchronizing servers from configuration (config as source of truth)")
297299

@@ -764,7 +766,9 @@ func (s *Server) StopServer() error {
764766
defer s.mu.Unlock()
765767

766768
if !s.running {
767-
return fmt.Errorf("server is not running")
769+
// Return nil instead of error to prevent race condition logs
770+
s.logger.Debug("Server stop requested but server is not running")
771+
return nil
768772
}
769773

770774
// Notify about server stopping

internal/storage/bbolt.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"go.etcd.io/bbolt"
12+
"go.etcd.io/bbolt/errors"
1213
"go.uber.org/zap"
1314
)
1415

@@ -30,7 +31,7 @@ func NewBoltDB(dataDir string, logger *zap.SugaredLogger) (*BoltDB, error) {
3031
logger.Warnf("Failed to open database on first attempt: %v", err)
3132

3233
// Check if it's a timeout or lock issue
33-
if err == bbolt.ErrTimeout {
34+
if err == errors.ErrTimeout {
3435
logger.Info("Database timeout detected, attempting recovery...")
3536

3637
// Try to backup and recreate if file exists

internal/tray/managers.go

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import (
1212
"go.uber.org/zap"
1313
)
1414

15+
const (
16+
actionEnable = "enable"
17+
actionDisable = "disable"
18+
textEnable = "Enable"
19+
textDisable = "Disable"
20+
)
21+
1522
// ServerStateManager manages server state synchronization between storage, config, and menu
1623
type ServerStateManager struct {
1724
server ServerInterface
@@ -150,9 +157,9 @@ func (m *ServerStateManager) UnquarantineServer(serverName string) error {
150157

151158
// EnableServer enables/disables a server and ensures all state is synchronized
152159
func (m *ServerStateManager) EnableServer(serverName string, enabled bool) error {
153-
action := "disable"
160+
action := actionDisable
154161
if enabled {
155-
action = "enable"
162+
action = actionEnable
156163
}
157164

158165
m.logger.Info("EnableServer called",
@@ -274,16 +281,17 @@ func (m *MenuManager) UpdateUpstreamServersMenu(servers []map[string]interface{}
274281

275282
// --- Create Menu Items for New Servers ---
276283
for serverName, serverData := range currentServerMap {
277-
if _, exists := m.serverMenuItems[serverName]; !exists {
278-
// This is a new server, create its menu item
279-
m.logger.Info("Creating menu item for new server", zap.String("server", serverName))
280-
status, tooltip := m.getServerStatusDisplay(serverData)
281-
serverMenuItem := m.upstreamServersMenu.AddSubMenuItem(status, tooltip)
282-
m.serverMenuItems[serverName] = serverMenuItem
283-
284-
// Create its action submenus
285-
m.createServerActionSubmenus(serverMenuItem, serverData)
284+
if _, exists := m.serverMenuItems[serverName]; exists {
285+
continue
286286
}
287+
// This is a new server, create its menu item
288+
m.logger.Info("Creating menu item for new server", zap.String("server", serverName))
289+
status, tooltip := m.getServerStatusDisplay(serverData)
290+
serverMenuItem := m.upstreamServersMenu.AddSubMenuItem(status, tooltip)
291+
m.serverMenuItems[serverName] = serverMenuItem
292+
293+
// Create its action submenus
294+
m.createServerActionSubmenus(serverMenuItem, serverData)
287295
}
288296
}
289297

@@ -391,7 +399,7 @@ func (m *MenuManager) ForceRefresh() {
391399
}
392400

393401
// getServerStatusDisplay returns display text and tooltip for a server
394-
func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (string, string) {
402+
func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (displayText, tooltip string) {
395403
serverName, _ := server["name"].(string)
396404
enabled, _ := server["enabled"].(bool)
397405
connected, _ := server["connected"].(bool)
@@ -415,10 +423,10 @@ func (m *MenuManager) getServerStatusDisplay(server map[string]interface{}) (str
415423
statusText = "disconnected"
416424
}
417425

418-
displayText := fmt.Sprintf("%s %s", statusIcon, serverName)
419-
tooltip := fmt.Sprintf("%s - %s", serverName, statusText)
426+
displayText = fmt.Sprintf("%s %s", statusIcon, serverName)
427+
tooltip = fmt.Sprintf("%s - %s", serverName, statusText)
420428

421-
return displayText, tooltip
429+
return
422430
}
423431

424432
// createServerActionSubmenus creates action submenus for a server (enable/disable, quarantine)
@@ -434,9 +442,9 @@ func (m *MenuManager) createServerActionSubmenus(serverMenuItem *systray.MenuIte
434442
// Enable/Disable action
435443
var enableText string
436444
if enabled {
437-
enableText = "Disable"
445+
enableText = textDisable
438446
} else {
439-
enableText = "Enable"
447+
enableText = textEnable
440448
}
441449
enableItem := serverMenuItem.AddSubMenuItem(enableText, fmt.Sprintf("%s server %s", enableText, serverName))
442450
m.serverActionItems[serverName] = enableItem
@@ -477,9 +485,9 @@ func (m *MenuManager) updateServerActionMenus(serverName string, server map[stri
477485
if actionItem, exists := m.serverActionItems[serverName]; exists {
478486
var enableText string
479487
if enabled {
480-
enableText = "Disable"
488+
enableText = textDisable
481489
} else {
482-
enableText = "Enable"
490+
enableText = textEnable
483491
}
484492
actionItem.SetTitle(enableText)
485493
actionItem.SetTooltip(fmt.Sprintf("%s server %s", enableText, serverName))

internal/tray/tray.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import (
2424
)
2525

2626
const (
27-
repo = "smart-mcp-proxy/mcpproxy-go" // Actual repository
27+
repo = "smart-mcp-proxy/mcpproxy-go" // Actual repository
28+
osDarwin = "darwin"
2829
)
2930

3031
//go:embed icon-mono-44.png
@@ -279,7 +280,7 @@ func (a *App) cleanup() {
279280
func (a *App) onReady() {
280281
systray.SetIcon(iconData)
281282
// On macOS, also set as template icon for better system integration
282-
if runtime.GOOS == "darwin" {
283+
if runtime.GOOS == osDarwin {
283284
systray.SetTemplateIcon(iconData, iconData)
284285
}
285286
a.updateTooltip()
@@ -448,7 +449,7 @@ func (a *App) updateTooltipFromStatusData(status map[string]interface{}) {
448449
}
449450

450451
// updateServersMenuFromStatusData is a legacy method, functionality is now in MenuManager
451-
func (a *App) updateServersMenuFromStatusData(status map[string]interface{}) {
452+
func (a *App) updateServersMenuFromStatusData(_ map[string]interface{}) {
452453
// This function is kept for reference during transition but the primary
453454
// logic is now handled by the MenuManager and SynchronizationManager.
454455
// We trigger a sync instead of manually updating here.
@@ -534,7 +535,7 @@ func (a *App) checkForUpdates() {
534535
// getLatestRelease fetches the latest release information from GitHub
535536
func (a *App) getLatestRelease() (*GitHubRelease, error) {
536537
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
537-
resp, err := http.Get(url)
538+
resp, err := http.Get(url) // #nosec G107 -- URL is constructed from known repo constant
538539
if err != nil {
539540
return nil, err
540541
}
@@ -560,7 +561,7 @@ func (a *App) findAssetURL(release *GitHubRelease) (string, error) {
560561

561562
// downloadAndApplyUpdate downloads and applies the update
562563
func (a *App) downloadAndApplyUpdate(url string) error {
563-
resp, err := http.Get(url)
564+
resp, err := http.Get(url) // #nosec G107 -- URL is from GitHub releases API which is trusted
564565
if err != nil {
565566
return err
566567
}
@@ -599,15 +600,17 @@ func (a *App) applyZipUpdate(body io.Reader) error {
599600
}
600601

601602
for _, f := range r.File {
602-
if !f.FileInfo().IsDir() {
603-
rc, err := f.Open()
604-
if err != nil {
605-
return err
606-
}
607-
defer rc.Close()
608-
609-
return update.Apply(rc, update.Options{TargetPath: executablePath})
603+
if f.FileInfo().IsDir() {
604+
continue
605+
}
606+
rc, err := f.Open()
607+
if err != nil {
608+
return err
610609
}
610+
611+
err = update.Apply(rc, update.Options{TargetPath: executablePath})
612+
rc.Close()
613+
return err
611614
}
612615

613616
return fmt.Errorf("no file found in zip archive to apply")
@@ -655,7 +658,7 @@ func (a *App) refreshMenusImmediate() {
655658
}
656659

657660
// handleServerAction is the centralized handler for all server-related menu actions.
658-
func (a *App) handleServerAction(serverName string, action string) {
661+
func (a *App) handleServerAction(serverName, action string) {
659662
var err error
660663
a.logger.Info("Handling server action", zap.String("server", serverName), zap.String("action", action))
661664

internal/tray/tray_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (m *MockServerInterface) GetUpstreamStats() map[string]interface{} {
4545
return m.upstreamStats
4646
}
4747

48-
func (m *MockServerInterface) StartServer(ctx context.Context) error {
48+
func (m *MockServerInterface) StartServer(_ context.Context) error {
4949
m.running = true
5050
return nil
5151
}

0 commit comments

Comments
 (0)