Skip to content

Commit 409f6a1

Browse files
authored
MCP-197 Terminate the JVM properly (#138)
1 parent 443af01 commit 409f6a1

File tree

6 files changed

+28
-21
lines changed

6 files changed

+28
-21
lines changed

src/main/java/org/sonarsource/sonarqube/mcp/SonarQubeMcpServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public SonarQubeMcpServer(Map<String, String> environment) {
121121
this.transportProvider = httpServerManager.getTransportProvider();
122122
} else {
123123
this.httpServerManager = null;
124-
this.transportProvider = new StdioServerTransportProvider(new ObjectMapper());
124+
this.transportProvider = new StdioServerTransportProvider(new ObjectMapper(), this::shutdown);
125125
}
126126

127127
initializeServices();

src/main/java/org/sonarsource/sonarqube/mcp/transport/StdioServerTransportProvider.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.concurrent.Executors;
4040
import java.util.concurrent.atomic.AtomicBoolean;
4141
import java.util.function.Function;
42+
import javax.annotation.Nullable;
4243
import org.slf4j.Logger;
4344
import org.slf4j.LoggerFactory;
4445
import reactor.core.publisher.Flux;
@@ -71,30 +72,37 @@ public class StdioServerTransportProvider implements McpServerTransportProvider
7172

7273
private final Sinks.One<Void> inboundReady = Sinks.one();
7374

75+
private final Runnable shutdownCallback;
76+
7477
/**
7578
* Creates a new StdioServerTransportProvider with the specified ObjectMapper and
76-
* System streams. Will call System.exit(0) when stdin closes (for Docker containers).
79+
* System streams. Will call shutdown callback when stdin closes (for Docker containers).
7780
*/
78-
public StdioServerTransportProvider(ObjectMapper objectMapper) {
79-
this(new JacksonMcpJsonMapper(objectMapper), System.in, System.out);
81+
public StdioServerTransportProvider(ObjectMapper objectMapper, Runnable shutdownCallback) {
82+
this(new JacksonMcpJsonMapper(objectMapper), System.in, System.out, shutdownCallback);
8083
}
8184

8285
/**
8386
* Creates a new StdioServerTransportProvider with the specified ObjectMapper and
84-
* streams. Automatically detects if custom streams are used (tests) to disable System.exit().
87+
* streams. Automatically detects if custom streams are used (tests) to disable shutdown callback.
8588
*
8689
* @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
8790
* @param inputStream The input stream to read from
8891
* @param outputStream The output stream to write to
8992
*/
9093
public StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputStream, OutputStream outputStream) {
94+
this(jsonMapper, inputStream, outputStream, null);
95+
}
96+
97+
private StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputStream, OutputStream outputStream, @Nullable Runnable shutdownCallback) {
9198
Assert.notNull(jsonMapper, "The JsonMapper can not be null");
9299
Assert.notNull(inputStream, "The InputStream can not be null");
93100
Assert.notNull(outputStream, "The OutputStream can not be null");
94101

95102
this.jsonMapper = jsonMapper;
96103
this.inputStream = inputStream;
97104
this.outputStream = outputStream;
105+
this.shutdownCallback = shutdownCallback;
98106
}
99107

100108
@Override
@@ -267,6 +275,16 @@ private void startInboundProcessing() {
267275
session.close();
268276
}
269277
inboundSink.tryEmitComplete();
278+
279+
// Trigger graceful shutdown when stdin closes if environment variable is set (avoid stale containers)
280+
if (shutdownCallback != null) {
281+
logger.info("stdin closed (EOF detected) - initiating graceful shutdown");
282+
try {
283+
shutdownCallback.run();
284+
} catch (Exception e) {
285+
logger.error("Error during graceful shutdown", e);
286+
}
287+
}
270288
}
271289
});
272290
}

src/test/java/org/sonarsource/sonarqube/mcp/SonarQubeMcpServerHttpTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ void should_not_create_http_manager_when_disabled(SonarQubeMcpServerTestHarness
104104

105105
harness.prepareMockWebServer(environment);
106106

107-
var server = new SonarQubeMcpServer(new StdioServerTransportProvider(new ObjectMapper()), null, environment);
107+
var server = new SonarQubeMcpServer(new StdioServerTransportProvider(new ObjectMapper(), null), null, environment);
108108

109109
assertThat(server.getMcpConfiguration().isHttpEnabled()).isFalse();
110110
}

