Skip to content

Commit 63edafe

Browse files
committed
feat(server): Add support for streamable http server
1 parent bb320ab commit 63edafe

File tree

9 files changed

+203
-12
lines changed

9 files changed

+203
-12
lines changed

src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import com.github.codeboyzhou.mcp.declarative.configuration.YAMLConfigurationLoader;
66
import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo;
77
import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo;
8+
import com.github.codeboyzhou.mcp.declarative.server.McpStreamableServerInfo;
89
import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpHttpSseServerFactory;
910
import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpServerFactory;
1011
import com.github.codeboyzhou.mcp.declarative.server.factory.ConfigurableMcpStdioServerFactory;
1112
import com.github.codeboyzhou.mcp.declarative.server.factory.McpHttpSseServerFactory;
13+
import com.github.codeboyzhou.mcp.declarative.server.factory.McpHttpStreamableServerFactory;
1214
import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerPromptFactory;
1315
import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerResourceFactory;
1416
import com.github.codeboyzhou.mcp.declarative.server.factory.McpServerToolFactory;
@@ -50,6 +52,12 @@ public void startSseServer(McpSseServerInfo serverInfo) {
5052
registerComponents(server);
5153
}
5254

55+
public void startStreamableServer(McpStreamableServerInfo serverInfo) {
56+
McpHttpStreamableServerFactory factory = new McpHttpStreamableServerFactory();
57+
McpAsyncServer server = factory.create(serverInfo);
58+
registerComponents(server);
59+
}
60+
5361
public void startServer(String configFileName) {
5462
Assert.notNull(configFileName, "configFileName must not be null");
5563
YAMLConfigurationLoader configLoader = new YAMLConfigurationLoader(configFileName);

src/main/java/com/github/codeboyzhou/mcp/declarative/server/McpHttpServer.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
11
package com.github.codeboyzhou.mcp.declarative.server;
22

3-
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
3+
import jakarta.servlet.http.HttpServlet;
44
import java.time.Duration;
55
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
66
import org.eclipse.jetty.ee10.servlet.ServletHolder;
77
import org.eclipse.jetty.server.Server;
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
1010

11-
public record McpHttpServer(int port) {
11+
public class McpHttpServer {
1212

1313
private static final Logger logger = LoggerFactory.getLogger(McpHttpServer.class);
1414

1515
private static final String DEFAULT_SERVLET_CONTEXT_PATH = "/";
1616

1717
private static final String DEFAULT_SERVLET_PATH = "/*";
1818

19-
public void start(HttpServletSseServerTransportProvider transportProvider) {
19+
private HttpServlet servlet;
20+
21+
private int port;
22+
23+
public McpHttpServer use(HttpServlet servlet) {
24+
this.servlet = servlet;
25+
return this;
26+
}
27+
28+
public McpHttpServer bind(int port) {
29+
this.port = port;
30+
return this;
31+
}
32+
33+
public void start() {
2034
ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS);
2135
handler.setContextPath(DEFAULT_SERVLET_CONTEXT_PATH);
2236

23-
ServletHolder servletHolder = new ServletHolder(transportProvider);
37+
ServletHolder servletHolder = new ServletHolder(servlet);
2438
handler.addServlet(servletHolder, DEFAULT_SERVLET_PATH);
2539

2640
Server httpserver = new Server(port);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.github.codeboyzhou.mcp.declarative.server;
2+
3+
import io.modelcontextprotocol.server.McpTransportContextExtractor;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import java.time.Duration;
6+
7+
public class McpStreamableServerInfo extends McpServerInfo {
8+
9+
private final int port;
10+
11+
private final String mcpEndpoint;
12+
13+
private final boolean disallowDelete;
14+
15+
private final McpTransportContextExtractor<HttpServletRequest> contextExtractor;
16+
17+
private final Duration keepAliveInterval;
18+
19+
private McpStreamableServerInfo(McpStreamableServerInfo.Builder builder) {
20+
super(builder);
21+
this.port = builder.port;
22+
this.mcpEndpoint = builder.mcpEndpoint;
23+
this.disallowDelete = builder.disallowDelete;
24+
this.contextExtractor = builder.contextExtractor;
25+
this.keepAliveInterval = builder.keepAliveInterval;
26+
}
27+
28+
public static McpStreamableServerInfo.Builder builder() {
29+
return new McpStreamableServerInfo.Builder();
30+
}
31+
32+
public int port() {
33+
return port;
34+
}
35+
36+
public String mcpEndpoint() {
37+
return mcpEndpoint;
38+
}
39+
40+
public boolean disallowDelete() {
41+
return disallowDelete;
42+
}
43+
44+
public McpTransportContextExtractor<HttpServletRequest> contextExtractor() {
45+
return contextExtractor;
46+
}
47+
48+
public Duration keepAliveInterval() {
49+
return keepAliveInterval;
50+
}
51+
52+
public static class Builder extends McpServerInfo.Builder<McpStreamableServerInfo.Builder> {
53+
54+
private int port;
55+
56+
private String mcpEndpoint = "/mcp";
57+
58+
private boolean disallowDelete = false;
59+
60+
private McpTransportContextExtractor<HttpServletRequest> contextExtractor =
61+
(request, context) -> context;
62+
63+
private Duration keepAliveInterval;
64+
65+
@Override
66+
protected McpStreamableServerInfo.Builder self() {
67+
return this;
68+
}
69+
70+
@Override
71+
public McpStreamableServerInfo build() {
72+
return new McpStreamableServerInfo(this);
73+
}
74+
75+
public McpStreamableServerInfo.Builder port(int port) {
76+
this.port = port;
77+
return self();
78+
}
79+
80+
public McpStreamableServerInfo.Builder mcpEndpoint(String mcpEndpoint) {
81+
this.mcpEndpoint = mcpEndpoint;
82+
return self();
83+
}
84+
85+
public McpStreamableServerInfo.Builder disallowDelete(boolean disallowDelete) {
86+
this.disallowDelete = disallowDelete;
87+
return self();
88+
}
89+
90+
public McpStreamableServerInfo.Builder contextExtractor(
91+
McpTransportContextExtractor<HttpServletRequest> contextExtractor) {
92+
this.contextExtractor = contextExtractor;
93+
return self();
94+
}
95+
96+
public McpStreamableServerInfo.Builder keepAliveInterval(Duration keepAliveInterval) {
97+
this.keepAliveInterval = keepAliveInterval;
98+
return self();
99+
}
100+
}
101+
}

src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/AbstractMcpServerFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo;
44
import io.modelcontextprotocol.spec.McpSchema;
5-
import io.modelcontextprotocol.spec.McpServerTransportProvider;
5+
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
66

77
public abstract class AbstractMcpServerFactory<
8-
T extends McpServerTransportProvider, S extends McpServerInfo>
8+
T extends McpServerTransportProviderBase, S extends McpServerInfo>
99
implements McpServerFactory<T, S> {
1010

1111
@Override

src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/ConfigurableMcpHttpSseServerFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ public McpAsyncServer create() {
4141
.instructions(configuration.instructions())
4242
.requestTimeout(Duration.ofMillis(configuration.requestTimeout()))
4343
.build();
44-
McpHttpServer httpServer = new McpHttpServer(configuration.sse().port());
44+
McpHttpServer httpServer = new McpHttpServer();
4545
NamedThreadFactory threadFactory = new NamedThreadFactory(McpHttpServer.class.getSimpleName());
4646
ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory);
47-
executor.execute(() -> httpServer.start(transportProvider));
47+
executor.execute(
48+
() -> httpServer.use(transportProvider).bind(configuration.sse().port()).start());
4849
return server;
4950
}
5051
}

src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpHttpSseServerFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ public McpAsyncServer create(McpSseServerInfo serverInfo) {
3434
.instructions(serverInfo.instructions())
3535
.requestTimeout(serverInfo.requestTimeout())
3636
.build();
37-
McpHttpServer httpServer = new McpHttpServer(serverInfo.port());
37+
McpHttpServer httpServer = new McpHttpServer();
3838
NamedThreadFactory threadFactory = new NamedThreadFactory(McpHttpServer.class.getSimpleName());
3939
ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory);
40-
executor.execute(() -> httpServer.start(transportProvider));
40+
executor.execute(() -> httpServer.use(transportProvider).bind(serverInfo.port()).start());
4141
return server;
4242
}
4343
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.github.codeboyzhou.mcp.declarative.server.factory;
2+
3+
import com.github.codeboyzhou.mcp.declarative.common.NamedThreadFactory;
4+
import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer;
5+
import com.github.codeboyzhou.mcp.declarative.server.McpStreamableServerInfo;
6+
import com.github.codeboyzhou.mcp.declarative.util.ObjectMappers;
7+
import io.modelcontextprotocol.server.McpAsyncServer;
8+
import io.modelcontextprotocol.server.McpServer;
9+
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
10+
import java.util.concurrent.ExecutorService;
11+
import java.util.concurrent.Executors;
12+
13+
public class McpHttpStreamableServerFactory
14+
extends AbstractMcpServerFactory<
15+
HttpServletStreamableServerTransportProvider, McpStreamableServerInfo> {
16+
17+
@Override
18+
public HttpServletStreamableServerTransportProvider transportProvider(
19+
McpStreamableServerInfo serverInfo) {
20+
return HttpServletStreamableServerTransportProvider.builder()
21+
.objectMapper(ObjectMappers.JSON_MAPPER)
22+
.mcpEndpoint(serverInfo.mcpEndpoint())
23+
.disallowDelete(serverInfo.disallowDelete())
24+
.contextExtractor(serverInfo.contextExtractor())
25+
.keepAliveInterval(serverInfo.keepAliveInterval())
26+
.build();
27+
}
28+
29+
@Override
30+
public McpAsyncServer create(McpStreamableServerInfo serverInfo) {
31+
HttpServletStreamableServerTransportProvider transportProvider = transportProvider(serverInfo);
32+
McpAsyncServer server =
33+
McpServer.async(transportProvider)
34+
.serverInfo(serverInfo.name(), serverInfo.version())
35+
.capabilities(serverCapabilities())
36+
.instructions(serverInfo.instructions())
37+
.requestTimeout(serverInfo.requestTimeout())
38+
.build();
39+
McpHttpServer httpServer = new McpHttpServer();
40+
NamedThreadFactory threadFactory = new NamedThreadFactory(McpHttpServer.class.getSimpleName());
41+
ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory);
42+
executor.execute(() -> httpServer.use(transportProvider).bind(serverInfo.port()).start());
43+
return server;
44+
}
45+
}

src/main/java/com/github/codeboyzhou/mcp/declarative/server/factory/McpServerFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo;
44
import io.modelcontextprotocol.server.McpAsyncServer;
55
import io.modelcontextprotocol.spec.McpSchema;
6-
import io.modelcontextprotocol.spec.McpServerTransportProvider;
6+
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
77

8-
public interface McpServerFactory<T extends McpServerTransportProvider, S extends McpServerInfo> {
8+
public interface McpServerFactory<
9+
T extends McpServerTransportProviderBase, S extends McpServerInfo> {
910

1011
T transportProvider(S serverInfo);
1112

src/test/java/com/github/codeboyzhou/mcp/declarative/McpServersTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.github.codeboyzhou.mcp.declarative.common.GuiceInjectorModule;
1111
import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo;
1212
import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo;
13+
import com.github.codeboyzhou.mcp.declarative.server.McpStreamableServerInfo;
1314
import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageClass;
1415
import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanBasePackageString;
1516
import com.github.codeboyzhou.mcp.declarative.server.TestMcpComponentScanDefault;
@@ -110,6 +111,26 @@ void testStartSseServer() {
110111
});
111112
}
112113

114+
@Test
115+
void testStartStreamableServer() {
116+
McpServers servers = McpServers.run(TestMcpComponentScanIsNull.class, EMPTY_ARGS);
117+
assertDoesNotThrow(
118+
() -> {
119+
McpStreamableServerInfo serverInfo =
120+
McpStreamableServerInfo.builder()
121+
.instructions("test-mcp-sync-sse-server-instructions")
122+
.requestTimeout(Duration.ofSeconds(20))
123+
.name("test-mcp-sync-sse-server")
124+
.version("1.0.0")
125+
.port(8080)
126+
.mcpEndpoint("/mcp")
127+
.disallowDelete(true)
128+
.keepAliveInterval(Duration.ofSeconds(60))
129+
.build();
130+
servers.startStreamableServer(serverInfo);
131+
});
132+
}
133+
113134
@Test
114135
void testStartServer() {
115136
assertDoesNotThrow(

0 commit comments

Comments
 (0)