Skip to content

Commit e215664

Browse files
4638 - temporarily fixing mcp connection to claude desktop
1 parent 998de49 commit e215664

3 files changed

Lines changed: 206 additions & 3 deletions

File tree

server/ee/libs/embedded/embedded-ai/embedded-ai-mcp-server/src/main/java/com/bytechef/ee/embedded/ai/mcp/server/config/EmbeddedMcpServerConfiguration.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import java.util.ArrayList;
8888
import java.util.List;
8989
import java.util.Map;
90+
import java.util.concurrent.ConcurrentHashMap;
9091
import org.apache.commons.lang3.StringUtils;
9192
import org.springframework.ai.mcp.McpToolUtils;
9293
import org.springframework.ai.mcp.server.webmvc.transport.WebMvcStreamableServerTransportProvider;
@@ -95,9 +96,12 @@
9596
import org.springframework.context.annotation.Configuration;
9697
import org.springframework.core.env.Environment;
9798
import org.springframework.core.task.TaskExecutor;
99+
import org.springframework.http.HttpHeaders;
100+
import org.springframework.http.MediaType;
98101
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
99102
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
100103
import org.springframework.web.servlet.function.RouterFunction;
104+
import org.springframework.web.servlet.function.ServerRequest;
101105
import org.springframework.web.servlet.function.ServerResponse;
102106

