Skip to content

Commit 18308d5

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

File tree

11 files changed

+198
-16
lines changed

11 files changed

+198
-16
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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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;
@@ -39,6 +40,7 @@
3940
* </pre>
4041
*
4142
* @author Christian Tzolov
43+
* @author Yanming Zhou
4244
* @since 1.0.0
4345
* @see SseParameters
4446
*/
@@ -68,8 +70,9 @@ public Map<String, SseParameters> getConnections() {
6870
*
6971
* @param url the URL endpoint for SSE communication with the MCP server
7072
* @param sseEndpoint the SSE endpoint for the MCP server
73+
* @param headers the custom HTTP headers for the MCP server
7174
*/
72-
public record SseParameters(String url, String sseEndpoint) {
75+
public record SseParameters(String url, String sseEndpoint, Map<String, List<String>> headers) {
7376
}
7477

7578
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
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;
@@ -39,6 +40,7 @@
3940
* </pre>
4041
*
4142
* @author Christian Tzolov
43+
* @author Yanming Zhou
4244
* @see ConnectionParameters
4345
*/
4446
@ConfigurationProperties(McpStreamableHttpClientProperties.CONFIG_PREFIX)
@@ -67,8 +69,9 @@ public Map<String, ConnectionParameters> getConnections() {
6769
*
6870
* @param url the URL endpoint for Streamable Http communication with the MCP server
6971
* @param endpoint the endpoint for the MCP server
72+
* @param headers the custom HTTP headers for the MCP server
7073
*/
71-
public record ConnectionParameters(String url, String endpoint) {
74+
public record ConnectionParameters(String url, String endpoint, Map<String, List<String>> headers) {
7275
}
7376

7477
}

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;
@@ -39,6 +40,7 @@
3940
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4041
import org.springframework.context.annotation.Bean;
4142
import org.springframework.core.log.LogAccessor;
43+
import org.springframework.util.CollectionUtils;
4244

4345
/**
4446
* Auto-configuration for Server-Sent Events (SSE) HTTP client transport in the Model
@@ -113,6 +115,17 @@ public List<NamedClientMcpTransport> sseHttpClientTransports(McpSseClientPropert
113115
.clientBuilder(HttpClient.newBuilder())
114116
.objectMapper(objectMapper);
115117

118+
Map<String, List<String>> headers = serverParameters.getValue().headers();
119+
if (!CollectionUtils.isEmpty(headers)) {
120+
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
121+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
122+
for (String value : entry.getValue()) {
123+
requestBuilder = requestBuilder.header(entry.getKey(), value);
124+
}
125+
}
126+
transportBuilder = transportBuilder.requestBuilder(requestBuilder);
127+
}
128+
116129
asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
117130
syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
118131
if (asyncHttpRequestCustomizer.getIfUnique() != null && syncHttpRequestCustomizer.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;
@@ -39,6 +40,7 @@
3940
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4041
import org.springframework.context.annotation.Bean;
4142
import org.springframework.core.log.LogAccessor;
43+
import org.springframework.util.CollectionUtils;
4244

4345
/**
4446
* Auto-configuration for Streamable HTTP client transport in the Model Context Protocol
@@ -119,6 +121,17 @@ public List<NamedClientMcpTransport> streamableHttpHttpClientTransports(
119121
.clientBuilder(HttpClient.newBuilder())
120122
.objectMapper(objectMapper);
121123

124+
Map<String, List<String>> headers = serverParameters.getValue().headers();
125+
if (!CollectionUtils.isEmpty(headers)) {
126+
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
127+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
128+
for (String value : entry.getValue()) {
129+
requestBuilder = requestBuilder.header(entry.getKey(), value);
130+
}
131+
}
132+
transportBuilder = transportBuilder.requestBuilder(requestBuilder);
133+
}
134+
122135
asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
123136
syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
124137
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
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3434
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3535
import org.springframework.context.annotation.Bean;
36+
import org.springframework.util.CollectionUtils;
3637
import org.springframework.web.reactive.function.client.WebClient;
3738

3839
/**
@@ -91,6 +92,13 @@ public List<NamedClientMcpTransport> sseWebFluxClientTransports(McpSseClientProp
9192

9293
for (Map.Entry<String, SseParameters> serverParameters : sseProperties.getConnections().entrySet()) {
9394
var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url());
95+
var headers = serverParameters.getValue().headers();
96+
if (!CollectionUtils.isEmpty(headers)) {
97+
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
98+
webClientBuilder = webClientBuilder.defaultHeader(entry.getKey(),
99+
entry.getValue().toArray(new String[0]));
100+
}
101+
}
94102
String sseEndpoint = serverParameters.getValue().sseEndpoint() != null
95103
? serverParameters.getValue().sseEndpoint() : "/sse";
96104
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
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3434
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3535
import org.springframework.context.annotation.Bean;
36+
import org.springframework.util.CollectionUtils;
3637
import org.springframework.web.reactive.function.client.WebClient;
3738

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

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

0 commit comments

Comments
 (0)