Merge pull request #53 from hobbyistlabs-coder/feature/agent-mcp-personas-3414193578461185744
feat(agent): allow assigning MCP servers to specific personas
This commit is contained in:
commit
c81d489899
8 changed files with 299 additions and 9 deletions
|
|
@ -26,5 +26,9 @@ func NewAgentCommand() *cobra.Command {
|
||||||
cmd.Flags().StringVarP(&sessionKey, "session", "s", "cli:default", "Session key")
|
cmd.Flags().StringVarP(&sessionKey, "session", "s", "cli:default", "Session key")
|
||||||
cmd.Flags().StringVarP(&model, "model", "", "", "Model to use")
|
cmd.Flags().StringVarP(&model, "model", "", "", "Model to use")
|
||||||
|
|
||||||
|
cmd.AddCommand(NewCreateCommand())
|
||||||
|
cmd.AddCommand(NewAssignMCPCommand())
|
||||||
|
cmd.AddCommand(NewRemoveMCPCommand())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
129
cmd/picoclaw/internal/agent/create.go
Normal file
129
cmd/picoclaw/internal/agent/create.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"jane/cmd/picoclaw/internal"
|
||||||
|
"jane/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCreateCommand() *cobra.Command {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
workspace string
|
||||||
|
sysPrompt string
|
||||||
|
model string
|
||||||
|
interactive bool
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a new agent persona",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if !interactive && name == "" {
|
||||||
|
return fmt.Errorf("name is required when not in interactive mode")
|
||||||
|
}
|
||||||
|
return createAgentCmd(name, workspace, sysPrompt, model, interactive)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&name, "name", "n", "", "Agent name")
|
||||||
|
cmd.Flags().StringVarP(&workspace, "workspace", "w", "", "Workspace path")
|
||||||
|
cmd.Flags().StringVarP(&sysPrompt, "system-prompt", "p", "", "System prompt / instructions")
|
||||||
|
cmd.Flags().StringVarP(&model, "model", "m", "", "Model configuration (primary)")
|
||||||
|
cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Interactive mode")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAgentCmd(name, workspace, sysPrompt, model string, interactive bool) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactive {
|
||||||
|
fmt.Printf("%s Creating new agent persona...\n\n", internal.Logo)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
fmt.Print("Agent Name: ")
|
||||||
|
nameInput, _ := reader.ReadString('\n')
|
||||||
|
nameInput = strings.TrimSpace(nameInput)
|
||||||
|
if nameInput != "" {
|
||||||
|
name = nameInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if workspace == "" {
|
||||||
|
fmt.Printf("Workspace path (default: ~/.picoclaw/workspace/%s): ", strings.ToLower(strings.ReplaceAll(name, " ", "_")))
|
||||||
|
workspaceInput, _ := reader.ReadString('\n')
|
||||||
|
workspaceInput = strings.TrimSpace(workspaceInput)
|
||||||
|
if workspaceInput != "" {
|
||||||
|
workspace = workspaceInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysPrompt == "" {
|
||||||
|
fmt.Print("System Prompt (optional): ")
|
||||||
|
sysPromptInput, _ := reader.ReadString('\n')
|
||||||
|
sysPromptInput = strings.TrimSpace(sysPromptInput)
|
||||||
|
if sysPromptInput != "" {
|
||||||
|
sysPrompt = sysPromptInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("agent name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := strings.ToLower(strings.ReplaceAll(name, " ", "-"))
|
||||||
|
id = strings.ReplaceAll(id, "_", "-")
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
id = uuid.New().String()[:8]
|
||||||
|
}
|
||||||
|
|
||||||
|
if workspace == "" {
|
||||||
|
var homePath string
|
||||||
|
if picoclawHome := os.Getenv("PICOCLAW_HOME"); picoclawHome != "" {
|
||||||
|
homePath = picoclawHome
|
||||||
|
} else {
|
||||||
|
userHome, _ := os.UserHomeDir()
|
||||||
|
homePath = filepath.Join(userHome, ".picoclaw")
|
||||||
|
}
|
||||||
|
workspace = filepath.Join(homePath, "workspace", strings.ToLower(strings.ReplaceAll(name, " ", "_")))
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelCfg *config.AgentModelConfig
|
||||||
|
if model != "" {
|
||||||
|
modelCfg = &config.AgentModelConfig{Primary: model}
|
||||||
|
}
|
||||||
|
|
||||||
|
newAgent := config.AgentConfig{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
Workspace: workspace,
|
||||||
|
SystemPrompt: sysPrompt,
|
||||||
|
Model: modelCfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Agents.List = append(cfg.Agents.List, newAgent)
|
||||||
|
|
||||||
|
if err := config.SaveConfig(internal.GetConfigPath(), cfg); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n✅ Successfully created agent persona '%s' with ID '%s'\n", name, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
113
cmd/picoclaw/internal/agent/mcp.go
Normal file
113
cmd/picoclaw/internal/agent/mcp.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"jane/cmd/picoclaw/internal"
|
||||||
|
"jane/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAssignMCPCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "assign-mcp <agent-id> <mcp-name>",
|
||||||
|
Short: "Assign an MCP server to an agent persona",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return assignMCPCmd(args[0], args[1])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignMCPCmd(agentID, mcpName string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for i, a := range cfg.Agents.List {
|
||||||
|
if a.ID == agentID {
|
||||||
|
found = true
|
||||||
|
|
||||||
|
// Check if already assigned
|
||||||
|
for _, m := range a.MCPServers {
|
||||||
|
if m == mcpName {
|
||||||
|
fmt.Printf("⚠️ MCP server '%s' is already assigned to agent '%s'\n", mcpName, agentID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Agents.List[i].MCPServers = append(cfg.Agents.List[i].MCPServers, mcpName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("agent with ID '%s' not found", agentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.SaveConfig(internal.GetConfigPath(), cfg); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Successfully assigned MCP server '%s' to agent '%s'\n", mcpName, agentID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoveMCPCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "remove-mcp <agent-id> <mcp-name>",
|
||||||
|
Short: "Remove an MCP server from an agent persona",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return removeMCPCmd(args[0], args[1])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeMCPCmd(agentID, mcpName string) error {
|
||||||
|
cfg, err := internal.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for i, a := range cfg.Agents.List {
|
||||||
|
if a.ID == agentID {
|
||||||
|
found = true
|
||||||
|
|
||||||
|
newMCPs := make([]string, 0, len(a.MCPServers))
|
||||||
|
removed := false
|
||||||
|
for _, m := range a.MCPServers {
|
||||||
|
if m == mcpName {
|
||||||
|
removed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newMCPs = append(newMCPs, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !removed {
|
||||||
|
fmt.Printf("⚠️ MCP server '%s' is not assigned to agent '%s'\n", mcpName, agentID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Agents.List[i].MCPServers = newMCPs
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("agent with ID '%s' not found", agentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.SaveConfig(internal.GetConfigPath(), cfg); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Successfully removed MCP server '%s' from agent '%s'\n", mcpName, agentID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,7 @@ type AgentInstance struct {
|
||||||
Tools *tools.ToolRegistry
|
Tools *tools.ToolRegistry
|
||||||
Subagents *config.SubagentsConfig
|
Subagents *config.SubagentsConfig
|
||||||
SkillsFilter []string
|
SkillsFilter []string
|
||||||
|
MCPServers []string
|
||||||
Candidates []providers.FallbackCandidate
|
Candidates []providers.FallbackCandidate
|
||||||
|
|
||||||
// Router is non-nil when model routing is configured and the light model
|
// Router is non-nil when model routing is configured and the light model
|
||||||
|
|
@ -122,12 +123,14 @@ func NewAgentInstance(
|
||||||
agentName := ""
|
agentName := ""
|
||||||
var subagents *config.SubagentsConfig
|
var subagents *config.SubagentsConfig
|
||||||
var skillsFilter []string
|
var skillsFilter []string
|
||||||
|
var mcpServers []string
|
||||||
|
|
||||||
if agentCfg != nil {
|
if agentCfg != nil {
|
||||||
agentID = routing.NormalizeAgentID(agentCfg.ID)
|
agentID = routing.NormalizeAgentID(agentCfg.ID)
|
||||||
agentName = agentCfg.Name
|
agentName = agentCfg.Name
|
||||||
subagents = agentCfg.Subagents
|
subagents = agentCfg.Subagents
|
||||||
skillsFilter = agentCfg.Skills
|
skillsFilter = agentCfg.Skills
|
||||||
|
mcpServers = agentCfg.MCPServers
|
||||||
}
|
}
|
||||||
|
|
||||||
maxIter := defaults.MaxToolIterations
|
maxIter := defaults.MaxToolIterations
|
||||||
|
|
@ -246,6 +249,7 @@ func NewAgentInstance(
|
||||||
Tools: toolsRegistry,
|
Tools: toolsRegistry,
|
||||||
Subagents: subagents,
|
Subagents: subagents,
|
||||||
SkillsFilter: skillsFilter,
|
SkillsFilter: skillsFilter,
|
||||||
|
MCPServers: mcpServers,
|
||||||
Candidates: candidates,
|
Candidates: candidates,
|
||||||
Router: router,
|
Router: router,
|
||||||
LightCandidates: lightCandidates,
|
LightCandidates: lightCandidates,
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,20 @@ func (al *AgentLoop) ensureMCPInitialized(ctx context.Context) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this agent explicitly restricts MCP servers
|
||||||
|
if agent != nil {
|
||||||
|
allowed := false
|
||||||
|
for _, allowedServer := range agent.MCPServers {
|
||||||
|
if allowedServer == serverName {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
continue // Skip registering this tool for this agent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mcpTool := tools.NewMCPTool(mcpManager, serverName, tool)
|
mcpTool := tools.NewMCPTool(mcpManager, serverName, tool)
|
||||||
|
|
||||||
if al.cfg.Tools.MCP.Discovery.Enabled {
|
if al.cfg.Tools.MCP.Discovery.Enabled {
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,15 @@ func (m AgentModelConfig) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Default bool `json:"default,omitempty"`
|
Default bool `json:"default,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Workspace string `json:"workspace,omitempty"`
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||||
Model *AgentModelConfig `json:"model,omitempty"`
|
Workspace string `json:"workspace,omitempty"`
|
||||||
Skills []string `json:"skills,omitempty"`
|
Model *AgentModelConfig `json:"model,omitempty"`
|
||||||
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
Skills []string `json:"skills,omitempty"`
|
||||||
|
MCPServers []string `json:"mcp_servers,omitempty"`
|
||||||
|
Subagents *SubagentsConfig `json:"subagents,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubagentsConfig struct {
|
type SubagentsConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -181,8 +181,8 @@ func TestConfig_BackwardCompat_NoAgentsList(t *testing.T) {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Agents.List) != 1 {
|
if len(cfg.Agents.List) != 5 {
|
||||||
t.Errorf("agents.list should have default clinician agent for backward compat, got %d", len(cfg.Agents.List))
|
t.Errorf("agents.list should have default agents for backward compat, got %d", len(cfg.Agents.List))
|
||||||
}
|
}
|
||||||
if len(cfg.Bindings) != 0 {
|
if len(cfg.Bindings) != 0 {
|
||||||
t.Errorf("bindings should be empty, got %d", len(cfg.Bindings))
|
t.Errorf("bindings should be empty, got %d", len(cfg.Bindings))
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,30 @@ func DefaultConfig() *Config {
|
||||||
Name: "Medical Persona",
|
Name: "Medical Persona",
|
||||||
Workspace: filepath.Join(homePath, "Obsidian_Vault", "Patients"),
|
Workspace: filepath.Join(homePath, "Obsidian_Vault", "Patients"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "coding",
|
||||||
|
Name: "Coding Persona",
|
||||||
|
Workspace: filepath.Join(homePath, "workspace", "code"),
|
||||||
|
MCPServers: []string{"github", "bash"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "google",
|
||||||
|
Name: "Google Persona",
|
||||||
|
Workspace: filepath.Join(homePath, "workspace", "google"),
|
||||||
|
MCPServers: []string{"gmail", "calendar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "communications",
|
||||||
|
Name: "Communications Persona",
|
||||||
|
Workspace: filepath.Join(homePath, "workspace", "communications"),
|
||||||
|
MCPServers: []string{"slack", "discord"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "financial",
|
||||||
|
Name: "Financial Persona",
|
||||||
|
Workspace: filepath.Join(homePath, "workspace", "finance"),
|
||||||
|
MCPServers: []string{"alpaca"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Bindings: []AgentBinding{},
|
Bindings: []AgentBinding{},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue