Fix staticcheck findings

Error message case and punctuation ending is harmless,
Unused code/structs helps maintenance,
Unused mutexes raise questions...

I've tried to fix locking for internal/mcp/client and bash,
(BashServer, MCPToolExecutor) and it runs without errors
compiled even with -race flag, but some comments
about the concurrency access patterns would help.

MCPToolExecutor is a pain point with its reconnecting logic.
This commit is contained in:
Tamás Gulácsi committed 2026-05-25 11:21:28 +02:00
1 parent a64e0ac7b5
commit a2ed0d9997
14 files changed
+182 -156

No files matched your search

+4 -6
View File
@@ -40,8 +40,6 @@ import (
"github.com/cheikh2shift/godex/modelquery"
)
var clipboardAvailable = true
const (
defaultConfigFile = "providers.yaml"
sessionFileName = "session.txt"
@@ -1445,8 +1443,8 @@ func handleMCPSubcommand(args []string, configPath string) error {
if err := config.Save(configPath, cfg); err != nil {
return err
}
fmt.Printf("Added MCP server %q to provider %q\n", serverName, provider.Name)
return nil
fmt.Printf("Added MCP server %q to provider %q\n", serverName, provider.Name)
return nil
case "serve":
return handleMCPServeSubcommand(args[1:])
case "remove":
@@ -2522,7 +2520,7 @@ func callTool(ctx context.Context, servers []MCPServer, name string, args map[st
allowedPaths := server.AllowedPaths()
if autoDenyRestrictedPaths {
log.Printf("[Hive] Auto-denied restricted path access: %s (allowed paths: %v)\n", path, allowedPaths)
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction.", path)
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction", path)
}
pathPromptMu.Lock()
selected := showPathRestrictionPrompt(path, allowedPaths)
@@ -2546,7 +2544,7 @@ func callTool(ctx context.Context, servers []MCPServer, name string, args map[st
case 2:
return "", ErrUserAborted
case 3:
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction.", path)
return "", fmt.Errorf("PATH_RESTRICTED: '%s' is not in allowed paths. Do NOT try to access this path. Find an alternative solution that does not require this file/path. If no alternative exists, respond with FINAL_ANSWER: and explain the restriction", path)
}
return "", err
}
+1 -3
View File
@@ -660,9 +660,7 @@ func (m *promptModel) handleScheduleTab(value string) bool {
return true
}
m.scheduleList = nil
for _, id := range matches {
m.scheduleList = append(m.scheduleList, id)
}
m.scheduleList = append(m.scheduleList, matches...)
m.resetCompletion()
return true
}
+13 -1
View File
@@ -332,6 +332,8 @@ func (s *BashServer) isPathAllowed(path string) bool {
if path == "" {
return true
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, allowed := range s.allowedPaths {
if strings.HasPrefix(path, allowed) || path == allowed {
return true
@@ -348,12 +350,14 @@ func (s *BashServer) runCommand(ctx context.Context, args map[string]interface{}
if !s.isCommandAllowed(command) {
if interpreterRegex.MatchString(command) {
return "", fmt.Errorf("INTERPRETER_BLOCKED: scripting interpreters (python, ruby, perl, etc.) are not allowed in run_command. Use run_python or run_node instead.")
return "", fmt.Errorf("INTERPRETER_BLOCKED: scripting interpreters (python, ruby, perl, etc.) are not allowed in run_command. Use run_python or run_node instead")
}
allowed, restrictedPath := s.checkCommandPaths(command)
if !allowed && restrictedPath != "" {
if s.autoConfirm {
s.mu.Lock()
s.allowedPaths = append(s.allowedPaths, restrictedPath)
s.mu.Unlock()
log.Printf("[BASH] Auto-confirmed restricted path: %s", restrictedPath)
} else {
return "", fmt.Errorf("PATH_RESTRICTED: path not allowed: %s", restrictedPath)
@@ -810,6 +814,8 @@ func (s *BashServer) AddPath(ctx context.Context, path string) error {
if path == "" {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.allowedPaths {
if p == path {
return nil
@@ -824,6 +830,8 @@ func (s *BashServer) TempAddPath(path string) {
if path == "" {
return
}
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.allowedPaths {
if p == path {
return
@@ -838,6 +846,8 @@ func (s *BashServer) AddURL(ctx context.Context, url string) error {
func (s *BashServer) RemovePath(ctx context.Context, path string) error {
path = strings.TrimSpace(path)
s.mu.Lock()
defer s.mu.Unlock()
for i, p := range s.allowedPaths {
if p == path {
s.allowedPaths = append(s.allowedPaths[:i], s.allowedPaths[i+1:]...)
@@ -862,7 +872,9 @@ func (s *BashServer) Close() error {
}
func (s *BashServer) SetAllowedPaths(paths []string) {
s.mu.Lock()
s.allowedPaths = sanitizeAllowedPaths(paths)
s.mu.Unlock()
}
func (s *BashServer) AllowedPathsUpdated() {}
+142 -81
View File
@@ -30,22 +30,21 @@ type Tool struct {
}
type MCPToolExecutor struct {
serverName string
server MCPServer
mcpClient *client.Client
tools []Tool
workingDir string
serverName string
server atomic.Value
mcpClient *client.Client
tools []Tool
workingDir string
mu sync.RWMutex
lastRequestID int64
requestIDMu sync.Mutex
}
func NewMCPServer(ctx context.Context, server MCPServer, workingDir string) (*MCPToolExecutor, error) {
executor := &MCPToolExecutor{
serverName: server.Name,
server: server,
workingDir: workingDir,
}
executor.server.Store(server)
if err := executor.connect(ctx); err != nil {
return nil, err
@@ -55,11 +54,22 @@ func NewMCPServer(ctx context.Context, server MCPServer, workingDir string) (*MC
}
func (m *MCPToolExecutor) connect(ctx context.Context) error {
args := m.buildArgs()
mcpClient, err := client.NewStdioMCPClient(m.server.Command, m.server.Env, args...)
server := m.server.Load().(MCPServer)
m.mu.Lock()
defer m.mu.Unlock()
mcpClient, tools, err := connect(ctx, server, buildArgs(server, m.workingDir))
if err != nil {
return fmt.Errorf("failed to create MCP client for %s: %w", m.server.Name, err)
return err
}
m.mcpClient = mcpClient
m.tools = tools
return nil
}
func connect(ctx context.Context, server MCPServer, args []string) (*client.Client, []Tool, error) {
mcpClient, err := client.NewStdioMCPClient(server.Command, server.Env, args...)
if err != nil {
return nil, nil, fmt.Errorf("failed to create MCP client for %s: %w", server.Name, err)
}
_, err = mcpClient.Initialize(ctx, mcp.InitializeRequest{
@@ -73,65 +83,76 @@ func (m *MCPToolExecutor) connect(ctx context.Context) error {
})
if err != nil {
_ = mcpClient.Close()
return fmt.Errorf("failed to initialize MCP client for %s: %w", m.server.Name, err)
return nil, nil, fmt.Errorf("failed to initialize MCP client for %s: %w", server.Name, err)
}
tools, err := listTools(ctx, mcpClient)
if err != nil {
_ = mcpClient.Close()
return fmt.Errorf("failed to list tools for %s: %w", m.server.Name, err)
return nil, nil, fmt.Errorf("failed to list tools for %s: %w", server.Name, err)
}
m.mcpClient = mcpClient
m.tools = tools
return nil
return mcpClient, tools, nil
}
// ping checks if the MCP client is still responsive
func (m *MCPToolExecutor) ping(ctx context.Context) error {
if m.mcpClient == nil {
return fmt.Errorf("client is nil")
}
// pingClient checks if the MCP client is still responsive
func pingClient(ctx context.Context, client client.MCPClient) error {
// Create a context with 2 second deadline
pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return m.mcpClient.Ping(pingCtx)
return client.Ping(pingCtx)
}
// ensureConnected pings the client and reconnects if needed
func (m *MCPToolExecutor) ensureConnected(ctx context.Context) error {
if m.mcpClient == nil {
return m.connect(ctx)
}
if err := m.ping(ctx); err != nil {
// Ping failed, close and reconnect
_ = m.mcpClient.Close()
m.mcpClient = nil
if err := m.connect(ctx); err != nil {
return fmt.Errorf("failed to reconnect after ping failure: %w", err)
m.mu.Lock()
client := m.mcpClient
wd := m.workingDir
if client != nil {
if pingClient(ctx, client) == nil {
m.mu.Unlock()
return nil
}
// Ping failed, close and reconnect
_ = client.Close()
m.mcpClient = nil
}
// Do not hold the lock till connecting
m.mu.Unlock()
server := m.server.Load().(MCPServer)
client, tools, err := connect(ctx, server, buildArgs(server, wd))
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
// Guard against another goroutine having reconnected while we were unlocked
if m.mcpClient != nil { // assume it's ok
_ = client.Close()
return nil
}
m.mcpClient, m.tools = client, tools
return nil
}
func (m *MCPToolExecutor) buildArgs() []string {
paths := m.server.AllowedPaths
func buildArgs(server MCPServer, workingDir string) []string {
paths := server.AllowedPaths
if len(paths) == 0 {
paths = []string{m.workingDir}
paths = []string{workingDir}
}
if m.server.Name == "filesystem" || contains(m.server.Args, "server-filesystem") {
if server.Name == "filesystem" || contains(server.Args, "server-filesystem") {
args := []string{"-y", "@modelcontextprotocol/server-filesystem"}
args = append(args, paths...)
return args
}
if len(m.server.Args) > 0 {
return m.server.Args
if len(server.Args) > 0 {
return server.Args
}
return nil
@@ -157,25 +178,37 @@ func listTools(ctx context.Context, mcpClient *client.Client) ([]Tool, error) {
}
func (m *MCPToolExecutor) Name() string {
return m.server.Name
return m.server.Load().(MCPServer).Name
}
func (m *MCPToolExecutor) Tools() []Tool {
m.mu.RLock()
defer m.mu.RUnlock()
return m.tools
}
func (m *MCPToolExecutor) AddPath(ctx context.Context, path string) error {
for _, p := range m.server.AllowedPaths {
if p == path {
return nil
server := m.server.Load().(MCPServer)
if func() bool {
for _, p := range server.AllowedPaths {
if p == path {
return true
}
}
server.AllowedPaths = append(server.AllowedPaths, path)
m.server.Store(server)
m.mu.Lock()
defer m.mu.Unlock()
_ = m.mcpClient.Close()
m.mcpClient = nil
return false
}() {
return nil
}
m.server.AllowedPaths = append(m.server.AllowedPaths, path)
_ = m.mcpClient.Close()
if err := m.connect(ctx); err != nil {
if err := m.ensureConnected(ctx); err != nil {
return fmt.Errorf("failed to reconnect with new path: %w", err)
}
@@ -187,33 +220,45 @@ func (m *MCPToolExecutor) AddURL(ctx context.Context, url string) error {
}
func (m *MCPToolExecutor) TempAddPath(path string) {
for _, p := range m.server.AllowedPaths {
server := m.server.Load().(MCPServer)
for _, p := range server.AllowedPaths {
if p == path {
return
}
}
m.server.AllowedPaths = append(m.server.AllowedPaths, path)
server.AllowedPaths = append(server.AllowedPaths, path)
m.server.Store(server)
}
func (m *MCPToolExecutor) RemovePath(ctx context.Context, path string) error {
found := -1
for i, p := range m.server.AllowedPaths {
if p == path {
found = i
break
if func() bool {
server := m.server.Load().(MCPServer)
found := -1
for i, p := range server.AllowedPaths {
if p == path {
found = i
break
}
}
}
if found == -1 {
if found == -1 {
return true
}
server.AllowedPaths = append(server.AllowedPaths[:found], server.AllowedPaths[found+1:]...)
m.server.Store(server)
m.mu.Lock()
defer m.mu.Unlock()
_ = m.mcpClient.Close()
m.mcpClient = nil
return false
}() {
return nil
}
m.server.AllowedPaths = append(m.server.AllowedPaths[:found], m.server.AllowedPaths[found+1:]...)
_ = m.mcpClient.Close()
if err := m.connect(ctx); err != nil {
if err := m.ensureConnected(ctx); err != nil {
return fmt.Errorf("failed to reconnect after removing path: %w", err)
}
@@ -225,11 +270,14 @@ func (m *MCPToolExecutor) RemoveURL(ctx context.Context, url string) error {
}
func (m *MCPToolExecutor) AllowedPaths() []string {
paths := m.server.AllowedPaths
if len(paths) == 0 {
return []string{m.workingDir}
server := m.server.Load().(MCPServer)
paths := server.AllowedPaths
if len(paths) != 0 {
return paths
}
return paths
m.mu.RLock()
defer m.mu.RUnlock()
return []string{m.workingDir}
}
func (m *MCPToolExecutor) CallTool(ctx context.Context, name string, arguments map[string]interface{}) (string, error) {
@@ -256,8 +304,10 @@ func (m *MCPToolExecutor) callToolWithRetry(ctx context.Context, name string, ar
}
if isRetryableError(err.Error()) && attempt < maxRetries {
m.mu.Lock()
_ = m.mcpClient.Close()
m.mcpClient = nil
m.mu.Unlock()
if reconnectErr := m.connect(ctx); reconnectErr == nil {
return m.callToolWithRetry(ctx, name, arguments, attempt+1)
}
@@ -269,7 +319,15 @@ func (m *MCPToolExecutor) callToolWithRetry(ctx context.Context, name string, ar
}
func (m *MCPToolExecutor) callToolWithCancellation(ctx context.Context, requestID int64, name string, arguments map[string]interface{}) (string, error) {
t := m.mcpClient.GetTransport()
var t transport.Interface
m.mu.RLock()
if client := m.mcpClient; client != nil {
t = client.GetTransport()
}
m.mu.RUnlock()
if t == nil {
return "", fmt.Errorf("nil client")
}
request := transport.JSONRPCRequest{
JSONRPC: mcp.JSONRPC_VERSION,
@@ -295,7 +353,7 @@ func (m *MCPToolExecutor) callToolWithCancellation(ctx context.Context, requestI
select {
case <-ctx.Done():
m.sendCancelledNotification(context.Background(), requestID, ctx.Err().Error())
sendCancelledNotification(context.Background(), t, requestID, ctx.Err().Error())
return "", ctx.Err()
case response := <-resultCh:
return m.parseToolResponse(response)
@@ -304,9 +362,7 @@ func (m *MCPToolExecutor) callToolWithCancellation(ctx context.Context, requestI
}
}
func (m *MCPToolExecutor) sendCancelledNotification(ctx context.Context, requestID int64, reason string) {
t := m.mcpClient.GetTransport()
func sendCancelledNotification(ctx context.Context, t transport.Interface, requestID int64, reason string) {
notification := mcp.JSONRPCNotification{
JSONRPC: mcp.JSONRPC_VERSION,
Notification: mcp.Notification{
@@ -368,16 +424,20 @@ func isRetryableError(errMsg string) bool {
}
func (m *MCPToolExecutor) Close() error {
if m.mcpClient != nil {
return m.closeWithTimeout(2 * time.Second)
m.mu.Lock()
client := m.mcpClient
m.mcpClient = nil
m.mu.Unlock()
if client != nil {
return closeWithTimeout(client, 2*time.Second)
}
return nil
}
func (m *MCPToolExecutor) closeWithTimeout(timeout time.Duration) error {
func closeWithTimeout(client *client.Client, timeout time.Duration) error {
done := make(chan error, 1)
go func() {
done <- m.mcpClient.Close()
done <- client.Close()
}()
select {
@@ -398,10 +458,11 @@ func contains(slice []string, val string) bool {
}
func (m *MCPToolExecutor) GetContext() string {
server := m.server.Load().(MCPServer)
var b strings.Builder
b.WriteString("MCP Server: " + m.server.Name + "\n")
b.WriteString("Command: " + m.server.Command + "\n")
b.WriteString("Args: " + strings.Join(m.server.Args, " ") + "\n")
b.WriteString("Allowed Paths: " + strings.Join(m.server.AllowedPaths, ", ") + "\n")
b.WriteString("MCP Server: " + server.Name + "\n")
b.WriteString("Command: " + server.Command + "\n")
b.WriteString("Args: " + strings.Join(server.Args, " ") + "\n")
b.WriteString("Allowed Paths: " + strings.Join(server.AllowedPaths, ", ") + "\n")
return b.String()
}
-11
View File
@@ -20,17 +20,6 @@ type schedulerExt struct {
ListTasks() []interface{}
GetTask(id string) interface{}
}
mu interface {
Lock()
Unlock()
}
provider interface {
GetProvider(cfg interface{}) (interface{}, error)
}
providerMu interface {
Lock()
Unlock()
}
}
type TaskInfo struct {
-1
View File
@@ -23,7 +23,6 @@ type cacheEntry struct {
type WebScraperServer struct {
allowedURLs []string
tools []Tool
browserCtx context.Context
browserCancel context.CancelFunc
autoConfirm bool
cache map[string]cacheEntry
+1 -8
View File
@@ -9,7 +9,6 @@ import (
"os/exec"
"runtime"
"strings"
"sync"
)
var DebugMode bool
@@ -19,12 +18,7 @@ type VisionResult struct {
Duration float64 `json:"duration_ms"`
}
var (
visionServer *exec.Cmd
visionServerOnce sync.Once
visionServerErr error
tesseractPath string
)
var visionServer *exec.Cmd
func visionDebug(format string, args ...interface{}) {
if DebugMode {
@@ -185,7 +179,6 @@ func StopVisionServer() {
if visionServer != nil && visionServer.Process != nil {
visionServer.Process.Signal(os.Kill)
visionServer = nil
visionServerOnce = sync.Once{}
}
}
-5
View File
@@ -19,11 +19,6 @@ type hfRepoInfo struct {
} `json:"siblings"`
}
type hfFileInfo struct {
Size int64 `json:"size"`
BlobHTTPURL string `json:"blobHTTPURL"`
}
func getGGUFFiles(ctx context.Context, modelID string) ([]string, error) {
url := fmt.Sprintf("%s/models/%s", huggingfaceAPI, modelID)
+1 -1
View File
@@ -175,7 +175,7 @@ func (g *geminiProvider) Cancel() {
}
func (g *geminiProvider) CallTool(ctx context.Context, name string, args map[string]interface{}) (string, error) {
return "", fmt.Errorf("Gemini provider does not support direct tool calls")
return "", fmt.Errorf("gemini provider does not support direct tool calls")
}
func (g *geminiProvider) ContextLimit() int {
+15 -16
View File
@@ -441,23 +441,22 @@ func promptQuantizationSelection(maxOptions int, defaultIdx int) (int, error) {
return 0, fmt.Errorf("no quantization options available")
}
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("\nSelect quantization (default: %d for Q4_K_M): ", defaultIdx+1)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
input = strings.TrimSpace(input)
if input == "" {
return defaultIdx, nil
}
idx, err := strconv.Atoi(input)
if err != nil || idx < 1 || idx > maxOptions {
fmt.Println("Invalid selection, using default Q4_K_M")
return defaultIdx, nil
}
return idx - 1, nil
fmt.Printf("\nSelect quantization (default: %d for Q4_K_M): ", defaultIdx+1)
input, err := reader.ReadString('\n')
if err != nil {
return 0, err
}
input = strings.TrimSpace(input)
if input == "" {
return defaultIdx, nil
}
idx, err := strconv.Atoi(input)
if err != nil || idx < 1 || idx > maxOptions {
fmt.Println("Invalid selection, using default Q4_K_M")
return defaultIdx, nil
}
return idx - 1, nil
}
func getCachedQuantization(modelID string) (string, bool) {
+1 -1
View File
@@ -476,7 +476,7 @@ func (o *ollamaProvider) Cancel() {
}
func (o *ollamaProvider) CallTool(ctx context.Context, name string, args map[string]interface{}) (string, error) {
return "", fmt.Errorf("Ollama provider does not support direct tool calls, use MCP servers configured in provider")
return "", fmt.Errorf("ollama provider does not support direct tool calls, use MCP servers configured in provider")
}
func (o *ollamaProvider) ContextLimit() int {
+1 -10
View File
@@ -44,15 +44,6 @@ type openRouterModelsResponse struct {
Data []openRouterModelInfo `json:"data"`
}
type toolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
type responseToolCall struct {
ID string
Name string
@@ -681,7 +672,7 @@ func parseResponsesOutput(body []byte) (string, string, []responseToolCall, resp
case "function_call":
name, _ := item["name"].(string)
id, _ := item["id"].(string)
argsRaw, _ := item["arguments"]
argsRaw := item["arguments"]
argsStr := ""
switch v := argsRaw.(type) {
case string:
+1 -7
View File
@@ -365,7 +365,7 @@ func (s *Scheduler) saveTask(task *ScheduledTask) error {
lastRunStr = task.LastRun.Format(time.RFC3339)
}
_, err := s.db.Exec(`
INSERT OR REPLACE INTO scheduled_tasks
INSERT OR REPLACE INTO scheduled_tasks
(id, prompt, interval_sec, run_at, is_repeating, working_dir, created_at, last_run, run_count, last_error, last_output, provider_type, provider_name, provider_model)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, task.ID, task.Prompt, task.IntervalSec, task.RunAt, task.IsRepeating, task.WorkingDir,
@@ -867,9 +867,3 @@ func (s *Scheduler) Close() {
s.wg.Wait()
s.db.Close()
}
type providerConfig struct {
Type string `json:"type"`
Name string `json:"name"`
Model string `json:"model"`
}
+2 -5
View File
@@ -59,7 +59,7 @@ func RunWizard(destination string) error {
return defaultVal
}
if existingProvider.Type != "" && provider.Type != "" &&
strings.ToLower(existingProvider.Type) != strings.ToLower(provider.Type) {
!strings.EqualFold(existingProvider.Type, provider.Type) {
switch field {
case "model", "description", "endpoint", "api_key_env":
return defaultVal
@@ -337,10 +337,7 @@ func looksLikePromptEcho(input string) bool {
return true
}
}
if strings.Contains(input, "]:") {
return true
}
return false
return strings.Contains(input, "]:")
}
type selectOption struct {