Skip to content

Commit 62242b6

Browse files
committed
test: add downstream client conformance coverage
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 6a535dd commit 62242b6

14 files changed

Lines changed: 515 additions & 16 deletions

.github/workflows/mcp_conformance.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ jobs:
2828
- name: Test conformance reporter
2929
run: tests/conformance/report-baseline-diff-test.sh
3030

31+
- name: Test client conformance adapter
32+
run: |
33+
python3 -m py_compile tests/conformance/write_client_config.py
34+
tests/conformance/client-under-test-test.sh
35+
tests/conformance/bless-client-baseline-test.sh
36+
3137
- name: Download the conformance binary
3238
uses: actions/download-artifact@v8.0.1
3339
with:
@@ -96,9 +102,12 @@ jobs:
96102
id: runner
97103
run: tests/conformance/run-conformance.sh
98104

105+
- name: Run scoped MCP 2026-07-28 client conformance
106+
run: tests/conformance/run-client-conformance.sh
107+
99108
- name: Report conformance baseline diff
100109
if: always()
101-
run: tests/conformance/report-baseline-diff.sh
110+
run: tests/conformance/report-baseline-diff.sh conformance-results/server
102111

103112
- name: Print live stack logs
104113
if: always()

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ conformance: ## Build the data plane and run official MCP 2026-07-28 conformance
2929
docker build -t "$(CF_DATAPLANE_IMAGE)" -f docker/conformance.Dockerfile .
3030
CF_DATAPLANE_IMAGE="$(CF_DATAPLANE_IMAGE)" tests/conformance/run-local.sh
3131

32-
conformance-bless: ## Run conformance and update the expected-failure baseline
32+
conformance-bless: ## Run conformance and update the server and client expected-failure baselines
3333
MCP_CONFORMANCE_BLESS=true $(MAKE) conformance
3434

3535
docs-serve: ## Serve the wiki book locally at http://127.0.0.1:3000

_context/wiki/testing.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,39 @@ These run in `cargo nextest run` with no Docker dependencies.
3737
## MCP Conformance CI
3838

3939
`.github/workflows/mcp_conformance.yml` runs the pinned official conformance
40-
suite `0.2.0-alpha.11` with `--requirements 2026-07-28`. Its small live path is
41-
official runner → nginx → checked-out dataplane → fixture proxy → official
42-
fixture, with the published `latest` control plane registering and publishing
40+
suite `0.2.0-alpha.11` for MCP `2026-07-28` in both directions. The server leg
41+
is official client → nginx → checked-out dataplane → fixture proxy → official
42+
server, with the published `latest` control plane registering and publishing
4343
the fixture through Redis. The backend-only proxy rewrites `Host` to
4444
`localhost:3000`, which the official fixture's DNS-rebinding protection
4545
requires, while leaving dataplane header protections unchanged. The control
4646
plane uses ephemeral SQLite, so PostgreSQL is unnecessary. The harness lives
4747
in `tests/conformance/`.
4848

49+
The scoped client leg then treats the dataplane as an MCP client: the official
50+
runner starts a scenario backend, the adapter publishes an isolated route to
51+
Redis, and a downstream `tools/call` makes the dataplane connect to that
52+
backend. It covers tool calls, per-request client metadata and protocol-version
53+
retry, standard MCP headers, and custom parameter headers. The control plane is
54+
stopped first so its periodic publisher cannot replace the scenario route or
55+
probe the observation backend. OAuth client scenarios remain a control-plane
56+
responsibility. Server and client results are written below `server/` and
57+
`client/`, with separate `expected-failures.yml` and
58+
`client-expected-failures.yml` baselines.
59+
60+
`make conformance` runs both legs locally, while `make conformance-bless` runs
61+
both and refreshes both expected-failure baselines from that run.
62+
4963
Because this conformance CLI cannot set a bearer header, nginx adds an
5064
ephemeral control-plane token when one is absent; there is no auth proxy or
5165
repository-owned JavaScript. A route probe prevents control-plane fallback.
52-
Counts and the official fixture log appear directly in the Actions log, and
53-
`expected-failures.yml` guards the current baseline. The job does not retain a
54-
separate conformance artifact. `upstream-fixture-failures.yml` records the
55-
pinned fixture's seven scored failures and one warning; its other 47 failures
56-
are extension or pending scenarios and are already unscored. CI prints the
57-
exact actual-versus-baseline diff, adds annotations for unexpected and stale
58-
entries, and writes the same comparison to the job summary.
66+
Counts and the official fixture log appear directly in the Actions log. The
67+
job does not retain a separate conformance artifact.
68+
`upstream-fixture-failures.yml` records the pinned fixture's seven scored
69+
failures and one warning; its other 47 failures are extension or pending
70+
scenarios and are already unscored. CI prints the exact server
71+
actual-versus-baseline diff, adds annotations for unexpected and stale entries,
72+
and writes the same comparison to the job summary.
5973

