Path: blob/master/lib/msf/core/mcp/server.rb
70339 views
# frozen_string_literal: true12module Msf::MCP3##4# MCP Server Wrapper for Metasploit Framework5#6# This class initializes and manages the MCP server with all registered tools.7# It provides a clean interface for starting/stopping the server and integrates8# with the Metasploit client and security layers.9#10# The Server expects fully configured and authenticated dependencies to be11# provided during initialization. It does not handle configuration loading12# or client authentication - those are responsibilities of the calling code.13#14class Server1516##17# Initialize the MCP server with required dependencies18#19# @param msf_client [Metasploit::Client] Configured and authenticated Metasploit client20# @param rate_limiter [Security::RateLimiter] Configured rate limiter21#22def initialize(msf_client:, rate_limiter:)23@msf_client = msf_client2425# Create server context (passed to all tool calls)26# Tools only need msf_client and rate_limiter27@server_context = {28msf_client: @msf_client,29rate_limiter: rate_limiter30}3132# Create MCP configuration with request lifecycle callbacks33mcp_config = ::MCP::Configuration.new34mcp_config.around_request = create_around_request35mcp_config.exception_reporter = create_exception_reporter3637# Initialize MCP server with all tools38@mcp_server = ::MCP::Server.new(39name: 'msfmcp',40version: Msf::MCP::Application::VERSION,41tools: [42Tools::SearchModules,43Tools::ModuleInfo,44Tools::HostInfo,45Tools::ServiceInfo,46Tools::VulnerabilityInfo,47Tools::NoteInfo,48Tools::CredentialInfo,49Tools::LootInfo50],51server_context: @server_context,52configuration: mcp_config53)54end5556##57# Start the MCP server with specified transport58#59# @param transport [Symbol] Transport type (:stdio or :http)60# @param host [String] Host address for HTTP transport (default: 'localhost')61# @param port [Integer] Port number for HTTP transport (default: 3000)62#63# @return [MCP::Server] The MCP server instance (for testing purposes)64# @raise [ArgumentError] If an unknown transport is specified65#66def start(transport: :stdio, host: 'localhost', port: 3000)67case transport68when :stdio69start_stdio70when :http71start_http(host, port)72else73raise ArgumentError, "Unknown transport: #{transport}. Use :stdio or :http"74end75end7677##78# Shutdown the MCP server and cleanup resources79#80def shutdown81@msf_client&.shutdown82@mcp_server = nil83end8485private8687##88# Start stdio transport (for CLI usage)89#90# @return [MCP::Server] The MCP server instance (for testing purposes)91#92def start_stdio93transport = ::MCP::Server::Transports::StdioTransport.new(@mcp_server)94transport.open95@mcp_server96end9798##99# Start HTTP transport (for web/network usage)100#101# The transport implements the Rack app interface (#call), so it is mounted102# directly. MCP-aware request/response logging is handled by the103# Middleware::RequestLogger middleware.104#105# @param host [String] Host address to bind to106# @param port [Integer] Port to listen on107#108# @return [MCP::Server] The MCP server instance (for testing purposes)109#110def start_http(host, port)111require 'rackup'112require 'rack/handler/puma'113114transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(@mcp_server)115116# Build the Rack application with logging middleware.117# The transport itself is a Rack app (implements #call).118rack_app = Rack::Builder.new do119use Msf::MCP::Middleware::RequestLogger120run transport121end122123Rackup::Handler::Puma.run(124rack_app,125Port: port,126Host: host,127Silent: true128)129130@mcp_server131end132133##134# Create around_request callback for MCP SDK135#136# This callback wraps every JSON-RPC request handler, providing access to137# both the instrumentation data and the response result. It replaces the138# deprecated +instrumentation_callback+ which only fires after completion139# and does not expose the result.140#141# The +data+ hash is populated by the SDK with:142# - :method — the JSON-RPC method name (e.g. "tools/call", "tools/list")143# - :tool_name, :prompt_name, :resource_uri — specific handler identifiers144# - :tool_arguments — arguments passed to a tool call145# - :client — client info hash (name, version)146# - :error — error type symbol (e.g. :tool_not_found, :internal_error)147# - :duration — added in the ensure block after this callback returns148#149# @return [Proc] Callback that wraps request execution and logs via Rex150#151def create_around_request152->(data, &request_handler) do153result = request_handler.call154155# Build message based on the type of request156message = if data[:error]157"MCP Error: #{data[:error]}"158elsif data[:tool_name]159"Tool call: #{data[:tool_name]}"160elsif data[:prompt_name]161"Prompt call: #{data[:prompt_name]}"162elsif data[:resource_uri]163"Resource call: #{data[:resource_uri]}"164elsif data[:method]165"Method call: #{data[:method]}"166else167"MCP request"168end169170context = data.dup171if result172message = "#{message} (ERROR)" if result[:isError]173context[:result] = result174end175176if data[:error] || result&.fetch(:isError, nil)177elog({ message: message, context: context }, LOG_SOURCE, LOG_ERROR)178else179ilog({ message: message, context: context }, LOG_SOURCE, LOG_INFO)180end181182result183end184end185186##187# Create exception reporter callback for MCP SDK188#189# This callback is invoked for any server exception during request processing,190# which are not tool execution errors.191# It receives:192# - exception: The Ruby exception object193# - context: Hash with :request (JSON string) or :notification (method name string)194#195# @return [Proc] Callback that logs exceptions via Rex196#197def create_exception_reporter198->(exception, context) do199return unless exception || context200201# Determine the context type and parse data202error_context = {}203204if context&.fetch(:request, nil)205error_context[:type] = 'request'206request = nil207begin208request = JSON.parse(context[:request])209rescue JSON::ParserError210# Not valid JSON, log raw data211error_context[:raw_data] = context[:request].inspect212else213error_context[:method] = request['method'] if request['method']214error_context[:params] = request['params'] if request['params']215end216elsif context&.fetch(:notification, nil)217error_context[:type] = 'notification'218# context[:notification] is the notification method name (string)219error_context[:method] = context[:notification]220else221error_context[:type] = 'unknown'222error_context[:raw_data] = context.inspect223end224225elog({226message: "Error during #{error_context[:type]} processing#{error_context[:method] ? " (#{error_context[:method]})" : ''}",227exception: exception,228context: error_context229}, LOG_SOURCE, LOG_ERROR)230end231end232end233end234235236