Class: Msf::MCP::Config::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/msf/core/mcp/config/loader.rb

Class Method Summary collapse

Class Method Details

.apply_defaults(config) ⇒ Object

Apply default values to configuration

Parameters:

  • config (Hash) —

    Configuration hash to modify in place



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
# File 'lib/msf/core/mcp/config/loader.rb', line 50

def self.apply_defaults(config)
  config[:msf_api] ||= {}
  config[:mcp] ||= {}
  config[:rate_limit] ||= {}
  config[:logging] ||= {}

  config[:msf_api][:type] ||= 'messagepack'
  config[:msf_api][:host] ||= Defaults::RPC_HOST
  config[:msf_api][:port] ||= (config[:msf_api][:type] == 'json-rpc') ? 8081 : Defaults::RPC_PORT

  config[:msf_api][:ssl] = config[:msf_api].fetch(:ssl, Defaults::RPC_SSL)
  config[:msf_api][:auto_start_rpc] = config[:msf_api].fetch(:auto_start_rpc, true)

  config[:msf_api][:endpoint] ||= case config[:msf_api][:type]
                                  when 'json-rpc'
                                    Msf::MCP::Metasploit::JsonRpcClient::DEFAULT_ENDPOINT
                                  else
                                    Msf::MCP::Metasploit::MessagePackClient::DEFAULT_ENDPOINT
                                  end

  config[:mcp][:transport] ||= 'stdio'
  config[:mcp][:dangerous_actions] = config[:mcp].fetch(:dangerous_actions, false)

  if config[:mcp][:transport] == 'http'
    config[:mcp][:host] ||= Defaults::MCP_HOST
    config[:mcp][:port] ||= Defaults::MCP_PORT
    config[:mcp][:min_threads] ||= Msf::MCP::Server::PUMA_MIN_THREADS
    config[:mcp][:max_threads] ||= Msf::MCP::Server::PUMA_MAX_THREADS
    config[:mcp][:workers] ||= Msf::MCP::Server::PUMA_WORKERS
  end

  # auth_token: only normalize if the key was explicitly provided.
  # Absent key means "not configured" -- the application layer generates
  # a token at startup so it can decide whether to print it.
  # nil or "" becomes nil (authentication disabled); non-empty string is used as-is.
  if config[:mcp].key?(:auth_token)
    val = config[:mcp][:auth_token]
    config[:mcp][:auth_token] = nil if val.nil? || (val.is_a?(String) && val.empty?)
  end

  config[:rate_limit][:enabled] = config[:rate_limit].fetch(:enabled, true)
  config[:rate_limit][:requests_per_minute] ||= Defaults::RATE_LIMIT_REQUESTS_PER_MINUTE
  config[:rate_limit][:burst_size] ||= 10

  config[:logging][:enabled] = config[:logging].fetch(:enabled, false)
  config[:logging][:level] ||= 'INFO'
  config[:logging][:log_file] ||= File.join(Msf::Config.log_directory, 'msfmcp.log')
  config[:logging][:sanitize] = config[:logging].fetch(:sanitize, true)
end

.apply_env_overrides(config) ⇒ Object

Apply environment variable overrides

Parameters:

  • config (Hash) —

    Configuration hash to modify in place



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
139
140
141
142
143
144
# File 'lib/msf/core/mcp/config/loader.rb', line 103

