cmd

package
v0.0.0-...-1ff66f9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 4, 2026 License: MIT Imports: 43 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	Version   = "dev"
	BuildTime = "unknown"
	GitCommit = "unknown"
)

Version information - set from main package

View Source
var ChatCmd = &cobra.Command{
	Use:   "chat",
	Short: "Enter interactive chat mode with the LLM",
	Long: `Chat mode provides a conversational interface with the LLM and is the primary way to interact with the client.
The LLM can execute queries, access data, and leverage other capabilities provided by the server.

This command uses the modern interface-based approach for LLM providers, supporting all configured
provider types including OpenAI, Anthropic, Ollama, and others.`,
	RunE: func(cmd *cobra.Command, args []string) error {

		chatConfig := parseChatConfig(cmd, args)

		outputMgr := output.GetGlobalManager()

		if outputMgr.ShouldShowStartupInfo() {
			bold := color.New(color.Bold)
			serversText := strings.Join(chatConfig.ServerNames, ", ")
			if serversText == "" {
				serversText = "none"
			}
			bold.Printf("Starting chat mode with servers: %s, provider: %s, model: %s\n\n",
				serversText, chatConfig.ProviderName, chatConfig.ModelName)
		}

		chatService := chat.NewService()
		return chatService.StartChat(chatConfig)
	},
}

ChatCmd represents the unified chat command

View Source
var ConfigCmd = &cobra.Command{
	Use:   "config",
	Short: "Configuration management commands",
	Long: `Manage mcp-cli configuration files.

Available subcommands:
  validate - Validate configuration file and check for security issues

Examples:
  mcp-cli config validate
  mcp-cli config validate --config custom-config.yaml`,
}

ConfigCmd represents the config command

View Source
var ConfigValidateCmd = &cobra.Command{
	Use:   "validate",
	Short: "Validate configuration file",
	Long: `Validates the configuration file for:
- Syntax errors
- Missing required fields
- Exposed API keys (security check)
- Template validation

Examples:
  mcp-cli config validate
  mcp-cli config validate --config custom-config.json`,
	RunE: func(cmd *cobra.Command, args []string) error {
		fmt.Println("Validating configuration...")

		configService := config.NewService()
		appConfig, err := configService.LoadConfig(configFile)
		if err != nil {
			fmt.Printf("❌ Failed to load config: %v\n", err)
			return err
		}

		if err := configService.ValidateConfig(appConfig); err != nil {
			fmt.Printf("❌ Configuration validation failed: %v\n", err)
			return err
		}

		fmt.Println("✓ Configuration syntax is valid")

		hasExposedKeys := false

		if appConfig.AI != nil && appConfig.AI.Interfaces != nil {
			for interfaceType, interfaceConfig := range appConfig.AI.Interfaces {
				for providerName, providerConfig := range interfaceConfig.Providers {
					if isExposedKey(providerConfig.APIKey) {
						fmt.Printf("⚠️  Warning: API key for %s/%s appears to be hardcoded\n",
							interfaceType, providerName)
						fmt.Println("   Consider moving to .env file: " + providerName + "_API_KEY")
						hasExposedKeys = true
					}
				}
			}
		}

		if appConfig.Embeddings != nil && appConfig.Embeddings.Interfaces != nil {
			for interfaceType, interfaceConfig := range appConfig.Embeddings.Interfaces {
				for providerName, providerConfig := range interfaceConfig.Providers {
					if isExposedKey(providerConfig.APIKey) {
						fmt.Printf("⚠️  Warning: Embedding API key for %s/%s appears to be hardcoded\n",
							interfaceType, providerName)
						fmt.Println("   Consider moving to .env file")
						hasExposedKeys = true
					}
				}
			}
		}

		if hasExposedKeys {
			fmt.Println("\n💡 Security Tip:")
			fmt.Println("   1. Create a .env file: cp .env.example .env")
			fmt.Println("   2. Add your keys: OPENAI_API_KEY=sk-...")
			fmt.Println("   3. Update config: \"api_key\": \"${OPENAI_API_KEY}\"")
			fmt.Println("   4. Add .env to .gitignore (already done)")
		} else {
			fmt.Println("✓ No exposed API keys found")
		}

		envPath := ".env"
		if _, err := os.Stat(envPath); os.IsNotExist(err) {
			fmt.Println("\n💡 Tip: Create a .env file for API keys")
			fmt.Println("   cp .env.example .env")
		} else {
			fmt.Println("✓ .env file found")
		}

		fmt.Println("\n✅ Configuration is valid!")

		if hasExposedKeys {
			fmt.Println("\n⚠️  However, you should move hardcoded API keys to .env file for security")
			os.Exit(1)
		}

		return nil
	},
}

ConfigValidateCmd validates the configuration file

