From 06ccc80da64a373bb3e886bfb4eb96579490bfa4 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 31 Mar 2026 23:13:05 +0200 Subject: [PATCH] Remove MCP layer; execute_code is the sole built-in tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop ollie/mcp, ollie/tools, ollie/ollama packages. Config now only carries hooks. main.go wires a single execute_code tool directly to the agent loop — no MCP discovery or dispatch needed. Co-Authored-By: Claude Sonnet 4.6 --- config/config.go | 13 +--- config/config_test.go | 56 +++++++++-------- main.go | 133 ++++++++++++----------------------------- mcp/client.go | 95 ----------------------------- mcp/client_test.go | 36 ----------- mcp/transport.go | 46 -------------- ollama/client.go | 86 -------------------------- tools/executor.go | 69 --------------------- tools/executor_test.go | 20 ------- 9 files changed, 68 insertions(+), 486 deletions(-) delete mode 100644 mcp/client.go delete mode 100644 mcp/client_test.go delete mode 100644 mcp/transport.go delete mode 100644 ollama/client.go delete mode 100644 tools/executor.go delete mode 100644 tools/executor_test.go diff --git a/config/config.go b/config/config.go index 5b6f854..9d8cde6 100644 --- a/config/config.go +++ b/config/config.go @@ -6,18 +6,7 @@ import ( ) type Config struct { - MCPServers map[string]ServerConfig `json:"mcpServers"` - Hooks map[string]string `json:"hooks,omitempty"` -} - -type ServerConfig struct { - Command string `json:"command,omitempty"` - Args []string `json:"args,omitempty"` - Env map[string]string `json:"env,omitempty"` - Type string `json:"type,omitempty"` - URL string `json:"url,omitempty"` - Headers map[string]string `json:"headers,omitempty"` - Disabled bool `json:"disabled,omitempty"` + Hooks map[string]string `json:"hooks,omitempty"` } func Load(path string) (*Config, error) { diff --git a/config/config_test.go b/config/config_test.go index 23dba24..010dffc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -6,44 +6,48 @@ import ( ) func TestLoad(t *testing.T) { - content := `{ - "mcpServers": { - "test": { - "command": "test-cmd", - "args": ["arg1"], - "env": {"KEY": "value"} - } - } - }` - + content := `{"hooks": {"stop": "notify-send done"}}` + tmpfile, err := os.CreateTemp("", "config*.json") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) - + if _, err := tmpfile.Write([]byte(content)); err != nil { t.Fatal(err) } tmpfile.Close() - + cfg, err := Load(tmpfile.Name()) if err != nil { t.Fatalf("Load failed: %v", err) } - - if len(cfg.MCPServers) != 1 { - t.Errorf("Expected 1 server, got %d", len(cfg.MCPServers)) - } - - server := cfg.MCPServers["test"] - if server.Command != "test-cmd" { - t.Errorf("Expected command 'test-cmd', got '%s'", server.Command) - } - if len(server.Args) != 1 || server.Args[0] != "arg1" { - t.Errorf("Expected args ['arg1'], got %v", server.Args) - } - if server.Env["KEY"] != "value" { - t.Errorf("Expected env KEY=value, got %v", server.Env) + + if cfg.Hooks["stop"] != "notify-send done" { + t.Errorf("Expected hook 'notify-send done', got %q", cfg.Hooks["stop"]) + } +} + +func TestLoadEmpty(t *testing.T) { + content := `{}` + + tmpfile, err := os.CreateTemp("", "config*.json") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write([]byte(content)); err != nil { + t.Fatal(err) + } + tmpfile.Close() + + cfg, err := Load(tmpfile.Name()) + if err != nil { + t.Fatalf("Load failed: %v", err) + } + if len(cfg.Hooks) != 0 { + t.Errorf("Expected no hooks, got %v", cfg.Hooks) } } diff --git a/main.go b/main.go index 76b9177..358297a 100644 --- a/main.go +++ b/main.go @@ -18,10 +18,28 @@ import ( "ollie/backend" "ollie/config" execpkg "ollie/exec" - "ollie/mcp" - "ollie/tools" ) +// executeCodeTool is the single built-in tool exposed to the model. +var executeCodeTool = backend.Tool{ + Name: "execute_code", + Description: "Execute shell code or a named tool script in a sandboxed environment. " + + "Use 'code' for inline bash, 'tool'+'args' for a named script, " + + "or 'pipe' for a sequence of {tool, args} steps.", + Parameters: json.RawMessage(`{ + "type": "object", + "properties": { + "code": {"type": "string", "description": "Inline shell code to run (bash)."}, + "language": {"type": "string", "description": "Language interpreter (default: bash)."}, + "timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."}, + "sandbox": {"type": "string", "description": "Sandbox name (default: default)."}, + "tool": {"type": "string", "description": "Named tool script to run instead of inline code."}, + "args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."}, + "pipe": {"type": "array", "description": "Pipeline: array of {tool, args} objects run in sequence."} + } + }`), +} + type model struct { textarea textarea.Model viewport viewport.Model @@ -33,45 +51,25 @@ type model struct { } func main() { - if len(os.Args) < 2 { - log.Fatal("Usage: ollie [model]") - } - modelName := os.Getenv("OLLIE_MODEL") - if modelName == "" && len(os.Args) > 2 { - modelName = os.Args[2] + if modelName == "" && len(os.Args) > 1 { + modelName = os.Args[1] } if modelName == "" { modelName = "qwen3:8b" } - cfg, err := config.Load(os.Args[1]) - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } - - // Connect MCP servers. - mcpExecutor := tools.NewExecutor() - for name, serverCfg := range cfg.MCPServers { - if serverCfg.Disabled || serverCfg.Command == "" { - continue - } - transport := mcp.NewSTDIOTransport(serverCfg.Command, serverCfg.Args, serverCfg.Env) - client, err := transport.Connect() + hooks := make(map[string]string) + if len(os.Args) > 2 { + cfg, err := config.Load(os.Args[2]) if err != nil { - log.Printf("Failed to connect to %s: %v", name, err) - continue + log.Fatalf("Failed to load config: %v", err) + } + if cfg.Hooks != nil { + hooks = cfg.Hooks } - mcpExecutor.AddServer(name, client) - log.Printf("Connected to MCP server: %s", name) } - mcpTools, err := mcpExecutor.ListTools() - if err != nil { - log.Fatalf("Failed to list tools: %v", err) - } - log.Printf("Loaded %d tools", len(mcpTools)) - be, err := backend.New() if err != nil { log.Fatalf("Failed to create backend: %v", err) @@ -86,16 +84,16 @@ func main() { loopcfg := agent.Config{ Backend: be, Model: modelName, - Tools: mcpToolsToBackend(mcpTools), - Exec: buildDispatch(builtinExec, mcpExecutor, mcpTools), + Tools: []backend.Tool{executeCodeTool}, + Exec: func(name string, args json.RawMessage) (string, error) { + if name == "execute_code" { + return dispatchBuiltinExec(builtinExec, args) + } + return "", fmt.Errorf("unknown tool: %s", name) + }, MaxSteps: 20, } - hooks := cfg.Hooks - if hooks == nil { - hooks = make(map[string]string) - } - ta := textarea.New() ta.Placeholder = "Type your message..." ta.Prompt = "" @@ -185,8 +183,6 @@ func (m model) runLoop(input string) tea.Cmd { display := append([]string{}, m.display...) return func() tea.Msg { - // First message: create a new session with the input as goal. - // Subsequent messages: append to existing session. if session == nil { session = agent.NewSession(input) } else { @@ -228,62 +224,7 @@ func (m model) View() string { // -- helpers -- -func mcpToolsToBackend(mcpTools []tools.ToolInfo) []backend.Tool { - out := make([]backend.Tool, len(mcpTools)) - for i, t := range mcpTools { - out[i] = backend.Tool{ - Name: t.Name, - Description: t.Description, - Parameters: t.InputSchema, - } - } - return out -} - -// buildDispatch routes tool calls to the MCP server that owns them, falling -// back to the built-in execute_code sandbox if not found via MCP. -func buildDispatch(builtin *execpkg.Executor, mcpExec *tools.Executor, mcpTools []tools.ToolInfo) agent.ToolExecutor { - serverOf := make(map[string]string, len(mcpTools)) - for _, t := range mcpTools { - serverOf[t.Name] = t.Server - } - - return func(name string, args json.RawMessage) (string, error) { - if server, ok := serverOf[name]; ok { - raw, err := mcpExec.Execute(server, name, args) - if err != nil { - return "", err - } - return extractMCPText(raw), nil - } - if name == "execute_code" { - return dispatchBuiltinExec(builtin, args) - } - return "", fmt.Errorf("unknown tool: %s", name) - } -} - -// extractMCPText unwraps {"content":[{"type":"text","text":"..."}]}. -func extractMCPText(raw json.RawMessage) string { - var result struct { - Content []struct { - Type string `json:"type"` - Text string `json:"text"` - } `json:"content"` - } - if err := json.Unmarshal(raw, &result); err != nil { - return string(raw) - } - var parts []string - for _, c := range result.Content { - if c.Type == "text" { - parts = append(parts, c.Text) - } - } - return strings.Join(parts, "\n") -} - -// dispatchBuiltinExec handles execute_code natively without MCP. +// dispatchBuiltinExec handles execute_code natively. func dispatchBuiltinExec(e *execpkg.Executor, args json.RawMessage) (string, error) { var a struct { Code string `json:"code"` diff --git a/mcp/client.go b/mcp/client.go deleted file mode 100644 index cd7cf51..0000000 --- a/mcp/client.go +++ /dev/null @@ -1,95 +0,0 @@ -package mcp - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "sync" -) - -type Message struct { - JSONRPC string `json:"jsonrpc"` - ID interface{} `json:"id,omitempty"` - Method string `json:"method,omitempty"` - Params json.RawMessage `json:"params,omitempty"` - Result json.RawMessage `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` -} - -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` -} - -type Client struct { - reader *bufio.Reader - writer io.Writer - mu sync.Mutex - nextID int -} - -func NewClient(r io.Reader, w io.Writer) *Client { - return &Client{ - reader: bufio.NewReader(r), - writer: w, - nextID: 1, - } -} - -func (c *Client) Call(method string, params interface{}) (json.RawMessage, error) { - c.mu.Lock() - id := c.nextID - c.nextID++ - c.mu.Unlock() - - req := Message{ - JSONRPC: "2.0", - ID: id, - Method: method, - Params: mustMarshal(params), - } - - if err := c.send(req); err != nil { - return nil, err - } - - resp, err := c.receive() - if err != nil { - return nil, err - } - - if resp.Error != nil { - return nil, fmt.Errorf("RPC error %d: %s", resp.Error.Code, resp.Error.Message) - } - - return resp.Result, nil -} - -func (c *Client) send(msg Message) error { - data, err := json.Marshal(msg) - if err != nil { - return err - } - c.mu.Lock() - defer c.mu.Unlock() - _, err = c.writer.Write(append(data, '\n')) - return err -} - -func (c *Client) receive() (*Message, error) { - line, err := c.reader.ReadBytes('\n') - if err != nil { - return nil, err - } - var msg Message - if err := json.Unmarshal(line, &msg); err != nil { - return nil, err - } - return &msg, nil -} - -func mustMarshal(v interface{}) json.RawMessage { - data, _ := json.Marshal(v) - return data -} diff --git a/mcp/client_test.go b/mcp/client_test.go deleted file mode 100644 index 6a62092..0000000 --- a/mcp/client_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package mcp - -import ( - "bytes" - "encoding/json" - "testing" -) - -func TestClientCall(t *testing.T) { - var buf bytes.Buffer - client := NewClient(&buf, &buf) - - go func() { - resp := Message{ - JSONRPC: "2.0", - ID: 1, - Result: json.RawMessage(`{"status":"ok"}`), - } - data, _ := json.Marshal(resp) - buf.Write(append(data, '\n')) - }() - - result, err := client.Call("test_method", map[string]string{"key": "value"}) - if err != nil { - t.Fatalf("Call failed: %v", err) - } - - var resultMap map[string]string - if err := json.Unmarshal(result, &resultMap); err != nil { - t.Fatalf("Unmarshal failed: %v", err) - } - - if resultMap["status"] != "ok" { - t.Errorf("Expected status 'ok', got '%s'", resultMap["status"]) - } -} diff --git a/mcp/transport.go b/mcp/transport.go deleted file mode 100644 index 54ab8eb..0000000 --- a/mcp/transport.go +++ /dev/null @@ -1,46 +0,0 @@ -package mcp - -import ( - "os/exec" -) - -type Transport interface { - Connect() (*Client, error) - Close() error -} - -type STDIOTransport struct { - cmd *exec.Cmd -} - -func NewSTDIOTransport(command string, args []string, env map[string]string) *STDIOTransport { - cmd := exec.Command(command, args...) - if env != nil { - for k, v := range env { - cmd.Env = append(cmd.Env, k+"="+v) - } - } - return &STDIOTransport{cmd: cmd} -} - -func (t *STDIOTransport) Connect() (*Client, error) { - stdin, err := t.cmd.StdinPipe() - if err != nil { - return nil, err - } - stdout, err := t.cmd.StdoutPipe() - if err != nil { - return nil, err - } - if err := t.cmd.Start(); err != nil { - return nil, err - } - return NewClient(stdout, stdin), nil -} - -func (t *STDIOTransport) Close() error { - if t.cmd.Process != nil { - return t.cmd.Process.Kill() - } - return nil -} diff --git a/ollama/client.go b/ollama/client.go deleted file mode 100644 index ad1c782..0000000 --- a/ollama/client.go +++ /dev/null @@ -1,86 +0,0 @@ -package ollama - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" -) - -type Client struct { - baseURL string - client *http.Client -} - -type ChatRequest struct { - Model string `json:"model"` - Messages []Message `json:"messages"` - Tools []Tool `json:"tools,omitempty"` - Stream bool `json:"stream"` -} - -type Message struct { - Role string `json:"role"` - Content string `json:"content"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` -} - -type Tool struct { - Type string `json:"type"` - Function Function `json:"function"` -} - -type Function struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters json.RawMessage `json:"parameters"` -} - -type ToolCall struct { - ID string `json:"id"` - Type string `json:"type"` - Function FunctionCall `json:"function"` -} - -type FunctionCall struct { - Name string `json:"name"` - Arguments json.RawMessage `json:"arguments"` -} - -type ChatResponse struct { - Message Message `json:"message"` - Done bool `json:"done"` -} - -func NewClient(baseURL string) *Client { - return &Client{ - baseURL: baseURL, - client: &http.Client{}, - } -} - -func (c *Client) Chat(req ChatRequest) (*ChatResponse, error) { - data, err := json.Marshal(req) - if err != nil { - return nil, err - } - - resp, err := c.client.Post(c.baseURL+"/api/chat", "application/json", bytes.NewReader(data)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, body) - } - - var chatResp ChatResponse - if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { - return nil, err - } - - return &chatResp, nil -} diff --git a/tools/executor.go b/tools/executor.go deleted file mode 100644 index b227d25..0000000 --- a/tools/executor.go +++ /dev/null @@ -1,69 +0,0 @@ -package tools - -import ( - "encoding/json" - "fmt" - "ollie/mcp" -) - -type Executor struct { - servers map[string]*mcp.Client -} - -func NewExecutor() *Executor { - return &Executor{ - servers: make(map[string]*mcp.Client), - } -} - -func (e *Executor) AddServer(name string, client *mcp.Client) { - e.servers[name] = client -} - -func (e *Executor) ListTools() ([]ToolInfo, error) { - var allTools []ToolInfo - for serverName, client := range e.servers { - result, err := client.Call("tools/list", nil) - if err != nil { - return nil, fmt.Errorf("server %s: %w", serverName, err) - } - var resp struct { - Tools []struct { - Name string `json:"name"` - Description string `json:"description"` - InputSchema json.RawMessage `json:"inputSchema"` - } `json:"tools"` - } - if err := json.Unmarshal(result, &resp); err != nil { - return nil, err - } - for _, t := range resp.Tools { - allTools = append(allTools, ToolInfo{ - Server: serverName, - Name: t.Name, - Description: t.Description, - InputSchema: t.InputSchema, - }) - } - } - return allTools, nil -} - -func (e *Executor) Execute(serverName, toolName string, args json.RawMessage) (json.RawMessage, error) { - client, ok := e.servers[serverName] - if !ok { - return nil, fmt.Errorf("server not found: %s", serverName) - } - params := map[string]interface{}{ - "name": toolName, - "arguments": args, - } - return client.Call("tools/call", params) -} - -type ToolInfo struct { - Server string - Name string - Description string - InputSchema json.RawMessage -} diff --git a/tools/executor_test.go b/tools/executor_test.go deleted file mode 100644 index f928aa9..0000000 --- a/tools/executor_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package tools - -import ( - "testing" -) - -func TestExecutorAddServer(t *testing.T) { - executor := NewExecutor() - - if len(executor.servers) != 0 { - t.Errorf("Expected 0 servers, got %d", len(executor.servers)) - } - - executor.AddServer("test", nil) - - if len(executor.servers) != 1 { - t.Errorf("Expected 1 server, got %d", len(executor.servers)) - } -} -