Restore MCP server support alongside built-in execute_code
Bring back ollie/mcp and ollie/tools packages. Config MCPServers field restored. MCP tools are discovered at startup and added to the agent tool list alongside execute_code. Dispatch routes by server name for MCP tools, falls back to built-in execute_code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f9ba6c8bce
commit
01b128fdf5
|
|
@ -6,7 +6,15 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Hooks map[string]string `json:"hooks,omitempty"`
|
||||
MCPServers map[string]ServerConfig `json:"mcpServers,omitempty"`
|
||||
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"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
|
|
|
|||
83
main.go
83
main.go
|
|
@ -18,6 +18,8 @@ import (
|
|||
"ollie/backend"
|
||||
"ollie/config"
|
||||
execpkg "ollie/exec"
|
||||
"ollie/mcp"
|
||||
"ollie/tools"
|
||||
)
|
||||
|
||||
const systemPrompt = `You are an autonomous agent. You have one tool: execute_code.
|
||||
|
|
@ -123,6 +125,7 @@ func main() {
|
|||
}
|
||||
|
||||
hooks := make(map[string]string)
|
||||
var cfg *config.Config
|
||||
cfgPath := ""
|
||||
if len(os.Args) > 2 {
|
||||
cfgPath = os.Args[2]
|
||||
|
|
@ -130,15 +133,41 @@ func main() {
|
|||
home, _ := os.UserHomeDir()
|
||||
cfgPath = home + "/.config/ollie/config.json"
|
||||
}
|
||||
if cfg, err := config.Load(cfgPath); err == nil {
|
||||
if c, err := config.Load(cfgPath); err == nil {
|
||||
cfg = c
|
||||
if cfg.Hooks != nil {
|
||||
hooks = cfg.Hooks
|
||||
}
|
||||
} else if len(os.Args) > 2 {
|
||||
// Only fatal if the path was explicitly provided.
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Connect MCP servers.
|
||||
mcpExec := tools.NewExecutor()
|
||||
if cfg != nil {
|
||||
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()
|
||||
if err != nil {
|
||||
log.Printf("Failed to connect to MCP server %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
mcpExec.AddServer(name, client)
|
||||
log.Printf("Connected to MCP server: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
mcpTools, err := mcpExec.ListTools()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list MCP tools: %v", err)
|
||||
}
|
||||
if len(mcpTools) > 0 {
|
||||
log.Printf("Loaded %d MCP tools", len(mcpTools))
|
||||
}
|
||||
|
||||
be, err := backend.New()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create backend: %v", err)
|
||||
|
|
@ -150,12 +179,25 @@ func main() {
|
|||
home+"/.cache/ollie/exec",
|
||||
)
|
||||
|
||||
allTools := append(mcpToolsToBackend(mcpTools), executeCodeTool)
|
||||
serverOf := make(map[string]string, len(mcpTools))
|
||||
for _, t := range mcpTools {
|
||||
serverOf[t.Name] = t.Server
|
||||
}
|
||||
|
||||
loopcfg := agent.Config{
|
||||
Backend: be,
|
||||
Model: modelName,
|
||||
SystemPrompt: systemPrompt,
|
||||
Tools: []backend.Tool{executeCodeTool},
|
||||
Exec: func(name string, args json.RawMessage) (string, error) {
|
||||
Tools: allTools,
|
||||
Exec: 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(builtinExec, args)
|
||||
}
|
||||
|
|
@ -356,6 +398,39 @@ func wordWrap(s string, width int) string {
|
|||
|
||||
// -- tool helpers --
|
||||
|
||||
// mcpToolsToBackend converts MCP tool descriptors to backend.Tool entries.
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func dispatchBuiltinExec(e *execpkg.Executor, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Message is a JSON-RPC 2.0 message.
|
||||
type Message struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int `json:"id,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Params interface{} `json:"params,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *RPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// RPCError is a JSON-RPC error object.
|
||||
type RPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *RPCError) Error() string {
|
||||
return fmt.Sprintf("rpc error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Client is a JSON-RPC client over a pair of streams (typically stdin/stdout
|
||||
// of a subprocess).
|
||||
type Client struct {
|
||||
mu sync.Mutex
|
||||
w io.Writer
|
||||
sc *bufio.Scanner
|
||||
nextID int
|
||||
}
|
||||
|
||||
// NewClient creates a Client from a reader and writer.
|
||||
func NewClient(r io.Reader, w io.Writer) *Client {
|
||||
return &Client{w: w, sc: bufio.NewScanner(r)}
|
||||
}
|
||||
|
||||
// Call sends a JSON-RPC request and returns the result.
|
||||
func (c *Client) Call(method string, params interface{}) (json.RawMessage, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.nextID++
|
||||
id := c.nextID
|
||||
|
||||
msg := Message{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := fmt.Fprintf(c.w, "%s\n", data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.sc.Scan() {
|
||||
if err := c.sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
var resp Message
|
||||
if err := json.Unmarshal(c.sc.Bytes(), &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Error != nil {
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package mcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// STDIOTransport launches a subprocess and connects a Client to its stdin/stdout.
|
||||
type STDIOTransport struct {
|
||||
command string
|
||||
args []string
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
// NewSTDIOTransport creates a transport that will launch the given command.
|
||||
func NewSTDIOTransport(command string, args []string, env map[string]string) *STDIOTransport {
|
||||
return &STDIOTransport{command: command, args: args, env: env}
|
||||
}
|
||||
|
||||
// Connect launches the subprocess and returns a connected Client.
|
||||
func (t *STDIOTransport) Connect() (*Client, error) {
|
||||
cmd := exec.Command(t.command, t.args...)
|
||||
for k, v := range t.env {
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := NewClient(stdout, stdin)
|
||||
|
||||
// MCP initialization handshake.
|
||||
if _, err := client.Call("initialize", map[string]interface{}{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]interface{}{},
|
||||
"clientInfo": map[string]interface{}{"name": "ollie", "version": "0.1"},
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("initialize: %w", err)
|
||||
}
|
||||
if _, err := client.Call("notifications/initialized", nil); err != nil {
|
||||
// Some servers don't respond to this notification; ignore errors.
|
||||
_ = err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
{
|
||||
"globalShortcut": "Ctrl+Space",
|
||||
"mcpServers": {
|
||||
"anvilmcp": {
|
||||
"command": "anvilmcp",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"agentSpawn": "anvillm-hook agentSpawn",
|
||||
"userPromptSubmit": "anvillm-hook userPromptSubmit",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ollie/mcp"
|
||||
)
|
||||
|
||||
// ToolInfo describes a tool provided by an MCP server.
|
||||
type ToolInfo struct {
|
||||
Server string
|
||||
Name string
|
||||
Description string
|
||||
InputSchema json.RawMessage
|
||||
}
|
||||
|
||||
// Executor routes tool calls to the MCP server that owns them.
|
||||
type Executor struct {
|
||||
servers map[string]*mcp.Client
|
||||
}
|
||||
|
||||
// NewExecutor creates an empty Executor.
|
||||
func NewExecutor() *Executor {
|
||||
return &Executor{servers: make(map[string]*mcp.Client)}
|
||||
}
|
||||
|
||||
// AddServer registers an MCP client under the given name.
|
||||
func (e *Executor) AddServer(name string, client *mcp.Client) {
|
||||
e.servers[name] = client
|
||||
}
|
||||
|
||||
// ListTools returns all tools advertised by all connected servers.
|
||||
func (e *Executor) ListTools() ([]ToolInfo, error) {
|
||||
var all []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 {
|
||||
all = append(all, ToolInfo{
|
||||
Server: serverName,
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
InputSchema: t.InputSchema,
|
||||
})
|
||||
}
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Execute calls a named tool on the named server.
|
||||
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)
|
||||
}
|
||||
return client.Call("tools/call", map[string]any{
|
||||
"name": toolName,
|
||||
"arguments": args,
|
||||
})
|
||||
}
|
||||
Reference in New Issue