def self.apply_env_overrides(config)
  # Ensure nested hashes exist
  config[:msf_api] ||= {}
  config[:mcp] ||= {}

  # MSF API overrides
  config[:msf_api][:type] = ENV['MSF_API_TYPE'] if ENV['MSF_API_TYPE']
  config[:msf_api][:host] = ENV['MSF_API_HOST'] if ENV['MSF_API_HOST']
  config[:msf_api][:port] = ENV['MSF_API_PORT'].to_i if ENV['MSF_API_PORT']
  config[:msf_api][:ssl] = parse_boolean(ENV['MSF_API_SSL']) if ENV['MSF_API_SSL'] && !ENV['MSF_API_SSL'].empty?
  config[:msf_api][:endpoint] = ENV['MSF_API_ENDPOINT'] if ENV['MSF_API_ENDPOINT']
  config[:msf_api][:user] = ENV['MSF_API_USER'] if ENV['MSF_API_USER']
  config[:msf_api][:password] = ENV['MSF_API_PASSWORD'] if ENV['MSF_API_PASSWORD']
  config[:msf_api][:token] = ENV['MSF_API_TOKEN'] if ENV['MSF_API_TOKEN']
  config[:msf_api][:auto_start_rpc] = parse_boolean(ENV['MSF_AUTO_START_RPC']) if ENV['MSF_AUTO_START_RPC']

  # MCP transport override
  config[:mcp][:transport] = ENV['MSF_MCP_TRANSPORT'] if ENV['MSF_MCP_TRANSPORT']

  # MCP server network overrides
  config[:mcp][:host] = ENV['MSF_MCP_HOST'] if ENV['MSF_MCP_HOST']
  config[:mcp][:port] = ENV['MSF_MCP_PORT'].to_i if ENV['MSF_MCP_PORT']

  # MCP authentication -- env var overrides config/default
  #   unset            -- leave whatever apply_defaults established
  #   set to ""        -- nil (disable authentication)
  #   set to non-empty -- use as the bearer token
  if ENV.key?('MSF_MCP_AUTH_TOKEN')
    mcp_token = ENV['MSF_MCP_AUTH_TOKEN']
    config[:mcp][:auth_token] = mcp_token.empty? ? nil : mcp_token
  end

  # MCP Puma server tuning overrides
  config[:mcp][:min_threads] = ENV['MSF_MCP_MIN_THREADS'].to_i if ENV['MSF_MCP_MIN_THREADS']
  config[:mcp][:max_threads] = ENV['MSF_MCP_MAX_THREADS'].to_i if ENV['MSF_MCP_MAX_THREADS']
  config[:mcp][:workers] = ENV['MSF_MCP_WORKERS'].to_i if ENV['MSF_MCP_WORKERS']

  # Dangerous actions gate override
  if ENV['MSF_MCP_DANGEROUS_ACTIONS'] && !ENV['MSF_MCP_DANGEROUS_ACTIONS'].empty?
    config[:mcp][:dangerous_actions] = parse_boolean(ENV['MSF_MCP_DANGEROUS_ACTIONS'])
  end
end

.load(file_path) ⇒ Hash

Load configuration from YAML file with environment variable overrides

Parameters:

  • file_path (String) —

    Path to YAML configuration file

Returns:

  • (Hash) —

    Configuration hash with symbolized keys

Raises:



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/msf/core/mcp/config/loader.rb', line 13

def self.load(file_path)
  unless File.exist?(file_path)
    raise ConfigurationError, "Configuration file not found: #{file_path}"
  end

  begin
    config = YAML.safe_load_file(file_path, symbolize_names: true)
  rescue Psych::SyntaxError => e
    raise ConfigurationError, "Invalid YAML syntax in #{file_path}: #{e.message}"
  end

  unless config.is_a?(Hash)
    raise ConfigurationError, "Configuration file must contain a YAML hash/dictionary"
  end

  apply_defaults(config)
  apply_env_overrides(config)
  config
end

.load_from_hash(config_hash) ⇒ Hash

Load configuration from hash (for testing)

Parameters:

  • config_hash (Hash) —

    Configuration hash

Returns:

  • (Hash) —

    Configuration hash with defaults and env overrides



37
38
39
40
41
42
# File 'lib/msf/core/mcp/config/loader.rb', line 37

def self.load_from_hash(config_hash)
  config = config_hash.dup
  apply_defaults(config)
  apply_env_overrides(config)
  config
end

.parse_boolean(value) ⇒ Boolean

Parse a string value into a boolean

Parameters:

  • value (String) —

    String to parse ('true', '1', 'yes' -> true; anything else -> false)

Returns:

  • (Boolean)


150
151
152
# File 'lib/msf/core/mcp/config/loader.rb', line 150

def self.parse_boolean(value)
  %w[true 1 yes].include?(value.to_s.downcase)
end