#!/bin/bash set -e # Conformance test script for comparing MCP server behavior between branches # Builds both main and current branch, runs various flag combinations, # and produces a conformance report with timing and diffs. # # Output: # - Progress/status messages go to stderr (for visibility in CI) # - Final report summary goes to stdout (for piping/capture) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" REPORT_DIR="$PROJECT_DIR/conformance-report" CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) # Colors for output (only used on stderr) RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper to print to stderr log() { echo -e "$@" >&2 } log "${BLUE}=== MCP Server Conformance Test ===${NC}" log "Current branch: $CURRENT_BRANCH" log "Report directory: $REPORT_DIR" # Find the common ancestor MERGE_BASE=$(git merge-base HEAD origin/main) log "Comparing against merge-base: $MERGE_BASE" log "" # Create report directory rm -rf "$REPORT_DIR" mkdir -p "$REPORT_DIR"/{main,branch,diffs} # Build binaries log "${YELLOW}Building binaries...${NC}" log "Building current branch ($CURRENT_BRANCH)..." go build -o "$REPORT_DIR/branch/github-mcp-server" ./cmd/github-mcp-server BRANCH_BUILD_OK=$? log "Building main branch (using temp worktree at merge-base)..." TEMP_WORKTREE=$(mktemp -d) git worktree add --quiet "$TEMP_WORKTREE" "$MERGE_BASE" (cd "$TEMP_WORKTREE" && go build -o "$REPORT_DIR/main/github-mcp-server" ./cmd/github-mcp-server) MAIN_BUILD_OK=$? git worktree remove --force "$TEMP_WORKTREE" if [ $BRANCH_BUILD_OK -ne 0 ] || [ $MAIN_BUILD_OK -ne 0 ]; then log "${RED}Build failed!${NC}" exit 1 fi log "${GREEN}Both binaries built successfully${NC}" log "" # MCP JSON-RPC messages INIT_MSG='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"conformance-test","version":"1.0.0"}}}' INITIALIZED_MSG='{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' LIST_TOOLS_MSG='{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' LIST_RESOURCES_MSG='{"jsonrpc":"2.0","id":3,"method":"resources/listTemplates","params":{}}' LIST_PROMPTS_MSG='{"jsonrpc":"2.0","id":4,"method":"prompts/list","params":{}}' # Function to normalize JSON for comparison # Sorts all arrays (including nested ones) and formats consistently # Also handles embedded JSON strings in "text" fields (from tool call responses) normalize_json() { local file="$1" if [ -s "$file" ]; then # First, try to parse and re-serialize any JSON embedded in text fields # This handles tool call responses where the result is JSON-in-a-string jq -S ' # Function to sort arrays recursively def deep_sort: if type == "array" then [.[] | deep_sort] | sort_by(tostring) elif type == "object" then to_entries | map(.value |= deep_sort) | from_entries else . end; # Walk the structure, and for any "text" field that looks like JSON array/object, parse and sort it walk( if type == "object" and .text and (.text | type == "string") and ((.text | startswith("[")) or (.text | startswith("{"))) then .text = ((.text | fromjson | deep_sort) | tojson) else . end ) | deep_sort ' "$file" 2>/dev/null > "${file}.tmp" && mv "${file}.tmp" "$file" fi } # Function to run MCP server and capture output with timing run_mcp_test() { local binary="$1" local name="$2" local flags="$3" local output_prefix="$4" local start_time end_time duration start_time=$(date +%s.%N) # Run the server with all list commands - each response is on its own line output=$( ( echo "$INIT_MSG" echo "$INITIALIZED_MSG" echo "$LIST_TOOLS_MSG" echo "$LIST_RESOURCES_MSG" echo "$LIST_PROMPTS_MSG" sleep 0.5 ) | GITHUB_PERSONAL_ACCESS_TOKEN=1 $binary stdio $flags 2>/dev/null ) end_time=$(date +%s.%N) duration=$(echo "$end_time - $start_time" | bc) # Parse and save each response by matching JSON-RPC id # Each line is a separate JSON response echo "$output" | while IFS= read -r line; do id=$(echo "$line" | jq -r '.id // empty' 2>/dev/null) case "$id" in 1) echo "$line" | jq -S '.' > "${output_prefix}_initialize.json" 2>/dev/null ;; 2) echo "$line" | jq -S '.' > "${output_prefix}_tools.json" 2>/dev/null ;; 3) echo "$line" | jq -S '.' > "${output_prefix}_resources.json" 2>/dev/null ;; 4) echo "$line" | jq -S '.' > "${output_prefix}_prompts.json" 2>/dev/null ;; esac done # Create empty files if not created (in case of errors or missing responses) touch "${output_prefix}_initialize.json" "${output_prefix}_tools.json" \ "${output_prefix}_resources.json" "${output_prefix}_prompts.json" # Normalize all JSON files for consistent comparison (sorts arrays, keys) for endpoint in initialize tools resources prompts; do normalize_json "${output_prefix}_${endpoint}.json" done echo "$duration" } # Test configurations - array of "name|flags" declare -a TEST_CONFIGS=( "default|" "read-only|--read-only" "toolsets-repos|--toolsets=repos" "toolsets-issues|--toolsets=issues" "toolsets-pull_requests|--toolsets=pull_requests" "toolsets-repos,issues|--toolsets=repos,issues" "toolsets-all|--toolsets=all" "tools-get_me|--tools=get_me" "tools-get_me,list_issues|--tools=get_me,list_issues" "toolsets-repos+read-only|--toolsets=repos --read-only" ) # Summary arrays declare -a TEST_NAMES declare -a MAIN_TIMES declare -a BRANCH_TIMES declare -a DIFF_STATUS log "${YELLOW}Running conformance tests...${NC}" log "" for config in "${TEST_CONFIGS[@]}"; do IFS='|' read -r test_name flags <<< "$config" log "${BLUE}Test: ${test_name}${NC}" log " Flags: ${flags:-}" # Create output directories mkdir -p "$REPORT_DIR/main/$test_name" mkdir -p "$REPORT_DIR/branch/$test_name" mkdir -p "$REPORT_DIR/diffs/$test_name" # Run standard test main_time=$(run_mcp_test "$REPORT_DIR/main/github-mcp-server" "main" "$flags" "$REPORT_DIR/main/$test_name/output") log " Main: ${main_time}s" branch_time=$(run_mcp_test "$REPORT_DIR/branch/github-mcp-server" "branch" "$flags" "$REPORT_DIR/branch/$test_name/output") log " Branch: ${branch_time}s" endpoints="initialize tools resources prompts" # Calculate time difference time_diff=$(echo "$branch_time - $main_time" | bc) if (( $(echo "$time_diff > 0" | bc -l) )); then log " Δ Time: ${RED}+${time_diff}s (slower)${NC}" else log " Δ Time: ${GREEN}${time_diff}s (faster)${NC}" fi # Generate diffs for each endpoint has_diff=false for endpoint in $endpoints; do main_file="$REPORT_DIR/main/$test_name/output_${endpoint}.json" branch_file="$REPORT_DIR/branch/$test_name/output_${endpoint}.json" diff_file="$REPORT_DIR/diffs/$test_name/${endpoint}.diff" if ! diff -u "$main_file" "$branch_file" > "$diff_file" 2>/dev/null; then has_diff=true lines=$(wc -l < "$diff_file" | tr -d ' ') log " ${YELLOW}${endpoint}: DIFF (${lines} lines)${NC}" else rm -f "$diff_file" # No diff, remove empty file log " ${GREEN}${endpoint}: OK${NC}" fi done # Store results TEST_NAMES+=("$test_name") MAIN_TIMES+=("$main_time") BRANCH_TIMES+=("$branch_time") if [ "$has_diff" = true ]; then DIFF_STATUS+=("DIFF") else DIFF_STATUS+=("OK") fi log "" done # Generate summary report REPORT_FILE="$REPORT_DIR/CONFORMANCE_REPORT.md" cat > "$REPORT_FILE" << EOF # MCP Server Conformance Report Generated: $(date) Current Branch: $CURRENT_BRANCH Compared Against: merge-base ($MERGE_BASE) ## Summary | Test | Main Time | Branch Time | Δ Time | Status | |------|-----------|-------------|--------|--------| EOF total_main=0 total_branch=0 diff_count=0 ok_count=0 for i in "${!TEST_NAMES[@]}"; do name="${TEST_NAMES[$i]}" main_t="${MAIN_TIMES[$i]}" branch_t="${BRANCH_TIMES[$i]}" status="${DIFF_STATUS[$i]}" delta=$(echo "$branch_t - $main_t" | bc) if (( $(echo "$delta > 0" | bc -l) )); then delta_str="+${delta}s" else delta_str="${delta}s" fi if [ "$status" = "DIFF" ]; then status_str="⚠️ DIFF" ((diff_count++)) || true else status_str="✅ OK" ((ok_count++)) || true fi total_main=$(echo "$total_main + $main_t" | bc) total_branch=$(echo "$total_branch + $branch_t" | bc) echo "| $name | ${main_t}s | ${branch_t}s | $delta_str | $status_str |" >> "$REPORT_FILE" done total_delta=$(echo "$total_branch - $total_main" | bc) if (( $(echo "$total_delta > 0" | bc -l) )); then total_delta_str="+${total_delta}s" else total_delta_str="${total_delta}s" fi cat >> "$REPORT_FILE" << EOF | **TOTAL** | **${total_main}s** | **${total_branch}s** | **$total_delta_str** | **$ok_count OK / $diff_count DIFF** | ## Statistics - **Tests Passed (no diff):** $ok_count - **Tests with Differences:** $diff_count - **Total Main Time:** ${total_main}s - **Total Branch Time:** ${total_branch}s - **Overall Time Delta:** $total_delta_str ## Detailed Diffs EOF # Add diff details to report for i in "${!TEST_NAMES[@]}"; do name="${TEST_NAMES[$i]}" status="${DIFF_STATUS[$i]}" if [ "$status" = "DIFF" ]; then echo "### $name" >> "$REPORT_FILE" echo "" >> "$REPORT_FILE" # Check all possible endpoints for endpoint in initialize tools resources prompts; do diff_file="$REPORT_DIR/diffs/$name/${endpoint}.diff" if [ -f "$diff_file" ] && [ -s "$diff_file" ]; then echo "#### ${endpoint}" >> "$REPORT_FILE" echo '```diff' >> "$REPORT_FILE" cat "$diff_file" >> "$REPORT_FILE" echo '```' >> "$REPORT_FILE" echo "" >> "$REPORT_FILE" fi done fi done log "${BLUE}=== Conformance Test Complete ===${NC}" log "" log "Report: ${GREEN}$REPORT_FILE${NC}" log "" # Output summary to stdout (for CI capture) echo "=== Conformance Test Summary ===" echo "Tests passed: $ok_count" echo "Tests with diffs: $diff_count" echo "Total main time: ${total_main}s" echo "Total branch time: ${total_branch}s" echo "Time delta: $total_delta_str" if [ $diff_count -gt 0 ]; then log "" log "${YELLOW}⚠️ Some tests have differences. Review the diffs in:${NC}" log " $REPORT_DIR/diffs/" echo "" echo "RESULT: DIFFERENCES FOUND" # Don't exit with error - diffs may be intentional improvements else echo "" echo "RESULT: ALL TESTS PASSED" fi