View Source
var EmbeddingsCmd = &cobra.Command{
	Use:   "embeddings [text]",
	Short: "Generate vector embeddings from text input",
	Long: `Generate vector embeddings from text input using various embedding models.

Supports multiple input sources:
- Direct text argument
- Standard input (stdin)
- File input

Text is automatically chunked using configurable strategies to handle
large inputs and optimize embedding quality.

Examples:
  # Basic usage with stdin
  echo "Your text here" | mcp-cli embeddings
  
  # File input with specific model
  mcp-cli embeddings --input-file document.txt --model text-embedding-3-large
  
  # Advanced chunking and output
  mcp-cli embeddings --chunk-strategy sentence --max-chunk-size 512 --output-format json --overlap 50
  
  # Direct text input
  mcp-cli embeddings "Analyze this specific text"
  
  # Show available models and strategies
  mcp-cli embeddings --show-models
  mcp-cli embeddings --show-strategies`,
	RunE: executeEmbeddings,
}

EmbeddingsCmd represents the embeddings command

View Source
var InitCmd = &cobra.Command{
	Use:   "init",
	Short: "Initialize mcp-cli configuration",
	Long: `Interactive setup wizard for mcp-cli configuration.

Creates a modular configuration structure with separate directories for
providers, embeddings, servers, and workflows.

Modes:
  --quick     Quick setup with minimal questions (uses ollama, no API keys)
  --full      Full setup with all configuration options
  (default)   Standard interactive setup

Examples:
  mcp-cli init              # Interactive setup
  mcp-cli init --quick      # Quick setup (ollama only)
  mcp-cli init --full       # Complete setup wizard`,
	RunE: runInit,
}

InitCmd initializes a new mcp-cli configuration

View Source
var InteractiveCmd = &cobra.Command{
	Use:   "interactive",
	Short: "Enter interactive mode with slash commands",
	Long: `Interactive mode provides a command-line interface with slash commands for direct interaction with the server.
You can query server information, list available tools and resources, and more.`,
	RunE: func(cmd *cobra.Command, args []string) error {

		serverNames, userSpecified := host.ProcessOptions(configFile, serverName, disableFilesystem, providerName, modelName)

		bold := color.New(color.Bold)
		bold.Printf("Starting interactive mode with server: %s, provider: %s, model: %s\n\n", serverName, providerName, modelName)

		logging.Info("Starting interactive mode")

		err := host.RunCommand(runInteractiveMode, configFile, serverNames, userSpecified)
		if err != nil {
			logging.Error("Error in interactive mode: %v", err)
			fmt.Fprintf(os.Stderr, "Error in interactive mode: %v\n", err)
			return err
		}

		return nil
	},
}

InteractiveCmd represents the interactive command

View Source
var QueryCmd = &cobra.Command{
	Use:   "query [question] or --input-data \"question\"",
	Short: "Ask a single question and get a response",
	Long: `Query mode asks a single question to the AI model and returns a response
without entering an interactive session. Perfect for scripting, automation,
and integration with other tools.

The question can be provided either as:
  • Positional argument: query "question"
  • --input-data flag: query --input-data "question"
  • stdin: echo "question" | query --input-data -

The query command supports:
  • Multiple MCP servers for tool access
  • Context from files (--context)
  • Custom system prompts (--system-prompt)
  • JSON output for parsing (--json)
  • Raw tool data output (--raw-data)
  • File output (--output)

Examples:
  # Basic query
  mcp-cli query "What is the current time?"
  
  # With specific servers and provider
  mcp-cli query --server filesystem,brave-search \
    --provider openai --model gpt-4o \
    "Search for MCP information and summarize"
  
  # With context file
  mcp-cli query --context context.txt \
    --system-prompt "You are a coding assistant" \
    "How do I implement a binary tree in Go?"
  
  # JSON output for parsing
  mcp-cli query --json "List the top 5 cloud providers" > results.json
  
  # Verbose mode (show all operations)
  mcp-cli query --noisy "What files are in this directory?"
  
  # Raw tool data (bypass AI summarization)
  mcp-cli query --raw-data "Show latest security incidents"
  
  # Output to file
  mcp-cli query "Analyze this code" --output analysis.txt
  
  # Using --input-data flag instead of positional argument
  mcp-cli query --input-data "What is the weather today?"
  
  # Both work the same way
  mcp-cli query "question" --provider anthropic
  mcp-cli query --provider anthropic --input-data "question"`,
	RunE: func(cmd *cobra.Command, args []string) error {

		redirectStdinIfNotTerminal()

		if noisy && !verbose {

			logging.SetDefaultLevel(logging.INFO)
			logging.Info("Noisy mode enabled for query command")
		}

		if maxTokens != 0 && maxTokens < 1 {
			if errorCodeOnly {
				os.Exit(query.ErrInvalidArgumentCode)
			}
			return fmt.Errorf("--max-tokens must be positive, got %d", maxTokens)
		}

		if contextFile != "" {
			if _, err := os.Stat(contextFile); os.IsNotExist(err) {
				if errorCodeOnly {
					os.Exit(query.ErrContextNotFoundCode)
				}
				return fmt.Errorf("context file does not exist: %s", contextFile)
			}
		}

		if outputFile != "" {

			outputDir := outputFile
			if idx := strings.LastIndex(outputFile, "/"); idx != -1 {
				outputDir = outputFile[:idx]
			} else if idx := strings.LastIndex(outputFile, "\\"); idx != -1 {
				outputDir = outputFile[:idx]
			} else {
				outputDir = "."
			}

			if stat, err := os.Stat(outputDir); err != nil {
				if errorCodeOnly {
					os.Exit(query.ErrOutputWriteCode)
				}
				return fmt.Errorf("output directory does not exist: %s", outputDir)
			} else if !stat.IsDir() {
				if errorCodeOnly {
					os.Exit(query.ErrOutputWriteCode)
				}
				return fmt.Errorf("output path is not a directory: %s", outputDir)
			}
		}

		// Get question from either positional args, query-specific --input-data, or root --input-data flag
		var question string
		if len(args) > 0 {

			question = strings.Join(args, " ")
		} else if queryInputData != "" {

			question = queryInputData
		} else if inputData != "" {

			question = inputData
		} else {

			cliErr := NewMissingArgumentError("question", "query", []string{
				`mcp-cli query "What is the capital of France?"`,
				`mcp-cli query --input-data "What is the capital of France?"`,
				`echo "What is the capital of France?" | mcp-cli query --input-data -`,
			})
			fmt.Fprintln(os.Stderr, cliErr.Format())

			if errorCodeOnly {
				os.Exit(query.ErrInvalidArgumentCode)
			}
			os.Exit(1)
		}

		serverNames, userSpecified := ProcessOptions(configFile, serverName, disableFilesystem, providerName, modelName)
		logging.Debug("Server names: %v", serverNames)
		logging.Debug("Using provider from config: %s", providerName)

		externalServers, needsSkills := infraSkills.SeparateSkillsFromServers(serverNames)
		logging.Debug("External servers: %v, needs built-in skills: %v", externalServers, needsSkills)

		externalUserSpecified := make(map[string]bool)
		for _, server := range externalServers {
			if userSpecified[server] {
				externalUserSpecified[server] = true
			}
		}

		enhancedAIOptions, err := host.GetEnhancedAIOptions(configFile, providerName, modelName)
		if err != nil {
			if errorCodeOnly {
				os.Exit(query.ErrConfigNotFoundCode)
			}
			return fmt.Errorf("error loading enhanced AI options: %w", err)
		}

		aiOptions := &host.AIOptions{
			Provider:      enhancedAIOptions.Provider,
			Model:         enhancedAIOptions.Model,
			APIKey:        enhancedAIOptions.APIKey,
			APIEndpoint:   enhancedAIOptions.APIEndpoint,
			InterfaceType: enhancedAIOptions.Interface,
		}

		if providerName != "" {
			aiOptions.Provider = providerName
			enhancedAIOptions.Provider = providerName
		}
		if modelName != "" {
			aiOptions.Model = modelName
			enhancedAIOptions.Model = modelName
		}

		if aiOptions.APIKey == "" {

			logging.Debug("No API key configured for provider %s (may not be required)", aiOptions.Provider)
		}

		// Load context file if provided
		var contextContent string
		if contextFile != "" {
			content, err := os.ReadFile(contextFile)
			if err != nil {
				if errorCodeOnly {
					os.Exit(query.ErrContextNotFoundCode)
				}
				return fmt.Errorf("failed to read context file: %w", err)
			}
			contextContent = string(content)
		}

		oldCfg, err := config.LoadConfig(configFile)
		if err == nil {

			if systemPrompt == "" {

				if len(serverNames) == 1 {
					configPrompt := oldCfg.GetSystemPrompt(serverNames[0])
					if configPrompt != "" {
						systemPrompt = configPrompt
						logging.Debug("Using system prompt from config for server: %s", serverNames[0])
					}
				}

				if systemPrompt == "" {
					if oldCfg.AI != nil && oldCfg.AI.DefaultSystemPrompt != "" {
						systemPrompt = oldCfg.AI.DefaultSystemPrompt
						logging.Debug("Using default system prompt from config")
					}
				}
			}
		}

		serverRawDataOverride := make(map[string]bool)
		if oldCfg != nil {

			settings := oldCfg.GetSettings()
			if settings != nil && settings.RawDataOverride {
				rawDataOutput = true
				logging.Debug("Raw data output enabled from global settings")
			}

			for _, name := range serverNames {
				serverSettings, err := oldCfg.GetServerSettings(name)
				if err == nil && serverSettings != nil && serverSettings.RawDataOverride {
					serverRawDataOverride[name] = true
					logging.Debug("Raw data output enabled for server: %s", name)
				}
			}
		}

		// ARCHITECTURAL FIX: Choose command options based on verbosity for clean output
		var commandOptions *host.CommandOptions
		if noisy || verbose {

			commandOptions = host.DefaultCommandOptions()
		} else {

			commandOptions = host.QuietCommandOptions()
		}

		// Initialize built-in skills service if needed
		var skillService *skillsvc.Service
		if needsSkills {

			configService := config.NewService()
			appConfig, err := configService.LoadConfig(configFile)
			if err != nil {
				if errorCodeOnly {
					os.Exit(query.ErrConfigNotFoundCode)
				}
				return fmt.Errorf("failed to load config for skills: %w", err)
			}

			skillService, err = infraSkills.InitializeBuiltinSkills(configFile, appConfig)
			if err != nil {
				if errorCodeOnly {
					os.Exit(query.ErrInitializationCode)
				}
				return fmt.Errorf("failed to initialize built-in skills: %w", err)
			}
			logging.Info("Built-in skills service initialized successfully")
		}

		// Run the query command with the given options (ONLY external servers)
		var result *query.QueryResult
		err = host.RunCommandWithOptions(func(conns []*host.ServerConnection) error {

			aiService := ai.NewService()
			llmProvider, err := aiService.InitializeProvider(configFile, providerName, modelName)
			if err != nil {
				if errorCodeOnly {
					os.Exit(query.ErrInitializationCode)
				}
				return fmt.Errorf("failed to initialize AI provider: %w", err)
			}

			// ARCHITECTURAL FIX: Create server manager (with skills if needed)
			var serverManager domain.MCPServerManager = NewHostServerManager(conns)
			if skillService != nil {
				logging.Info("Wrapping query server manager with built-in skills support")
				serverManager = infraSkills.NewSkillsAwareServerManager(serverManager, skillService)
			}

			handler := query.NewQueryHandlerWithServerManager(serverManager, llmProvider, aiOptions, systemPrompt)

			if contextContent != "" {
				handler.AddContext(contextContent)
			}

			if maxTokens > 0 {
				handler.SetMaxTokens(maxTokens)
			}

			result, err = handler.Execute(question)
			if err != nil {

				if errorCodeOnly {
					exitCode := query.GetExitCode(err)
					os.Exit(exitCode)
				}
				return fmt.Errorf("query failed: %w", err)
			}

			return nil
		}, configFile, externalServers, externalUserSpecified, commandOptions)

		if err != nil {
			return err
		}

		if result != nil && len(result.ToolCalls) > 0 {

			applyRawDataOutput := rawDataOutput

			for _, conn := range result.ServerConnections {
				if serverRawDataOverride[conn] {
					applyRawDataOutput = true
					break
				}
			}

			if applyRawDataOutput {
				rawData := extractRawData(result.ToolCalls)
				if rawData != "" {

					result.Response = rawData
				}
			}
		}

		if result != nil {
			if jsonOutput {

				jsonData, err := json.MarshalIndent(result, "", "  ")
				if err != nil {
					if errorCodeOnly {
						os.Exit(query.ErrOutputFormatCode)
					}
					return fmt.Errorf("failed to format JSON response: %w", err)
				}

				if outputFile != "" {
					err = os.WriteFile(outputFile, jsonData, 0644)
					if err != nil {
						if errorCodeOnly {
							os.Exit(query.ErrOutputWriteCode)
						}
						return fmt.Errorf("failed to write output file: %w", err)
					}
				} else {
					fmt.Println(string(jsonData))
				}
			} else {

				if outputFile != "" {
					err = os.WriteFile(outputFile, []byte(result.Response), 0644)
					if err != nil {
						if errorCodeOnly {
							os.Exit(query.ErrOutputWriteCode)
						}
						return fmt.Errorf("failed to write output file: %w", err)
					}
				} else {

					writer := output.NewWriter()
					defer writer.Close()
					writer.Println(result.Response)
				}
			}
		}

		return nil
	},
}

