Skip to content

Commit 0a4c857

Browse files
authored
Support for MCP features and SSE (#34)
* Depend on SNAPSHOT version of Helidon. Initial switch to SSE connection to support MCP features. * Add a context object to a session for features to store any relevant state. Refactored logger feature. * Improve tests for logging using SSE and streamable HTTP.
1 parent 9ebe34c commit 0a4c857

File tree

16 files changed

+577
-97
lines changed

16 files changed

+577
-97
lines changed

examples/calendar/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<parent>
2424
<groupId>io.helidon.applications</groupId>
2525
<artifactId>helidon-se</artifactId>
26-
<version>4.3.0-M1</version>
26+
<version>4.3.0-SNAPSHOT</version>
2727
<relativePath/>
2828
</parent>
2929

examples/weather-application/mcp-client/pom.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<parent>
2424
<groupId>io.helidon.applications</groupId>
2525
<artifactId>helidon-se</artifactId>
26-
<version>4.3.0-M1</version>
26+
<version>4.3.0-SNAPSHOT</version>
2727
<relativePath/>
2828
</parent>
2929

@@ -86,4 +86,24 @@
8686
</plugin>
8787
</plugins>
8888
</build>
89+
90+
<repositories>
91+
<repository>
92+
<id>central-snapshot</id>
93+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
94+
<snapshots>
95+
<enabled>true</enabled>
96+
</snapshots>
97+
</repository>
98+
</repositories>
99+
100+
<pluginRepositories>
101+
<pluginRepository>
102+
<id>central-snapshot</id>
103+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
104+
<snapshots>
105+
<enabled>true</enabled>
106+
</snapshots>
107+
</pluginRepository>
108+
</pluginRepositories>
89109
</project>

examples/weather-application/mcp-server-declarative/pom.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<parent>
2424
<groupId>io.helidon.applications</groupId>
2525
<artifactId>helidon-se</artifactId>
26-
<version>4.3.0-M1</version>
26+
<version>4.3.0-SNAPSHOT</version>
2727
<relativePath/>
2828
</parent>
2929

@@ -126,4 +126,24 @@
126126
</plugin>
127127
</plugins>
128128
</build>
129+
130+
<repositories>
131+
<repository>
132+
<id>central-snapshot</id>
133+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
134+
<snapshots>
135+
<enabled>true</enabled>
136+
</snapshots>
137+
</repository>
138+
</repositories>
139+
140+
<pluginRepositories>
141+
<pluginRepository>
142+
<id>central-snapshot</id>
143+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
144+
<snapshots>
145+
<enabled>true</enabled>
146+
</snapshots>
147+
</pluginRepository>
148+
</pluginRepositories>
129149
</project>

examples/weather-application/mcp-server/pom.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<parent>
2424
<groupId>io.helidon.applications</groupId>
2525
<artifactId>helidon-se</artifactId>
26-
<version>4.3.0-M1</version>
26+
<version>4.3.0-SNAPSHOT</version>
2727
<relativePath/>
2828
</parent>
2929

@@ -75,4 +75,24 @@
7575
</plugin>
7676
</plugins>
7777
</build>
78+
79+
<repositories>
80+
<repository>
81+
<id>central-snapshot</id>
82+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
83+
<snapshots>
84+
<enabled>true</enabled>
85+
</snapshots>
86+
</repository>
87+
</repositories>
88+
89+
<pluginRepositories>
90+
<pluginRepository>
91+
<id>central-snapshot</id>
92+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
93+
<snapshots>
94+
<enabled>true</enabled>
95+
</snapshots>
96+
</pluginRepository>
97+
</pluginRepositories>
7898
</project>

pom.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
<properties>
6262
<version.java>21</version.java>
63-
<helidon.version>4.3.0-M1</helidon.version>
63+
<helidon.version>4.3.0-SNAPSHOT</helidon.version>
6464

6565
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
6666
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@@ -591,4 +591,24 @@
591591
</modules>
592592
</profile>
593593
</profiles>
594+
595+
<repositories>
596+
<repository>
597+
<id>central-snapshot</id>
598+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
599+
<snapshots>
600+
<enabled>true</enabled>
601+
</snapshots>
602+
</repository>
603+
</repositories>
604+
605+
<pluginRepositories>
606+
<pluginRepository>
607+
<id>central-snapshot</id>
608+
<url>https://central.sonatype.com/repository/maven-snapshots</url>
609+
<snapshots>
610+
<enabled>true</enabled>
611+
</snapshots>
612+
</pluginRepository>
613+
</pluginRepositories>
594614
</project>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.extensions.mcp.server;
17+
18+
import java.util.Objects;
19+
import java.util.Optional;
20+
21+
import io.helidon.common.context.Context;
22+
import io.helidon.webserver.sse.SseSink;
23+
24+
/**
25+
* Base class for all MCP features.
26+
*/
27+
class McpFeature {
28+
29+
private final McpSession session;
30+
private final SseSink sseSink;
31+
32+
McpFeature(McpSession session) {
33+
Objects.requireNonNull(session, "session is null");
34+
this.session = session;
35+
this.sseSink = null;
36+
}
37+
38+
McpFeature(McpSession session, SseSink sseSink) {
39+
Objects.requireNonNull(session, "session is null");
40+
Objects.requireNonNull(sseSink, "sseSink is null");
41+
this.session = session;
42+
this.sseSink = sseSink;
43+
}
44+
45+
protected McpSession session() {
46+
return session;
47+
}
48+
49+
protected Optional<SseSink> sseSink() {
50+
return Optional.ofNullable(sseSink);
51+
}
52+
53+
protected Context context() {
54+
return session.context();
55+
}
56+
}

server/src/main/java/io/helidon/extensions/mcp/server/McpFeatures.java

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,86 @@
1616

1717
package io.helidon.extensions.mcp.server;
1818

19+
import java.util.Objects;
20+
import java.util.Optional;
21+
22+
import io.helidon.webserver.jsonrpc.JsonRpcResponse;
23+
import io.helidon.webserver.sse.SseSink;
24+
1925
/**
2026
* Support for optional client features like {@link McpProgress} and {@link McpLogger}.
2127
*/
2228
public final class McpFeatures {
23-
private final McpProgress progress;
24-
private final McpLogger logger;
29+
private final JsonRpcResponse response;
30+
private final McpSession session;
31+
32+
private McpProgress progress;
33+
private McpLogger logger;
34+
private SseSink sseSink;
2535

2636
McpFeatures(McpSession session) {
27-
this.logger = new McpLogger(session);
28-
this.progress = new McpProgress(session);
37+
Objects.requireNonNull(session, "session is null");
38+
this.session = session;
39+
this.response = null;
40+
}
41+
42+
McpFeatures(McpSession session, JsonRpcResponse response) {
43+
Objects.requireNonNull(response, "response is null");
44+
Objects.requireNonNull(session, "session is null");
45+
this.response = response;
46+
this.session = session;
2947
}
3048

3149
/**
3250
* Get a {@link McpProgress} feature.
3351
*
34-
* @return progress
52+
* @return progress the MCP progress
3553
*/
3654
public McpProgress progress() {
55+
if (progress == null) {
56+
if (response != null) {
57+
sseSink = getOrCreateSseSink();
58+
progress = new McpProgress(session, sseSink);
59+
} else {
60+
progress = new McpProgress(session);
61+
}
62+
}
3763
return progress;
3864
}
3965

4066
/**
4167
* Get a {@link McpLogger} feature.
4268
*
43-
* @return logging
69+
* @return logging the MCP logger
4470
*/
4571
public McpLogger logger() {
72+
if (logger == null) {
73+
if (response != null) {
74+
sseSink = getOrCreateSseSink();
75+
logger = new McpLogger(session, sseSink);
76+
} else {
77+
logger = new McpLogger(session);
78+
}
79+
}
4680
return logger;
4781
}
82+
83+
/**
84+
* Get access to underlying SSE sink, if available. This method is package private.
85+
*
86+
* @return optional SSE sink
87+
*/
88+
Optional<SseSink> sseSink() {
89+
return Optional.ofNullable(sseSink);
90+
}
91+
92+
/**
93+
* Get or create an SSE sink for this instance.
94+
*
95+
* @return the SSE sink
96+
*/
97+
private SseSink getOrCreateSseSink() {
98+
Objects.requireNonNull(response, "response is null");
99+
return sseSink != null ? sseSink : response.sink(SseSink.TYPE);
100+
}
48101
}

server/src/main/java/io/helidon/extensions/mcp/server/McpLogger.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@
1818

1919
import java.util.Objects;
2020

21+
import io.helidon.http.sse.SseEvent;
22+
import io.helidon.webserver.sse.SseSink;
23+
2124
/**
2225
* Mcp logger to send notification to the client.
2326
*/
24-
public final class McpLogger {
27+
public final class McpLogger extends McpFeature {
28+
2529
private final String name;
26-
private final McpSession session;
27-
private Level level;
2830

2931
McpLogger(McpSession session) {
30-
this.session = session;
31-
this.level = Level.INFO;
32+
super(session);
33+
this.name = "helidon-logger";
34+
}
35+
36+
McpLogger(McpSession session, SseSink sseSink) {
37+
super(session, sseSink);
3238
this.name = "helidon-logger";
3339
}
3440

@@ -41,8 +47,16 @@ public final class McpLogger {
4147
public void log(Level level, String message) {
4248
Objects.requireNonNull(level, "level must not be null");
4349
Objects.requireNonNull(message, "message must not be null");
44-
if (level.ordinal() >= this.level.ordinal()) {
45-
session.send(McpJsonRpc.createLoggingNotification(level, name, message));
50+
51+
if (level.ordinal() >= level().ordinal()) {
52+
if (sseSink().isPresent()) {
53+
sseSink().get().emit(SseEvent.builder()
54+
.name("message")
55+
.data(McpJsonRpc.createLoggingNotification(level, name, message))
56+
.build());
57+
} else {
58+
session().send(McpJsonRpc.createLoggingNotification(level, name, message));
59+
}
4660
}
4761
}
4862

@@ -109,8 +123,23 @@ public void alert(String message) {
109123
log(Level.ALERT, message);
110124
}
111125

126+
/**
127+
* Get level for this logger.
128+
*
129+
* @return the level
130+
*/
131+
McpLogger.Level level() {
132+
return context().get(Level.class).orElse(Level.INFO);
133+
}
134+
135+
/**
136+
* Set level on the session since there could be multiple instances of this
137+
* class with streamable HTTP.
138+
*
139+
* @param level the level
140+
*/
112141
void setLevel(McpLogger.Level level) {
113-
this.level = level;
142+
context().register(level);
114143
}
115144

116145
/**

0 commit comments

Comments
 (0)