Skip to content

Commit a05f692

Browse files
authored
Merge pull request #499 from mkouba/issue-491_1-7-x
http: make transport compatible with Quarkus 3.30+ [1.7.x]
2 parents 5f50bc2 + d1c4b17 commit a05f692

File tree

7 files changed

+171
-122
lines changed

7 files changed

+171
-122
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.quarkiverse.mcp.server.sse.deployment;
2+
3+
import java.util.List;
4+
5+
import io.quarkiverse.mcp.server.sse.runtime.McpServerEndpoints;
6+
import io.quarkiverse.mcp.server.sse.runtime.McpServerEndpoints.McpServerEndpoint;
7+
import io.quarkus.builder.item.SimpleBuildItem;
8+
9+
final class McpServerEndpointsBuildItem extends SimpleBuildItem {
10+
11+
private final List<McpServerEndpoint> endpoints;
12+
13+
McpServerEndpointsBuildItem(List<McpServerEndpoint> endpoints) {
14+
this.endpoints = endpoints;
15+
}
16+
17+
List<McpServerEndpoints.McpServerEndpoint> getEndpoints() {
18+
return endpoints;
19+
}
20+
21+
}
Lines changed: 46 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
11
package io.quarkiverse.mcp.server.sse.deployment;
22

33
import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
4+
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
45

56
import java.util.ArrayList;
67
import java.util.HashSet;
78
import java.util.List;
89
import java.util.Map;
910
import java.util.Set;
1011

11-
import jakarta.enterprise.inject.spi.EventContext;
12-
13-
import org.jboss.jandex.DotName;
12+
import jakarta.inject.Singleton;
1413

1514
import io.quarkiverse.mcp.server.deployment.ServerNameBuildItem;
15+
import io.quarkiverse.mcp.server.sse.runtime.McpServerEndpoints;
16+
import io.quarkiverse.mcp.server.sse.runtime.McpServerEndpoints.McpServerEndpoint;
17+
import io.quarkiverse.mcp.server.sse.runtime.McpServerEndpointsLogger;
1618
import io.quarkiverse.mcp.server.sse.runtime.SseMcpMessageHandler;
1719
import io.quarkiverse.mcp.server.sse.runtime.SseMcpServerRecorder;
18-
import io.quarkiverse.mcp.server.sse.runtime.SseMcpServerRecorder.McpServerEndpoints;
1920
import io.quarkiverse.mcp.server.sse.runtime.StreamableHttpMcpMessageHandler;
2021
import io.quarkiverse.mcp.server.sse.runtime.config.McpSseServerBuildTimeConfig;
2122
import io.quarkiverse.mcp.server.sse.runtime.config.McpSseServersBuildTimeConfig;
2223
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
23-
import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem;
24-
import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem;
24+
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
2525
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
26-
import io.quarkus.arc.processor.ObserverConfigurator;
2726
import io.quarkus.deployment.annotations.BuildProducer;
2827
import io.quarkus.deployment.annotations.BuildStep;
2928
import io.quarkus.deployment.annotations.Consume;
3029
import io.quarkus.deployment.annotations.Record;
3130
import io.quarkus.deployment.builditem.FeatureBuildItem;
32-
import io.quarkus.gizmo.Gizmo;
33-
import io.quarkus.gizmo.MethodCreator;
34-
import io.quarkus.gizmo.MethodDescriptor;
35-
import io.quarkus.gizmo.ResultHandle;
36-
import io.quarkus.vertx.http.HttpServerStart;
37-
import io.quarkus.vertx.http.HttpsServerStart;
3831
import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem;
3932
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
4033
import io.quarkus.vertx.http.deployment.spi.RouteBuildItem;
41-
import io.vertx.core.http.HttpServerOptions;
4234

