|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +# Runs the upstream MCP conformance client through an OpenShell sandbox. |
| 6 | +# |
| 7 | +# The modelcontextprotocol/conformance GitHub Action starts a real MCP test |
| 8 | +# server in the GitHub Actions job container and invokes this script with that |
| 9 | +# server URL. This script starts the normal Docker-backed OpenShell e2e gateway, |
| 10 | +# creates a sandbox from the prebuilt conformance client image, and runs the |
| 11 | +# upstream TypeScript everything-client inside that sandbox. That keeps the MCP |
| 12 | +# client/server traffic in the OpenShell proxy data path. |
| 13 | +# |
| 14 | +# Conformance server URLs usually point at localhost in the job container. |
| 15 | +# Sandboxes are separate Docker containers, so localhost would point back at the |
| 16 | +# sandbox itself. The wrapper rewrites local URLs to host.openshell.internal, |
| 17 | +# which e2e/with-docker-gateway.sh attaches to the job container on the e2e |
| 18 | +# Docker network. |
| 19 | + |
| 20 | +set -euo pipefail |
| 21 | + |
| 22 | +usage() { |
| 23 | + echo "usage: $0 <conformance-server-url>" >&2 |
| 24 | +} |
| 25 | + |
| 26 | +if [ "$#" -ne 1 ]; then |
| 27 | + usage |
| 28 | + exit 2 |
| 29 | +fi |
| 30 | + |
| 31 | +# Parse the conformance runner's server URL and render the OpenShell policy. |
| 32 | +prepare_conformance_target() { |
| 33 | + local server_url=$1 |
| 34 | + local policy_file=$2 |
| 35 | + local policy_template=$3 |
| 36 | + |
| 37 | + python3 - "${server_url}" "${policy_file}" "${policy_template}" <<'PY' |
| 38 | +import json |
| 39 | +import string |
| 40 | +import sys |
| 41 | +from pathlib import Path |
| 42 | +from urllib.parse import urlparse, urlunparse |
| 43 | +
|
| 44 | +raw_url, policy_file, policy_template = sys.argv[1:4] |
| 45 | +parsed = urlparse(raw_url) |
| 46 | +
|
| 47 | +if parsed.scheme not in ("http", "https"): |
| 48 | + raise SystemExit(f"unsupported conformance server URL scheme: {parsed.scheme!r}") |
| 49 | +
|
| 50 | +host = parsed.hostname |
| 51 | +if not host: |
| 52 | + raise SystemExit(f"conformance server URL is missing a host: {raw_url}") |
| 53 | +
|
| 54 | +target_host = "host.openshell.internal" if host in {"localhost", "127.0.0.1", "::1"} else host |
| 55 | +port = parsed.port or (443 if parsed.scheme == "https" else 80) |
| 56 | +path = parsed.path or "/" |
| 57 | +netloc_host = f"[{target_host}]" if ":" in target_host and not target_host.startswith("[") else target_host |
| 58 | +netloc = f"{netloc_host}:{port}" |
| 59 | +rewritten = urlunparse((parsed.scheme, netloc, path, parsed.params, parsed.query, parsed.fragment)) |
| 60 | +
|
| 61 | +template = string.Template(Path(policy_template).read_text(encoding="utf-8")) |
| 62 | +policy = template.substitute( |
| 63 | + host=json.dumps(target_host), |
| 64 | + port=str(port), |
| 65 | + path=json.dumps(path), |
| 66 | +) |
| 67 | +Path(policy_file).write_text(policy, encoding="utf-8") |
| 68 | +
|
| 69 | +print(rewritten) |
| 70 | +PY |
| 71 | +} |
| 72 | + |
| 73 | +SERVER_URL="$1" |
| 74 | +CLIENT_IMAGE="${OPENSHELL_MCP_CONFORMANCE_CLIENT_IMAGE:?set OPENSHELL_MCP_CONFORMANCE_CLIENT_IMAGE to the prebuilt conformance client image}" |
| 75 | +ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" |
| 76 | +POLICY_TEMPLATE="${ROOT}/e2e/mcp-conformance/policy-template.yaml" |
| 77 | + |
| 78 | +POLICY_FILE="$(mktemp "${TMPDIR:-/tmp}/openshell-mcp-conformance-policy.XXXXXX.yaml")" |
| 79 | +trap 'rm -f "${POLICY_FILE}"' EXIT |
| 80 | + |
| 81 | +CLIENT_SERVER_URL="$(prepare_conformance_target "${SERVER_URL}" "${POLICY_FILE}" "${POLICY_TEMPLATE}")" |
| 82 | + |
| 83 | +ENV_ARGS=() |
| 84 | +# These environment variables are set by the upstream conformance test runner |
| 85 | +# before it invokes the configured client command. Forward them into the |
| 86 | +# sandbox because the sandboxed TypeScript client depends on them to select the |
| 87 | +# scenario and read scenario-specific context. |
| 88 | +for NAME in MCP_CONFORMANCE_SCENARIO MCP_CONFORMANCE_CONTEXT MCP_CONFORMANCE_PROTOCOL_VERSION; do |
| 89 | + if [ -n "${!NAME+x}" ]; then |
| 90 | + ENV_ARGS+=(--env "${NAME}=${!NAME}") |
| 91 | + fi |
| 92 | +done |
| 93 | + |
| 94 | +# shellcheck source=e2e/support/gateway-common.sh disable=SC1091 |
| 95 | +source "${ROOT}/e2e/support/gateway-common.sh" |
| 96 | +TARGET_DIR="$(e2e_cargo_target_dir "${ROOT}")" |
| 97 | +OPENSHELL_BIN="${OPENSHELL_BIN:-${TARGET_DIR}/debug/openshell}" |
| 98 | +export OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE="${OPENSHELL_E2E_DOCKER_SANDBOX_IMAGE:-${CLIENT_IMAGE}}" |
| 99 | + |
| 100 | +# shellcheck disable=SC2016 |
| 101 | +"${ROOT}/e2e/with-docker-gateway.sh" \ |
| 102 | + "${OPENSHELL_BIN}" sandbox create \ |
| 103 | + --from "${CLIENT_IMAGE}" \ |
| 104 | + --policy "${POLICY_FILE}" \ |
| 105 | + "${ENV_ARGS[@]}" \ |
| 106 | + -- \ |
| 107 | + sh -c 'cd /opt/mcp-conformance && exec ./node_modules/.bin/tsx examples/clients/typescript/everything-client.ts "$1"' \ |
| 108 | + sh "${CLIENT_SERVER_URL}" |
0 commit comments