103107
/**
@@ -136,7 +140,57 @@ WebMvcStreamableServerTransportProvider embeddedWebMvcStreamableHttpServerTransp
136140

137141
@Bean
138142
RouterFunction<ServerResponse> embeddedMcpRouterFunction() {
139-
return embeddedWebMvcStreamableHttpServerTransportProvider().getRouterFunction();
143+
Map<String, String> sessionIdCache = new ConcurrentHashMap<>();
144+
145+
return embeddedWebMvcStreamableHttpServerTransportProvider()
146+
.getRouterFunction()
147+
.filter((request, next) -> {
148+
List<MediaType> accept = request.headers()
149+
.accept();
150+
ServerRequest workingRequest;
151+
152+
if (accept.contains(MediaType.TEXT_EVENT_STREAM) && accept.contains(MediaType.APPLICATION_JSON)) {
153+
workingRequest = request;
154+
} else {
155+
workingRequest = ServerRequest.from(request)
156+
.headers(headers -> headers.set(
157+
HttpHeaders.ACCEPT,
158+
MediaType.APPLICATION_JSON_VALUE + ", " + MediaType.TEXT_EVENT_STREAM_VALUE))
159+
.build();
160+
}
161+
162+
// Inject cached session ID for clients (e.g. supergateway) that do not forward Mcp-Session-Id
163+
String secretKey = request.pathVariable(SECRET_KEY);
164+
165+
if (request.headers()
166+
.header("Mcp-Session-Id")
167+
.isEmpty()) {
168+
String cachedSessionId = sessionIdCache.get(secretKey);
169+
170+
if (cachedSessionId != null) {
171+
final String sessionId = cachedSessionId;
172+
173+
workingRequest = ServerRequest.from(workingRequest)
174+
.headers(headers -> headers.set("Mcp-Session-Id", sessionId))
175+
.build();
176+
}
177+
}
178+
179+
ServerResponse response = next.handle(workingRequest);
180+
181+
// Cache the session ID returned by the initialize response
182+
String sessionId = response.headers()
183+
.getFirst("Mcp-Session-Id");
184+
185+
if (sessionId != null) {
186+
sessionIdCache.put(secretKey, sessionId);
187+
} else if (response.statusCode()
188+
.value() == 404) {
189+
sessionIdCache.remove(secretKey);
190+
}
191+
192+
return response;
193+
});
140194
}
141195

142196
@Bean

server/libs/ai/mcp/mcp-server/src/main/java/com/bytechef/ai/mcp/server/config/ManagementMcpServerConfiguration.java

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import io.modelcontextprotocol.spec.McpSchema;
3434
import java.util.ArrayList;
3535
import java.util.List;
36+
import java.util.Map;
37+
import java.util.concurrent.ConcurrentHashMap;
3638
import org.jspecify.annotations.Nullable;
3739
import org.springframework.ai.mcp.McpToolUtils;
3840
import org.springframework.ai.mcp.server.webmvc.transport.WebMvcStreamableServerTransportProvider;
@@ -42,9 +44,13 @@
4244
import org.springframework.context.annotation.Bean;
4345
import org.springframework.context.annotation.Configuration;
4446
import org.springframework.context.annotation.Primary;
47+
import org.springframework.http.HttpHeaders;
48+
import org.springframework.http.MediaType;
4549
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
4650
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
51+
import org.springframework.web.servlet.function.EntityResponse;
4752
import org.springframework.web.servlet.function.RouterFunction;
53+
import org.springframework.web.servlet.function.ServerRequest;
4854
import org.springframework.web.servlet.function.ServerResponse;
4955

5056
/**
@@ -87,7 +93,96 @@ WebMvcStreamableServerTransportProvider webMvcStreamableHttpServerTransportProvi
8793

8894
@Bean
8995
RouterFunction<ServerResponse> mcpRouterFunction() {
90-
return webMvcStreamableHttpServerTransportProvider().getRouterFunction();
96+
Map<String, String> sessionIdCache = new ConcurrentHashMap<>();
97+
98+
return webMvcStreamableHttpServerTransportProvider()
99+
.getRouterFunction()
100+
.filter((request, next) -> {
101+
List<MediaType> accept = request.headers()
102+
.accept();
103+
ServerRequest workingRequest;
104+
105+
if ("GET".equals(request.method()
106+
.name())) {
107+
// GET SSE requests need only text/event-stream — do not inject application/json
108+
workingRequest = request;
109+
} else if (accept.contains(MediaType.TEXT_EVENT_STREAM)
110+
&& accept.contains(MediaType.APPLICATION_JSON)) {
111+
workingRequest = request;
112+
} else {
113+
// POST requests require both Accept types in Spring AI M3
114+
workingRequest = ServerRequest.from(request)
115+
.headers(headers -> headers.set(
116+
HttpHeaders.ACCEPT,
117+
MediaType.APPLICATION_JSON_VALUE + ", " + MediaType.TEXT_EVENT_STREAM_VALUE))
118+
.build();
119+
}
120+
121+
// Inject cached session ID for clients (e.g. supergateway) that do not forward Mcp-Session-Id
122+
String secretKey = request.pathVariable("secretKey");
123+
124+
if (request.headers()
125+
.header("Mcp-Session-Id")
126+
.isEmpty()) {
127+
String cachedSessionId = sessionIdCache.get(secretKey);
128+
129+
if (cachedSessionId != null) {
130+
final String sessionId = cachedSessionId;
131+
132+
workingRequest = ServerRequest.from(workingRequest)
133+
.headers(headers -> headers.set("Mcp-Session-Id", sessionId))
134+
.build();
135+
}
136+
}
137+
138+
ServerResponse response = next.handle(workingRequest);
139+
140+
// Cache the session ID returned by the initialize response
141+
String sessionId = response.headers()
142+
.getFirst("Mcp-Session-Id");
143+
144+
if (sessionId != null) {
145+
sessionIdCache.put(secretKey, sessionId);
146+
147+
// Downgrade protocol version in the initialize response.
148+
// supergateway (3.x) uses @modelcontextprotocol/sdk npm which only supports up to
149+
// 2025-06-18; it forwards Claude Desktop's 2025-11-25 but then rejects the server
150+
// response if it echoes 2025-11-25 back.
151+
response = downgradeMcpProtocolVersion(response, sessionId);
152+
} else if (response.statusCode()
153+
.value() == 404) {
154+
sessionIdCache.remove(secretKey);
155+
}
156+
157+
return response;
158+
});
159+
}
160+
161+
private static ServerResponse downgradeMcpProtocolVersion(ServerResponse response, String sessionId) {
162+
if (!(response instanceof EntityResponse<?> entityResponse)) {
163+
return response;
164+
}
165+
166+
Object entity = entityResponse.entity();
167+
168+
if (!(entity instanceof McpSchema.JSONRPCResponse jsonRpcResponse)) {
169+
return response;
170+
}
171+
172+
if (!(jsonRpcResponse.result() instanceof McpSchema.InitializeResult initResult)) {
173+
return response;
174+
}
175+
176+
McpSchema.InitializeResult downgraded = new McpSchema.InitializeResult(
177+
"2025-06-18", initResult.capabilities(), initResult.serverInfo(), initResult.instructions());
178+
179+
McpSchema.JSONRPCResponse newJsonRpcResponse = new McpSchema.JSONRPCResponse(
180+
jsonRpcResponse.jsonrpc(), jsonRpcResponse.id(), downgraded, null);
181+
182+
return ServerResponse.ok()
183+
.contentType(MediaType.APPLICATION_JSON)
184+
.header("Mcp-Session-Id", sessionId)
185+
.body(newJsonRpcResponse);
91186
}
92187

93188
@Bean

server/libs/automation/automation-ai/automation-ai-mcp-server/src/main/java/com/bytechef/automation/ai/mcp/server/config/AutomationMcpServerConfiguration.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,20 @@
8383
import java.util.ArrayList;
8484
import java.util.List;
8585
import java.util.Map;
86+
import java.util.concurrent.ConcurrentHashMap;
8687
import org.springframework.ai.mcp.McpToolUtils;
8788
import org.springframework.ai.mcp.server.webmvc.transport.WebMvcStreamableServerTransportProvider;
8889
import org.springframework.context.ApplicationEventPublisher;
8990
import org.springframework.context.annotation.Bean;
9091
import org.springframework.context.annotation.Configuration;
9192
import org.springframework.core.env.Environment;
9293
import org.springframework.core.task.TaskExecutor;
94+
import org.springframework.http.HttpHeaders;
95+
import org.springframework.http.MediaType;
9396
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
9497
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
9598
import org.springframework.web.servlet.function.RouterFunction;
99+
import org.springframework.web.servlet.function.ServerRequest;
96100
import org.springframework.web.servlet.function.ServerResponse;
97101

98102
/**
@@ -117,7 +121,57 @@ WebMvcStreamableServerTransportProvider automationWebMvcStreamableHttpServerTran
117121

118122
@Bean
119123
RouterFunction<ServerResponse> automationMcpRouterFunction() {
120-
return automationWebMvcStreamableHttpServerTransportProvider().getRouterFunction();
124+
Map<String, String> sessionIdCache = new ConcurrentHashMap<>();
125+
126+
return automationWebMvcStreamableHttpServerTransportProvider()
127+
.getRouterFunction()
128+
.filter((request, next) -> {
129+
List<MediaType> accept = request.headers()
130+
.accept();
131+
ServerRequest workingRequest;
132+
133+
if (accept.contains(MediaType.TEXT_EVENT_STREAM) && accept.contains(MediaType.APPLICATION_JSON)) {
134+
workingRequest = request;
135+
} else {
136+
workingRequest = ServerRequest.from(request)
137+
.headers(headers -> headers.set(
138+
HttpHeaders.ACCEPT,
139+
MediaType.APPLICATION_JSON_VALUE + ", " + MediaType.TEXT_EVENT_STREAM_VALUE))
140+
.build();
141+
}
142+
143+
// Inject cached session ID for clients (e.g. supergateway) that do not forward Mcp-Session-Id
144+
String secretKey = request.pathVariable(SECRET_KEY);
145+
146+
if (request.headers()
147+
.header("Mcp-Session-Id")
148+
.isEmpty()) {
149+
String cachedSessionId = sessionIdCache.get(secretKey);
150+
151+
if (cachedSessionId != null) {
152+
final String sessionId = cachedSessionId;
153+
154+
workingRequest = ServerRequest.from(workingRequest)
155+
.headers(headers -> headers.set("Mcp-Session-Id", sessionId))
156+
.build();
157+
}
158+
}
159+
160+
ServerResponse response = next.handle(workingRequest);
161+
162+
// Cache the session ID returned by the initialize response
163+
String sessionId = response.headers()
164+
.getFirst("Mcp-Session-Id");
165+
166+
if (sessionId != null) {
167+
sessionIdCache.put(secretKey, sessionId);
168+
} else if (response.statusCode()
169+
.value() == 404) {
170+
sessionIdCache.remove(secretKey);
171+
}
172+
173+
return response;
174+
});
121175
}
122176

123177
@Bean

0 commit comments

Comments
 (0)