4335
public class SseMcpServerProcessor {
4436

@@ -50,8 +42,11 @@ FeatureBuildItem feature() {
5042
@BuildStep
5143
void addBeans(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
5244
additionalBeans.produce(
53-
AdditionalBeanBuildItem.builder().setUnremovable()
54-
.addBeanClasses(SseMcpMessageHandler.class, StreamableHttpMcpMessageHandler.class).build());
45+
AdditionalBeanBuildItem.builder()
46+
.setUnremovable()
47+
.addBeanClasses(SseMcpMessageHandler.class, StreamableHttpMcpMessageHandler.class,
48+
McpServerEndpointsLogger.class)
49+
.build());
5550
}
5651

5752
@BuildStep
@@ -64,79 +59,55 @@ void serverNames(McpSseServersBuildTimeConfig config, BuildProducer<ServerNameBu
6459
@Record(RUNTIME_INIT)
6560
@Consume(SyntheticBeansRuntimeInitBuildItem.class)
6661
@BuildStep
67-
void registerEndpoints(McpSseServersBuildTimeConfig config, HttpRootPathBuildItem httpRootPath,
62+
void registerEndpoints(McpServerEndpointsBuildItem mcpServerEndpoints,
6863
SseMcpServerRecorder recorder,
6964
BodyHandlerBuildItem bodyHandler,
70-
BuildProducer<RouteBuildItem> routes,
71-
ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
72-
BuildProducer<ObserverConfiguratorBuildItem> observers) {
73-
74-
Set<String> rootPaths = new HashSet<>();
75-
List<McpServerEndpoints> endpoints = new ArrayList<>();
76-
for (Map.Entry<String, McpSseServerBuildTimeConfig> e : config.servers().entrySet()) {
77-
String serverName = e.getKey();
78-
String rootPath = e.getValue().sse().rootPath();
79-
if (!rootPaths.add(rootPath)) {
80-
throw new IllegalStateException("Multiple server configurations define the same root path: " + rootPath);
81-
}
82-
83-
// By default /mcp
84-
String mcpPath = httpRootPath.relativePath(rootPath);
85-
65+
BuildProducer<RouteBuildItem> routes) {
66+
for (McpServerEndpoint endpoint : mcpServerEndpoints.getEndpoints()) {
8667
// Streamable HTTP transport
87-
routes.produce(RouteBuildItem.newFrameworkRoute(mcpPath)
68+
routes.produce(RouteBuildItem.newFrameworkRoute(endpoint.mcpPath)
8869
.withRouteCustomizer(recorder.addBodyHandler(bodyHandler.getHandler()))
89-
.withRequestHandler(recorder.createMcpEndpointHandler(serverName))
70+
.withRequestHandler(recorder.createMcpEndpointHandler(endpoint.serverName))
9071
.build());
91-
9272
// SSE/HTTP transport
93-
String ssePath = mcpPath.endsWith("/") ? mcpPath + "sse" : mcpPath + "/sse";
94-
routes.produce(RouteBuildItem.newFrameworkRoute(ssePath)
95-
.withRequestHandler(recorder.createSseEndpointHandler(mcpPath, serverName))
73+
routes.produce(RouteBuildItem.newFrameworkRoute(endpoint.ssePath)
74+
.withRequestHandler(recorder.createSseEndpointHandler(endpoint.mcpPath, endpoint.serverName))
9675
.build());
97-
routes.produce(RouteBuildItem.newFrameworkRoute(mcpPath + "/" + "messages/:id")
76+
routes.produce(RouteBuildItem.newFrameworkRoute(endpoint.mcpPath + "/" + "messages/:id")
9877
.withRouteCustomizer(recorder.addBodyHandler(bodyHandler.getHandler()))
99-
.withRequestHandler(recorder.createMessagesEndpointHandler(serverName))
78+
.withRequestHandler(recorder.createMessagesEndpointHandler(endpoint.serverName))
10079
.build());
101-
102-
endpoints.add(new McpServerEndpoints(serverName, mcpPath, ssePath));
10380
}
81+
}
10482

105-
// Create synthetic observers for HttpServerStart and HttpsServerStart
106-
// so that the info is logged after the server is started
107-
ObserverConfigurator httpStartConfigurator = observerRegistrationPhase.getContext()
108-
.configure()
109-
.async(true)
110-
.beanClass(DotName.createSimple(SseMcpServerRecorder.class))
111-
.observedType(HttpServerStart.class)
112-
.notify(mc -> logMcpServerEndpoints(HttpServerStart.class, mc, endpoints));
113-
ObserverConfigurator httpsStartConfigurator = observerRegistrationPhase.getContext()
114-
.configure()
115-
.async(true)
116-
.beanClass(DotName.createSimple(SseMcpServerRecorder.class))
117-
.observedType(HttpsServerStart.class)
118-
.notify(mc -> logMcpServerEndpoints(HttpsServerStart.class, mc, endpoints));
119-
observers.produce(new ObserverConfiguratorBuildItem(httpStartConfigurator, httpsStartConfigurator));
83+
@Record(STATIC_INIT)
84+
@BuildStep
85+
void registerMcpEndpointsBean(McpServerEndpointsBuildItem mcpServerEndpoints, SseMcpServerRecorder recorder,
86+
BuildProducer<SyntheticBeanBuildItem> syntheticBeans) {
87+
syntheticBeans.produce(SyntheticBeanBuildItem.configure(McpServerEndpoints.class)
88+
.scope(Singleton.class)
89+
.createWith(recorder.createMcpServerEndpoints(mcpServerEndpoints.getEndpoints()))
90+
.done());
12091
}
12192

122-
private void logMcpServerEndpoints(Class<?> eventType, MethodCreator mc, List<McpServerEndpoints> endpoints) {
123-
ResultHandle event = mc.invokeInterfaceMethod(MethodDescriptor.ofMethod(EventContext.class, "getEvent", Object.class),
124-
mc.getMethodParam(0));
125-
ResultHandle httpServerOptions = mc.invokeVirtualMethod(
126-
MethodDescriptor.ofMethod(eventType, "options", HttpServerOptions.class),
127-
event);
128-
ResultHandle list = Gizmo.newArrayList(mc);
129-
for (McpServerEndpoints e : endpoints) {
130-
ResultHandle mcpe = mc.newInstance(MethodDescriptor.ofConstructor(McpServerEndpoints.class,
131-
String.class, String.class, String.class), mc.load(e.serverName), mc.load(e.mcpPath),
132-
mc.load(e.ssePath));
133-
Gizmo.listOperations(mc).on(list).add(mcpe);
93+
@BuildStep
94+
McpServerEndpointsBuildItem collectMcpServerEndpoints(McpSseServersBuildTimeConfig config,
95+
HttpRootPathBuildItem httpRootPath) {
96+
List<McpServerEndpoint> endpoints = new ArrayList<>();
97+
Set<String> rootPaths = new HashSet<>();
98+
for (Map.Entry<String, McpSseServerBuildTimeConfig> e : config.servers().entrySet()) {
99+
String serverName = e.getKey();
100+
String rootPath = e.getValue().sse().rootPath();
101+
if (!rootPaths.add(rootPath)) {
102+
throw new IllegalStateException("Multiple server configurations define the same root path: " + rootPath);
103+
}
104+
// Streamable HTTP transport, by default /mcp
105+
String mcpPath = httpRootPath.relativePath(rootPath);
106+
// SSE/HTTP transport, by default /mcp/sse
107+
String ssePath = mcpPath.endsWith("/") ? mcpPath + "sse" : mcpPath + "/sse";
108+
endpoints.add(new McpServerEndpoint(serverName, mcpPath, ssePath));
134109
}
135-
mc.invokeStaticMethod(
136-
MethodDescriptor.ofMethod(SseMcpServerRecorder.class, "logEndpoints", void.class, List.class,
137-
HttpServerOptions.class),
138-
list, httpServerOptions);
139-
mc.returnVoid();
110+
return new McpServerEndpointsBuildItem(endpoints);
140111
}
141112

142113
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkiverse.mcp.server.sse.runtime;
2+
3+
import java.util.List;
4+
5+
public class McpServerEndpoints {
6+
7+
final List<McpServerEndpoints.McpServerEndpoint> endpoints;
8+
9+
McpServerEndpoints(List<McpServerEndpoint> endpoints) {
10+
this.endpoints = endpoints;
11+
}
12+
13+
public static class McpServerEndpoint {
14+
15+
public String serverName;
16+
public String mcpPath;
17+
public String ssePath;
18+
19+
public McpServerEndpoint(String serverName, String mcpPath, String ssePath) {
20+
this.serverName = serverName;
21+
this.mcpPath = mcpPath;
22+
this.ssePath = ssePath;
23+
}
24+
25+
}
26+
27+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.quarkiverse.mcp.server.sse.runtime;
2+
3+
import java.util.List;
4+
5+
import jakarta.enterprise.context.Dependent;
6+
import jakarta.enterprise.event.ObservesAsync;
7+
8+
import org.jboss.logging.Logger;
9+
10+
import io.quarkiverse.mcp.server.McpServer;
11+
import io.quarkiverse.mcp.server.sse.runtime.McpServerEndpoints.McpServerEndpoint;
12+
import io.quarkus.vertx.http.HttpServerStart;
13+
import io.quarkus.vertx.http.HttpsServerStart;
14+
import io.vertx.core.http.HttpServerOptions;
15+
16+
@Dependent
17+
public class McpServerEndpointsLogger {
18+
19+
void onHttpServerStart(@ObservesAsync HttpServerStart start, McpServerEndpoints endpoints) {
20+
logEndpoints(endpoints.endpoints, start.options());
21+
}
22+
23+
void onHttpsServerStart(@ObservesAsync HttpsServerStart start, McpServerEndpoints endpoints) {
24+
logEndpoints(endpoints.endpoints, start.options());
25+
}
26+
27+
private void logEndpoints(List<McpServerEndpoint> endpoints, HttpServerOptions httpServerOptions) {
28+
Logger log = Logger.getLogger("io.quarkiverse.mcp.server");
29+
// base is scheme://host:port
30+
String base = new StringBuilder(httpServerOptions.isSsl() ? "https://" : "http://")
31+
.append(httpServerOptions.getHost())
32+
.append(":")
33+
.append(httpServerOptions.getPort())
34+
.toString();
35+
for (McpServerEndpoints.McpServerEndpoint e : endpoints) {
36+
String serverInfo = "";
37+
if (!McpServer.DEFAULT.equals(e.serverName)) {
38+
serverInfo = " [" + e.serverName + "]";
39+
}
40+
log.infof("MCP%s HTTP transport endpoints [streamable: %s, SSE: %s]", serverInfo, base + e.mcpPath,
41+
base + e.ssePath);
42+
}
43+
44+
}
45+
46+
}

transports/sse/runtime/src/main/java/io/quarkiverse/mcp/server/sse/runtime/SseMcpServerRecorder.java

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import java.util.List;
1111
import java.util.Map.Entry;
1212
import java.util.function.Consumer;
13+
import java.util.function.Function;
1314

1415
import org.jboss.logging.Logger;
1516

1617
import io.quarkiverse.mcp.server.McpLog;
17-
import io.quarkiverse.mcp.server.McpServer;
1818
import io.quarkiverse.mcp.server.runtime.ConnectionManager;
1919
import io.quarkiverse.mcp.server.runtime.McpConnectionBase;
2020
import io.quarkiverse.mcp.server.runtime.McpMessageHandler;
@@ -25,14 +25,14 @@
2525
import io.quarkiverse.mcp.server.sse.runtime.config.McpSseServersBuildTimeConfig;
2626
import io.quarkus.arc.Arc;
2727
import io.quarkus.arc.ArcContainer;
28+
import io.quarkus.arc.SyntheticCreationalContext;
2829
import io.quarkus.runtime.RuntimeValue;
2930
import io.quarkus.runtime.annotations.Recorder;
3031
import io.vertx.core.Handler;
3132
import io.vertx.core.MultiMap;
3233
import io.vertx.core.http.HttpConnection;
3334
import io.vertx.core.http.HttpHeaders;
3435
import io.vertx.core.http.HttpMethod;
35-
import io.vertx.core.http.HttpServerOptions;
3636
import io.vertx.core.http.HttpServerRequest;
3737
import io.vertx.core.http.HttpServerResponse;
3838
import io.vertx.core.json.JsonObject;
@@ -240,37 +240,15 @@ public void handle(RoutingContext ctx) {
240240
};
241241
}
242242

243-
public static void logEndpoints(List<McpServerEndpoints> endpoints, HttpServerOptions httpServerOptions) {
244-
Logger log = Logger.getLogger("io.quarkiverse.mcp.server");
245-
// base is scheme://host:port
246-
String base = new StringBuilder(httpServerOptions.isSsl() ? "https://" : "http://")
247-
.append(httpServerOptions.getHost())
248-
.append(":")
249-
.append(httpServerOptions.getPort())
250-
.toString();
251-
for (McpServerEndpoints e : endpoints) {
252-
String serverInfo = "";
253-
if (!McpServer.DEFAULT.equals(e.serverName)) {
254-
serverInfo = " [" + e.serverName + "]";
255-
}
256-
log.infof("MCP%s HTTP transport endpoints [streamable: %s, SSE: %s]", serverInfo, base + e.mcpPath,
257-
base + e.ssePath);
258-
}
259-
260-
}
261-
262-
public static class McpServerEndpoints {
263-
264-
public String serverName;
265-
public String mcpPath;
266-
public String ssePath;
267-
268-
public McpServerEndpoints(String serverName, String mcpPath, String ssePath) {
269-
this.serverName = serverName;
270-
this.mcpPath = mcpPath;
271-
this.ssePath = ssePath;
272-
}
243+
public Function<SyntheticCreationalContext<McpServerEndpoints>, McpServerEndpoints> createMcpServerEndpoints(
244+
List<McpServerEndpoints.McpServerEndpoint> endpoints) {
245+
return new Function<SyntheticCreationalContext<McpServerEndpoints>, McpServerEndpoints>() {
273246

247+
@Override
248+
public McpServerEndpoints apply(SyntheticCreationalContext<McpServerEndpoints> t) {
249+
return new McpServerEndpoints(endpoints);
250+
}
251+
};
274252
}
275253

276254
}

transports/websocket/deployment/src/main/java/io/quarkiverse/mcp/server/websocket/deployment/WebSocketMcpServerProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ void generateEndpoints(McpWebSocketServersBuildTimeConfig config, BuildProducer<
6868
.className(endpointClassName)
6969
.superClass(WebSocketMcpMessageHandler.class)
7070
.build();
71-
// @WebSocket(path = "/foo/bar")
71+
// @WebSocket(path = "/foo/bar", inboundProcessingMode = InboundProcessingMode.CONCURRENT)
7272
endpointCreator.addAnnotation(
7373
AnnotationInstance.builder(WebSocket.class)
7474
.add("path", e.getValue().websocket().endpointPath())

0 commit comments

Comments
 (0)