How I Fixed a Core MCP Transport Compatibility Bug in LangChain4j
This is a technical retrospective of one of my merged open-source contributions in the LangChain4j core MCP client path.
Primary References
- Issue: langchain4j/langchain4j#4582
- Pull Request: langchain4j/langchain4j#4584
- Merged commit: 88486da56c7b56d2833c76a1a9748d897ffebf35
- Main source file: StreamableHttpMcpTransport.java
- Test file: StreamableHttpMcpTransportTest.java
Background
The issue looked small at first glance, but it sat on a high-leverage path: MCP stream transport initialization. If transport negotiation fails or hangs, the agent runtime cannot reliably discover tools or complete startup flows.
The failure mode was tied to protocol negotiation behavior across heterogeneous MCP server implementations and proxies. In practical terms, this created intermittent incompatibility for streamable HTTP MCP sessions.
What Was Broken
During transport setup, HTTP version behavior could become environment-sensitive. Some server setups did not behave consistently under upgrade/negotiation paths, causing stream sessions to become fragile.
This kind of bug is expensive in production because it appears as downstream instability: users see flaky tool calls, while logs often only show generic transport errors.
Design Goals for the Fix
- Keep default behavior safe for the majority path.
- Retain explicit configurability for edge deployments.
- Avoid unnecessary public API leakage of low-level JDK HTTP details.
- Add regression tests directly at the transport boundary.
Implementation (with Source Links)
The final merged change set evolved through several review rounds and API-shape refinements. Two representative commits from that process:
- 7c3388d - force HTTP/1.1 for streamable HTTP transport (initial fix)
- 108e3cc - default HTTP/2 with explicit HTTP/1.1 opt-in (review-driven refinement)
Core transport idea (simplified excerpt)
// Simplified shape of the merged approach
HttpClient.Builder clientBuilder = HttpClient.newBuilder();
if (configuredVersion == HTTP_1_1) {
clientBuilder.version(HttpClient.Version.HTTP_1_1);
} else {
clientBuilder.version(HttpClient.Version.HTTP_2);
}
HttpClient client = clientBuilder.build();
Caller-side opt-in pattern (simplified)
StreamableHttpMcpTransport transport = StreamableHttpMcpTransport.builder()
.baseUri(serverUri)
.setHttpVersion1_1() // explicit downgrade when server requires it
.build();
Regression Test Strategy
I added and updated tests around transport version behavior so future changes cannot silently regress interoperability.
Test intent (simplified)
@Test
void shouldApplyConfiguredHttpVersionForStreamableTransport() {
StreamableHttpMcpTransport transport = StreamableHttpMcpTransport.builder()
.baseUri(URI.create("http://localhost:8080/mcp"))
.setHttpVersion1_1()
.build();
assertThat(extractHttpClientVersion(transport)).isEqualTo(HttpClient.Version.HTTP_1_1);
}
Validation and CI
Validation used module-scoped tests and format gates, then repeated verification after each maintainer feedback round:
./mvnw -pl langchain4j-mcp,langchain4j-http-client -am -Dtest=StreamableHttpMcpTransportTest -Dsurefire.failIfNoSpecifiedTests=false test
I also addressed style gate failures (Spotless) in the same PR to keep the merge path clean.
Why This PR Is High Value
- Scope: core runtime compatibility, not a cosmetic patch.
- Impact: directly improves MCP transport reliability across environments.
- Engineering quality: review-driven API refinement plus targeted regression tests.
- Career signal: demonstrates diagnosis, implementation, testing, and maintainer collaboration end-to-end.
Takeaway
For open-source contributors targeting real engineering credibility, this is the pattern to pursue: pick a small but critical failure mode, keep the fix minimal and testable, and iterate fast with maintainers until merge.