# Official MCP conformance suite, run against a real `serve --http` gateway.
#
# Gate E of the release matrix (#550). Our own live tiers assert what we
# believe the protocol requires; this asserts what the specification's
# maintainers say it requires, which is the part we cannot mark our own
# homework on.
#
# Scope, stated plainly: the published suite has NO 2026-07-28 server vectors
# yet. Its CLI accepts 2025-03-26 / 2025-06-18 / 2025-11-25 / draft / extension,
# and `--spec-version draft` lists zero server scenarios. So this run certifies
# the generation we still serve for back-compat, not the modern surface. The
# upstream repo already carries `server-stateless` (SEP-2575 discover) and
# `tasks-lifecycle` scenarios that are not in the npm build; when they ship,
# they cover exactly what we built and belong here. `check-for-new-scenarios`
# below exists to notice that rather than rely on someone remembering.
name: MCP Conformance

on:
  pull_request:
    paths:
      - "src/mcp_hangar/fastmcp_server/**"
      - "src/mcp_hangar/server/**"
      - "tests/conformance/**"
      - ".github/workflows/conformance.yml"
      - "pyproject.toml"
  push:
    branches: [mcp2]
  schedule:
    - cron: "17 5 * * 2" # weekly; also how the scenario-drift check runs
  workflow_dispatch: {}

permissions:
  contents: read

