Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
6 changes: 5 additions & 1 deletion spec/lib/msf/core/mcp/application_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@
describe '#start_mcp_server' do
let(:mock_mcp_server) { instance_double(Msf::MCP::Server) }

before do
allow(mock_mcp_server).to receive(:class).and_return(Msf::MCP::Server)
end

it 'starts server with stdio transport by default' do
app = described_class.new([], output: output)
app.instance_variable_set(:@config, valid_config)
Expand Down Expand Up @@ -413,7 +417,7 @@
app.send(:start_mcp_server)

expect(output.string).to include('Starting MCP server on HTTP transport...')
expect(output.string).to include('Server listening on http://0.0.0.0:3000')
expect(output.string).to include('MCP server listening on http://0.0.0.0:3000')
end

it 'uses default host and port for HTTP transport' do
Expand Down
6 changes: 5 additions & 1 deletion spec/plugins/mcp/console_dispatcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def initialize(**_args); end

let(:mock_server_class) do
Class.new do
def self.generate_auth_token
'a' * 64
end

def initialize(**_args); end

def start(**_args); end
Expand Down Expand Up @@ -249,7 +253,7 @@ def shutdown; end
context 'when server is stopped' do
it 'starts the server successfully' do
mcp_plugin.start_server({})
expect(@output.join("\n")).to include('MCP server started')
expect(@output.join("\n")).to include('MCP server listening')
end

it 'sets the server instance' do
Expand Down
10 changes: 9 additions & 1 deletion spec/plugins/mcp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def initialize(**_args); end

let(:mock_server_class) do
Class.new do
def self.generate_auth_token
'a' * 64
end

def initialize(**_args); end
def start(**_args); end
def shutdown; end
Expand Down Expand Up @@ -123,7 +127,7 @@ def shutdown; end
end

it 'prints a status message with the listening address' do
expect(@output.join("\n")).to include('MCP server started on localhost:3000 (transport: http)')
expect(@output.join("\n")).to include('MCP server listening on http://localhost:3000/')
end

it 'stores the server configuration' do
Expand Down Expand Up @@ -166,6 +170,10 @@ def shutdown; end
context 'when port is already in use' do
let(:failing_server_class) do
Class.new do
def self.generate_auth_token
'a' * 64
end

def initialize(**_args); end

def start(**_args)
Expand Down
Loading