package mcp import ( "context" "slices" "github.com/mudler/LocalAI/core/config" mcpRemote "github.com/mudler/LocalAI/core/services/mcp" "github.com/mudler/LocalAI/pkg/functions" "github.com/mudler/xlog" ) // ToolExecutor abstracts MCP tool discovery and execution. // Implementations handle local (in-process sessions) vs distributed (NATS) modes. type ToolExecutor interface { // DiscoverTools returns the tool function schemas available from MCP servers. DiscoverTools(ctx context.Context) ([]functions.Function, error) // IsTool returns true if the given function name is an MCP tool. IsTool(name string) bool // ExecuteTool executes an MCP tool by name with the given JSON arguments. ExecuteTool(ctx context.Context, toolName, arguments string) (string, error) // HasTools returns true if any MCP tools are available. HasTools() bool } // LocalToolExecutor uses in-process MCP sessions for tool operations. type LocalToolExecutor struct { tools []MCPToolInfo } // NewLocalToolExecutor creates a ToolExecutor from local named sessions. // It discovers tools immediately and caches the result. func NewLocalToolExecutor(ctx context.Context, sessions []NamedSession) *LocalToolExecutor { tools, err := DiscoverMCPTools(ctx, sessions) if err != nil { xlog.Error("Failed to discover MCP tools (local)", "error", err) } return &LocalToolExecutor{tools: tools} } func (e *LocalToolExecutor) DiscoverTools(_ context.Context) ([]functions.Function, error) { var fns []functions.Function for _, t := range e.tools { fns = append(fns, t.Function) } return fns, nil } func (e *LocalToolExecutor) IsTool(name string) bool { return IsMCPTool(e.tools, name) } func (e *LocalToolExecutor) ExecuteTool(ctx context.Context, toolName, arguments string) (string, error) { return ExecuteMCPToolCall(ctx, e.tools, toolName, arguments) } func (e *LocalToolExecutor) HasTools() bool { return len(e.tools) > 0 } // DistributedToolExecutor routes tool operations through NATS to agent workers. type DistributedToolExecutor struct { natsClient MCPNATSClient modelName string remote config.MCPGenericConfig[config.MCPRemoteServers] stdio config.MCPGenericConfig[config.MCPSTDIOServers] toolDefs []mcpRemote.MCPToolDef } // NewDistributedToolExecutor creates a ToolExecutor that routes through NATS. // It discovers tools immediately via a NATS request-reply to an agent worker. func NewDistributedToolExecutor(ctx context.Context, natsClient MCPNATSClient, modelName string, remote config.MCPGenericConfig[config.MCPRemoteServers], stdio config.MCPGenericConfig[config.MCPSTDIOServers], ) *DistributedToolExecutor { e := &DistributedToolExecutor{ natsClient: natsClient, modelName: modelName, remote: remote, stdio: stdio, } resp, err := DiscoverMCPToolsRemote(ctx, natsClient, modelName, remote, stdio) if err != nil { xlog.Error("Failed to discover MCP tools (distributed)", "error", err) } else if resp != nil { e.toolDefs = resp.Tools } return e } func (e *DistributedToolExecutor) DiscoverTools(_ context.Context) ([]functions.Function, error) { var fns []functions.Function for _, td := range e.toolDefs { fns = append(fns, td.Function) } return fns, nil } func (e *DistributedToolExecutor) IsTool(name string) bool { return slices.ContainsFunc(e.toolDefs, func(td mcpRemote.MCPToolDef) bool { return td.ToolName == name }) } func (e *DistributedToolExecutor) ExecuteTool(ctx context.Context, toolName, arguments string) (string, error) { return ExecuteMCPToolCallRemote(ctx, e.natsClient, e.modelName, e.remote, e.stdio, toolName, arguments) } func (e *DistributedToolExecutor) HasTools() bool { return len(e.toolDefs) > 0 } // NewToolExecutor creates the appropriate ToolExecutor based on the current mode. // When natsClient is non-nil, returns a DistributedToolExecutor that routes through NATS. // When natsClient is nil, creates local sessions and returns a LocalToolExecutor. func NewToolExecutor(ctx context.Context, natsClient MCPNATSClient, modelName string, remote config.MCPGenericConfig[config.MCPRemoteServers], stdio config.MCPGenericConfig[config.MCPSTDIOServers], enabledServers []string, ) ToolExecutor { if natsClient != nil { return NewDistributedToolExecutor(ctx, natsClient, modelName, remote, stdio) } sessions, err := NamedSessionsFromMCPConfig(modelName, remote, stdio, enabledServers) if err != nil || len(sessions) == 0 { if err != nil { xlog.Error("Failed to create MCP sessions", "error", err) } return &LocalToolExecutor{} // empty, HasTools() returns false } return NewLocalToolExecutor(ctx, sessions) }