jobs:
  conformance:
    name: Server conformance (baselined)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
        with:
          python-version: "3.11"
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
        with:
          node-version: "24"

      - name: Install the gateway
        run: |
          python -m pip install --upgrade pip
          pip install -e .

      - name: Start `serve --http` with a subprocess backend
        run: |
          cat > conformance-config.yaml <<'EOF'
          logging:
            level: WARNING
          mcp_servers:
            math:
              mode: subprocess
              command: ["python", "examples/provider_math/server.py"]
              idle_ttl_s: 300
          EOF
          mcp-hangar --config conformance-config.yaml serve --http \
            --host 127.0.0.1 --port 52444 > gateway.log 2>&1 &
          for _ in $(seq 1 60); do
            if curl -sf -o /dev/null http://127.0.0.1:52444/health/live; then
              echo "gateway healthy"; exit 0
            fi
            sleep 1
          done
          echo "::error::gateway never became healthy"; tail -50 gateway.log; exit 1

      # Pinned, not @latest: a suite that changes under us turns a red build
      # into a guessing game about whose change caused it. `check-for-new-
      # scenarios` is what watches for movement.
      - name: Run the conformance suite
        run: |
          npx -y @modelcontextprotocol/conformance@0.1.16 server \
            --url http://127.0.0.1:52444/mcp \
            --suite core \
            --expected-failures tests/conformance/baseline.yml

      - name: Gateway log on failure
        if: failure()
        run: tail -100 gateway.log

  # Gate E is blocked on the ecosystem, not on us: the 2026-07-28 server vectors
  # do not exist yet, and no released MCP client speaks that generation either
  # (the Inspector pins TypeScript SDK v1.x, and npm has no 2.x SDK at all).
  # Both unblock on someone else's release, so this watches for them instead of
  # relying on a person remembering to look.
  #
  # Condition checks, not diffs: each step asks "has the thing we are waiting
  # for happened?" so a quiet week is silent rather than nagging.
  # Advisory throughout -- it must never redden a PR for an upstream release.
  check-for-new-scenarios:
    name: Ecosystem watch (gate E blockers)
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    continue-on-error: true
    steps:
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
        with:
          node-version: "24"
      # Compared against BOTH dist-tags. `latest` alone is a blind spot: the
      # 2026-07-28 server vectors and the whole `tasks-*` extension family
      # landed on `alpha` (0.2.0-alpha.x) while `latest` sat unchanged at
      # 0.1.16 -- so a check watching only `latest` reported "no changes" for
      # the exact release gate E was waiting for.
      - name: Compare the pinned scenario list against the published tags
        run: |
          npx -y @modelcontextprotocol/conformance@0.1.16 list > pinned.txt 2>&1 || true
          for tag in latest alpha next beta; do
            npx -y "@modelcontextprotocol/conformance@${tag}" list > "${tag}.txt" 2>&1 || continue
            if ! diff -q pinned.txt "${tag}.txt" >/dev/null; then
              echo "::notice::conformance@${tag} has scenarios the pin does not -- consider bumping."
              diff -u pinned.txt "${tag}.txt" || true
            fi
            if grep -q "2026-07-28" "${tag}.txt"; then
              echo "::notice::conformance@${tag} carries 2026-07-28 server vectors -- gate E's modern half is unblocked."
            fi
            if grep -q "tasks-" "${tag}.txt"; then
              echo "::notice::conformance@${tag} carries tasks-* extension scenarios -- the SEP-2663 relay can be certified against them."
            fi
          done

      # The tasks half of gate E is blocked on the suite, not on us.
      #
      # `@modelcontextprotocol/conformance@alpha` now ships seven `tasks-*`
      # extension scenarios -- an external audit of exactly the SEP-2663 relay
      # this repo serves. They cannot be run against Hangar today, and the
      # reason is structural rather than a missing fixture name:
      #
      #   * the scenarios require `greet` / `slow_compute` in `tools/list`;
      #   * Hangar exposes backend tool names only in `front_door` topology,
      #     which projects per tenant and therefore needs an identity -- with
      #     auth off it advertises ZERO tools, by design and verified;
      #   * `conformance server` has no `--header` / auth option, so it cannot
      #     present one.
      #
      # Those two are mutually exclusive, so this unblocks when the suite grows
      # a way to authenticate. Watching for that instead of re-discovering it.
      - name: Can the conformance suite authenticate yet?
        run: |
          help=$(npx -y @modelcontextprotocol/conformance@alpha server --help 2>&1 || echo "")
          if [ -z "${help}" ]; then
            echo "::notice::Could not read the conformance CLI help -- this check needs updating."
            exit 0
          fi
          if printf '%s' "${help}" | grep -qiE '\-\-header|\-\-auth|\-\-bearer|\-\-token'; then
            echo "::notice::conformance server now accepts auth/header options -- the tasks-* scenarios can finally be run against a front_door gateway; revisit gate E."
          else
            echo "Still no auth option on `conformance server` -- tasks-* remain unrunnable against Hangar."
          fi

      # The interop half of gate E needs a real client that speaks 2026-07-28.
      #
      # This used to ask whether `@modelcontextprotocol/sdk` had published a
      # 2.x. It never will: the TypeScript SDK v2 shipped under NEW package
      # names -- `@modelcontextprotocol/core` / `client` / `server`, all at
      # 2.0.0 -- while `sdk` continues on the v1 line and is still being
      # released. The check therefore answered "still on the v1 line" about a
      # package that had already been superseded: a false all-clear rather than
      # a stale one, silent about the very event it existed to catch.
      #
      # Watching the successor packages instead. Their EXISTENCE at a 2.x is
      # the signal, so there is no version comparison to get wrong.
      - name: Has the TypeScript SDK v2 shipped?
        run: |
          unblocked=0
          for pkg in core client server; do
            version=$(npm view "@modelcontextprotocol/${pkg}" version 2>/dev/null || echo "")
            if [ -z "${version}" ]; then
              echo "@modelcontextprotocol/${pkg}: not published"
              continue
            fi
            echo "@modelcontextprotocol/${pkg} = ${version}"
            major=$(printf '%s' "${version}" | sed -E 's/^[^0-9]*//; s/[.-].*//')
            case "${major}" in
              ''|*[!0-9]*) echo "::notice::Could not read a major from ${version} -- this check needs updating." ;;
              *) [ "${major}" -ge 2 ] && unblocked=1 ;;
            esac
          done
          if [ "${unblocked}" = "1" ]; then
            echo "::notice::The TypeScript SDK v2 packages are published -- a client that speaks 2026-07-28 is now possible; revisit gate E interop."
          else
            echo "No 2.x among the successor packages yet."
          fi

      # The Inspector is the interop candidate, useful to us once it rides an
      # SDK that knows server/discover and the Mcp-Method routing.
      #
      # Retargeted for the same rename: it no longer depends on
      # `@modelcontextprotocol/sdk` at all, so reading that key returned
      # "unknown" forever. That failure was at least loud -- it reported a
      # broken check rather than an all-clear -- but it still could not answer
      # the question.
      - name: Has the Inspector moved to the v2 packages?
        run: |
          manifest=$(curl -sf https://raw.githubusercontent.com/modelcontextprotocol/inspector/main/package.json || echo "")
          if [ -z "${manifest}" ]; then
            echo "::notice::Could not read the Inspector manifest -- this check needs updating."
            exit 0
          fi
          pin=$(printf '%s' "${manifest}" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const p=JSON.parse(d);const deps=p.dependencies||{};console.log(deps["@modelcontextprotocol/client"]||deps["@modelcontextprotocol/core"]||"absent")})')
          if [ "${pin}" = "absent" ]; then
            echo "The Inspector is not on the v2 packages yet."
          else
            echo "::notice::The Inspector now depends on the v2 packages (${pin}) -- it may speak 2026-07-28; revisit gate E interop."
          fi
