feat(commands): add MCP slash commands and tool details

This commit is contained in:
afjcjsbx
2026-04-15 19:38:30 +02:00
parent 6ca7311273
commit 276f5425f0
11 changed files with 521 additions and 4 deletions
+208
View File
@@ -4,11 +4,15 @@ package agent
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/commands"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
)
@@ -133,6 +137,120 @@ func (al *AgentLoop) buildCommandsRuntime(
Config: cfg,
ListAgentIDs: registry.ListAgentIDs,
ListDefinitions: al.cmdRegistry.Definitions,
ListMCPServers: func(ctx context.Context) []commands.MCPServerInfo {
if cfg == nil {
return nil
}
if len(cfg.Tools.MCP.Servers) == 0 {
return nil
}
if err := al.ensureMCPInitialized(ctx); err != nil {
logger.WarnCF("agent", "Failed to refresh MCP status for command",
map[string]any{
"error": err.Error(),
})
}
connected := make(map[string]int)
if manager := al.mcp.getManager(); manager != nil {
for serverName, conn := range manager.GetServers() {
connected[serverName] = len(conn.Tools)
}
}
servers := make([]commands.MCPServerInfo, 0, len(cfg.Tools.MCP.Servers))
for serverName, serverCfg := range cfg.Tools.MCP.Servers {
toolCount, isConnected := connected[serverName]
servers = append(servers, commands.MCPServerInfo{
Name: serverName,
Enabled: serverCfg.Enabled,
Deferred: serverIsDeferred(cfg.Tools.MCP.Discovery.Enabled, serverCfg),
Connected: isConnected,
ToolCount: toolCount,
})
}
sort.Slice(servers, func(i, j int) bool {
return strings.ToLower(servers[i].Name) < strings.ToLower(servers[j].Name)
})
return servers
},
ListMCPTools: func(ctx context.Context, serverName string) ([]commands.MCPToolInfo, error) {
if cfg == nil {
return nil, fmt.Errorf("command unavailable: config not loaded")
}
serverName = strings.TrimSpace(serverName)
if serverName == "" {
return nil, fmt.Errorf("server name is required")
}
resolvedName := ""
var serverCfg config.MCPServerConfig
for name, candidate := range cfg.Tools.MCP.Servers {
if strings.EqualFold(name, serverName) {
resolvedName = name
serverCfg = candidate
break
}
}
if resolvedName == "" {
return nil, fmt.Errorf("MCP server '%s' is not configured", serverName)
}
if !serverCfg.Enabled {
return nil, fmt.Errorf("MCP server '%s' is configured but disabled", resolvedName)
}
if !cfg.Tools.IsToolEnabled("mcp") {
return nil, fmt.Errorf("MCP integration is disabled")
}
if err := al.ensureMCPInitialized(ctx); err != nil {
logger.WarnCF("agent", "Failed to initialize MCP runtime for command",
map[string]any{
"server": resolvedName,
"error": err.Error(),
})
}
manager := al.mcp.getManager()
if manager == nil {
return nil, fmt.Errorf("MCP server '%s' is configured but not connected", resolvedName)
}
conn, ok := manager.GetServer(resolvedName)
if !ok {
return nil, fmt.Errorf("MCP server '%s' is configured but not connected", resolvedName)
}
toolInfos := make([]commands.MCPToolInfo, 0, len(conn.Tools))
for _, tool := range conn.Tools {
if tool == nil {
continue
}
name := strings.TrimSpace(tool.Name)
if name == "" {
continue
}
description := strings.TrimSpace(tool.Description)
if description == "" {
description = fmt.Sprintf("MCP tool from %s server", resolvedName)
}
toolInfos = append(toolInfos, commands.MCPToolInfo{
Name: name,
Description: description,
Parameters: summarizeMCPToolParameters(tool.InputSchema),
})
}
sort.Slice(toolInfos, func(i, j int) bool {
return toolInfos[i].Name < toolInfos[j].Name
})
return toolInfos, nil
},
GetEnabledChannels: func() []string {
if al.channelManager == nil {
return nil
@@ -236,6 +354,96 @@ func (al *AgentLoop) buildCommandsRuntime(
return rt
}
func summarizeMCPToolParameters(schema any) []commands.MCPToolParameterInfo {
schemaMap := normalizeMCPSchema(schema)
properties, ok := schemaMap["properties"].(map[string]any)
if !ok || len(properties) == 0 {
return nil
}
required := make(map[string]struct{})
switch raw := schemaMap["required"].(type) {
case []string:
for _, name := range raw {
required[name] = struct{}{}
}
case []any:
for _, value := range raw {
name, ok := value.(string)
if ok {
required[name] = struct{}{}
}
}
}
names := make([]string, 0, len(properties))
for name := range properties {
names = append(names, name)
}
sort.Strings(names)
params := make([]commands.MCPToolParameterInfo, 0, len(names))
for _, name := range names {
param := commands.MCPToolParameterInfo{Name: name}
if propMap, ok := properties[name].(map[string]any); ok {
if typeName, ok := propMap["type"].(string); ok {
param.Type = strings.TrimSpace(typeName)
}
if desc, ok := propMap["description"].(string); ok {
param.Description = strings.TrimSpace(desc)
}
}
_, param.Required = required[name]
params = append(params, param)
}
return params
}
func normalizeMCPSchema(schema any) map[string]any {
if schema == nil {
return map[string]any{
"type": "object",
"properties": map[string]any{},
"required": []string{},
}
}
if schemaMap, ok := schema.(map[string]any); ok {
return schemaMap
}
var jsonData []byte
switch raw := schema.(type) {
case json.RawMessage:
jsonData = raw
case []byte:
jsonData = raw
}
if jsonData == nil {
var err error
jsonData, err = json.Marshal(schema)
if err != nil {
return map[string]any{
"type": "object",
"properties": map[string]any{},
"required": []string{},
}
}
}
var result map[string]any
if err := json.Unmarshal(jsonData, &result); err != nil {
return map[string]any{
"type": "object",
"properties": map[string]any{},
"required": []string{},
}
}
return result
}
func (al *AgentLoop) setPendingSkills(sessionKey string, skillNames []string) {
sessionKey = strings.TrimSpace(sessionKey)
if sessionKey == "" || len(skillNames) == 0 {