Remove MCP layer; execute_code is the sole built-in tool
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 <noreply@anthropic.com>
This commit is contained in:
parent
25cc7a8c33
commit
06ccc80da6
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
133
main.go
133
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 <config.json> [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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue