Module: HTM::MCP::CLI
- Defined in:
- lib/htm/mcp/cli.rb
Overview
CLI commands for htm_mcp executable
Class Method Summary collapse
- .apply_config(config) ⇒ Object
- .apply_section(config, section_name, values) ⇒ Object
- .check_database_config! ⇒ Object
- .check_migration_status ⇒ Object
- .command?(arg) ⇒ Boolean
- .deep_merge(base, override) ⇒ Object
- .extract_dbname(url_or_name) ⇒ Object
-
.handle_config_option(args) ⇒ Object
Handle -c / –config option, modifying args in place Returns true if config was loaded, nil otherwise.
- .list_rake_tasks(pattern: nil) ⇒ Object
- .load_config_file(path) ⇒ Object
- .load_htm_rake_tasks ⇒ Object
- .output_default_config ⇒ Object
- .print_error_suggestion(error_message) ⇒ Object
- .print_help ⇒ Object
- .run(args) ⇒ Object
- .run_rake(args) ⇒ Object
- .run_setup ⇒ Object
- .run_stats ⇒ Object
- .run_verify ⇒ Object
- .suggestion_lines_for(msg) ⇒ Object
Class Method Details
.apply_config(config) ⇒ Object
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/htm/mcp/cli.rb', line 310 def apply_config(config) HTM.configure do |c| # Apply nested sections apply_section(c, :database, config[:database]) apply_section(c, :service, config[:service]) apply_section(c, :embedding, config[:embedding]) apply_section(c, :tag, config[:tag]) apply_section(c, :proposition, config[:proposition]) apply_section(c, :chunking, config[:chunking]) apply_section(c, :circuit_breaker, config[:circuit_breaker]) apply_section(c, :relevance, config[:relevance]) apply_section(c, :job, config[:job]) apply_section(c, :providers, config[:providers]) # Apply top-level scalars c.week_start = config[:week_start] if config[:week_start] c.connection_timeout = config[:connection_timeout] if config[:connection_timeout] c.telemetry_enabled = config[:telemetry_enabled] unless config[:telemetry_enabled].nil? c.log_level = config[:log_level] if config[:log_level] end end |
.apply_section(config, section_name, values) ⇒ Object
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/htm/mcp/cli.rb', line 332 def apply_section(config, section_name, values) return unless values.is_a?(Hash) section = config.send(section_name) values.each do |key, value| next if value.nil? if value.is_a?(Hash) # Handle nested sections (like providers.openai) subsection = section.send(key) value.each do |subkey, subvalue| subsection.send("#{subkey}=", subvalue) unless subvalue.nil? end else section.send("#{key}=", value) end end end |
.check_database_config! ⇒ Object
140 141 142 143 144 145 146 |
# File 'lib/htm/mcp/cli.rb', line 140 def check_database_config! return if ENV['HTM_DATABASE__URL'] || ENV['HTM_DATABASE__NAME'] warn "Error: Database not configured." warn "Set HTM_DATABASE__URL or HTM_DATABASE__NAME environment variable." warn "Run 'htm_mcp help' for details." exit 1 end |
.check_migration_status ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/htm/mcp/cli.rb', line 214 def check_migration_status migrations_path = File.('../../../db/migrate', __dir__) # Get available migrations from files available_migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file| { version: File.basename(file).split('_').first, name: File.basename(file, '.rb') } end available_migrations = available_migrations.sort_by { |m| m[:version] } # Ensure Sequel connection for migration check HTM::SequelConfig.establish_connection! # Get applied migrations from database applied_versions = begin HTM.db[:schema_migrations].select_order_map(:version) rescue Sequel::DatabaseError [] end puts "Migration Status" puts "-" * 80 if available_migrations.empty? puts " No migration files found" return 0 end available_migrations.each do |migration| applied = applied_versions.include?(migration[:version]) status_mark = applied ? "+" : "-" puts " #{status_mark} #{migration[:name]}" end applied_count = applied_versions.length pending_count = available_migrations.length - applied_count puts "-" * 80 puts " #{applied_count} applied, #{pending_count} pending" pending_count end |
.command?(arg) ⇒ Boolean
449 450 451 |
# File 'lib/htm/mcp/cli.rb', line 449 def command?(arg) %w[help version setup init verify stats config server stdio rake].include?(arg.downcase) end |
.deep_merge(base, override) ⇒ Object
300 301 302 303 304 305 306 307 308 |
# File 'lib/htm/mcp/cli.rb', line 300 def deep_merge(base, override) base.merge(override) do |_key, old_val, new_val| if old_val.is_a?(Hash) && new_val.is_a?(Hash) deep_merge(old_val, new_val) else new_val.nil? ? old_val : new_val end end end |
.extract_dbname(url_or_name) ⇒ Object
153 154 155 156 157 158 159 160 161 162 |
# File 'lib/htm/mcp/cli.rb', line 153 def extract_dbname(url_or_name) return url_or_name unless url_or_name&.include?("://") # Extract database name from URL like postgresql://user@host:port/dbname if url_or_name =~ %r{/([^/?]+)(?:\?|$)} ::Regexp.last_match(1) else "htm_development" end end |
.handle_config_option(args) ⇒ Object
Handle -c / –config option, modifying args in place Returns true if config was loaded, nil otherwise
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 |
# File 'lib/htm/mcp/cli.rb', line 427 def handle_config_option(args) config_idx = args.index('-c') || args.index('--config') return nil unless config_idx # Remove the -c/--config flag args.delete_at(config_idx) # Check if next arg is a path (not another flag or command) next_arg = args[config_idx] if next_arg.nil? || next_arg.start_with?('-') || command?(next_arg) # No path provided - output default config and exit output_default_config exit 0 else # Path provided - load config file config_path = args.delete_at(config_idx) load_config_file(config_path) true end end |
.list_rake_tasks(pattern: nil) ⇒ Object
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 |
# File 'lib/htm/mcp/cli.rb', line 503 def list_rake_tasks(pattern: nil) load_htm_rake_tasks # Collect tasks with descriptions, sorted by name tasks = Rake.application.tasks .select { |t| t.comment && t.name.start_with?('htm:') } .sort_by(&:name) # Filter by pattern if provided (matches task name) if pattern tasks = tasks.select { |t| t.name.include?(pattern) } end if tasks.empty? if pattern puts "No HTM rake tasks matching '#{pattern}'" else puts "No HTM rake tasks found" end return end if pattern puts "HTM rake tasks matching '#{pattern}':" else puts "Available HTM rake tasks:" end puts # Find max task name length for alignment max_len = tasks.map { |t| t.name.length }.max || 0 tasks.each do |task| printf " %-#{max_len}s # %s\n", task.name, task.comment end puts puts "Run with: htm_mcp rake <task_name>" puts "Example: htm_mcp rake htm:db:stats" end |
.load_config_file(path) ⇒ Object
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
# File 'lib/htm/mcp/cli.rb', line 269 def load_config_file(path) unless File.exist?(path) warn "Error: Config file not found: #{path}" exit 1 end begin require 'yaml' config_data = YAML.safe_load_file(path, permitted_classes: [Symbol], symbolize_names: true, aliases: true) || {} # Determine which section to use based on environment env = HTM::Config.env.to_sym base = config_data[:defaults] || {} env_overrides = config_data[env] || {} # Merge base with environment-specific overrides merged = deep_merge(base, env_overrides) apply_config(merged) warn "Loaded configuration from: #{path}" warn "Environment: #{env}" rescue => e warn "Error loading config file: #{e.}" warn e.backtrace.first(5).join("\n") if ENV['DEBUG'] exit 1 end end |
.load_htm_rake_tasks ⇒ Object
490 491 492 493 494 495 496 497 498 499 500 501 |
# File 'lib/htm/mcp/cli.rb', line 490 def load_htm_rake_tasks # Clear any existing tasks to avoid conflicts Rake::TaskManager. = true Rake.application = Rake::Application.new Rake.application.init('htm_mcp') # Load all HTM task files tasks_dir = File.('../../tasks', __dir__) Dir.glob(File.join(tasks_dir, '*.rake')).each do |rake_file| load rake_file end end |
.output_default_config ⇒ Object
259 260 261 262 263 264 265 266 267 |
# File 'lib/htm/mcp/cli.rb', line 259 def output_default_config defaults_path = File.('../config/defaults.yml', __dir__) if File.exist?(defaults_path) puts File.read(defaults_path) else warn "Error: defaults.yml not found at #{defaults_path}" exit 1 end end |
.print_error_suggestion(error_message) ⇒ Object
148 149 150 151 |
# File 'lib/htm/mcp/cli.rb', line 148 def print_error_suggestion() warn "" suggestion_lines_for(.to_s.downcase).each { |line| warn line } end |
.print_help ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/htm/mcp/cli.rb', line 9 def print_help puts <<~HELP HTM MCP Server v#{HTM::VERSION} - Memory management for AI assistants USAGE: htm_mcp [COMMAND] COMMANDS: server Start the MCP server (default if no command given) stdio Alias for server (for MCP client compatibility) setup Initialize the database schema init Alias for setup verify Verify database connection and extensions stats Show memory statistics config Output default configuration to STDOUT rake Run HTM rake tasks (use -T [pattern] to list) version Show HTM version help Show this help message ENVIRONMENT VARIABLES: Note: Nested config uses double underscores (e.g., HTM_EMBEDDING__PROVIDER) Environment: HTM_ENV Environment name: development, test, production (priority: HTM_ENV > RAILS_ENV > RACK_ENV > 'development') Database: HTM_DATABASE__URL PostgreSQL connection URL (preferred) Example: postgresql://user:pass@localhost:5432/htm_development HTM_DATABASE__HOST Database host (default: localhost) HTM_DATABASE__PORT Database port (default: 5432) HTM_DATABASE__NAME Database name HTM_DATABASE__USER Database username HTM_DATABASE__PASSWORD Database password HTM_DATABASE__SSLMODE SSL mode (default: prefer) HTM_DATABASE__POOL_SIZE Connection pool size (default: 10) Embedding: HTM_EMBEDDING__PROVIDER Provider (default: ollama) HTM_EMBEDDING__MODEL Model (default: nomic-embed-text:latest) HTM_EMBEDDING__DIMENSIONS Dimensions (default: 768) HTM_EMBEDDING__TIMEOUT Timeout seconds (default: 120) HTM_EMBEDDING__MAX_DIMENSION Max dimensions (default: 2000) Tag Extraction: HTM_TAG__PROVIDER Provider (default: ollama) HTM_TAG__MODEL Model (default: gemma3:latest) HTM_TAG__TIMEOUT Timeout seconds (default: 180) HTM_TAG__MAX_DEPTH Max hierarchy depth (default: 4) Proposition Extraction: HTM_PROPOSITION__PROVIDER Provider (default: ollama) HTM_PROPOSITION__MODEL Model (default: gemma3:latest) HTM_PROPOSITION__TIMEOUT Timeout seconds (default: 180) HTM_PROPOSITION__ENABLED Enable extraction (default: false) Chunking: HTM_CHUNKING__SIZE Max chars per chunk (default: 1024) HTM_CHUNKING__OVERLAP Chunk overlap chars (default: 64) Job Backend: HTM_JOB__BACKEND Backend: inline, thread, active_job, sidekiq Provider API Keys: HTM_PROVIDERS__OLLAMA__URL Ollama URL (default: http://localhost:11434) HTM_PROVIDERS__OPENAI__API_KEY OpenAI API key HTM_PROVIDERS__ANTHROPIC__API_KEY Anthropic API key HTM_PROVIDERS__GEMINI__API_KEY Google Gemini API key HTM_PROVIDERS__AZURE__API_KEY Azure OpenAI API key HTM_PROVIDERS__AZURE__ENDPOINT Azure OpenAI endpoint Other: HTM_LOG_LEVEL Log level (default: info) HTM_CONNECTION_TIMEOUT Connection timeout seconds (default: 30) HTM_TELEMETRY_ENABLED Enable OpenTelemetry (default: false) OPTIONS: -c, --config [PATH] Without PATH: output default config to STDOUT With PATH: load config from YAML file EXAMPLES: # Generate a config file template htm_mcp --config > my_config.yml # Start server with custom config htm_mcp --config my_config.yml # First-time setup export HTM_DATABASE__URL="postgresql://postgres@localhost:5432/htm" htm_mcp setup # Verify connection htm_mcp verify # Use test database HTM_ENV=test htm_mcp setup HTM_ENV=test htm_mcp stats # Start MCP server (for Claude Desktop) htm_mcp # List available rake tasks htm_mcp rake -T htm_mcp rake --tasks # List tasks matching a pattern htm_mcp rake -T htm:jobs htm_mcp rake -T db # Run rake tasks htm_mcp rake htm:db:stats htm_mcp rake htm:tags:tree htm_mcp rake 'htm:tags:tree[database]' CLAUDE DESKTOP CONFIGURATION: Add to ~/.config/claude/claude_desktop_config.json: { "mcpServers": { "htm-memory": { "command": "/path/to/htm_mcp", "env": { "HTM_DATABASE__URL": "postgresql://postgres@localhost:5432/htm_development" } } } } HELP end |
.run(args) ⇒ Object
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/htm/mcp/cli.rb', line 387 def run(args) args = args.dup # Handle -c / --config option first (can be combined with other commands) handle_config_option(args) # Process remaining command case args[0]&.downcase when 'help', '-h', '--help' print_help when 'version', '-v', '--version' puts "HTM #{HTM::VERSION}" when 'setup', 'init' run_setup when 'verify' run_verify when 'stats' run_stats when 'config' output_default_config when 'rake' run_rake(args[1..] || []) when 'server', 'stdio', nil # Return false to indicate server should start # 'stdio' is accepted for compatibility with MCP clients that pass it as an argument return false when /^-/ warn "Unknown option: #{args[0]}" warn "Run 'htm_mcp help' for usage." exit 1 else warn "Unknown command: #{args[0]}" warn "Run 'htm_mcp help' for usage." exit 1 end true end |
.run_rake(args) ⇒ Object
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'lib/htm/mcp/cli.rb', line 453 def run_rake(args) require 'rake' # Handle --tasks / -T to list available tasks (with optional pattern) if args.empty? || args.first == '--tasks' || args.first == '-T' # Check for optional pattern after -T/--tasks pattern = nil if ['--tasks', '-T'].include?(args.first) pattern = args[1] # May be nil if no pattern provided end list_rake_tasks(pattern: pattern) return end task_name = args.shift # Load HTM rake tasks load_htm_rake_tasks # Check if task exists unless Rake::Task.task_defined?(task_name) warn "Unknown rake task: #{task_name}" warn "Run 'htm_mcp rake --tasks' to see available tasks." exit 1 end # Set remaining args as task arguments if any # Rake tasks use ARGV for arguments in brackets like task[arg1,arg2] begin Rake::Task[task_name].invoke rescue => e warn "Rake task failed: #{e.}" warn e.backtrace.first(5).join("\n") if ENV['DEBUG'] exit 1 end end |
.run_setup ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/htm/mcp/cli.rb', line 164 def run_setup puts "HTM Database Setup" puts "==================" puts check_database_config! begin HTM::Database.setup puts puts "Database initialized successfully!" puts "You can now start the MCP server with: htm_mcp" rescue => e warn "Setup failed: #{e.}" print_error_suggestion(e.) warn e.backtrace.first(5).join("\n") if ENV['DEBUG'] exit 1 end end |
.run_stats ⇒ Object
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/htm/mcp/cli.rb', line 351 def run_stats puts "HTM Memory Statistics" puts "=====================" puts check_database_config! begin HTM::SequelConfig.establish_connection! total_nodes = HTM::Models::Node.count deleted_nodes = HTM::Models::Node.deleted.count = HTM::Models::Node..count = HTM::Models::Tag.count total_robots = HTM::Models::Robot.count total_files = HTM::Models::FileSource.count # Get database size db_size = HTM.db.fetch( "SELECT pg_size_pretty(pg_database_size(current_database())) AS size" ).first[:size] puts "Nodes: #{total_nodes} active, #{deleted_nodes} deleted, #{} with embeddings" puts "Tags: #{}" puts "Robots: #{total_robots}" puts "Files: #{total_files}" puts puts "Database size: #{db_size}" rescue => e warn "Stats failed: #{e.}" print_error_suggestion(e.) warn e.backtrace.first(5).join("\n") if ENV['DEBUG'] exit 1 end end |
.run_verify ⇒ Object
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/htm/mcp/cli.rb', line 184 def run_verify puts "HTM Database Verification" puts "=========================" puts check_database_config! begin HTM::Database.info puts # Check migration status pending = check_migration_status puts if pending.positive? warn "Warning: #{pending} pending migration(s) detected." warn " Run 'htm_mcp setup' to apply pending migrations." puts end puts "Database connection verified!" rescue => e warn "Verification failed: #{e.}" print_error_suggestion(e.) warn e.backtrace.first(5).join("\n") if ENV['DEBUG'] exit 1 end end |
.suggestion_lines_for(msg) ⇒ Object
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 |
# File 'lib/htm/mcp/cli.rb', line 546 def suggestion_lines_for(msg) if msg.include?("does not exist") && !msg.include?("role") dbname = extract_dbname(ENV['HTM_DATABASE__URL'] || ENV.fetch('HTM_DATABASE__NAME', nil)) ["Suggestion: The database does not exist. Create it with:", " createdb #{dbname}", "Then initialize the schema with:", " htm_mcp setup"] elsif msg.include?("password authentication failed") || msg.include?("no password supplied") ["Suggestion: Check your database credentials.", "Verify HTM_DATABASE__URL has correct username and password:", " postgresql://USER:PASSWORD@localhost:5432/DATABASE"] elsif msg.include?("connection refused") || msg.include?("could not connect") ["Suggestion: PostgreSQL server is not running or not accepting connections.", "Start PostgreSQL with:", " brew services start postgresql@17 # macOS with Homebrew", " sudo systemctl start postgresql # Linux"] elsif msg.include?("role") && msg.include?("does not exist") ["Suggestion: The database user does not exist. Create it with:", " createuser -s YOUR_USERNAME"] elsif msg.include?("permission denied") ["Suggestion: The user lacks permission to access this database.", "Grant access or use a different user with appropriate privileges."] elsif msg.include?("timeout") || msg.include?("timed out") ["Suggestion: Connection timed out. Check:", " - PostgreSQL is running", " - Firewall allows connections on port 5432", " - Host address is correct"] elsif msg.include?("extension") && msg.include?("vector") ["Suggestion: pgvector extension is not installed. Install it with:", " brew install pgvector # macOS", "Then enable it in your database:", " psql -d DATABASE -c 'CREATE EXTENSION vector;'"] else ["Suggestion: Run 'htm_mcp help' for configuration details."] end end |