Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions lib/msf/core/mcp/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def start_mcp_server

if transport == :http
@output.puts "Starting MCP server on HTTP transport..."
@output.puts "Server listening on http://#{Rex::Socket.to_authority(host, port)}/"
@output.puts "MCP server listening on http://#{Rex::Socket.to_authority(host, port)}/"
auth_token = resolve_auth_token
case auth_token
when :disabled
Expand All @@ -292,7 +292,7 @@ def start_mcp_server
@output.puts "Authentication: enabled"
auth_token = @config.dig(:mcp, :auth_token)
when :generated
auth_token = SecureRandom.hex(32)
auth_token = @mcp_server.class.generate_auth_token
@output.puts "Authentication: Bearer token (auto-generated)"
@output.puts " Configure your MCP client with: Authorization: Bearer #{auth_token}"
else
Expand All @@ -304,15 +304,19 @@ def start_mcp_server
max_threads = @config.dig(:mcp, :max_threads) || Msf::MCP::Server::PUMA_MAX_THREADS
workers = @config.dig(:mcp, :workers) || Msf::MCP::Server::PUMA_WORKERS

@mcp_server.start(
transport: :http,
host: host,
port: port,
auth_token: auth_token,
min_threads: min_threads,
max_threads: max_threads,
workers: workers
)
begin
@mcp_server.start(
transport: :http,
host: host,
port: port,
auth_token: auth_token,
min_threads: min_threads,
max_threads: max_threads,
workers: workers
)
rescue Errno::EADDRINUSE => e
@output.puts "#{e.class}: #{e.message}"
Comment thread
zeroSteiner marked this conversation as resolved.
end
else
@output.puts "Starting MCP server on stdio transport..."
@output.puts "Server ready - waiting for MCP requests"
Expand Down
6 changes: 6 additions & 0 deletions lib/msf/core/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ def shutdown
@mcp_server = nil
end

# Generate a random authentication token
# @return [String]
def self.generate_auth_token
SecureRandom.hex(32)
end

private

##
Expand Down
31 changes: 24 additions & 7 deletions plugins/mcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class McpCommandDispatcher
# Valid option keys accepted by `mcp start` and `mcp restart`
unless defined?(VALID_OPTIONS)
VALID_OPTIONS = %w[
AuthToken
ServerHost ServerPort
RpcHost RpcPort RpcUser RpcPass RpcSSL
RateLimit
Expand Down Expand Up @@ -67,6 +68,7 @@ def cmd_mcp_help
print_line(' help - Show this help message')
print_line
print_line('Options (for start/restart):')
print_line(' AuthToken=<token> MCP server authentication token (default: random, blank: disabled)')
print_line(' ServerHost=<host> MCP server bind address (default: localhost)')
print_line(' ServerPort=<port> MCP server port (default: 3000)')
print_line(' RpcHost=<host> RPC server host (default: 127.0.0.1)')
Expand Down Expand Up @@ -130,7 +132,8 @@ def parse_options(args)
opts = {}
args.each do |arg|
key, value = arg.split('=', 2)
unless key && value && !value.empty?
# AuthToken can be blank while the others have to be set (blank is explicitly disabled)
unless key && value && (key == 'AuthToken' || !value.empty?)
Comment thread
zeroSteiner marked this conversation as resolved.
Outdated
print_error("Invalid option format: #{arg} (expected Key=Value)")
return nil
end
Expand Down Expand Up @@ -286,13 +289,27 @@ def start_mcp_server(rpc, config)

host = mcp_config[:host]
port = mcp_config[:port]
if mcp_config.key?(:auth_token)
auth_token = mcp_config[:auth_token]
Comment thread
zeroSteiner marked this conversation as resolved.
auth_token_generated = false
else
auth_token = @mcp_server.class.generate_auth_token
auth_token_generated = true
end
Comment thread
zeroSteiner marked this conversation as resolved.

@server_thread = framework.threads.spawn('MCPServer', false) do
@mcp_server.start(transport: :http, host: host, port: port)
print_status("Starting MCP server on HTTP transport...")
Comment thread
zeroSteiner marked this conversation as resolved.
Outdated
@mcp_server.start(transport: :http, host: host, port: port, auth_token: auth_token)
Comment thread
zeroSteiner marked this conversation as resolved.
end

@started_at = Time.now
print_server_status(mcp_config)
print_status("MCP server listening on http://#{Rex::Socket.to_authority(mcp_config[:host], mcp_config[:port])}/")
if auth_token_generated
print_status("Authentication: Bearer token (auto-generated)")
print_status(" Configure your MCP client with: Authorization: Bearer #{auth_token}")
else
print_status("Authentication: #{auth_token ? 'enabled' : 'disabled'}")
end
Comment on lines 301 to +312

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this code is duplicated, I took care so that what is printed is consistent between the plugin and the msfmcpd stand-alone utility, so it's not visually obvious that they're different.

rescue Msf::MCP::Metasploit::AuthenticationError => e
raise Msf::MCP::Metasploit::AuthenticationError, "RPC authentication failed: #{e.message}"
rescue Msf::MCP::Metasploit::ConnectionError => e
Expand All @@ -301,10 +318,6 @@ def start_mcp_server(rpc, config)
raise Msf::MCP::Error, "Address already in use: #{Rex::Socket.to_authority(mcp_config[:host], mcp_config[:port])}"
end

def print_server_status(mcp_config)
print_status("MCP server started on #{Rex::Socket.to_authority(mcp_config[:host], mcp_config[:port])} (transport: http)")
end

def format_uptime
return 'N/A' unless @started_at

Expand Down Expand Up @@ -442,6 +455,10 @@ def resolve_config(opts)
port: Integer(opts['ServerPort'] || Msf::MCP::Config::Defaults::MCP_PORT)
}

if opts.key?('AuthToken')
mcp_config[:auth_token] = opts['AuthToken'].blank? ? nil : opts['AuthToken']
end

rate_limit_value = Integer(opts['RateLimit'] || Msf::MCP::Config::Defaults::RATE_LIMIT_REQUESTS_PER_MINUTE)

{
Expand Down
Loading