Files
OpenShell/e2e/mcp-conformance/client-through-openshell.sh
krishicksandddurst 7bce1223dc feat(policy): add JSON-RPC and MCP L7 policies (#1865)
Add policy schema, proto, provider profile, OPA, and L7 proxy support for
`protocol: json-rpc` and `protocol: mcp`. Generic JSON-RPC endpoints match
exact method names only, with `method: "*"` as the all-method sentinel;
wildcard/glob methods and params matchers are rejected.

Parse JSON-RPC request bodies and batches in the forward proxy, deny
response-shaped client frames, limit receive-stream GET allowance to MCP
endpoints, and redact params in decision logs. Preserve L7 rule params on the
proto load path so MCP `tools/call` tool filters behave like YAML-loaded
policies.

Add MCP conformance coverage, JSON-RPC L7 e2e coverage, and docs for the new
protocols and current matcher limitations.

Signed-off-by: Kris Hicks <khicks@nvidia.com>
Co-authored-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
2026-06-26 15:52:16 -07:00

101 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Runs the upstream MCP conformance client in an OpenShell-managed sandbox.
#
# The modelcontextprotocol/conformance runner runs in a separate container and
# posts the URL of its MCP test server to a host bridge, which invokes this
# script with that URL. The parent harness creates one reusable conformance
# client sandbox for the whole scenario list before this script is invoked. This
# wrapper verifies the active gateway is reachable, applies the per-scenario
# server policy to that sandbox, and runs the upstream TypeScript
# everything-client inside it so its MCP traffic crosses the sandbox proxy.
set -euo pipefail
usage() {
echo "usage: $0 <conformance-server-url>" >&2
}
if [ "$#" -ne 1 ]; then
usage
exit 2
fi
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
# shellcheck source=e2e/support/gateway-common.sh disable=SC1091
source "${ROOT}/e2e/support/gateway-common.sh"
if [ -z "${OPENSHELL_BIN:-}" ]; then
TARGET_DIR="$(e2e_cargo_target_dir "${ROOT}")"
OPENSHELL_BIN="${TARGET_DIR}/debug/openshell"
fi
require_active_gateway() {
local status_output
if ! status_output="$("${OPENSHELL_BIN}" status 2>&1)"; then
echo "ERROR: no reachable active OpenShell gateway for MCP conformance." >&2
echo " Run e2e/mcp-conformance.sh so it starts one shared Docker-backed gateway." >&2
echo "=== openshell status output ===" >&2
printf '%s\n' "${status_output}" >&2
echo "=== end openshell status output ===" >&2
exit 2
fi
}
SERVER_URL="$1"
CLIENT_SANDBOX="${OPENSHELL_MCP_CONFORMANCE_CLIENT_SANDBOX:?set OPENSHELL_MCP_CONFORMANCE_CLIENT_SANDBOX to the reusable conformance client sandbox name}"
POLICY_TEMPLATE="${ROOT}/e2e/mcp-conformance/policy-template.yaml"
POLICY_WAIT="${OPENSHELL_MCP_CONFORMANCE_POLICY_WAIT:-0}"
POLICY_WAIT_TIMEOUT="${OPENSHELL_MCP_CONFORMANCE_POLICY_WAIT_TIMEOUT:-60}"
CLIENT_TIMEOUT_SECONDS="${OPENSHELL_MCP_CONFORMANCE_CLIENT_TIMEOUT_SECONDS:-120}"
require_active_gateway
POLICY_FILE="$(mktemp "${TMPDIR:-/tmp}/openshell-mcp-conformance-policy.XXXXXX.yaml")"
trap 'rm -f "${POLICY_FILE}"' EXIT
CLIENT_SERVER_URL="$(python3 "${ROOT}/e2e/mcp-conformance/render-policy.py" "${SERVER_URL}" "${POLICY_FILE}" "${POLICY_TEMPLATE}")"
ENV_ARGS=()
# These environment variables are set by the upstream conformance test runner
# before it invokes the configured client command. Forward them into the
# sandbox because the sandboxed TypeScript client depends on them to select the
# scenario and read scenario-specific context.
for NAME in MCP_CONFORMANCE_SCENARIO MCP_CONFORMANCE_CONTEXT MCP_CONFORMANCE_PROTOCOL_VERSION; do
if [ -n "${!NAME+x}" ]; then
ENV_ARGS+=(--env "${NAME}=${!NAME}")
fi
done
POLICY_SET_COMMAND=(
"${OPENSHELL_BIN}" policy set "${CLIENT_SANDBOX}"
--policy "${POLICY_FILE}"
)
if [ "${POLICY_WAIT}" = "1" ]; then
POLICY_SET_COMMAND+=(--wait --timeout "${POLICY_WAIT_TIMEOUT}")
fi
"${POLICY_SET_COMMAND[@]}"
# Exec request validation rejects newline/control characters in command
# arguments, so keep the sandbox-side script as a single argument without
# embedded newlines.
# shellcheck disable=SC2016
SANDBOX_CLIENT_SCRIPT='cd /opt/mcp-conformance; case "${MCP_CONFORMANCE_SCENARIO:-}" in tools_call|tools-call) client=examples/clients/typescript/test2.ts ;; sse-retry) client=examples/clients/typescript/sse-retry-test.ts ;; *) client=examples/clients/typescript/everything-client.ts ;; esac; exec ./node_modules/.bin/tsx "$client" "$1"'
SANDBOX_COMMAND=(
"${OPENSHELL_BIN}" sandbox exec
--name "${CLIENT_SANDBOX}"
--no-tty
--timeout "${CLIENT_TIMEOUT_SECONDS}"
"${ENV_ARGS[@]}"
--
sh -c "${SANDBOX_CLIENT_SCRIPT}" \
sh "${CLIENT_SERVER_URL}"
)
"${SANDBOX_COMMAND[@]}" </dev/null