Skip to content

Commit 4a933e6

Browse files
committed
Deferred only analyzer loading
1 parent 46fe339 commit 4a933e6

File tree

3 files changed

+60
-63
lines changed

3 files changed

+60
-63
lines changed

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

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Objects;
3333
import java.util.concurrent.CompletableFuture;
3434
import java.util.concurrent.ExecutionException;
35+
import java.util.concurrent.TimeoutException;
3536
import java.util.function.Function;
3637
import javax.annotation.Nullable;
3738
import org.sonarsource.sonarqube.mcp.bridge.SonarQubeIdeBridgeClient;
@@ -44,6 +45,7 @@
4445
import org.sonarsource.sonarqube.mcp.serverapi.ServerApi;
4546
import org.sonarsource.sonarqube.mcp.serverapi.ServerApiHelper;
4647
import org.sonarsource.sonarqube.mcp.serverapi.ServerApiProvider;
48+
import org.sonarsource.sonarqube.mcp.serverapi.features.Feature;
4749
import org.sonarsource.sonarqube.mcp.slcore.BackendService;
4850
import org.sonarsource.sonarqube.mcp.tools.Tool;
4951
import org.sonarsource.sonarqube.mcp.tools.ToolExecutor;
@@ -129,7 +131,7 @@ public SonarQubeMcpServer(Map<String, String> environment) {
129131
this.transportProvider = new StdioServerTransportProvider(new ObjectMapper(), this::shutdown);
130132
}
131133

132-
initializeBasicServices();
134+
initializeBasicServicesAndTools();
133135
}
134136

135137
public void start() {
@@ -138,7 +140,6 @@ public void start() {
138140
httpServerManager.startServer().join();
139141
}
140142

141-
// Build and start MCP server immediately with NO tools, they will be added dynamically once background initialization completes
142143
Function<Object, McpSyncServer> serverBuilder = provider -> {
143144
var builder = switch (provider) {
144145
case McpServerTransportProvider p -> McpServer.sync(p);
@@ -151,7 +152,7 @@ public void start() {
151152
"Analyze code, monitor project health, investigate issues, and understand quality gates. " +
152153
"Note: Tools are being loaded in the background and will be available shortly.")
153154
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).logging().build())
154-
// Start with no tools - they will be added dynamically after initialization
155+
.tools(filterForEnabledTools(supportedTools).stream().map(this::toSpec).toArray(McpServerFeatures.SyncToolSpecification[]::new))
155156
.build();
156157
};
157158

@@ -168,10 +169,9 @@ public void start() {
168169
}
169170

