package agent import ( "context" "encoding/json" "strings" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" "github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/pkg/stdio" "github.com/mudler/LocalAGI/pkg/xlog" "github.com/sashabaranov/go-openai/jsonschema" ) var _ types.Action = &mcpAction{} type MCPServer struct { URL string `json:"url"` Token string `json:"token"` } type MCPSTDIOServer struct { Args []string `json:"args"` Env []string `json:"env"` Cmd string `json:"cmd"` } type mcpAction struct { mcpClient *client.Client inputSchema ToolInputSchema toolName string toolDescription string } func (a *mcpAction) Plannable() bool { return true } func (m *mcpAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { // Convertir params en format attendu par mark3labs/mcp-go args := make(map[string]interface{}) if err := params.Unmarshal(&args); err != nil { return types.ActionResult{}, err } // Créer une requête d'appel d'outil request := mcp.CallToolRequest{ Params: mcp.CallToolParams{ Name: m.toolName, Arguments: args, }, } // Appeler l'outil result, err := m.mcpClient.CallTool(ctx, request) if err != nil { xlog.Error("Failed to call tool", "error", err.Error()) return types.ActionResult{}, err } xlog.Debug("MCP response", "response", result) // Traiter le résultat textResult := "" // Extraire le texte du résultat selon le format de mark3labs/mcp-go for _, content := range result.Content { if textContent, ok := mcp.AsTextContent(content); ok { textResult += textContent.Text + "\n" } else { xlog.Error("Unsupported content type", "type", content) } } // Si c'est une erreur, retourner le contenu de l'erreur comme résultat // plutôt que de faire échouer complètement l'action if result.IsError { xlog.Error("MCP tool returned error", "tool", m.toolName, "error", textResult) // Fournir des suggestions spécifiques selon le type d'erreur errorMessage := textResult if strings.Contains(strings.ToLower(textResult), "not found") { if m.toolName == "web-search-web_url_read" { errorMessage = "L'URL spécifiée n'a pas pu être trouvée. Essayez plutôt d'utiliser l'outil de recherche web 'web-search-searxng_web_search' pour chercher des informations sur ce sujet." } else { errorMessage = "Ressource non trouvée: " + textResult } } // Retourner le message d'erreur comme résultat pour que l'agent puisse réagir return types.ActionResult{ Result: "Erreur: " + errorMessage, }, nil } return types.ActionResult{ Result: textResult, }, nil } func (m *mcpAction) Definition() types.ActionDefinition { props := map[string]jsonschema.Definition{} dat, err := json.Marshal(m.inputSchema.Properties) if err != nil { xlog.Error("Failed to marshal input schema", "error", err.Error()) } json.Unmarshal(dat, &props) return types.ActionDefinition{ Name: types.ActionDefinitionName(m.toolName), Description: m.toolDescription, Required: m.inputSchema.Required, //Properties: , Properties: props, } } type ToolInputSchema struct { Type string `json:"type"` Properties map[string]interface{} `json:"properties,omitempty"` Required []string `json:"required,omitempty"` } func (a *Agent) addTools(mcpClient *client.Client) (types.Actions, error) { var generatedActions types.Actions xlog.Debug("Initializing client") // Initialize the client initRequest := mcp.InitializeRequest{ Params: mcp.InitializeParams{ ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, Capabilities: mcp.ClientCapabilities{}, ClientInfo: mcp.Implementation{ Name: "LocalAGI", Version: "1.0.0", }, }, } response, e := mcpClient.Initialize(a.context, initRequest) if e != nil { xlog.Error("Failed to initialize client", "error", e.Error()) return nil, e } xlog.Debug("Client initialized: %v", response.Instructions) // List tools using the new API listRequest := mcp.ListToolsRequest{} tools, err := mcpClient.ListTools(a.context, listRequest) if err != nil { xlog.Error("Failed to list tools", "error", err.Error()) return nil, err } for _, t := range tools.Tools { desc := t.Description xlog.Debug("Tool", "name", t.Name, "description", desc) dat, err := json.Marshal(t.InputSchema) if err != nil { xlog.Error("Failed to marshal input schema", "error", err.Error()) } xlog.Debug("Input schema", "tool", t.Name, "schema", string(dat)) // XXX: This is a wild guess, to verify (data types might be incompatible) var inputSchema ToolInputSchema err = json.Unmarshal(dat, &inputSchema) if err != nil { xlog.Error("Failed to unmarshal input schema", "error", err.Error()) } // Create a new action with Client + tool generatedActions = append(generatedActions, &mcpAction{ mcpClient: mcpClient, toolName: t.Name, inputSchema: inputSchema, toolDescription: desc, }) } return generatedActions, nil } func (a *Agent) initMCPActions() error { a.mcpActions = nil var err error generatedActions := types.Actions{} // MCP HTTP Servers for _, mcpServer := range a.options.mcpServers { // Créer un transport HTTP avec les options appropriées var httpTransport *transport.StreamableHTTP var err error if mcpServer.Token != "" { // Utiliser les headers avec token headers := map[string]string{ "Authorization": "Bearer " + mcpServer.Token, } httpTransport, err = transport.NewStreamableHTTP(mcpServer.URL, transport.WithHTTPHeaders(headers)) } else { httpTransport, err = transport.NewStreamableHTTP(mcpServer.URL) } if err != nil { xlog.Error("Failed to create HTTP transport", "server", mcpServer, "error", err.Error()) continue } // Créer le client avec le transport mcpClient := client.NewClient(httpTransport) // Démarrer le client if err := mcpClient.Start(a.context); err != nil { xlog.Error("Failed to start MCP client", "server", mcpServer, "error", err.Error()) continue } xlog.Debug("Adding tools for MCP server", "server", mcpServer) actions, err := a.addTools(mcpClient) if err != nil { xlog.Error("Failed to add tools for MCP server", "server", mcpServer, "error", err.Error()) } generatedActions = append(generatedActions, actions...) } // MCP STDIO Servers a.closeMCPSTDIOServers() // Make sure we stop all previous servers if any is active if a.options.mcpPrepareScript != "" { xlog.Debug("Preparing MCP box", "script", a.options.mcpPrepareScript) client := stdio.NewClient(a.options.mcpBoxURL) client.RunProcess(a.context, "/bin/bash", []string{"-c", a.options.mcpPrepareScript}, []string{}) } for _, mcpStdioServer := range a.options.mcpStdioServers { // Créer un transport STDIO stdioTransport := transport.NewStdio(mcpStdioServer.Cmd, mcpStdioServer.Env, mcpStdioServer.Args...) // Créer le client avec le transport mcpClient := client.NewClient(stdioTransport) // Démarrer le client if err := mcpClient.Start(a.context); err != nil { xlog.Error("Failed to start MCP STDIO client", "error", err.Error()) continue } xlog.Debug("Adding tools for MCP server (stdio)", "server", mcpStdioServer) actions, err := a.addTools(mcpClient) if err != nil { xlog.Error("Failed to add tools for MCP server", "server", mcpStdioServer, "error", err.Error()) } generatedActions = append(generatedActions, actions...) } a.mcpActions = generatedActions return err } func (a *Agent) closeMCPSTDIOServers() { client := stdio.NewClient(a.options.mcpBoxURL) client.StopGroup(a.Character.Name) }