Allow composing request customizers on the HTTP client transport builders
Maintainer antworten meist innerhalb von 3 Tagen
@Kehrlann arbeitet bereits daran.
Seit 07.8.2026.
Bewertung
Dieses Issue wurde noch nicht bewertet.
Beschreibung
Expected Behavior
A caller should be able to add a request customizer to a transport builder without discarding the ones already registered on it.
Two shapes would work. The smaller one is a getter, so a caller can compose by hand:
var existing = builder.getAsyncHttpRequestCustomizer();
builder.asyncHttpRequestCustomizer(new DelegatingMcpAsyncHttpClientRequestCustomizer(List.of(existing, mine)));
The better one is an additive setter alongside the existing replace-all one:
public Builder addAsyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer customizer) {
Assert.notNull(customizer, "customizer must not be null");
this.httpRequestCustomizers.add(customizer);
return this;
}
with build() collapsing the list through DelegatingMcpAsyncHttpClientRequestCustomizer, plus the sync twin for McpSyncHttpClientRequestCustomizer.
Either would apply to both HttpClientStreamableHttpTransport.Builder and HttpClientSseClientTransport.Builder.
Making the existing setter additive would be the cleanest API, but it would change behavior for anyone who calls it twice today and expects a replacement, so it probably belongs in a major version.
Current Behavior
Both builders hold exactly one customizer, and the setter assigns it. On main at fd00498:
// HttpClientStreamableHttpTransport
private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer; // 129
private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP; // 721
this.httpRequestCustomizer = asyncHttpRequestCustomizer; // 852
// HttpClientSseClientTransport
private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer; // 124
private McpAsyncHttpClientRequestCustomizer httpRequestCustomizer = McpAsyncHttpClientRequestCustomizer.NOOP; // 192
this.httpRequestCustomizer = asyncHttpRequestCustomizer; // 301
The sync overload routes through McpAsyncHttpClientRequestCustomizer.fromSync(...) into the same field, so a sync customizer and an async one overwrite each other as well.
There is no add... variant and no getter. You can't install a customizer without discarding whatever was there before, and you can't find out that you did.
There's no way to work around it outside the SDK either. The field is private, the builder is the only path to it, and by the time you hold a built transport, the customizer has already been captured.
DelegatingMcpAsyncHttpClientRequestCustomizer and DelegatingMcpSyncHttpClientRequestCustomizer already exist in io.modelcontextprotocol.client.transport.customizer and do exactly the chaining the additive setter needs. The builders just don't use them.
Context
For an application customizing its own transport, one slot is enough. It stops being enough once more than one party wants a header on outbound MCP requests: an auth integration attaching a credential, a tracing library adding a correlation ID, the application adding something of its own. They all target the same setter, and the last call replaces the rest with no error and nothing logged.
A library in that position can't guarantee its header is present. The symptom is a missing header at runtime on a request that otherwise looks fine, rather than anything at startup.
Alternatives considered:
- Install a chain of our own and document "please don't call
asyncHttpRequestCustomizerdirectly". That's a convention, not a contract, and it breaks silently. - Collect every participant before
build()and set the composed customizer once. This works, but only the code that owns the builder can do it. In a Spring Boot application, that's the autoconfiguration, so the problem moves a layer up instead of getting solved, and the composition logic has to be rebuilt by every framework that wraps the SDK. This is what we do today. - Wrap the built transport. Not viable, the customizer is consumed inside the transport's own request paths.
The workaround holds, but it puts the responsibility in the wrong place. A getter on its own would unblock callers immediately without changing any existing behavior.
- Vorherrschende Sprache
- Java
- Sterne
- 3.7k
- Forks
- 1.2k
- Ø Merge
- 2 T. 5 Std.
- Gemergte PRs (30 T.)
- 4
Entwicklungsumgebung
- Kein Dockerfile und keine Docker-Compose-Datei
- Keine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus modelcontextprotocol/java-sdk
-
HttpServletStreamableServerTransportProvider: GET stream sends no status or headers until the first eventEvtl. vergeben @karthiksenv hat das vor 2 Tagen übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 86/100
modelcontextprotocol/java-sdk#1155 ·
Maintainer antworten meist innerhalb von 3 Tagen
-
Client request handlers that complete empty send no JSON-RPC responseEvtl. vergeben @1fanwang hat das vor 32 Tagen übernommen. Offenarea/client bug P2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 84/100
modelcontextprotocol/java-sdk#1124 · 5 Kommentare ·
Maintainer antworten meist innerhalb von 3 Tagen
-
ServerCapabilities.logging is added unconditionally, overriding the caller's explicit capabilitiesEvtl. vergeben @1yuxiangJ hat das vor 47 Tagen übernommen. Offenbug P2 ready for work
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
modelcontextprotocol/java-sdk#1086 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 3 Tagen
-
Reject listRoots if not supported by client, without sending any requestEvtl. vergeben @nikita-kibitkin hat das vor 71 Tagen übernommen. Offenenhancement good first issue P3
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
modelcontextprotocol/java-sdk#1067 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 3 Tagen
-
StdioClientTransport missing explicit UTF-8 charset in InputStreamReader (same issue as #295, but on client side)Evtl. vergeben @suryateja-g13 hat das vor 142 Tagen übernommen. Offenbug P2 ready for work
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 74/100
modelcontextprotocol/java-sdk#898 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 3 Tagen
Alle Issues in modelcontextprotocol/java-sdk
Ähnliche Issues
-
component/operate kind/feature-request
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
Maintainer antworten meist innerhalb von 1 Tag
-
Forge coverage prompts carry text the agent cannot act onEvtl. vergeben @graalvmbot hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
oracle/graalvm-reachability-metadata#10572 ·
Maintainer antworten meist innerhalb von 1 Tag
-
[CI] Core CI doesn't run for changes to amoro-format-lance (and amoro-web)Evtl. vergeben @MarkAlex1234 hat das heute übernommen. Offen
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
Maintainer antworten meist innerhalb von 2 Tagen
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
-
area/docs
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
Maintainer antworten meist innerhalb von 1 Tag