6074
## Full-Stack Integration Harness
6175

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5+
state_dir="$(mktemp -d "${TMPDIR:-/tmp}/contextforge-client-baseline-test.XXXXXX")"
6+
results_dir="${state_dir}/results"
7+
baseline_file="${state_dir}/client-expected-failures.yml"
8+
9+
cleanup() {
10+
rm -rf -- "${state_dir}"
11+
}
12+
trap cleanup EXIT INT TERM
13+
14+
mkdir -p \
15+
"${results_dir}/tools_call-2026-08-20T00-00-00-000Z" \
16+
"${results_dir}/request-metadata-2026-08-20T00-00-00-000Z" \
17+
"${results_dir}/http-standard-headers-2026-08-20T00-00-00-000Z" \
18+
"${results_dir}/http-custom-headers-2026-08-20T00-00-00-000Z"
19+
20+
cat > "${baseline_file}" <<'EOF'
21+
client: []
22+
EOF
23+
for scenario in tools_call request-metadata http-standard-headers; do
24+
cat > "${results_dir}/${scenario}-2026-08-20T00-00-00-000Z/checks.json" <<'EOF'
25+
[{"id":"passes","status":"SUCCESS"}]
26+
EOF
27+
done
28+
cat > "${results_dir}/http-custom-headers-2026-08-20T00-00-00-000Z/checks.json" <<'EOF'
29+
[
30+
{"id":"mirrors","status":"SUCCESS"},
31+
{"id":"mirrors","status":"FAILURE"},
32+
{"id":"encodes","status":"WARNING"},
33+
{"id":"omits-null","status":"SUCCESS"}
34+
]
35+
EOF
36+
37+
"${script_dir}/bless-client-baseline.sh" "${results_dir}" "${baseline_file}"
38+
39+
cat > "${state_dir}/expected.yml" <<'EOF'
40+
# Generated by `make conformance-bless` from scoped client findings.
41+
# OAuth client scenarios are control-plane responsibilities and are not run here.
42+
client:
43+
- http-custom-headers:encodes
44+
- http-custom-headers:mirrors
45+
EOF
46+
diff -u "${state_dir}/expected.yml" "${baseline_file}"
47+
48+
second_output="$("${script_dir}/bless-client-baseline.sh" "${results_dir}" "${baseline_file}")"
49+
grep --fixed-strings --quiet -- 'was already current' <<< "${second_output}"
50+
51+
echo 'client conformance baseline tests passed'
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ "$#" -gt 2 ]; then
5+
echo "Usage: bless-client-baseline.sh [results-dir [baseline-file]]" >&2
6+
exit 2
7+
fi
8+
9+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
10+
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
11+
results_dir="${1:-${repo_root}/conformance-results/client}"
12+
baseline_file="${2:-${script_dir}/client-expected-failures.yml}"
13+
scenarios=(tools_call request-metadata http-standard-headers http-custom-headers)
14+
15+
for command in awk cmp cp jq mktemp sed sort; do
16+
if ! command -v "${command}" > /dev/null 2>&1; then
17+
echo "Required command not found: ${command}" >&2
18+
exit 2
19+
fi
20+
done
21+
if [ ! -d "${results_dir}" ]; then
22+
echo "No client conformance results directory: ${results_dir}" >&2
23+
exit 2
24+
fi
25+
if [ ! -f "${baseline_file}" ]; then
26+
echo "Client conformance baseline not found: ${baseline_file}" >&2
27+
exit 2
28+
fi
29+
30+
state_dir="$(mktemp -d "${TMPDIR:-/tmp}/contextforge-client-baseline.XXXXXX")"
31+
findings="${state_dir}/findings.txt"
32+
baseline_entries="${state_dir}/baseline-entries.txt"
33+
candidate="${state_dir}/client-expected-failures.yml"
34+
35+
cleanup() {
36+
rm -f -- "${findings}" "${baseline_entries}" "${candidate}"
37+
rmdir -- "${state_dir}"
38+
}
39+
trap cleanup EXIT INT TERM
40+
41+
: > "${findings}"
42+
for scenario in "${scenarios[@]}"; do
43+
matches=("${results_dir}/${scenario}-"*/checks.json)
44+
if [ ! -e "${matches[0]}" ]; then
45+
echo "No client conformance results found for scenario: ${scenario}" >&2
46+
exit 2
47+
fi
48+
checks_file="${matches[$((${#matches[@]} - 1))]}"
49+
jq --raw-output --arg scenario "${scenario}" '
50+
def severity($status):
51+
if $status == "FAILURE" then 3
52+
elif $status == "WARNING" then 2
53+
elif $status == "SUCCESS" then 1
54+
else 0
55+
end;
56+
57+
reduce (.[] | select(.status != "INFO")) as $check
58+
({};
59+
($check.id) as $id |
60+
if .[$id] == null or severity($check.status) >= severity(.[$id].status)
61+
then .[$id] = $check
62+
else .
63+
end) |
64+
to_entries[] |
65+
select(.value.status == "FAILURE" or .value.status == "WARNING") |
66+
($scenario + ":" + .key)
67+
' "${checks_file}" >> "${findings}"
68+
done
69+
LC_ALL=C sort -u -o "${findings}" "${findings}"
70+
71+
awk '
72+
/^client:$/ { in_client = 1; next }
73+
in_client && /^[^[:space:]]/ { exit }
74+
in_client && /^[[:space:]]*-[[:space:]]+/ {
75+
line = $0
76+
sub(/^[[:space:]]*-[[:space:]]+/, "", line)
77+
sub(/[[:space:]]+#.*$/, "", line)
78+
print line
79+
}
80+
' "${baseline_file}" | LC_ALL=C sort -u > "${baseline_entries}"
81+
82+
{
83+
# shellcheck disable=SC2016 # Backticks are literal Markdown.
84+
echo '# Generated by `make conformance-bless` from scoped client findings.'
85+
echo '# OAuth client scenarios are control-plane responsibilities and are not run here.'
86+
if [ -s "${findings}" ]; then
87+
echo 'client:'
88+
sed 's/^/ - /' "${findings}"
89+
else
90+
echo 'client: []'
91+
fi
92+
} > "${candidate}"
93+
94+
if cmp --silent "${findings}" "${baseline_entries}"; then
95+
echo "Client conformance baseline was already current: ${baseline_file}"
96+
else
97+
cp "${candidate}" "${baseline_file}"
98+
echo "Client conformance baseline updated: ${baseline_file}"
99+
fi
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Dataplane-owned upstream MCP client findings for the scoped client lane.
2+
# OAuth scenarios are control-plane responsibilities and are not run here.
3+
client:
4+
# The upstream client does not yet mirror x-mcp-header tool arguments into
5+
# Mcp-Param-* request headers. Keep the null/omission checks as required
6+
# passes by baselining only the affected checks, not the whole scenario.
7+
- http-custom-headers:sep-2243-client-supports-custom-headers
8+
- http-custom-headers:sep-2243-client-mirrors-designated-params
9+
- http-custom-headers:sep-2243-client-encode-values
10+
- http-custom-headers:sep-2243-client-base64-unsafe
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5+
state_dir="$(mktemp -d "${TMPDIR:-/tmp}/contextforge-client-adapter-test.XXXXXX")"
6+
fake_bin="${state_dir}/bin"
7+
docker_args="${state_dir}/docker-args"
8+
curl_bodies="${state_dir}/curl-bodies"
9+
10+
cleanup() {
11+
rm -rf -- "${state_dir}"
12+
}
13+
trap cleanup EXIT INT TERM
14+
15+
mkdir -p "${fake_bin}"
16+
cat > "${fake_bin}/docker" <<'EOF'
17+
#!/usr/bin/env bash
18+
printf '%s\n' "$*" > "${FAKE_DOCKER_ARGS}"
19+
EOF
20+
cat > "${fake_bin}/curl" <<'EOF'
21+
#!/usr/bin/env bash
22+
while [ "$#" -gt 0 ]; do
23+
if [ "$1" = "--data" ]; then
24+
shift
25+
printf '%s\n' "$1" >> "${FAKE_CURL_BODIES}"
26+
fi
27+
shift
28+
done
29+
printf '%s\n' 'data: {"jsonrpc":"2.0","id":1,"result":{"content":[]}}'
30+
EOF
31+
chmod +x "${fake_bin}/docker" "${fake_bin}/curl"
32+
33+
export PATH="${fake_bin}:${PATH}"
34+
export FAKE_DOCKER_ARGS="${docker_args}"
35+
export FAKE_CURL_BODIES="${curl_bodies}"
36+
export MCP_CONFORMANCE_PROTOCOL_VERSION=2026-07-28
37+
export MCP_CONFORMANCE_SUBJECT=test-subject
38+
export MCP_CONFORMANCE_CLIENT_SERVER_ID=test-client-server
39+
export MCP_CONFORMANCE_PORT=18080
40+
export MCP_CONFORMANCE_SCENARIO=http-custom-headers
41+
export MCP_CONFORMANCE_CONTEXT='{
42+
"name": "http-custom-headers",
43+
"toolCalls": [
44+
{"name": "first", "arguments": {"region": "west"}},
45+
{"name": "second", "arguments": {"verbose": null}}
46+
]
47+
}'
48+
49+
"${script_dir}/client-under-test.sh" "http://localhost:43123/mcp"
50+
51+
grep --fixed-strings --quiet -- 'http://host.docker.internal:43123/mcp' "${docker_args}"
52+
grep --fixed-strings --quiet -- '["first","second"]' "${docker_args}"
53+
test "$(wc -l < "${curl_bodies}" | tr -d '[:space:]')" -eq 2
54+
jq --exit-status --slurp '
55+
length == 2 and
56+
.[0].method == "tools/call" and
57+
.[0].params.name == "first" and
58+
.[0].params.arguments.region == "west" and
59+
.[0].params._meta["io.modelcontextprotocol/protocolVersion"] == "2026-07-28" and
60+
.[1].params.name == "second" and
61+
.[1].params.arguments.verbose == null
62+
' "${curl_bodies}" > /dev/null
63+
64+
echo 'client conformance adapter tests passed'

0 commit comments

Comments
 (0)