QueryCmd represents the query command

View Source
var RagCmd = &cobra.Command{
	Use:   "rag",
	Short: "RAG (Retrieval-Augmented Generation) operations",
	Long: `Perform RAG operations using MCP vector servers.

RAG enables semantic search across vector databases connected via MCP.
Supports multi-strategy search, query expansion, and result fusion.

Examples:
  # Show RAG configuration
  mcp-cli rag config
  
  # Search directly
  mcp-cli rag search "What are the MFA requirements?"`,
}

RagCmd represents the rag command

View Source
var RagConfigCmd = &cobra.Command{
	Use:   "config",
	Short: "Show RAG configuration",
	Long:  `Display the current RAG configuration including servers, strategies, and fusion settings.`,
	RunE:  executeRagConfig,
}

RagConfigCmd shows RAG configuration

View Source
var RagSearchCmd = &cobra.Command{
	Use:   "search [query]",
	Short: "Search using RAG",
	Long: `Perform a RAG search against configured vector databases.

RAG (Retrieval-Augmented Generation) uses semantic similarity to find
relevant documents from vector databases. Results are ranked by relevance.

Examples:
  # Basic search
  mcp-cli rag search "authentication requirements"
  
  # Search with more results
  mcp-cli rag search "access control policies" --top-k 10
  
  # Use specific server
  mcp-cli rag search "encryption" --server pgvector
  
  # Multi-strategy search with fusion
  mcp-cli rag search "security controls" --strategies default,context --fusion rrf
  
  # Enable query expansion
  mcp-cli rag search "MFA" --expand

Output:
  Returns JSON with query, results, and metadata including:
  - Matched document identifiers and text
  - Similarity scores (higher = more relevant)
  - Total results and execution time`,
	Args: cobra.ExactArgs(1),
	RunE: executeRagSearch,
}

