First off: the move to a genuinely strict, modern-only mode in the latest cfex-mcp-fast-time-server image is a great step — having a reference server that speaks only 2026-07-28 (stateless, SEP-2575) is exactly what's needed to conformance-test modern MCP clients, and the /version endpoint now advertising mcp_versions: ["2026-07-28"] makes that intent unambiguous.
While testing ContextForge's migrated federation path (mcp.client.Client, mcp==2.0.0b2 / mcp_types wire models) against that image, I ran into two wire-shape mismatches between the server's modern responses and the SDK's 2026-07-28 models. Individually small, but together they mean no negotiation mode can currently connect to the strict image: auto, legacy, and an explicit 2026-07-28 pin all fail.
Environment
- Image:
ghcr.io/ibm/cfex-mcp-fast-time-server:latest @ sha256:2f085782c3aeb0dc149e91f0e6a1326f1b14cababdd4a780478a09fbb5117824 (pulled 2026-07-22), run as --protocol 2026-07-28 --strict
- Client:
mcp==2.0.0b2 with the standalone mcp_types package (mcp_types/v2026_07_28 wire models)
- Context: ContextForge gateway (
mcp-context-forge 2.0.0 prerelease) federating to the server as an upstream, but everything below reproduces with the bare SDK.
Behavior matrix (verified by curl today)
| Probe |
Result |
server/discover with namespaced _meta (io.modelcontextprotocol/protocolVersion, …) |
HTTP 200, DiscoverResult (see Gap 1) |
initialize proposing 2025-11-25 |
JSON-RPC -32602 "Unsupported protocol version", data.supported: ["2026-07-28"] |
initialize proposing 2026-07-28 |
JSON-RPC -32602 (same body) — expected, modern mode is handshake-less |
The legacy-initialize removal itself looks intentional (and welcome for strictness) — the issue is what remains on the modern path.
Gap 1 — DiscoverResult.serverInfo is nested under _meta instead of top-level
The server's server/discover response currently looks like:
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "fast-time-server", "version": "0.1.0" } },
"capabilities": { "tools": {} },
"instructions": "Ultra-fast MCP test server.",
"resultType": "complete",
"supportedVersions": ["2026-07-28"]
}
}
But mcp_types.DiscoverResult requires server_info (alias serverInfo) as a required top-level field of the result object (mcp_types/_types.py, class DiscoverResult(CacheableResult) — server_info: Implementation, alongside supported_versions and capabilities). Because serverInfo is absent at the top level, SDK-side validation fails:
pydantic.ValidationError: serverInfo — Field required
Impact on Client(mode="auto"): negotiate_auto() treats an unparseable discover result as "not modern evidence" and falls back to the legacy initialize handshake at 2025-11-25. In the previous image that silently "worked" (the legacy path was still accepted, masking the problem). Now that legacy initialize is rejected (-32602), the fallback fails too, so auto mode cannot connect at all:
MCPError: Unsupported protocol version (from the legacy fallback initialize)
Suggested shape (matches the SDK model):
{
"result": {
"serverInfo": { "name": "fast-time-server", "version": "0.1.0" },
"capabilities": { "tools": {} },
"instructions": "Ultra-fast MCP test server.",
"resultType": "complete",
"supportedVersions": ["2026-07-28"]
}
}
(If keeping serverInfo in _meta is deliberate per a newer SEP revision, it'd be worth confirming against the mcp_types maintainers — but as of 2.0.0b2 the wire model expects it top-level.)
Gap 2 — modern tools/list responses omit required cacheScope / ttlMs
With an explicit Client(mode="2026-07-28") pin (which skips the probe and adopts the version directly), the handshake succeeds, but tools/list then fails validation:
pydantic.ValidationError: 2 validation errors for ListToolsResult
cacheScope — Field required
ttlMs — Field required
In the mcp_types/v2026_07_28 wire models, CacheableResult fields are required and defaultless: cache_scope: Literal["private", "public"] (alias cacheScope) and ttl_ms: int >= 0 (alias ttlMs) — the docstring notes "Both fields are required on the 2026-07-28 wire." The server's modern list results currently include neither, so the pinned mode can't complete a tools/list.
A conservative fix that matches the SDK's own defaulting behavior: emit "cacheScope": "private", "ttlMs": 0 on modern list results (immediately stale, no shared caching).
Reproduction
# Gap 1: discover succeeds at the wire but serverInfo is in the wrong place
curl -s -X POST http://localhost:9080/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'MCP-Method: server/discover' \
-d '{"jsonrpc":"2.0","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"probe","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}},"id":1}'
# All three SDK modes fail
python - <<'EOF'
import asyncio
from mcp import Client
async def probe(mode):
try:
async with Client("http://localhost:9080/mcp", mode=mode) as c:
await c.list_tools()
print(f"{mode}: OK")
except Exception as e:
print(f"{mode}: FAIL")
for m in ["auto", "legacy", "2026-07-28"]:
asyncio.run(probe(m))
EOF
Impact
Any SDK-2.0.0b2 client — ContextForge's federation path included — currently cannot register or call the strict image in any mode. With Gap 1 fixed, auto mode should negotiate 2026-07-28 end-to-end; with Gap 2 fixed, explicit version pins and modern catalog sync should complete. Happy to re-run our full negotiation E2E matrix (auto/legacy/pin, wire-level captures) against a patched image and report back.
First off: the move to a genuinely strict, modern-only mode in the latest
cfex-mcp-fast-time-serverimage is a great step — having a reference server that speaks only2026-07-28(stateless, SEP-2575) is exactly what's needed to conformance-test modern MCP clients, and the/versionendpoint now advertisingmcp_versions: ["2026-07-28"]makes that intent unambiguous.While testing ContextForge's migrated federation path (
mcp.client.Client,mcp==2.0.0b2/mcp_typeswire models) against that image, I ran into two wire-shape mismatches between the server's modern responses and the SDK's 2026-07-28 models. Individually small, but together they mean no negotiation mode can currently connect to the strict image:auto,legacy, and an explicit2026-07-28pin all fail.Environment
ghcr.io/ibm/cfex-mcp-fast-time-server:latest@sha256:2f085782c3aeb0dc149e91f0e6a1326f1b14cababdd4a780478a09fbb5117824(pulled 2026-07-22), run as--protocol 2026-07-28 --strictmcp==2.0.0b2with the standalonemcp_typespackage (mcp_types/v2026_07_28wire models)mcp-context-forge2.0.0 prerelease) federating to the server as an upstream, but everything below reproduces with the bare SDK.Behavior matrix (verified by curl today)
server/discoverwith namespaced_meta(io.modelcontextprotocol/protocolVersion, …)DiscoverResult(see Gap 1)initializeproposing2025-11-25-32602"Unsupported protocol version",data.supported: ["2026-07-28"]initializeproposing2026-07-28-32602(same body) — expected, modern mode is handshake-lessThe legacy-initialize removal itself looks intentional (and welcome for strictness) — the issue is what remains on the modern path.
Gap 1 —
DiscoverResult.serverInfois nested under_metainstead of top-levelThe server's
server/discoverresponse currently looks like:{ "id": 1, "jsonrpc": "2.0", "result": { "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "fast-time-server", "version": "0.1.0" } }, "capabilities": { "tools": {} }, "instructions": "Ultra-fast MCP test server.", "resultType": "complete", "supportedVersions": ["2026-07-28"] } }But
mcp_types.DiscoverResultrequiresserver_info(aliasserverInfo) as a required top-level field of the result object (mcp_types/_types.py,class DiscoverResult(CacheableResult)—server_info: Implementation, alongsidesupported_versionsandcapabilities). BecauseserverInfois absent at the top level, SDK-side validation fails:Impact on
Client(mode="auto"):negotiate_auto()treats an unparseable discover result as "not modern evidence" and falls back to the legacyinitializehandshake at2025-11-25. In the previous image that silently "worked" (the legacy path was still accepted, masking the problem). Now that legacy initialize is rejected (-32602), the fallback fails too, soautomode cannot connect at all:Suggested shape (matches the SDK model):
{ "result": { "serverInfo": { "name": "fast-time-server", "version": "0.1.0" }, "capabilities": { "tools": {} }, "instructions": "Ultra-fast MCP test server.", "resultType": "complete", "supportedVersions": ["2026-07-28"] } }(If keeping
serverInfoin_metais deliberate per a newer SEP revision, it'd be worth confirming against themcp_typesmaintainers — but as of2.0.0b2the wire model expects it top-level.)Gap 2 — modern
tools/listresponses omit requiredcacheScope/ttlMsWith an explicit
Client(mode="2026-07-28")pin (which skips the probe and adopts the version directly), the handshake succeeds, buttools/listthen fails validation:In the
mcp_types/v2026_07_28wire models,CacheableResultfields are required and defaultless:cache_scope: Literal["private", "public"](aliascacheScope) andttl_ms: int >= 0(aliasttlMs) — the docstring notes "Both fields are required on the 2026-07-28 wire." The server's modern list results currently include neither, so the pinned mode can't complete atools/list.A conservative fix that matches the SDK's own defaulting behavior: emit
"cacheScope": "private", "ttlMs": 0on modern list results (immediately stale, no shared caching).Reproduction
Impact
Any SDK-2.0.0b2 client — ContextForge's federation path included — currently cannot register or call the strict image in any mode. With Gap 1 fixed,
automode should negotiate2026-07-28end-to-end; with Gap 2 fixed, explicit version pins and modern catalog sync should complete. Happy to re-run our full negotiation E2E matrix (auto/legacy/pin, wire-level captures) against a patched image and report back.