Skip to content

Commit ca0af02

Browse files
committed
Support custom HTTP headers for MCP transport
Closes GH-3948 Signed-off-by: Yanming Zhou <[email protected]>
1 parent bd1834d commit ca0af02

File tree

14 files changed

+204
-19
lines changed

14 files changed

+204
-19
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package org.springframework.ai.mcp.client.common.autoconfigure.properties;
1818

1919
import java.util.HashMap;
20+
import java.util.List;
2021
import java.util.Map;
2122

2223
import org.springframework.boot.context.properties.ConfigurationProperties;
24+
import org.springframework.lang.Nullable;
2325

2426
/**
2527
* Configuration properties for Server-Sent Events (SSE) based MCP client connections.
@@ -53,6 +55,7 @@
5355
* </pre>
5456
*
5557
* @author Christian Tzolov
58+
* @author Yanming Zhou
5659
* @since 1.0.0
5760
* @see SseParameters
5861
*/
@@ -82,8 +85,9 @@ public Map<String, SseParameters> getConnections() {
8285
*
8386
* @param url the URL endpoint for SSE communication with the MCP server
8487
* @param sseEndpoint the SSE endpoint for the MCP server
88+
* @param headers the custom HTTP headers for the MCP server
8589
*/
86-
public record SseParameters(String url, String sseEndpoint) {
90+
public record SseParameters(String url, String sseEndpoint, @Nullable Map<String, List<String>> headers) {
8791
}
8892

8993
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStreamableHttpClientProperties.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package org.springframework.ai.mcp.client.common.autoconfigure.properties;
1818

1919
import java.util.HashMap;
20+
import java.util.List;
2021
import java.util.Map;
2122

2223
import org.springframework.boot.context.properties.ConfigurationProperties;
24+
import org.springframework.lang.Nullable;
2325

2426
/**
2527
* Configuration properties for Streamable Http client connections.
@@ -39,6 +41,7 @@
3941
* </pre>
4042
*
4143
* @author Christian Tzolov
44+
* @author Yanming Zhou
4245
* @see ConnectionParameters
4346
*/
4447
@ConfigurationProperties(McpStreamableHttpClientProperties.CONFIG_PREFIX)
@@ -67,8 +70,9 @@ public Map<String, ConnectionParameters> getConnections() {
6770
*
6871
* @param url the URL endpoint for Streamable Http communication with the MCP server
6972
* @param endpoint the endpoint for the MCP server
73+
* @param headers the custom HTTP headers for the MCP server
7074
*/
71-
public record ConnectionParameters(String url, String endpoint) {
75+
public record ConnectionParameters(String url, String endpoint, @Nullable Map<String, List<String>> headers) {
7276
}
7377

7478
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.ai.mcp.client.common.autoconfigure.properties;
1818

19+
import java.util.List;
1920
import java.util.Map;
2021

2122
import org.junit.jupiter.api.Test;
@@ -30,6 +31,7 @@
3031
* Tests for {@link McpSseClientProperties}.
3132
*
3233
* @author Christian Tzolov
34+
* @author Yanming Zhou
3335
*/
3436
class McpSseClientPropertiesTests {
3537

@@ -105,7 +107,7 @@ void connectionWithNullUrl() {
105107
void sseParametersRecord() {
106108
String url = "http://test-server:8080/events";
107109
String sseUrl = "/sse";
108-
McpSseClientProperties.SseParameters params = new McpSseClientProperties.SseParameters(url, sseUrl);
110+
McpSseClientProperties.SseParameters params = new McpSseClientProperties.SseParameters(url, sseUrl, null);
109111

110112
assertThat(params.url()).isEqualTo(url);
111113
assertThat(params.sseEndpoint()).isEqualTo(sseUrl);
@@ -114,7 +116,7 @@ void sseParametersRecord() {
114116
@Test
115117
void sseParametersRecordWithNullSseEndpoint() {
116118
String url = "http://test-server:8080/events";
117-
McpSseClientProperties.SseParameters params = new McpSseClientProperties.SseParameters(url, null);
119+
McpSseClientProperties.SseParameters params = new McpSseClientProperties.SseParameters(url, null, null);
118120

119121
assertThat(params.url()).isEqualTo(url);
120122
assertThat(params.sseEndpoint()).isNull();
@@ -129,7 +131,8 @@ void configPrefixConstant() {
129131
void yamlConfigurationBinding() {
130132
this.contextRunner
131133
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080/events",
132-
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081/events")
134+
"spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081/events",
135+
"spring.ai.mcp.client.sse.connections.server2.headers.Authorization=Bearer <access_token>")
133136
.run(context -> {
134137
McpSseClientProperties properties = context.getBean(McpSseClientProperties.class);
135138
assertThat(properties.getConnections()).hasSize(2);
@@ -139,6 +142,8 @@ void yamlConfigurationBinding() {
139142
assertThat(properties.getConnections().get("server2").url())
140143
.isEqualTo("http://otherserver:8081/events");
141144
assertThat(properties.getConnections().get("server2").sseEndpoint()).isNull();
145+
assertThat(properties.getConnections().get("server2").headers()).containsEntry("Authorization",
146+
List.of("Bearer <access_token>"));
142147
});
143148
}
144149

@@ -150,21 +155,21 @@ void connectionMapManipulation() {
150155

151156
// Add a connection
152157
connections.put("server1",
153-
new McpSseClientProperties.SseParameters("http://localhost:8080/events", "/sse"));
158+
new McpSseClientProperties.SseParameters("http://localhost:8080/events", "/sse", null));
154159
assertThat(properties.getConnections()).hasSize(1);
155160
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://localhost:8080/events");
156161
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/sse");
157162

158163
// Add another connection
159164
connections.put("server2",
160-
new McpSseClientProperties.SseParameters("http://otherserver:8081/events", null));
165+
new McpSseClientProperties.SseParameters("http://otherserver:8081/events", null, null));
161166
assertThat(properties.getConnections()).hasSize(2);
162167
assertThat(properties.getConnections().get("server2").url()).isEqualTo("http://otherserver:8081/events");
163168
assertThat(properties.getConnections().get("server2").sseEndpoint()).isNull();
164169

165170
// Replace a connection
166171
connections.put("server1",
167-
new McpSseClientProperties.SseParameters("http://newserver:8082/events", "/events"));
172+
new McpSseClientProperties.SseParameters("http://newserver:8082/events", "/events", null));
168173
assertThat(properties.getConnections()).hasSize(2);
169174
assertThat(properties.getConnections().get("server1").url()).isEqualTo("http://newserver:8082/events");
170175
assertThat(properties.getConnections().get("server1").sseEndpoint()).isEqualTo("/events");

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.ai.mcp.client.httpclient.autoconfigure;
1818

1919
import java.net.http.HttpClient;
20+
import java.net.http.HttpRequest;
2021
import java.util.ArrayList;
2122
import java.util.List;
2223
import java.util.Map;
@@ -42,6 +43,7 @@
4243
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4344
import org.springframework.context.annotation.Bean;
4445
import org.springframework.core.log.LogAccessor;
46+
import org.springframework.util.CollectionUtils;
4547

4648
/**
4749
* Auto-configuration for Server-Sent Events (SSE) HTTP client transport in the Model
@@ -125,6 +127,17 @@ public List<NamedClientMcpTransport> sseHttpClientTransports(McpSseClientConnect
125127
.clientBuilder(HttpClient.newBuilder())
126128
.jsonMapper(new JacksonMcpJsonMapper(objectMapper));
127129

130+
Map<String, List<String>> headers = serverParameters.getValue().headers();
131+
if (!CollectionUtils.isEmpty(headers)) {
132+
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
133+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
134+
for (String value : entry.getValue()) {
135+
requestBuilder = requestBuilder.header(entry.getKey(), value);
136+
}
137+
}
138+
transportBuilder = transportBuilder.requestBuilder(requestBuilder);
139+
}
140+
128141
asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
129142
syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
130143
if (asyncHttpRequestCustomizer.getIfUnique() != null

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.ai.mcp.client.httpclient.autoconfigure;
1818

1919
import java.net.http.HttpClient;
20+
import java.net.http.HttpRequest;
2021
import java.util.ArrayList;
2122
import java.util.List;
2223
import java.util.Map;
@@ -40,6 +41,7 @@
4041
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4142
import org.springframework.context.annotation.Bean;
4243
import org.springframework.core.log.LogAccessor;
44+
import org.springframework.util.CollectionUtils;
4345

4446
/**
4547
* Auto-configuration for Streamable HTTP client transport in the Model Context Protocol
@@ -116,6 +118,17 @@ public List<NamedClientMcpTransport> streamableHttpHttpClientTransports(
116118
.clientBuilder(HttpClient.newBuilder())
117119
.jsonMapper(new JacksonMcpJsonMapper(objectMapper));
118120

121+
Map<String, List<String>> headers = serverParameters.getValue().headers();
122+
if (!CollectionUtils.isEmpty(headers)) {
123+
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
124+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
125+
for (String value : entry.getValue()) {
126+
requestBuilder = requestBuilder.header(entry.getKey(), value);
127+
}
128+
}
129+
transportBuilder = transportBuilder.requestBuilder(requestBuilder);
130+
}
131+
119132
asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
120133
syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
121134
if (asyncHttpRequestCustomizer.getIfUnique() != null && syncHttpRequestCustomizer.getIfUnique() != null) {

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.ai.mcp.client.autoconfigure;
1818

1919
import java.lang.reflect.Field;
20+
import java.net.URI;
21+
import java.net.http.HttpRequest;
2022
import java.util.List;
2123

2224
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -37,6 +39,7 @@
3739
* Tests for {@link SseHttpClientTransportAutoConfiguration}.
3840
*
3941
* @author Christian Tzolov
42+
* @author Yanming Zhou
4043
*/
4144
public class SseHttpClientTransportAutoConfigurationTests {
4245

@@ -153,10 +156,38 @@ void mixedConnectionsWithAndWithoutCustomSseEndpoint() {
153156
});
154157
}
155158

159+
@Test
160+
void customHttpHeaders() {
161+
this.applicationContext
162+
.withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080",
163+
"spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse",
164+
"spring.ai.mcp.client.sse.connections.server1.headers.Authorization=Bearer <access_token>")
165+
.run(context -> {
166+
List<NamedClientMcpTransport> transports = context.getBean("sseHttpClientTransports", List.class);
167+
assertThat(transports).hasSize(1);
168+
assertThat(transports.get(0).name()).isEqualTo("server1");
169+
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class);
170+
171+
HttpRequest.Builder builder = getRequestBuilder(
172+
(HttpClientSseClientTransport) transports.get(0).transport());
173+
assertThat(builder.uri(new URI("http://localhost:8080")).build().headers().firstValue("Authorization"))
174+
.hasValue("Bearer <access_token>");
175+
});
176+
}
177+
156178
private String getSseEndpoint(HttpClientSseClientTransport transport) {
157-
Field privateField = ReflectionUtils.findField(HttpClientSseClientTransport.class, "sseEndpoint");
179+
return getField(transport, "sseEndpoint", String.class);
180+
}
181+
182+
private HttpRequest.Builder getRequestBuilder(HttpClientSseClientTransport transport) {
183+
return getField(transport, "requestBuilder", HttpRequest.Builder.class);
184+
}
185+
186+
@SuppressWarnings("unchecked")
187+
private <T> T getField(HttpClientSseClientTransport transport, String fieldName, Class<T> type) {
188+
Field privateField = ReflectionUtils.findField(HttpClientSseClientTransport.class, fieldName);
158189
ReflectionUtils.makeAccessible(privateField);
159-
return (String) ReflectionUtils.getField(privateField, transport);
190+
return (T) ReflectionUtils.getField(privateField, transport);
160191
}
161192

162193
@Configuration

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationTests.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.ai.mcp.client.autoconfigure;
1818

1919
import java.lang.reflect.Field;
20+
import java.net.URI;
21+
import java.net.http.HttpRequest;
2022
import java.util.List;
2123

2224
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -162,10 +164,38 @@ void mixedConnectionsWithAndWithoutCustomEndpoint() {
162164
});
163165
}
164166

167+
@Test
168+
void customHttpHeaders() {
169+
this.applicationContext.withPropertyValues(
170+
"spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080",
171+
"spring.ai.mcp.client.streamable-http.connections.server1.headers.Authorization=Bearer <access_token>")
172+
.run(context -> {
173+
List<NamedClientMcpTransport> transports = context.getBean("streamableHttpHttpClientTransports",
174+
List.class);
175+
assertThat(transports).hasSize(1);
176+
assertThat(transports.get(0).name()).isEqualTo("server1");
177+
assertThat(transports.get(0).transport()).isInstanceOf(HttpClientStreamableHttpTransport.class);
178+
179+
HttpRequest.Builder builder = getRequestBuilder(
180+
(HttpClientStreamableHttpTransport) transports.get(0).transport());
181+
assertThat(builder.uri(new URI("http://localhost:8080")).build().headers().firstValue("Authorization"))
182+
.hasValue("Bearer <access_token>");
183+
});
184+
}
185+
165186
private String getStreamableHttpEndpoint(HttpClientStreamableHttpTransport transport) {
166-
Field privateField = ReflectionUtils.findField(HttpClientStreamableHttpTransport.class, "endpoint");
187+
return getField(transport, "endpoint", String.class);
188+
}
189+
190+
private HttpRequest.Builder getRequestBuilder(HttpClientStreamableHttpTransport transport) {
191+
return getField(transport, "requestBuilder", HttpRequest.Builder.class);
192+
}
193+
194+
@SuppressWarnings("unchecked")
195+
private <T> T getField(HttpClientStreamableHttpTransport transport, String fieldName, Class<T> type) {
196+
Field privateField = ReflectionUtils.findField(HttpClientStreamableHttpTransport.class, fieldName);
167197
ReflectionUtils.makeAccessible(privateField);
168-
return (String) ReflectionUtils.getField(privateField, transport);
198+
return (T) ReflectionUtils.getField(privateField, transport);
169199
}
170200

171201
@Configuration

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3737
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3838
import org.springframework.context.annotation.Bean;
39+
import org.springframework.util.CollectionUtils;
3940
import org.springframework.web.reactive.function.client.WebClient;
4041

4142
/**
@@ -99,6 +100,13 @@ public List<NamedClientMcpTransport> sseWebFluxClientTransports(McpSseClientConn
99100

100101
for (Map.Entry<String, SseParameters> serverParameters : connectionDetails.getConnections().entrySet()) {
101102
var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url());
103+
var headers = serverParameters.getValue().headers();
104+
if (!CollectionUtils.isEmpty(headers)) {
105+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
106+
webClientBuilder = webClientBuilder.defaultHeader(entry.getKey(),
107+
entry.getValue().toArray(new String[0]));
108+
}
109+
}
102110
String sseEndpoint = serverParameters.getValue().sseEndpoint() != null
103111
? serverParameters.getValue().sseEndpoint() : "/sse";
104112
var transport = WebFluxSseClientTransport.builder(webClientBuilder)

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3535
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3636
import org.springframework.context.annotation.Bean;
37+
import org.springframework.util.CollectionUtils;
3738
import org.springframework.web.reactive.function.client.WebClient;
3839

3940
/**
@@ -98,6 +99,13 @@ public List<NamedClientMcpTransport> streamableHttpWebFluxClientTransports(
9899
var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url());
99100
String streamableHttpEndpoint = serverParameters.getValue().endpoint() != null
100101
? serverParameters.getValue().endpoint() : "/mcp";
102+
var headers = serverParameters.getValue().headers();
103+
if (!CollectionUtils.isEmpty(headers)) {
104+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
105+
webClientBuilder = webClientBuilder.defaultHeader(entry.getKey(),
106+
entry.getValue().toArray(new String[0]));
107+
}
108+
}
101109

102110
var transport = WebClientStreamableHttpTransport.builder(webClientBuilder)
103111
.endpoint(streamableHttpEndpoint)

0 commit comments

Comments
 (0)