RagSearchCmd performs RAG search

View Source
var (

	// RootCmd represents the base command when called without any subcommands
	RootCmd = &cobra.Command{
		Use:   "mcp-cli",
		Short: "MCP Command-Line Tool - Interact with AI models and MCP servers",
		Long:  getColorizedHelp(),
		PersistentPreRun: func(cmd *cobra.Command, args []string) {

			cmdName := cmd.Name()
			if cmdName == "init" || cmdName == "help" || cmdName == "completion" || cmdName == "serve" {
				return
			}

			checkConfigExists(configFile)

			// Determine output configuration based on command and flags
			var outputConfig *models.OutputConfig

			isQueryCommand := cmd.Name() == "query"
			isTemplateMode := workflowName != ""
			isEmbeddingsCommand := cmd.Name() == "embeddings"

			if verbose {

				outputConfig = models.NewVerboseOutputConfig()
			} else if isQueryCommand || isTemplateMode || isEmbeddingsCommand {

				outputConfig = models.NewQuietOutputConfig()
			} else {

				outputConfig = models.NewDefaultOutputConfig()
			}

			if noColor {
				outputConfig.ShowColors = false
			}

			outputManager := output.NewManager(outputConfig)
			output.SetGlobalManager(outputManager)

			configureLegacyLogging(outputConfig)

			if providerName == "" {
				configService := config.NewService()
				if appConfig, err := configService.LoadConfig(configFile); err == nil {
					if appConfig.AI != nil && appConfig.AI.DefaultProvider != "" {
						providerName = appConfig.AI.DefaultProvider
						logging.Debug("Using default provider from config: %s", providerName)
					}
				}
			}
		},

		Run: func(cmd *cobra.Command, args []string) {

			if workflowName != "" {
				if err := executeWorkflow(); err != nil {
					logging.Error("Template execution failed: %v", err)
					os.Exit(1)
				}
				return
			}

			if err := ChatCmd.RunE(cmd, args); err != nil {
				os.Exit(1)
			}
		},
	}
)
View Source
var ServeCmd = &cobra.Command{
	Use:   "serve [runas-config]",
	Short: "Run as an MCP server exposing workflow templates as tools",
	Long: `Serve mode runs mcp-cli as an MCP server, exposing your workflow templates
as callable MCP tools that other applications can use.

This allows applications like Claude Desktop, IDEs, or other MCP clients to:
  • Execute your custom workflow templates as tools
  • Chain multiple AI operations together
  • Access your configured AI providers and MCP servers

The serve command requires a "runas" configuration file that defines:
  • Server name and version
  • Which templates to expose as tools
  • Input/output mappings for each tool
  • Optional provider/model overrides

Example usage:
  # Start MCP server with specific config
  mcp-cli serve config/runas/research_agent.yaml
  
  # With verbose logging for debugging
  mcp-cli serve --verbose config/runas/code_reviewer.yaml
  
  # Using the --serve flag
  mcp-cli --serve config/runas/data_analyst.yaml

Claude Desktop Configuration:
  Add to your Claude Desktop config (claude_desktop_config.json):
  
  {
    "mcpServers": {
      "research-agent": {
        "command": "/path/to/mcp-cli",
        "args": ["serve", "/path/to/config/runas/research_agent.yaml"]
      }
    }
  }`,
	Args: cobra.MaximumNArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {

		runasConfigPath := serveConfig
		if len(args) > 0 {
			runasConfigPath = args[0]
		}

		if runasConfigPath == "" {
			return fmt.Errorf("runas config file is required")
		}

		if !verbose {
			logging.SetDefaultLevel(logging.ERROR)
		}

		logging.Info("Starting MCP server mode with config: %s", runasConfigPath)

		runasLoader := runas.NewLoader()
		runasConfig, created, err := runasLoader.LoadOrDefault(runasConfigPath)
		if err != nil {
			return fmt.Errorf("failed to load runas config: %w", err)
		}

		if created {
			fmt.Fprintf(os.Stderr, "Created example runas config at: %s\n", runasConfigPath)
			fmt.Fprintf(os.Stderr, "Please edit the file to configure your MCP server.\n")
			return nil
		}

		logging.Info("Loaded runas config: %s", runasConfig.ServerInfo.Name)

		actualConfigFile := configFile
		if actualConfigFile == "config.yaml" {

			exePath, err := os.Executable()
			if err != nil {
				return fmt.Errorf("failed to determine executable path: %w", err)
			}
			exeDir := filepath.Dir(exePath)
			actualConfigFile = filepath.Join(exeDir, "config.yaml")
			logging.Info("Using config file: %s", actualConfigFile)
		}

		configDir := filepath.Dir(actualConfigFile)
		originalWd, _ := os.Getwd()
		if err := os.Chdir(configDir); err != nil {
			return fmt.Errorf("failed to change to config directory %s: %w", configDir, err)
		}
		logging.Info("Changed working directory to: %s (was: %s)", configDir, originalWd)

		configService := infraConfig.NewService()
		appConfig, err := configService.LoadConfig(actualConfigFile)
		if err != nil {
			return fmt.Errorf("failed to load application config from %s: %w", actualConfigFile, err)
		}

		logging.Info("Loaded %d workflows from config", len(appConfig.Workflows))

		if len(runasConfig.Templates) > 0 {
			logging.Info("Processing %d template source(s)...", len(runasConfig.Templates))

			for _, templateSrc := range runasConfig.Templates {

				basename := filepath.Base(templateSrc.ConfigSource)
				templateName := strings.TrimSuffix(basename, filepath.Ext(basename))

				_, existsV1 := appConfig.Workflows[templateName]
				templateV2, existsV2 := appConfig.Workflows[templateName]

				if !existsV1 && !existsV2 {
					return fmt.Errorf("template source '%s' points to unknown template: %s",
						templateSrc.ConfigSource, templateName)
				}

				toolName := templateSrc.Name
				if toolName == "" {
					toolName = templateName
				}

				toolDescription := templateSrc.Description
				if toolDescription == "" && existsV2 {
					toolDescription = templateV2.Description
				}

				inputSchema := map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"input_data": map[string]interface{}{
							"type":        "string",
							"description": "Input data for the template workflow",
						},
					},
					"required": []string{"input_data"},
				}

				tool := runas.ToolExposure{
					Template:    templateName,
					Name:        toolName,
					Description: toolDescription,
					InputSchema: inputSchema,
					InputMapping: map[string]string{
						"input_data": "{{input_data}}",
					},
				}

				runasConfig.Tools = append(runasConfig.Tools, tool)
				logging.Info("Created tool '%s' from template '%s' (source: %s)",
					toolName, templateName, templateSrc.ConfigSource)
			}

			logging.Info("Processed %d template(s) into %d total tool(s)",
				len(runasConfig.Templates), len(runasConfig.Tools))
		}

		skillService, err := infraSkills.InitializeBuiltinSkills(configFile, appConfig)
		if err != nil {
			return fmt.Errorf("failed to initialize built-in skills: %w", err)
		}
		logging.Info("Built-in skills initialized successfully")

		if runasConfig.RunAsType == runas.RunAsTypeMCPSkills || runasConfig.RunAsType == runas.RunAsTypeProxySkills {
			logging.Info("Auto-discovering skills for mcp-skills server type")

			logging.Info("Generating MCP tools from already-initialized skills")

			discoveredSkills := skillService.ListSkills()

			if skillNames != "" {

				requestedSkills := strings.Split(skillNames, ",")
				for i := range requestedSkills {
					requestedSkills[i] = strings.TrimSpace(requestedSkills[i])
				}

				if runasConfig.SkillsConfig == nil {
					runasConfig.SkillsConfig = &runas.SkillsConfig{}
				}
				runasConfig.SkillsConfig.IncludeSkills = requestedSkills
				runasConfig.SkillsConfig.ExcludeSkills = nil

				logging.Info("Using skills from command-line flag: %v", requestedSkills)
			}

			// Filter skills based on include/exclude lists
			var filteredSkills []string
			for _, skillName := range discoveredSkills {
				if runasConfig.ShouldIncludeSkill(skillName) {
					filteredSkills = append(filteredSkills, skillName)
				} else {
					logging.Info("Excluding skill: %s", skillName)
				}
			}

			logging.Info("Exposing %d skills as MCP tools", len(filteredSkills))

			runasConfig.Tools = make([]runas.ToolExposure, 0, len(filteredSkills)+1)

			for _, skillName := range filteredSkills {
				skill, exists := skillService.GetSkill(skillName)
				if !exists {
					continue
				}

				tool := runas.ToolExposure{
					Name:        skill.GetMCPToolName(),
					Description: skill.GetToolDescription(),
					Template:    "load_skill",
					InputSchema: skill.GetMCPInputSchema(),
					InputMapping: map[string]string{
						"skill_name": skillName,
					},
				}

				runasConfig.Tools = append(runasConfig.Tools, tool)
				logging.Info("Created tool '%s' for skill '%s'", tool.Name, skillName)
			}

			executeCodeTool := runas.ToolExposure{
				Name: "execute_skill_code",
				Description: "[SKILL CODE EXECUTION] Execute code with access to a skill's helper libraries. " +
					"Use this to: (1) Create documents dynamically, (2) Process files with custom logic, " +
					"(3) Use skill helper libraries (e.g., Document class from docx skill). " +
					"The code executes in a sandboxed environment with the skill's scripts/ directory " +
					"available for imports via PYTHONPATH.",
				Template: "execute_skill_code",
				InputSchema: map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"skill_name": map[string]interface{}{
							"type":        "string",
							"description": "Name of skill whose helper libraries to use (e.g., 'docx', 'pdf', 'xlsx')",
						},
						"language": map[string]interface{}{
							"type":        "string",
							"enum":        []string{"python", "bash"},
							"description": "Programming language ('python' or 'bash')",
							"default":     "python",
						},
						"code": map[string]interface{}{
							"type":        "string",
							"description": "Code to execute (Python or Bash). Can import from 'scripts' module to use skill helper libraries.",
						},
						"files": map[string]interface{}{
							"type":        "object",
							"description": "Optional files to make available in workspace (filename -> base64 content)",
						},
					},
					"required": []string{"skill_name", "code"},
				},
			}

			runasConfig.Tools = append(runasConfig.Tools, executeCodeTool)

			logging.Info("Generated %d MCP tools from skills (including execute_skill_code)", len(runasConfig.Tools))
		}

		for i, tool := range runasConfig.Tools {

			if tool.Template == "load_skill" || tool.Template == "execute_skill_code" {
				continue
			}

			logging.Debug("Checking tool %d: name=%s, template=%s", i, tool.Name, tool.Template)
			logging.Debug("Total workflows loaded: %d", len(appConfig.Workflows))

			_, existsV1 := appConfig.Workflows[tool.Template]
			_, existsV2 := appConfig.Workflows[tool.Template]

			if !existsV1 && !existsV2 {

				logging.Error("Template '%s' not found. Loaded workflows:", tool.Template)
				count := 0
				for key := range appConfig.Workflows {
					if count < 10 {
						logging.Error("  - %s", key)
						count++
					}
				}
				return fmt.Errorf("tool %d (%s) references unknown template: %s",
					i, tool.Name, tool.Template)
			}
		}

		if runasConfig.RunAsType == runas.RunAsTypeProxy || runasConfig.RunAsType == runas.RunAsTypeProxySkills {

			return startProxyServer(runasConfig, appConfig, configService, skillService)
		}

		return startStdioServer(runasConfig, appConfig, configService, skillService)
	},
}

