# frozen_string_literal: true # Starts the conformance server and runs `npx @modelcontextprotocol/conformance@alpha` against it. require "English" require "net/http" require_relative "server" module Conformance class ServerRunner # Timeout for waiting for the Puma server to start. SERVER_START_TIMEOUT = 20 SERVER_POLL_INTERVAL = 0.5 SERVER_HEALTH_CHECK_RETRIES = (SERVER_START_TIMEOUT / SERVER_POLL_INTERVAL).to_i def initialize(port: Server::DEFAULT_PORT, scenario: nil, spec_version: nil, verbose: false) @port = port @scenario = scenario @spec_version = spec_version @verbose = verbose end def run command = build_command server_pid = start_server run_conformance(command, server_pid: server_pid) end private def build_command expected_failures_yml = File.expand_path("expected_failures.yml", __dir__) npx_command = ["npx", "--yes", "@modelcontextprotocol/conformance@alpha", "server", "--url", "http://localhost:#{@port}/mcp"] npx_command += ["--scenario", @scenario] if @scenario npx_command += ["--spec-version", @spec_version] if @spec_version npx_command += ["--verbose"] if @verbose npx_command += ["--expected-failures", expected_failures_yml] npx_command end def start_server puts "Starting conformance server on port #{@port}..." server_pid = fork do Conformance::Server.new(port: @port).start end health_url = URI("http://localhost:#{@port}/health") ready = false SERVER_HEALTH_CHECK_RETRIES.times do begin response = Net::HTTP.get_response(health_url) if response.code == "200" ready = true break end rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout # not ready yet end sleep(SERVER_POLL_INTERVAL) end unless ready warn("ERROR: Conformance server did not start within #{SERVER_START_TIMEOUT} seconds") terminate_server(server_pid) exit(1) end puts "Server ready. Running conformance tests..." server_pid end def run_conformance(command, server_pid:) puts "Command: #{command.join(" ")}\n\n" conformance_exit_code = nil begin system(*command) conformance_exit_code = $CHILD_STATUS.exitstatus ensure terminate_server(server_pid) end exit(conformance_exit_code || 1) unless conformance_exit_code == 0 end def terminate_server(pid) Process.kill("TERM", pid) rescue Errno::ESRCH # process already exited ensure begin Process.wait(pid) rescue Errno::ECHILD # already reaped end end end end