#!/bin/bash
#
# Run the MCP Conformance Test Framework against a live plain-mcp server.
#
# Usage: plain-mcp/tests/conformance/run
#
# Starts `plain server` on a local port, waits for it to become ready, runs
# the pinned `@modelcontextprotocol/conformance` CLI, then stops the server.
# Exits with the conformance CLI's exit code.
#
# Errors out (exit 1) if `npx` is not available on PATH.

set -e

HERE="$(cd "$(dirname "$0")" && pwd)"
PORT="${MCP_CONFORMANCE_PORT:-18765}"
# 2026-07-28 scenarios only exist in the 0.2 alphas — the `latest` release
# still tops out at 2025-11-25. Pinned rather than tracking the `alpha` tag so
# a run is reproducible; bump it when 0.2.0 ships stable.
CONFORMANCE_PACKAGE="@modelcontextprotocol/conformance@0.2.0-alpha.10"
# No trailing slash — Plain canonicalizes to the slashless path, so `/mcp/` would
# 308-redirect. Normal MCP clients follow the redirect, but the conformance
# dns-rebinding probe makes a raw request that doesn't, so it must hit `/mcp`.
URL="http://127.0.0.1:${PORT}/mcp"
BASELINE="${HERE}/expected-failures.yml"

bold() { printf '\033[1m%s\033[0m\n' "$1"; }

if ! command -v npx >/dev/null 2>&1; then
    bold "Error: npx not available — install Node.js to run the MCP conformance suite."
    exit 1
fi

# Refuse to start onto an occupied port. Otherwise the readiness probe below
# is answered by whatever is already listening — a stale server from a killed
# run, most likely — and the suite silently grades that process instead.
if curl -s -o /dev/null --max-time 2 "${URL}"; then
    bold "Error: something is already serving ${URL}. Stop it, or set MCP_CONFORMANCE_PORT."
    exit 1
fi

bold "Starting plain-mcp server on ${URL}"

cd "${HERE}"
PLAIN_SETTINGS_MODULE=app.settings \
PYTHONPATH="${HERE}" \
    uv run plain server \
        -b "127.0.0.1:${PORT}" \
        --workers 1 \
        --no-access-log \
    >/tmp/plain-mcp-conformance.log 2>&1 &
SERVER_PID=$!

cleanup() {
    if kill -0 "${SERVER_PID}" 2>/dev/null; then
        kill "${SERVER_PID}" 2>/dev/null || true
        wait "${SERVER_PID}" 2>/dev/null || true
    fi
}
trap cleanup EXIT INT TERM

# Wait for the endpoint to respond at all. Any status counts — a bare GET gets
# a 405, which still proves the server is up and routing. The conformance CLI
# negotiates the protocol itself, so duplicating a valid MCP request here would
# only mean maintaining the envelope and protocol version in two places.
for i in $(seq 1 40); do
    # Liveness first: a server that died on startup must be reported as that,
    # not probed for.
    if ! kill -0 "${SERVER_PID}" 2>/dev/null; then
        bold "Server died before becoming ready. Last 40 log lines:"
        tail -n 40 /tmp/plain-mcp-conformance.log || true
        exit 1
    fi
    if curl -s -o /dev/null "${URL}"; then
        break
    fi
    sleep 0.25
done

bold "Running MCP conformance suite (2026-07-28)"

set +e
npx --yes "${CONFORMANCE_PACKAGE}" server \
    --url "${URL}" \
    --suite all \
    --spec-version 2026-07-28 \
    --expected-failures "${BASELINE}"
STATUS=$?
set -e

# Second pass at a classic revision, grading the compatibility branch
# (`MCPView.handle_classic_message`) against the handshake-era protocol it
# serves. 2025-11-25 is the newest classic version the branch offers, and
# date filtering is cumulative, so this covers the older revisions'
# scenarios too. Its own baseline lists the optional features plain-mcp
# doesn't implement on either path.
bold "Running MCP conformance suite (classic, 2025-11-25)"

set +e
npx --yes "${CONFORMANCE_PACKAGE}" server \
    --url "${URL}" \
    --suite all \
    --spec-version 2025-11-25 \
    --expected-failures "${HERE}/expected-failures-classic.yml"
CLASSIC_STATUS=$?
set -e

if [ "${STATUS}" -ne 0 ] || [ "${CLASSIC_STATUS}" -ne 0 ]; then
    exit 1
fi
exit 0