ServeCmd represents the serve command

View Source
var ServersCmd = &cobra.Command{
	Use:   "servers",
	Short: "List all available MCP servers",
	Long: `List all MCP servers configured in the system.

This includes:
- Servers from config/servers/*.yaml
- RunAs servers from config/runas/*.yaml (templates as MCP servers)

Use these server names with --server flag in chat and query modes.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return listServers()
	},
}

ServersCmd lists all available MCP servers

View Source
var SkillsCmd = &cobra.Command{
	Use:   "skills",
	Short: "List all available skills",
	Long: `List all skills configured in the system.

Skills are defined in config/skills/ directory and can be used to extend
Claude's capabilities with specialized knowledge and helper functions.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return executeListSkills()
	},
}

SkillsCmd lists all available skills

View Source
var VersionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print version information",
	Long:  `Print detailed version information including build time and git commit.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("mcp-cli version %s\n", Version)
		fmt.Printf("Built: %s\n", BuildTime)
		fmt.Printf("Commit: %s\n", GitCommit)
	},
}

VersionCmd represents the version command

View Source
var WorkflowsCmd = &cobra.Command{
	Use:   "workflows",
	Short: "List all available workflows",
	Long: `List all workflow templates configured in the system.

Workflows are defined in YAML files in config/workflows/ directory.
Use these workflow names with --workflow flag on the root command.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return executeListWorkflows()
	},
}

