13b2f5e14d
Removes five unused funcs/vars/fields that golangci-lint had been flagging (anthropic.toolCallDoneEvent, mistral.translateMessages, hook.newError, subprocess.vibeParser.lastAssistantMsgID, tui.cBase), two ineffectual assignments (tui/rendering.go visible-window loop, subprocess stream_test setup), and a stale if/HasPrefix that's now a strings.TrimPrefix. Wires errcheck onto every subprocess / stream lifecycle path so a failed close or shutdown is at least logged rather than silently dropped: - engine/loop.go: stream.Close on both the error and success paths - mcp/manager.go: Shutdown when StartAll partial-fails; Transport close after Initialize failure - mcp/transport.go: stdin.Close + syscall.Kill on graceful-timeout fallback - slm/download.go: Close propagated as a named-return error on the success path; explicitly discarded on the rollback path - slm/classifier.go, slm/manager.go, hook/prompt.go, context/summarize.go, config/write.go, cmd/gnoma/main.go, tool/fs/grep.go: explicit ignores or error logging on Close / Shutdown / WalkDir / Scanln Production-code errcheck and ineffassign are now zero. Remaining golangci-lint output is test-only Close-in-defer noise plus stylistic staticcheck QF suggestions, left alone.
112 lines
3.0 KiB
Go
112 lines
3.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
|
)
|
|
|
|
// Manager coordinates multiple MCP server lifecycles and tool registration.
|
|
type Manager struct {
|
|
clients map[string]*Client
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewManager creates an MCP manager.
|
|
func NewManager(logger *slog.Logger) *Manager {
|
|
return &Manager{
|
|
clients: make(map[string]*Client),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// StartAll starts all configured MCP servers, discovers tools, and registers
|
|
// them in the tool registry. Servers start sequentially to simplify error handling.
|
|
func (m *Manager) StartAll(ctx context.Context, servers []ServerConfig, registry *tool.Registry) error {
|
|
for _, srv := range servers {
|
|
client, err := m.startServer(ctx, srv)
|
|
if err != nil {
|
|
if shutdownErr := m.Shutdown(); shutdownErr != nil {
|
|
m.logger.Warn("partial shutdown after server-start failure",
|
|
"failed_server", srv.Name, "shutdown_error", shutdownErr)
|
|
}
|
|
return fmt.Errorf("mcp server %q: %w", srv.Name, err)
|
|
}
|
|
|
|
tools, err := client.ListTools(ctx)
|
|
if err != nil {
|
|
if shutdownErr := m.Shutdown(); shutdownErr != nil {
|
|
m.logger.Warn("partial shutdown after list-tools failure",
|
|
"failed_server", srv.Name, "shutdown_error", shutdownErr)
|
|
}
|
|
return fmt.Errorf("mcp server %q: list tools: %w", srv.Name, err)
|
|
}
|
|
|
|
m.registerTools(srv, tools, client, registry)
|
|
m.clients[srv.Name] = client
|
|
|
|
m.logger.Info("mcp server started",
|
|
"name", srv.Name,
|
|
"tools", len(tools),
|
|
"replace", srv.ReplaceDefault,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown gracefully stops all MCP server processes.
|
|
func (m *Manager) Shutdown() error {
|
|
var firstErr error
|
|
for name, client := range m.clients {
|
|
if err := client.Close(); err != nil && firstErr == nil {
|
|
firstErr = fmt.Errorf("mcp shutdown %q: %w", name, err)
|
|
}
|
|
}
|
|
m.clients = make(map[string]*Client)
|
|
return firstErr
|
|
}
|
|
|
|
func (m *Manager) startServer(ctx context.Context, srv ServerConfig) (*Client, error) {
|
|
tr := NewTransport(srv.Command, srv.Args, srv.Env, m.logger)
|
|
|
|
if err := tr.Start(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := NewClient(tr, m.logger)
|
|
|
|
initCtx, cancel := context.WithTimeout(ctx, srv.Timeout)
|
|
defer cancel()
|
|
|
|
if err := client.Initialize(initCtx); err != nil {
|
|
if closeErr := tr.Close(); closeErr != nil {
|
|
m.logger.Warn("transport close after init failure", "error", closeErr)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (m *Manager) registerTools(srv ServerConfig, tools []MCPTool, client *Client, registry *tool.Registry) {
|
|
for _, mt := range tools {
|
|
adapter := NewAdapter(srv.Name, mt, client)
|
|
|
|
// Explicit mapping: if this MCP tool name has a replace_default entry,
|
|
// register it under the built-in's name instead of mcp__{server}__{tool}.
|
|
if builtinName, ok := srv.ReplaceDefault[mt.Name]; ok {
|
|
adapter.SetOverrideName(builtinName)
|
|
}
|
|
|
|
registry.Register(adapter)
|
|
m.logger.Debug("mcp tool registered",
|
|
"name", adapter.Name(),
|
|
"server", srv.Name,
|
|
"mcp_name", mt.Name,
|
|
)
|
|
}
|
|
}
|