170171
/**
171-
* Quick initialization - only creates basic services needed for configuration validation.
172-
* Heavy operations (version check, plugin download, backend init) are deferred to background.
172+
* Quick operations only - heavy operations (plugin download, backend init) are deferred to background.
173173
*/
174-
private void initializeBasicServices() {
174+
private void initializeBasicServicesAndTools() {
175175
this.backendService = new BackendService(mcpConfiguration);
176176
this.httpClientProvider = new HttpClientProvider(mcpConfiguration.getUserAgent());
177177
this.toolExecutor = new ToolExecutor(backendService, initializationFuture);
@@ -181,21 +181,21 @@ private void initializeBasicServices() {
181181
if (mcpConfiguration.isHttpEnabled()) {
182182
var initServerApi = createServerApiWithToken(mcpConfiguration.getSonarQubeToken());
183183
this.sonarQubeVersionChecker = new SonarQubeVersionChecker(initServerApi);
184+
loadBackendIndependentTools(initServerApi);
184185
} else {
185186
this.serverApi = initializeServerApi(mcpConfiguration);
186187
this.sonarQubeVersionChecker = new SonarQubeVersionChecker(serverApi);
188+
loadBackendIndependentTools(serverApi);
187189
}
190+
191+
sonarQubeVersionChecker.failIfSonarQubeServerVersionIsNotSupported();
188192
}
189193

190194
/**
191195
* Heavy initialization that runs in background after the server has started.
192196
*/
193197
private void initializeBackgroundServices() {
194198
try {
195-
sonarQubeVersionChecker.failIfSonarQubeServerVersionIsNotSupported();
196-
197-
loadBackendIndependentTools();
198-
199199
PluginsSynchronizer pluginsSynchronizer;
200200
if (mcpConfiguration.isHttpEnabled()) {
201201
var initServerApi = createServerApiWithToken(mcpConfiguration.getSonarQubeToken());
@@ -229,20 +229,19 @@ private void initializeBackgroundServices() {
229229
* These can be loaded BEFORE plugin synchronization (which is slow).
230230
* This makes most tools available to users within seconds instead of minutes.
231231
*/
232-
private void loadBackendIndependentTools() {
233-
var independentTools = new ArrayList<Tool>();
232+
private void loadBackendIndependentTools(ServerApi serverApi) {
234233
if (mcpConfiguration.isSonarCloud()) {
235-
independentTools.add(new ListEnterprisesTool(this));
234+
supportedTools.add(new ListEnterprisesTool(this));
236235
} else {
237-
independentTools.addAll(List.of(
236+
supportedTools.addAll(List.of(
238237
new SystemHealthTool(this),
239238
new SystemInfoTool(this),
240239
new SystemLogsTool(this),
241240
new SystemPingTool(this),
242241
new SystemStatusTool(this)));
243242
}
244243

245-
independentTools.addAll(List.of(
244+
supportedTools.addAll(List.of(
246245
new ChangeIssueStatusTool(this),
247246
new SearchMyProjectsTool(this),
248247
new SearchIssuesTool(this),
@@ -259,7 +258,11 @@ private void loadBackendIndependentTools() {
259258
new ListWebhooksTool(this),
260259
new ListPortfoliosTool(this, mcpConfiguration.isSonarCloud())));
261260

262-
registerAndNotifyBatch(independentTools);
261+
var scaSupportedOnSQC = serverApi.isSonarQubeCloud() && serverApi.scaApi().isScaEnabled();
262+
var scaSupportedOnSQS = !serverApi.isSonarQubeCloud() && serverApi.featuresApi().listFeatures().contains(Feature.SCA);
263+
if (scaSupportedOnSQC || scaSupportedOnSQS) {
264+
supportedTools.add(new SearchDependencyRisksTool(this, sonarQubeVersionChecker));
265+
}
263266
}
264267

265268
/**
@@ -284,22 +287,24 @@ private void loadBackendDependentTools() {
284287
LOG.info("Standard analysis mode (no IDE bridge)");
285288
dependentTools.add(new AnalysisTool(backendService, this));
286289
}
287-
dependentTools.add(new SearchDependencyRisksTool(this, sonarQubeVersionChecker));
288290

289291
registerAndNotifyBatch(dependentTools);
290292
var filterReason = mcpConfiguration.isReadOnlyMode() ? "category and read-only filtering" : "category filtering";
291293
LOG.info("All tools loaded: " + this.supportedTools.size() + " tools after " + filterReason);
292294
}
293295

296+
private List<Tool> filterForEnabledTools(List<Tool> toolsToFilter) {
297+
return toolsToFilter.stream()
298+
.filter(tool -> mcpConfiguration.isToolCategoryEnabled(tool.getCategory()))
299+
.filter(tool -> !mcpConfiguration.isReadOnlyMode() || tool.definition().annotations().readOnlyHint())
300+
.toList();
301+
}
302+
294303
/**
295304
* Registers a batch of tools after filtering based on configuration.
296-
* Tools are filtered by category and read-only mode before being registered.
297305
*/
298306
private void registerAndNotifyBatch(List<Tool> tools) {
299-
var filteredTools = tools.stream()
300-
.filter(tool -> mcpConfiguration.isToolCategoryEnabled(tool.getCategory()))
301-
.filter(tool -> !mcpConfiguration.isReadOnlyMode() || tool.definition().annotations().readOnlyHint())
302-
.toList();
307+
var filteredTools = filterForEnabledTools(tools);
303308

304309
this.supportedTools.addAll(filteredTools);
305310

@@ -402,6 +407,19 @@ public void shutdown() {
402407
}
403408
isShutdown = true;
404409

410+
// Wait for background initialization to complete or cancel it
411+
if (!initializationFuture.isDone()) {
412+
LOG.info("Waiting for background initialization to complete before shutdown...");
413+
try {
414+
initializationFuture.get(30, java.util.concurrent.TimeUnit.SECONDS);
415+
} catch (TimeoutException | ExecutionException e) {
416+
LOG.warn("Background initialization did not complete within 30 seconds, proceeding with shutdown");
417+
initializationFuture.cancel(true);
418+
} catch (Exception e) {
419+
LOG.error("Background initialization failed or was interrupted", e);
420+
}
421+
}
422+
405423
// Stop HTTP server if running
406424
if (httpServerManager != null) {
407425
try {
@@ -414,7 +432,9 @@ public void shutdown() {
414432
}
415433

416434
try {
417-
httpClientProvider.shutdown();
435+
if (httpClientProvider != null) {
436+
httpClientProvider.shutdown();
437+
}
418438
} catch (Exception e) {
419439
LOG.error("Error shutting down HTTP client", e);
420440
}
@@ -438,7 +458,7 @@ public SonarQubeMcpServer(McpServerTransportProviderBase transportProvider, @Nul
438458
this.mcpConfiguration = new McpServerLaunchConfiguration(environment);
439459
this.transportProvider = transportProvider;
440460
this.httpServerManager = httpServerManager;
441-
initializeBasicServices();
461+
initializeBasicServicesAndTools();
442462
}
443463

444464
// Package-private getters for testing

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ void should_have_same_tools_regardless_of_transport(SonarQubeMcpServerTestHarnes
7777

7878
harness.prepareMockWebServer(environment);
7979

80-
var stdioServer = new SonarQubeMcpServer(environment);
80+
var stdioServer = new SonarQubeMcpServer(
81+
new StdioServerTransportProvider(new ObjectMapper(), null),
82+
null,
83+
environment);
8184
stdioServer.start();
82-
stdioServer.waitForInitialization(); // Wait for tools to load
85+
stdioServer.waitForInitialization();
8386
var stdioTools = stdioServer.getSupportedTools().stream()
8487
.map(tool -> tool.definition().name())
8588
.sorted()
@@ -88,15 +91,16 @@ void should_have_same_tools_regardless_of_transport(SonarQubeMcpServerTestHarnes
8891
environment.put("SONARQUBE_TRANSPORT", "http");
8992
var httpServer = new SonarQubeMcpServer(environment);
9093
httpServer.start();
91-
httpServer.waitForInitialization(); // Wait for tools to load
94+
httpServer.waitForInitialization();
9295
var httpTools = httpServer.getSupportedTools().stream()
9396
.map(tool -> tool.definition().name())
9497
.sorted()
9598
.toList();
96-
99+
100+
assertThat(stdioTools).isNotEmpty();
97101
assertThat(httpTools)
98-
.isEqualTo(stdioTools)
99-
.isNotEmpty();
102+
.isNotEmpty()
103+
.isEqualTo(stdioTools);
100104
}
101105

102106
@SonarQubeMcpServerTest

src/test/java/org/sonarsource/sonarqube/mcp/tools/dependencyrisks/SearchDependencyRisksToolTests.java

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
3131
import static com.github.tomakehurst.wiremock.client.WireMock.get;
32-
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
3332
import static org.assertj.core.api.Assertions.assertThat;
3433
import static org.sonarsource.sonarqube.mcp.harness.SonarQubeMcpTestClient.assertResultEquals;
3534
import static org.sonarsource.sonarqube.mcp.harness.SonarQubeMcpTestClient.assertSchemaEquals;
@@ -161,7 +160,7 @@ void it_should_validate_output_schema_and_annotations(SonarQubeMcpServerTestHarn
161160
class WithSonarCloudServer {
162161

163162
@SonarQubeMcpServerTest
164-
void it_should_find_tool_even_if_sca_is_disabled(SonarQubeMcpServerTestHarness harness) {
163+
void it_should_not_find_tool_if_sca_is_disabled(SonarQubeMcpServerTestHarness harness) {
165164
harness.getMockSonarQubeServer().stubFor(get(ScaApi.FEATURE_ENABLED_PATH + "?organization=org").willReturn(aResponse().withResponseBody(
166165
Body.fromJsonBytes("""
167166
{
@@ -173,26 +172,11 @@ void it_should_find_tool_even_if_sca_is_disabled(SonarQubeMcpServerTestHarness h
173172
"SONARQUBE_ORG", "org"));
174173

175174
assertThat(mcpClient.listTools())
175+
.isNotEmpty()
176176
.extracting(McpSchema.Tool::name)
177-
.contains(SearchDependencyRisksTool.TOOL_NAME);
177+
.doesNotContain(SearchDependencyRisksTool.TOOL_NAME);
178178
}
179179

180-
@SonarQubeMcpServerTest
181-
void it_should_return_an_error_if_the_sca_feature_is_disabled(SonarQubeMcpServerTestHarness harness) {
182-
harness.getMockSonarQubeServer().stubFor(get(ScaApi.FEATURE_ENABLED_PATH + "?organization=org").willReturn(aResponse().withResponseBody(
183-
Body.fromJsonBytes("""
184-
{
185-
"enabled": false
186-
}
187-
""".getBytes(StandardCharsets.UTF_8)))));
188-
var mcpClient = harness.newClient(Map.of(
189-
"SONARQUBE_CLOUD_URL", harness.getMockSonarQubeServer().baseUrl(),
190-
"SONARQUBE_ORG", "org"));
191-
192-
var result = mcpClient.callTool(SearchDependencyRisksTool.TOOL_NAME, Map.of(SearchDependencyRisksTool.PROJECT_KEY_PROPERTY, "my-project"));
193-
194-
assertThat(result).isEqualTo(new McpSchema.CallToolResult("Search Dependency Risks tool is not available in your SonarQube Cloud organization because Advanced Security is not enabled.", true));
195-
}
196180
}
197181

198182
@Nested
@@ -208,18 +192,6 @@ void it_should_return_an_error_if_the_request_fails_due_to_token_permission(Sona
208192
assertThat(result).isEqualTo(new McpSchema.CallToolResult("An error occurred during the tool execution: SonarQube answered with Forbidden. Please verify your token has the required permissions for this operation.", true));
209193
}
210194

211-
@SonarQubeMcpServerTest
212-
void it_should_return_an_error_if_the_sca_feature_is_disabled(SonarQubeMcpServerTestHarness harness) {
213-
harness.getMockSonarQubeServer().stubFor(get(FeaturesApi.FEATURES_LIST_PATH).willReturn(okJson("""
214-
[""]
215-
""")));
216-
var mcpClient = harness.newClient();
217-
218-
var result = mcpClient.callTool(SearchDependencyRisksTool.TOOL_NAME, Map.of(SearchDependencyRisksTool.PROJECT_KEY_PROPERTY, "my-project"));
219-
220-
assertThat(result).isEqualTo(new McpSchema.CallToolResult("Search Dependency Risks tool is not available for SonarQube Server because Advanced Security is not enabled.", true));
221-
}
222-
223195
@SonarQubeMcpServerTest
224196
void it_should_return_an_error_if_project_key_is_missing(SonarQubeMcpServerTestHarness harness) {
225197
var mcpClient = harness.newClient();
@@ -239,7 +211,7 @@ void it_should_not_find_tool_if_version_not_sufficient(SonarQubeMcpServerTestHar
239211
}
240212

241213
@SonarQubeMcpServerTest
242-
void it_should_find_tool_even_if_sca_is_disabled(SonarQubeMcpServerTestHarness harness) {
214+
void it_should_not_find_tool_if_sca_is_disabled(SonarQubeMcpServerTestHarness harness) {
243215
harness.getMockSonarQubeServer().stubFor(get(FeaturesApi.FEATURES_LIST_PATH).willReturn(aResponse().withResponseBody(
244216
Body.fromJsonBytes("""
245217
["prioritized-rules","from-sonarqube-update","multiple-alm"]
@@ -248,8 +220,9 @@ void it_should_find_tool_even_if_sca_is_disabled(SonarQubeMcpServerTestHarness h
248220
var mcpClient = harness.newClient();
249221

250222
assertThat(mcpClient.listTools())
223+
.isNotEmpty()
251224
.extracting(McpSchema.Tool::name)
252-
.contains(SearchDependencyRisksTool.TOOL_NAME);
225+
.doesNotContain(SearchDependencyRisksTool.TOOL_NAME);
253226
}
254227

255228
@SonarQubeMcpServerTest

0 commit comments

Comments
 (0)