WorkflowsCmd lists all available workflows

Functions

func Execute

func Execute() error

Execute adds all child commands to the root command and sets flags appropriately.

func ExtractFlagName

func ExtractFlagName(errStr string) string

ExtractFlagName extracts the flag name from an error string

func FindSimilarCommands

func FindSimilarCommands(commandName string, cmd *cobra.Command) []string

FindSimilarCommands finds commands similar to the given name

func FindSimilarFlags

func FindSimilarFlags(flagName string, cmd *cobra.Command) []string

FindSimilarFlags finds flags similar to the given name using Levenshtein distance

func ProcessOptions

func ProcessOptions(configFile, serverFlag string, disableFilesystem bool, provider string, model string) ([]string, map[string]bool)

ProcessOptions processes command-line options and returns the server names

Types

type CLIError

type CLIError struct {
	Type        string   // "unknown_flag", "missing_arg", "invalid_value", "unknown_command"
	Message     string   // The error message
	Suggestions []string // Possible corrections
	Examples    []string // Usage examples
	HelpCommand string   // Command to get more help
}

CLIError represents a user-friendly CLI error

func NewInvalidValueError

func NewInvalidValueError(flagName string, value string, validValues []string, commandName string) *CLIError

NewInvalidValueError creates an error for invalid flag values

