MCP protocol compatibility¶
Preloop's preferred MCP URL is /mcp. Existing /mcp/v1 configurations use
exactly the same authenticated transport. The app internally rewrites /mcp
to /mcp/v1 before routing; it does not redirect the HTTP request. Neither the
protocol revision nor the Python SDK major version introduces a new URL.
Tested matrix¶
The migration uses released mcp==2.3.0 and fastmcp==4.1.3. All three
application dependency locks include those versions and the SDK's httpx2
transport. The table distinguishes wire tests from tests using a separate
installation of the previous SDK.
| Protocol revision | Inbound /mcp and /mcp/v1 |
Outbound Streamable HTTP |
|---|---|---|
2026-07-28 |
server/discover, direct requests with per-request metadata; no initialization or protocol session |
Discovery, version validation, per-request metadata and mirrored headers |
2025-11-25 |
initialize, initialized notification, listing and calls |
Discovery rejection followed by initialization; server-issued session IDs, GET stream and DELETE cleanup |
2025-06-18 |
Same legacy path, with the requested revision retained | Same legacy path with the selected revision retained |
2025-03-26 |
Same legacy path; absent version header remains compatible | Same legacy path with the selected revision retained |
2024-11-05 |
SDK compatibility on the Streamable HTTP endpoint | SDK compatibility on Streamable HTTP |
The last row does not add the deprecated, separate HTTP+SSE transport from 2024. Preloop already used Streamable HTTP for these SDK compatibility connections. Neither inbound nor outbound URLs automatically fall back to an HTTP+SSE endpoint.
Wire tests exercise every row. Separate cross-major process tests use
mcp==1.30.0 / fastmcp==3.4.11: the new outbound pool connects to that
released legacy server, and the legacy client lists and calls tools on both
Preloop URLs. These are synthetic local peers, not claims of compatibility
with every hosted provider.
Preloop retains its existing stateless legacy HTTP configuration. An inbound legacy initialization does not mint a session ID or require sticky routing; a standalone GET stream is unavailable. Outbound connections still honor upstream servers that assign legacy session IDs. Each pool operation owns and closes its transport in the same async task.
Validation and execution boundary¶
For modern HTTP requests the SDK validates protocol, method, name, and annotated
parameter headers against the body. Missing or mismatched required headers
return HTTP 400 / JSON-RPC -32020. Unsupported versions return HTTP 400 /
-32022 with requested and supported; unknown modern methods return HTTP
404 / -32601. Modern GET and DELETE return 405; session and resumption
headers do not establish a modern session. Origin protection is explicitly
enabled, including for deployments where FastMCP's default would disable it.
SDK 2.3 routes eras by the HTTP version header. Preloop adds a bounded ASGI guard so modern body metadata with a missing, duplicated, or legacy header cannot enter the legacy handler. Authentication remains outside this guard. The guard inspects only requests that could enter the legacy handler, uses the existing artifact request ceiling, and spools large bodies to disk. It releases its parsed tree before SDK dispatch; modern requests go directly to the SDK. FastMCP 4.1.3 does not expose the SDK's request-size option, so a lifespan adapter sets the SDK manager's body limit to that same artifact ceiling. The SDK owns the remaining schema validation and request dispatch.
Outbound negotiation uses ClientSession.discover() before any operation.
An HTTP 400 discovery rejection without a recognized modern error, or an
HTTP 200 JSON-RPC -32601 (method not found) without an explicit modern response
version header, permits fallback to initialize. The latter supports legacy
JSON-RPC servers that report unknown methods with HTTP 200. Authentication
failures, redirects, outages, and modern errors (including HTTP 404 / -32601)
do not cause a downgrade. Supported modern version selection
and its bounded discovery retry are provided by the SDK. Legacy era selection
is cached on the pool entry; changing connection configuration invalidates that
entry. Modern connections discover on each short-lived session.
The pool lists modern tool schemas before dispatch so the SDK can emit
Mcp-Param-* headers and exclude invalid x-mcp-header definitions. It uses the
lower-level session API, which does not automatically replay a tool call after
a header mismatch. HTTP redirects are rejected before the SDK's own redirect
handler can resend a POST. A lost response, auth error, or disconnect never
replays a dispatched tool. OAuth lifecycle refresh, when present, remains
outside this adapter and completes before dispatch.
Modern and legacy calls enter the same DynamicFastMCP.call_tool policy and
approval path. Tests assert that policy denial and approval refusal prevent
upstream invocation, and allowed calls dispatch once. SSE progress remains
request-scoped; disconnecting a modern response stream cancels its handler.
The existing OAuth discovery, consent, authorization and approval regression
suites run with the new packages.
Manual scans return HTTP 502 when discovery fails, even if cached tools remain. The console refreshes the server's unhealthy status and displays the diagnostic; a cached inventory is never reported as a successful new scan.
Python API migration¶
streamablehttp_clientis replaced bystreamable_http_client(url, http_client=...), which yields two streams. Preloop owns thehttpx2.AsyncClientlifetime, timeouts, auth and response policy. Non-MCP HTTP services continue to usehttpx.- Protocol models use snake_case Python attributes, including
input_schema,structured_content,is_error, andmime_type. JSON keeps its specified camelCase names; usemodel_dump(by_alias=True)when building wire blocks. SDK v2 ignores unknown annotation fields; the defined tool hints are retained. - FastMCP's
ToolResultcomes fromfastmcp.tools. Removedtask_metaarguments are no longer forwarded. Approval progress reads FastMCP's raw metadata dictionary. No background task extension is advertised by this change. - The older low-level adapter uses SDK
add_request_handlerwith explicit result models. It obtains identity from the authenticated middleware context, never from client-supplied_meta. - SDK-native fixture servers use
MCPServer, with HTTP host/port supplied when running the transport. Resource URIs are strings. Python representations use snake_case; JSON content blocks retain the protocol spelling.
Reproducing the cross-major checks¶
Create a separate environment; do not downgrade the application's environment:
export PRELOOP_DISABLE_TELEMETRY=true
uv venv /tmp/preloop-mcp-legacy
uv pip install --python /tmp/preloop-mcp-legacy/bin/python \
'mcp==1.30.0' 'fastmcp==3.4.11'
MCP_LEGACY_PYTHON=/tmp/preloop-mcp-legacy/bin/python \
.venv/bin/pytest backend/tests/services/test_mcp_released_interop.py
.venv/bin/pytest backend/tests/services/test_mcp_protocol.py \
backend/tests/services/test_mcp_outbound_protocol.py
The process tests listen only on disposable loopback ports. Broader database regressions require the migrated local PostgreSQL setup described in native development.
Normative and released API sources¶
Audited on 2026-10-22:
- Protocol revision registry
- 2026-07-28 versioning
- 2026-07-28 Streamable HTTP
- Python SDK migration guide
- MCP 2.3.0 and FastMCP 4.1.3 package metadata and installed source signatures