## Motivation and Context The npm `latest` dist-tag of `@modelcontextprotocol/conformance` points at 0.1.16, the old 0.1 line, while the 2026-07-28 requirement revisions and the frozen-requirements framework live in the 0.2.0 alpha line (`alpha` currently resolves 0.2.0-alpha.11). The runners invoked the bare package name, so `rake conformance` and the conformance CI job resolve whatever `latest` points at: a fresh machine silently downgrades to the old suite while a warm npx cache may keep serving a newer one, and the suite actually in use depends on the environment. Pinning the `alpha` dist-tag puts every run on the line the frozen conformance requirements are anchored to (0.2.0-alpha.10, published 2026-07-27). Running the real alpha suite surfaced the actual client-leg state, so the expected-failures baseline is updated to match: `initialize` now passes (the harness accepts 2026-07-28, fulfilling the TODO on the old entry), and the scenarios pending client-side work are recorded with their reasons: the 2026-07-28 stateless lifecycle and the SEP-2322 MRTR driver, SEP-2243 custom-header mirroring, client-side JSON Schema preservation, the DPoP and WIF authorization extensions, and one `auth/authorization-server-migration` check added to the suite after the alpha.10 anchor. The manual `npx` examples in `conformance/README.md` carry the same pin so local reproduction runs the same suite as the rake task. ## How Has This Been Tested? `npx --yes @modelcontextprotocol/conformance@alpha --version` resolves 0.2.0-alpha.11, and `bundle exec rake conformance` passes the baseline check on both legs: the server leg reports 73 passed with 0 failed and no expected failures, and the client leg fails exactly the scenarios the baseline records. ## Breaking Changes None. The change is confined to the conformance tooling; the shipped gem is untouched.
101 lines
2.7 KiB
Ruby
101 lines
2.7 KiB
Ruby
# 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
|