From ea07f6d3a0ae2e9b9e953dbcfe991f0d51a3f2b6 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 6 Apr 2026 20:36:20 +0200 Subject: [PATCH] mcp,tools,main: shut down MCP servers on agent switch Client gains a Close() method that closes stdin (giving the subprocess a clean EOF) then kills and reaps the process. Executor.Close() calls Close() on all registered clients. agentEnv now carries the mcpExec so the /agent command can close the outgoing executor before building the new one. Co-Authored-By: Claude Sonnet 4.6 --- main.go | 8 ++++++++ mcp/client.go | 23 +++++++++++++++++++++++ mcp/transport.go | 2 +- tools/executor.go | 7 +++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index f94a880..5386a3d 100644 --- a/main.go +++ b/main.go @@ -204,6 +204,7 @@ type model struct { backendName string // e.g. "ollama", "openrouter", "openai" agentName string // active agent config name, e.g. "default" agentsDir string // path to ~/.config/ollie/agents/ + mcpExec *tools.Executor // current agent's MCP executor; closed on agent switch builtinExec *execpkg.Executor confirmPtr *agent.ConfirmFn // indirection so startAgent can set it per-run ctxOverhead int // fixed per-request char overhead (system prompt + tool schemas) @@ -243,6 +244,7 @@ type timeoutMsg struct{} // agentEnv holds the runtime state derived from an agent config file. type agentEnv struct { + mcpExec *tools.Executor // kept so it can be closed on agent switch tools []backend.Tool exec agent.ToolExecutor confirm *agent.ConfirmFn // pointer filled in by startAgent per-run @@ -339,6 +341,7 @@ func buildAgentEnv(cfg *config.Config, builtinExec *execpkg.Executor) agentEnv { } return agentEnv{ + mcpExec: mcpExec, tools: allTools, exec: execFn, confirm: confirmPtr, @@ -445,6 +448,7 @@ func main() { backendName: backendName, agentName: agentName, agentsDir: agentsDir, + mcpExec: env.mcpExec, builtinExec: builtinExec, confirmPtr: env.confirm, ctxOverhead: env.ctxOverhead, @@ -856,7 +860,11 @@ func (m *model) handleCommand(input string) bool { m.display = append(m.display, fmt.Sprintf("Error: agent %q: %v", name, err)) return true } + if m.mcpExec != nil { + m.mcpExec.Close() + } env := buildAgentEnv(cfg, m.builtinExec) + m.mcpExec = env.mcpExec m.hooks = env.hooks m.loopcfg.SystemPrompt = env.systemPrompt m.loopcfg.Tools = env.tools diff --git a/mcp/client.go b/mcp/client.go index 2664261..013860e 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "os/exec" "sync" ) @@ -32,9 +33,11 @@ func (e *RPCError) Error() string { // of a subprocess). type Client struct { mu sync.Mutex + wc io.WriteCloser w io.Writer sc *bufio.Scanner nextID int + cmd *exec.Cmd } // NewClient creates a Client from a reader and writer. @@ -42,6 +45,26 @@ func NewClient(r io.Reader, w io.Writer) *Client { return &Client{w: w, sc: bufio.NewScanner(r)} } +// newClientWithProcess creates a Client that owns the given subprocess. +// Close() will shut it down. +func newClientWithProcess(r io.Reader, wc io.WriteCloser, cmd *exec.Cmd) *Client { + return &Client{wc: wc, w: wc, sc: bufio.NewScanner(r), cmd: cmd} +} + +// Close shuts down the underlying subprocess: closes its stdin so it receives +// EOF, then kills the process if it is still running. +func (c *Client) Close() error { + var err error + if c.wc != nil { + err = c.wc.Close() + } + if c.cmd != nil && c.cmd.Process != nil { + c.cmd.Process.Kill() + c.cmd.Wait() // reap the process; ignore errors + } + return err +} + // Call sends a JSON-RPC request and returns the result. func (c *Client) Call(method string, params interface{}) (json.RawMessage, error) { c.mu.Lock() diff --git a/mcp/transport.go b/mcp/transport.go index b035e95..531f374 100644 --- a/mcp/transport.go +++ b/mcp/transport.go @@ -37,7 +37,7 @@ func (t *STDIOTransport) Connect() (*Client, error) { return nil, err } - client := NewClient(stdout, stdin) + client := newClientWithProcess(stdout, stdin, cmd) // MCP initialization handshake. if _, err := client.Call("initialize", map[string]interface{}{ diff --git a/tools/executor.go b/tools/executor.go index 03096a8..df578ec 100644 --- a/tools/executor.go +++ b/tools/executor.go @@ -29,6 +29,13 @@ func (e *Executor) AddServer(name string, client *mcp.Client) { e.servers[name] = client } +// Close shuts down all connected MCP servers. +func (e *Executor) Close() { + for _, client := range e.servers { + client.Close() + } +} + // ListTools returns all tools advertised by all connected servers. func (e *Executor) ListTools() ([]ToolInfo, error) { var all []ToolInfo