214 lines
5.4 KiB
Go
214 lines
5.4 KiB
Go
// PicoClaw - Ultra-lightweight personal AI agent
|
|
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
|
|
// License: MIT
|
|
//
|
|
// Copyright (c) 2026 PicoClaw contributors
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
"github.com/sipeed/picoclaw/pkg/mcp"
|
|
"github.com/sipeed/picoclaw/pkg/tools"
|
|
)
|
|
|
|
type mcpRuntime struct {
|
|
initOnce sync.Once
|
|
mu sync.Mutex
|
|
manager *mcp.Manager
|
|
initErr error
|
|
}
|
|
|
|
func (r *mcpRuntime) setManager(manager *mcp.Manager) {
|
|
r.mu.Lock()
|
|
r.manager = manager
|
|
r.initErr = nil
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
func (r *mcpRuntime) getInitErr() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.initErr
|
|
}
|
|
|
|
func (r *mcpRuntime) takeManager() *mcp.Manager {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
manager := r.manager
|
|
r.manager = nil
|
|
return manager
|
|
}
|
|
|
|
func (r *mcpRuntime) hasManager() bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.manager != nil
|
|
}
|
|
|
|
func (r *mcpRuntime) getManager() *mcp.Manager {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.manager
|
|
}
|
|
|
|
// EnsureMCPInitialized loads MCP servers/tools once so both Run() and direct
|
|
// agent mode share the same initialization path.
|
|
func (al *AgentLoop) EnsureMCPInitialized(ctx context.Context) error {
|
|
if !al.cfg.Tools.IsToolEnabled("mcp") {
|
|
return nil
|
|
}
|
|
|
|
if len(al.cfg.Tools.MCP.Servers) == 0 {
|
|
logger.WarnCF("agent", "MCP is enabled but no servers are configured, skipping MCP initialization", nil)
|
|
return nil
|
|
}
|
|
|
|
findValidServer := false
|
|
for _, serverCfg := range al.cfg.Tools.MCP.Servers {
|
|
if serverCfg.Enabled {
|
|
findValidServer = true
|
|
}
|
|
}
|
|
if !findValidServer {
|
|
logger.WarnCF("agent", "MCP is enabled but no valid servers are configured, skipping MCP initialization", nil)
|
|
return nil
|
|
}
|
|
|
|
al.mcp.initOnce.Do(func() {
|
|
mcpManager := mcp.NewManager()
|
|
|
|
defaultAgent := al.registry.GetDefaultAgent()
|
|
workspacePath := al.cfg.WorkspacePath()
|
|
if defaultAgent != nil && defaultAgent.Workspace != "" {
|
|
workspacePath = defaultAgent.Workspace
|
|
}
|
|
|
|
if err := mcpManager.LoadFromMCPConfig(ctx, al.cfg.Tools.MCP, workspacePath); err != nil {
|
|
logger.WarnCF("agent", "Failed to load MCP servers, MCP tools will not be available",
|
|
map[string]any{
|
|
"error": err.Error(),
|
|
})
|
|
if closeErr := mcpManager.Close(); closeErr != nil {
|
|
logger.ErrorCF("agent", "Failed to close MCP manager",
|
|
map[string]any{
|
|
"error": closeErr.Error(),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
al.mcp.setManager(mcpManager)
|
|
|
|
// Register MCP and discovery tools for all currently known agents
|
|
agentIDs := al.registry.ListAgentIDs()
|
|
for _, agentID := range agentIDs {
|
|
agent, ok := al.registry.GetAgent(agentID)
|
|
if !ok {
|
|
continue
|
|
}
|
|
al.RegisterMCPToolsToAgent(agentID, agent)
|
|
}
|
|
|
|
logger.InfoCF("agent", "MCP initialization complete",
|
|
map[string]any{
|
|
"server_count": len(mcpManager.GetServers()),
|
|
"agent_count": len(agentIDs),
|
|
})
|
|
})
|
|
|
|
return al.mcp.getInitErr()
|
|
}
|
|
|
|
// RegisterMCPToolsToAgent registers all currently active MCP tools and discovery tools to the given agent instance.
|
|
func (al *AgentLoop) RegisterMCPToolsToAgent(agentID string, agent *AgentInstance) {
|
|
if !al.cfg.Tools.MCP.Enabled {
|
|
return
|
|
}
|
|
|
|
mcpManager := al.mcp.getManager()
|
|
if mcpManager == nil {
|
|
return
|
|
}
|
|
|
|
// 1. Register MCP server tools
|
|
servers := mcpManager.GetServers()
|
|
uniqueTools := 0
|
|
totalRegistrations := 0
|
|
|
|
for serverName, conn := range servers {
|
|
uniqueTools += len(conn.Tools)
|
|
|
|
serverCfg := al.cfg.Tools.MCP.Servers[serverName]
|
|
registerAsHidden := serverIsDeferred(al.cfg.Tools.MCP.Discovery.Enabled, serverCfg)
|
|
|
|
for _, tool := range conn.Tools {
|
|
mcpTool := tools.NewMCPTool(mcpManager, serverName, tool)
|
|
mcpTool.SetWorkspace(agent.Workspace)
|
|
mcpTool.SetMaxInlineTextRunes(al.cfg.Tools.MCP.GetMaxInlineTextChars())
|
|
|
|
if registerAsHidden {
|
|
agent.Tools.RegisterHidden(mcpTool)
|
|
} else {
|
|
agent.Tools.Register(mcpTool)
|
|
}
|
|
totalRegistrations++
|
|
}
|
|
}
|
|
|
|
if totalRegistrations > 0 {
|
|
logger.DebugCF("agent", "Registered MCP tools to agent",
|
|
map[string]any{
|
|
"agent_id": agentID,
|
|
"server_count": len(servers),
|
|
"tool_count": totalRegistrations,
|
|
})
|
|
}
|
|
|
|
// 2. Initializes Discovery Tools only if enabled by configuration
|
|
if al.cfg.Tools.MCP.Discovery.Enabled {
|
|
useBM25 := al.cfg.Tools.MCP.Discovery.UseBM25
|
|
useRegex := al.cfg.Tools.MCP.Discovery.UseRegex
|
|
|
|
if useBM25 || useRegex {
|
|
ttl := al.cfg.Tools.MCP.Discovery.TTL
|
|
if ttl <= 0 {
|
|
ttl = 5
|
|
}
|
|
maxSearchResults := al.cfg.Tools.MCP.Discovery.MaxSearchResults
|
|
if maxSearchResults <= 0 {
|
|
maxSearchResults = 5
|
|
}
|
|
|
|
if useRegex {
|
|
agent.Tools.Register(tools.NewRegexSearchTool(agent.Tools, ttl, maxSearchResults))
|
|
}
|
|
if useBM25 {
|
|
agent.Tools.Register(tools.NewBM25SearchTool(agent.Tools, ttl, maxSearchResults))
|
|
}
|
|
|
|
logger.DebugCF("agent", "Initialized tool discovery for agent", map[string]any{
|
|
"agent_id": agentID, "bm25": useBM25, "regex": useRegex,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// serverIsDeferred reports whether an MCP server's tools should be registered
|
|
// as hidden (deferred/discovery mode).
|
|
//
|
|
// The per-server Deferred field takes precedence over the global discoveryEnabled
|
|
// default. When Deferred is nil, discoveryEnabled is used as the fallback.
|
|
func serverIsDeferred(discoveryEnabled bool, serverCfg config.MCPServerConfig) bool {
|
|
if !discoveryEnabled {
|
|
return false
|
|
}
|
|
if serverCfg.Deferred != nil {
|
|
return *serverCfg.Deferred
|
|
}
|
|
return true
|
|
}
|