func NewMissingArgumentError

func NewMissingArgumentError(argumentName string, commandName string, examples []string) *CLIError

NewMissingArgumentError creates an error for missing arguments

func NewUnknownCommandError

func NewUnknownCommandError(commandName string, parentCmd *cobra.Command) *CLIError

NewUnknownCommandError creates an error for unknown commands

func NewUnknownFlagError

func NewUnknownFlagError(flagName string, cmd *cobra.Command) *CLIError

NewUnknownFlagError creates an error for unknown flags

func (*CLIError) Format

func (e *CLIError) Format() string

Format returns a user-friendly error message

type HostServerAdapter

type HostServerAdapter struct {
	// contains filtered or unexported fields
}

HostServerAdapter adapts host.ServerConnection to domain.MCPServer interface

func (*HostServerAdapter) ExecuteTool

func (hsa *HostServerAdapter) ExecuteTool(ctx context.Context, toolName string, arguments map[string]interface{}) (string, error)

func (*HostServerAdapter) GetConfig

func (hsa *HostServerAdapter) GetConfig() *config.ServerConfig

func (*HostServerAdapter) GetServerName

func (hsa *HostServerAdapter) GetServerName() string

func (*HostServerAdapter) GetTools

func (hsa *HostServerAdapter) GetTools() ([]domain.Tool, error)

func (*HostServerAdapter) IsRunning

func (hsa *HostServerAdapter) IsRunning() bool

func (*HostServerAdapter) Start

func (hsa *HostServerAdapter) Start(ctx context.Context) error

func (*HostServerAdapter) Stop

func (hsa *HostServerAdapter) Stop() error

type HostServerManager

type HostServerManager struct {
	// contains filtered or unexported fields
}

HostServerManager adapts host.ServerConnection to domain.MCPServerManager interface

func NewHostServerManager

func NewHostServerManager(connections []*host.ServerConnection) *HostServerManager

func (*HostServerManager) ExecuteTool

func (hsm *HostServerManager) ExecuteTool(ctx context.Context, toolName string, arguments map[string]interface{}) (string, error)

func (*HostServerManager) GetAvailableTools

func (hsm *HostServerManager) GetAvailableTools() ([]domain.Tool, error)

func (*HostServerManager) GetServer

func (hsm *HostServerManager) GetServer(serverName string) (domain.MCPServer, bool)

func (*HostServerManager) ListServers

func (hsm *HostServerManager) ListServers() map[string]domain.MCPServer

func (*HostServerManager) StartServer

func (hsm *HostServerManager) StartServer(ctx context.Context, serverName string, cfg *config.ServerConfig) (domain.MCPServer, error)

func (*HostServerManager) StopAll

func (hsm *HostServerManager) StopAll() error

func (*HostServerManager) StopServer

func (hsm *HostServerManager) StopServer(serverName string) error

type InitConfig

type InitConfig struct {
	Providers           []string
	Servers             []string
	IncludeOllama       bool
	IncludeOpenAI       bool
	IncludeAnthropic    bool
	IncludeDeepSeek     bool
	IncludeGemini       bool
	IncludeOpenRouter   bool
	IncludeLMStudio     bool
	IncludeMoonshot     bool
	IncludeBedrock      bool
	IncludeAzureFoundry bool
	IncludeVertexAI     bool
	DefaultProvider     string
	IncludeSkills       bool
	IncludeRAG          bool
}

InitConfig holds configuration choices

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL