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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

## [0.6.1] - 2026-10-29

### Fixed

- Preserve legacy fixture tool results while their SSE response stream is
disconnected so clients can recover them with `Last-Event-ID`. Validate
delayed resumption repeatedly before publishing fixture images. This removes
catalog-order-dependent header-validation timeouts and the fixture's SSE
polling timeout.

## [0.6.0] - 2026-10-14

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cf-integration"
version = "0.6.0"
version = "0.6.1"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
Expand Down
5 changes: 5 additions & 0 deletions docker/mcp-conformance-server.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ RUN git clone https://github.com/modelcontextprotocol/conformance.git mcp-confor
WORKDIR /opt/mcp-conformance/examples/servers/typescript
RUN npm ci

COPY docker/mcp-typescript-sdk-resumption.patch /tmp/mcp-typescript-sdk-resumption.patch
RUN test "$(node -p "require('./node_modules/@modelcontextprotocol/sdk/package.json').version")" = "1.29.0" \
&& git -C /opt/mcp-conformance apply --check --directory=examples/servers/typescript/node_modules/@modelcontextprotocol/sdk /tmp/mcp-typescript-sdk-resumption.patch \
&& git -C /opt/mcp-conformance apply --directory=examples/servers/typescript/node_modules/@modelcontextprotocol/sdk /tmp/mcp-typescript-sdk-resumption.patch

COPY docker/mcp-conformance.patch /tmp/mcp-conformance.patch
RUN git apply --check /tmp/mcp-conformance.patch && git apply /tmp/mcp-conformance.patch

Expand Down
30 changes: 30 additions & 0 deletions docker/mcp-typescript-sdk-resumption.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--- a/dist/esm/server/webStandardStreamableHttp.js
+++ b/dist/esm/server/webStandardStreamableHttp.js
@@ -699,14 +699,16 @@
throw new Error(`No connection established for request ID: ${String(requestId)}`);
}
const stream = this._streamMapping.get(streamId);
- if (!this._enableJsonResponse && stream?.controller && stream?.encoder) {
+ if (!this._enableJsonResponse) {
// For SSE responses, generate event ID if event store is provided
let eventId;
if (this._eventStore) {
eventId = await this._eventStore.storeEvent(streamId, message);
}
// Write the event to the response stream
- this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
+ if (stream?.controller && stream?.encoder) {
+ this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
+ }
}
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
this._requestResponseMap.set(requestId, message);
@@ -717,7 +719,7 @@
const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id));
if (allResponsesReady) {
if (!stream) {
- throw new Error(`No connection established for request ID: ${String(requestId)}`);
+ return;
}
if (this._enableJsonResponse && stream.resolveJson) {
// All responses ready, send as JSON
49 changes: 48 additions & 1 deletion scripts/smoke_harness_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,58 @@
import subprocess
import time
from pathlib import Path
from urllib.request import Request, urlopen


def docker(*arguments):
"""Run Docker and return its output, failing on nonzero exit."""
return subprocess.check_output(["docker", *arguments], text=True).strip()


def verify_resumption(container):
"""Recover results produced while a legacy response stream is disconnected."""
address = docker("port", container, "3000/tcp")
url = f"http://{address}/mcp"
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"MCP-Protocol-Version": "2025-11-25",
}

def post(message):
request = Request(url, data=json.dumps(message).encode(), headers=headers)
return urlopen(request, timeout=10)

with post({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "resumption-smoke", "version": "1"}},
}) as response:
headers["Mcp-Session-Id"] = response.headers["Mcp-Session-Id"]
response.read()
try:
with post({"jsonrpc": "2.0", "method": "notifications/initialized"}) as response:
assert response.status == 202, response.status
for request_id in range(2, 5):
with post({"jsonrpc": "2.0", "id": request_id, "method": "tools/call", "params": {"name": "test_reconnection", "arguments": {}}}) as response:
priming = response.read().decode()
fields = dict(line.split(":", 1) for line in priming.splitlines() if ":" in line)
event_id = fields["id"].strip()
time.sleep(int(fields["retry"].strip()) / 1000)
request = Request(url, headers={**headers, "Last-Event-ID": event_id})
with urlopen(request, timeout=10) as response:
for line in response:
if line.startswith(b"data:"):
message = json.loads(line[5:])
break
else:
raise AssertionError("Resumed stream ended without the tool result")
assert message["id"] == request_id, message
assert "Reconnection test completed successfully" in message["result"]["content"][0]["text"], message
finally:
with urlopen(Request(url, headers=headers, method="DELETE"), timeout=10) as response:
assert response.status == 200, response.status


def main():
"""Smoke test images before their release tags are published."""
version = os.environ["IMAGE_VERSION"]
Expand All @@ -36,7 +81,7 @@ def main():
docker("run", "--rm", "--entrypoint", "mcp-inspector", f"{prefix}tools:{version}", "--help")

for era in ("legacy", "modern", "dual"):
container = docker("run", "--detach", "--env", f"MCP_CONFORMANCE_SERVER_ERA={era}", f"{prefix}fixture:{version}")
container = docker("run", "--detach", "--publish", "127.0.0.1::3000", "--env", f"MCP_CONFORMANCE_SERVER_ERA={era}", f"{prefix}fixture:{version}")
try:
for _ in range(60):
result = subprocess.run(
Expand All @@ -51,6 +96,8 @@ def main():
time.sleep(1)
else:
raise RuntimeError(f"{era} fixture did not become ready: {docker('logs', container)}")
if era in ("legacy", "dual"):
verify_resumption(container)
finally:
docker("rm", "--force", container)
print("All harness images passed smoke checks")
Expand Down
1 change: 1 addition & 0 deletions src/infrastructure/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ static ASSETS: LazyLock<Vec<EmbeddedAsset>> = LazyLock::new(|| {
asset!("docker/nginx.cf-dataplane.conf"),
asset!("docker/nginx.cf-dataplane-standalone.conf.template"),
asset!("docker/mcp-conformance.patch"),
asset!("docker/mcp-typescript-sdk-resumption.patch"),
asset!("docker/helpers.Dockerfile"),
asset!("docker/controlplane-principal.cel"),
asset!("scripts/live_protocol/sitecustomize.py"),
Expand Down
Loading