Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ jobs:
- runner: windows-2025
target: x86_64-pc-windows-msvc
binary: cf-integration.exe
- runner: windows-11-arm
target: aarch64-pc-windows-msvc
binary: cf-integration.exe
runs-on: ${{ matrix.runner }}
permissions:
attestations: write
Expand All @@ -107,10 +110,23 @@ jobs:
persist-credentials: false

- name: Install Rust toolchain
run: rustup toolchain install 1.97.0 --profile minimal
shell: bash
env:
TARGET: ${{ matrix.target }}
run: rustup toolchain install 1.97.0 --profile minimal --target "$TARGET"

- name: Build release binary
run: cargo build --release --locked --bin cf-integration
shell: bash
env:
TARGET: ${{ matrix.target }}
run: cargo build --release --locked --bin cf-integration --target "$TARGET"

- name: Smoke test release binary
shell: bash
env:
BINARY: ${{ matrix.binary }}
TARGET: ${{ matrix.target }}
run: '"target/$TARGET/release/$BINARY" --help'

- name: Package release binary
shell: bash
Expand All @@ -119,7 +135,7 @@ jobs:
TARGET: ${{ matrix.target }}
run: |
mkdir -p dist/package
cp "target/release/$BINARY" dist/package/
cp "target/$TARGET/release/$BINARY" dist/package/
archive="cf-integration-$TARGET.tgz"
tar -C dist/package -czf "dist/$archive" "$BINARY"
if command -v sha256sum >/dev/null; then
Expand Down
54 changes: 33 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ cargo binstall cf-integration
cf-integration --help
```

The release archives cover x86-64 and ARM64 Linux, x86-64 and Apple Silicon
macOS, and x86-64 Windows. `cargo-binstall` is required because Cargo's native
`cargo install` command always compiles a crate locally. The CLI still uses the
tracked Compose overlays and scripts at runtime, so run it from this repository
checkout or set `CF_INTEGRATION_ROOT` to the checkout path.
The release archives cover x86-64 and ARM64 Linux, macOS, and Windows.
`cargo-binstall` is required because Cargo's native `cargo install` command
always compiles a crate locally. The CLI still uses the tracked Compose overlays
and scripts at runtime, so run it from this repository checkout or set
`CF_INTEGRATION_ROOT` to the checkout path.

The checked-in `rust-toolchain.toml` selects Rust 1.97.0 with rustfmt and
Clippy. To build and install the locked CLI from this checkout instead:
Expand Down Expand Up @@ -78,10 +78,12 @@ Test server on demand.

## Lanes and protocol versions

Probe, load, live, and Inspector use the same target options:
`--lane controlplane|dataplane` and `--protocol-version YYYY-MM-DD`.
`controlplane` targets the stock control-plane topology and raw `/mcp`;
`dataplane` targets nginx, the Rust dataplane, and the virtual-server route.
Probe, load, and Inspector use `--lane controlplane|dataplane` plus
`--protocol-version YYYY-MM-DD`. `controlplane` targets the stock physical
control-plane topology and raw `/mcp`; `dataplane` targets nginx, the Rust
dataplane, and the virtual-server route. Live and conformance use semantic
workflow lanes: `fixture-direct`, `built-in-data-plane`, and
`external-data-plane`.

Single-lane commands resolve their lane in this order:

Expand All @@ -93,10 +95,12 @@ They resolve the protocol version from explicit `--protocol-version`, then
`MCP_PROTOCOL_VERSION`, then `2025-11-25`. That session-oriented default is
the working contract of the current `latest` dataplane image. Pass
`--protocol-version 2026-07-28` explicitly to exercise the implemented
stateless readiness path as the future architecture lands. Live protocol tests
and conformance also accept `fixture-direct`; other workflows reject it
because they have no direct-fixture execution path. Conformance defaults to
all three lanes and its pinned `2026-07-28` protocol version.
stateless readiness path as the future architecture lands. For live and
conformance, a resolved `controlplane` stack selects `built-in-data-plane`,
while a resolved `dataplane` stack selects `external-data-plane`. Other
workflows reject `fixture-direct` because they have no direct-fixture execution
path. Conformance defaults to all three lanes and its pinned `2026-07-28`
protocol version.

`--topology` remains a compatibility alias for `--lane` on workflows.
Conformance also retains `--client-version` and `--spec-version` as aliases for
Expand Down Expand Up @@ -231,10 +235,10 @@ generated artifacts for credential leakage.
Run the control-plane repository's live gateway tests against either topology:

```bash
cf-integration live --lane dataplane --group mcp
cf-integration live --lane dataplane --group rbac
cf-integration live --lane dataplane --group protocol
cf-integration live --lane dataplane --group all
cf-integration live --lane external-data-plane --group mcp
cf-integration live --lane external-data-plane --group rbac
cf-integration live --lane external-data-plane --group protocol
cf-integration live --lane external-data-plane --group all

# Run the upstream protocol suite directly against its reference fixture.
cf-integration live \
Expand Down Expand Up @@ -277,9 +281,16 @@ It always:
- provisions the pinned official fixture;
- runs every applicable official server scenario;
- defaults to MCP `2026-07-28`;
- runs fixture-direct, controlplane, and dataplane lanes;
- runs fixture-direct, built-in-data-plane, and external-data-plane lanes;
- routes both gateway lanes through `/servers/{virtual_host_id}/mcp` using the
same unscoped ephemeral catalog-token contract as the control-plane job;
- disables rate limiting and embedded Rust MCP handling, and uses one Gunicorn
worker, matching the control-plane conformance job;
- builds the control plane with `ENABLE_RUST=false` and
`ENABLE_RUST_MCP_RMCP=false` when `CF_COMPOSE_BUILD=true`;
- passes an empty expected-failure file to the official runner;
- records raw failures without suppression;
- hides setup and runner output in artifact logs while showing live progress;
- removes temporary API resources, fixture services, and stacks;
- writes a comparison report even when a lane reports protocol failures.

Expand Down Expand Up @@ -323,15 +334,16 @@ dual-era fallback.
The three lanes are:

1. official oracle directly to the official TypeScript fixture;
2. official oracle through the control-plane public MCP route;
3. official oracle through nginx and the Rust dataplane route.
2. official oracle through the routed Python built-in data-plane endpoint;
3. official oracle through the same route backed by the external Rust data
plane.

Select exact lanes by repeating `--lane`:

```bash
cf-integration conformance run \
--lane fixture-direct \
--lane dataplane
--lane external-data-plane
```

Supported client revisions are explicit and use the same pinned runner and
Expand Down
36 changes: 18 additions & 18 deletions crates/compliance/src/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ impl fmt::Display for ConformanceServerEra {
pub enum ConformanceTarget {
/// Official oracle connected directly to the pinned TypeScript fixture.
Fixture,
/// Official oracle routed through the Python control plane.
Controlplane,
/// Official oracle routed through the Rust dataplane.
Dataplane,
/// Official oracle routed through the Python built-in data plane.
BuiltInDataPlane,
/// Official oracle routed through the external Rust data plane.
ExternalDataPlane,
}

impl ConformanceTarget {
Expand All @@ -102,8 +102,8 @@ impl ConformanceTarget {
pub const fn label(self) -> &'static str {
match self {
Self::Fixture => "fixture direct",
Self::Controlplane => "control-plane",
Self::Dataplane => "dataplane",
Self::BuiltInDataPlane => "built-in data-plane route",
Self::ExternalDataPlane => "external data-plane route",
}
}
}
Expand Down Expand Up @@ -723,13 +723,13 @@ pub enum ComparisonClassification {
AllCompliant,
/// Only the direct fixture run fails.
FixtureOnlyFailure,
/// Only the control-plane path fails.
/// Only the built-in data-plane route fails.
ControlplaneOnlyFailure,
/// Only the dataplane path fails.
/// Only the external data-plane route fails.
DataplaneOnlyFailure,
/// The direct fixture and control-plane path fail.
/// The direct fixture and built-in data-plane route fail.
FixtureAndControlplaneFailure,
/// The direct fixture and dataplane path fail.
/// The direct fixture and external data-plane route fail.
FixtureAndDataplaneFailure,
/// Both gateway paths fail while the direct fixture passes.
GatewaysOnlyFailure,
Expand Down Expand Up @@ -764,10 +764,10 @@ impl ComparisonClassification {
match self {
Self::AllCompliant => "all compliant",
Self::FixtureOnlyFailure => "fixture-only failure",
Self::ControlplaneOnlyFailure => "control-plane only failure",
Self::DataplaneOnlyFailure => "dataplane only failure",
Self::FixtureAndControlplaneFailure => "fixture + control-plane failure",
Self::FixtureAndDataplaneFailure => "fixture + dataplane failure",
Self::ControlplaneOnlyFailure => "built-in data-plane only failure",
Self::DataplaneOnlyFailure => "external data-plane only failure",
Self::FixtureAndControlplaneFailure => "fixture + built-in data-plane failure",
Self::FixtureAndDataplaneFailure => "fixture + external data-plane failure",
Self::GatewaysOnlyFailure => "both gateways only failure",
Self::SharedFailure => "shared failure",
Self::FixtureFailure => "fixture failure",
Expand All @@ -777,7 +777,7 @@ impl ComparisonClassification {
}
}

/// Classifies direct-fixture, control-plane, and dataplane scenario outcomes.
/// Classifies direct-fixture, built-in, and external data-plane route outcomes.
#[must_use]
pub fn classify_outcomes(
fixture: ScenarioOutcome,
Expand Down Expand Up @@ -1012,12 +1012,12 @@ pub fn render_comparison_markdown(report: &ComparisonReport) -> String {
|scenario: &ScenarioComparison| scenario.fixture_failed_checks,
),
(
"Control plane",
"Built-in data-plane route",
|scenario: &ScenarioComparison| scenario.controlplane,
|scenario: &ScenarioComparison| scenario.controlplane_failed_checks,
),
(
"Dataplane",
"External data-plane route",
|scenario: &ScenarioComparison| scenario.dataplane,
|scenario: &ScenarioComparison| scenario.dataplane_failed_checks,
),
Expand Down Expand Up @@ -1069,7 +1069,7 @@ pub fn render_comparison_markdown(report: &ComparisonReport) -> String {

output.push_str("\n## Scenarios\n\n");
output.push_str(
"| Scenario | Fixture direct | Control plane | Dataplane | Classification | Specification references |\n",
"| Scenario | Fixture direct | Built-in data-plane route | External data-plane route | Classification | Specification references |\n",
);
output.push_str("|---|---|---|---|---|---|\n");
let mut scenarios: Vec<_> = report.scenarios.iter().collect();
Expand Down
4 changes: 2 additions & 2 deletions crates/compliance/tests/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,8 @@ fn report_renders_raw_counts_and_no_expected_failure_column() {

assert!(markdown.contains("- Client specification: `2026-07-28`"));
assert!(markdown.contains("- Upstream server era: `modern`"));
assert!(markdown.contains("| Control plane | 0 | 1 | 27 |"));
assert!(markdown.contains("| Dataplane | 0 | 1 | 28 |"));
assert!(markdown.contains("| Built-in data-plane route | 0 | 1 | 27 |"));
assert!(markdown.contains("| External data-plane route | 0 | 1 | 28 |"));
assert!(markdown.contains("server\\|stateless"));
assert!(!markdown.contains("Expected by"));

Expand Down
33 changes: 32 additions & 1 deletion crates/mcp/src/auth_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ impl AuthProxy {
Self::start_with_protocol_version(upstream, bearer_token, None).await
}

/// Starts a proxy for a routed endpoint backed by the built-in data plane.
///
/// Unlike [`Self::start`], this does not require the Rust data-plane
/// response marker merely because the endpoint uses `/servers/{id}/mcp`.
///
/// # Errors
///
/// Returns the same errors as [`Self::start`].
pub async fn start_builtin_data_plane(
upstream: Url,
bearer_token: impl AsRef<str>,
) -> Result<Self, AuthProxyError> {
Self::start_configured(upstream, bearer_token, None, false).await
}

/// Starts a proxy that also rewrites MCP initialize requests to one version.
///
/// # Errors
Expand All @@ -103,6 +118,22 @@ impl AuthProxy {
upstream: Url,
bearer_token: impl AsRef<str>,
protocol_version: Option<&str>,
) -> Result<Self, AuthProxyError> {
let require_dataplane_backend = is_dataplane_endpoint(&upstream);
Self::start_configured(
upstream,
bearer_token,
protocol_version,
require_dataplane_backend,
)
.await
}

async fn start_configured(
upstream: Url,
bearer_token: impl AsRef<str>,
protocol_version: Option<&str>,
require_dataplane_backend: bool,
) -> Result<Self, AuthProxyError> {
validate_upstream(&upstream)?;
let mut authorization = HeaderValue::from_str(&format!("Bearer {}", bearer_token.as_ref()))
Expand All @@ -125,7 +156,7 @@ impl AuthProxy {
.map_err(|_| AuthProxyError::EndpointConfiguration)?;

let state = Arc::new(ProxyState {
require_dataplane_backend: is_dataplane_endpoint(&upstream),
require_dataplane_backend,
upstream,
authorization,
proxy_path,
Expand Down
30 changes: 30 additions & 0 deletions crates/mcp/tests/auth_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,3 +590,33 @@ async fn dataplane_proxy_requires_one_exact_backend_marker_before_forwarding() {
upstream.shutdown().await;
}
}

#[tokio::test]
async fn builtin_data_plane_proxy_allows_a_routed_controlplane_response() {
let upstream = TestServer::start(
Router::new()
.route("/servers/test/mcp", any(backend_marker_handler))
.with_state(BackendMarkerResponse {
markers: vec!["controlplane"],
}),
"servers/test/mcp",
)
.await;
let proxy = AuthProxy::start_builtin_data_plane(upstream.url.clone(), INJECTED_TOKEN)
.await
.expect("built-in data-plane proxy should start");

let response = client()
.get(proxy.url().clone())
.send()
.await
.expect("routed built-in response should be forwarded");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.text().await.expect("body should read"),
"private-upstream-body"
);

proxy.shutdown().await.expect("proxy should shut down");
upstream.shutdown().await;
}
10 changes: 10 additions & 0 deletions crates/platform/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ impl ComposeProject {
self
}

/// Applies the control-plane runtime settings used by conformance runs.
#[must_use]
pub fn with_conformance_runtime(mut self, repository_root: &Path) -> Self {
let overlay = repository_root.join("docker/docker-compose.cf-conformance-runtime.yaml");
if !self.files.contains(&overlay) {
self.files.push(overlay);
}
self
}

/// Enables the isolated official MCP conformance server fixture.
#[must_use]
pub fn with_conformance_fixture(self, repository_root: &Path) -> Self {
Expand Down
Loading