src/test/java/org/sonarsource/sonarqube/mcp/SonarQubeMcpServerIdeBridgeTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ void should_add_bridge_related_tools_when_ide_bridge_is_available(SonarQubeMcpSe
5959

6060
harness.prepareMockWebServer(environment);
6161

62-
var server = new SonarQubeMcpServer(new StdioServerTransportProvider(new ObjectMapper()), null, environment);
62+
var server = new SonarQubeMcpServer(new StdioServerTransportProvider(new ObjectMapper(), null), null, environment);
6363
server.start();
6464

6565
var supportedTools = server.getSupportedTools();
@@ -78,7 +78,7 @@ void should_add_analysis_tool_when_ide_bridge_is_not_available(SonarQubeMcpServe
7878

7979
harness.prepareMockWebServer(environment);
8080

81-
var server = new SonarQubeMcpServer(new StdioServerTransportProvider(new ObjectMapper()), null, environment);
81+
var server = new SonarQubeMcpServer(new StdioServerTransportProvider(new ObjectMapper(), null), null, environment);
8282
server.start();
8383

8484
var supportedTools = server.getSupportedTools();

src/test/java/org/sonarsource/sonarqube/mcp/harness/SonarQubeMcpServerTestHarness.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ public class SonarQubeMcpServerTestHarness extends TypeBasedParameterResolver<So
5555
"SONARQUBE_TOKEN", "token");
5656
private final List<McpSyncClient> clients = new ArrayList<>();
5757
private Path tempStoragePath;
58-
private final List<SonarQubeMcpServer> servers = new ArrayList<>();
5958
private final MockWebServer mockSonarQubeServer = new MockWebServer();
6059

6160
@Override
@@ -72,15 +71,6 @@ public void beforeEach(ExtensionContext context) {
7271
public void afterEach(ExtensionContext context) {
7372
clients.forEach(McpSyncClient::closeGracefully);
7473
clients.clear();
75-
servers.forEach(server -> {
76-
try {
77-
server.shutdown();
78-
} catch (Exception e) {
79-
// Log error but continue cleanup
80-
System.err.println("Error shutting down server: " + e.getMessage());
81-
}
82-
});
83-
servers.clear();
8474
cleanupTempStoragePath();
8575
mockSonarQubeServer.stop();
8676
}
@@ -142,7 +132,6 @@ public SonarQubeMcpTestClient newClient(Map<String, String> overriddenEnv) {
142132
.loggingConsumer(SonarQubeMcpServerTestHarness::printLogs).build();
143133
client.initialize();
144134
this.clients.add(client);
145-
this.servers.add(server);
146135
return new SonarQubeMcpTestClient(client);
147136
}
148137

src/test/java/org/sonarsource/sonarqube/mcp/transport/StdioServerTransportProviderTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class StdioServerTransportProviderTest {
3737
* We need to use reflection to set the session field directly to avoid the background threads.
3838
*/
3939
private StdioServerTransportProvider createProviderWithMockSession(McpServerSession mockSession) throws Exception {
40-
var provider = new StdioServerTransportProvider(new ObjectMapper());
40+
var provider = new StdioServerTransportProvider(new ObjectMapper(), null);
4141
var sessionField = StdioServerTransportProvider.class.getDeclaredField("session");
4242
sessionField.setAccessible(true);
4343
sessionField.set(provider, mockSession);
@@ -46,7 +46,7 @@ private StdioServerTransportProvider createProviderWithMockSession(McpServerSess
4646

4747
@Test
4848
void closeGracefully_should_return_empty_mono_when_session_is_null() {
49-
var provider = new StdioServerTransportProvider(new ObjectMapper());
49+
var provider = new StdioServerTransportProvider(new ObjectMapper(), null);
5050

5151
var result = provider.closeGracefully();
5252

0 commit comments

Comments
 (0)