Repository navigation
Request customizers can't tell which connection a request belongs to #1074
Description
Activity
Thanks for the detailed writeup.
You're right, as of now, we have adopted he pattern one customizer per client (see
McpClientCustomizer). If we fix #1073 , would that be enough?Note that with the current implementation, you don't need a URL-to-name map. You can put the connection name (or any per-connection metadata) in the
McpTransportContext. Depends on the client type1. Sync clients —
transportContextProviderMcpClient.sync(transport) .transportContextProvider(() -> McpTransportContext.create( Map.of("connectionName", "first-server"))) .build();
This supplier is invoked and written into the Reactor context before every blocking call, so your customizer sees it:
String connection = (String) context.get("connectionName");
2. Async clients — Reactor context directly
There's no
contextProviderhook for async; you write the context around the call yourself:client.callTool(request) .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, McpTransportContext.create(Map.of("connectionName", "first-server")))) .subscribe();
Usually it's propagated from the caller (e.g. Spring AI). If you'd like passing the context around explicitly, you could extend
McpAsyncClientto add stuff inside the context before every call. It's clunky but works.- addedwaiting for userWaiting for user feedback or more detailsWaiting for user feedback or more details
on Aug 7, 2026 iuliiasobolevska commented
on Aug 8, 2026 ContributorAuthorMore actionsThank you for taking a look!
Agree on the transport context, and that's what we ship today. We put the connection name in under a constant and expose a reader for it so an application's own customizer can pull it back out. There's no URL-to-name map anywhere; I should have phrased it better in the issue.
Fixing #1073 wouldn't be enough. These two issues are independent. #1073 is about how many customizers survive on a transport and in what order. It doesn't tell any of them which connection they're on.
The one-customizer-per-client pattern does cover framework code:
McpClientCustomizeris handed the connection name at build time, so we build one instance per connection and close over it. What's left is application code. Once spring-projects/spring-ai#6716 is released, the supported hook would be a bareMcpAsyncHttpClientRequestCustomizerbean, and that's one bean applied to every connection. It can't close over a name because it was constructed before any connection existed.The transport context covers that case too, but only if whoever built the client owns
transportContextProvider. That setter assigns rather than composes (McpClient.java#L503), so an application that sets its own silently drops ours. It's also sync only,AsyncSpechas no equivalent. And there's no agreed key for the connection name. We made up our own string, so an application customizer has to read a key that's specific to us, and anyone else doing the same thing picks a different one. A key defined by the SDK would be something a customizer could read, no matter who built the client.A signature change would be a breaking change and not suitable for a P3. The minimal change that satisfies the ask is static metadata on the transport builder:
HttpClientStreamableHttpTransport.builder(url) .metadata(Map.of("connectionName", "first-server")) .build();
merged into the context the transport already extracts before each
customize(...)call. One transport is one connection, so the name is always right for the connection the customizer is on. It doesn't go throughtransportContextProvider, so an application setting its own can't drop it. And it works for async today, without #1075. If the SDK documents the key name, that covers the naming point too.Let's step back.
Why does the customizer need to know which transport a request is linked to? What exactly are you trying to accomplish? What's your use-case?
(If you are using an AI agent, please refrain from explaining all the details and focus on your use-case. For example, avoid "Someone doing X would result in Y" unless it is something your users actually do)
Expected Behavior
A request customizer should be able to find out which configured connection the request it is customizing belongs to.
The cheaper option is a well-known key in the transport context, seeded by the transport, so no signature changes:
The transport already knows the name when it is built, so it would set it once in the builder and merge it into whatever context the caller supplied.
The other option is to put it in the signature:
This is breaking, but it lines up with
McpClientCustomizer#customize(String name, T spec)in Spring AI, which already takes the name first. If the interface is going to change shape at some point anyway, that's the more consistent end state.Current Behavior
A request customizer runs for every outbound HTTP request on a transport, but isn't told which connection the request belongs to. On
mainatfd00498:The only discriminator available is the endpoint URI. There is no connection name in the context either: nothing in
mcp-coredefines or sets one, and the transport passes the caller's context through untouched.HttpClientStreamableHttpTransportreads it the same way on all four request paths, at lines 205, 291, 430, and 514:So a customizer that needs per-connection behavior has to build its own URL-to-name map and reverse-match on the endpoint for every request. That map has to be assembled by re-reading the same configuration that the framework already parsed, and the match breaks as soon as a URL is templated, redirected, or differs by a trailing slash.
The name is known at the point the customizer is installed, since the transport is built per connection. It just isn't carried through to the call.
Context
Callers configure connections by name. In Spring AI, for example:
Anything per-connection needs that name: a different credential or audience per server, per-connection metrics, per-connection logging. We hit this with credentials, where each MCP server is a distinct audience, and the token must be minted for the correct one.
Alternatives considered:
endpoint. Fragile for the reasons above, and it duplicates configuration parsing.Is there a reason the connection name was deliberately omitted from the customizer contract? I might be missing something.