Skip to content

Commit 55d3e12

Browse files
committed
Deferred only analyzer loading
1 parent d9c62a8 commit 55d3e12

File tree

2 files changed

+33
-56
lines changed

2 files changed

+33
-56
lines changed

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

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.sonarsource.sonarqube.mcp.serverapi.ServerApi;
4545
import org.sonarsource.sonarqube.mcp.serverapi.ServerApiHelper;
4646
import org.sonarsource.sonarqube.mcp.serverapi.ServerApiProvider;
47+
import org.sonarsource.sonarqube.mcp.serverapi.features.Feature;
4748
import org.sonarsource.sonarqube.mcp.slcore.BackendService;
4849
import org.sonarsource.sonarqube.mcp.tools.Tool;
4950
import org.sonarsource.sonarqube.mcp.tools.ToolExecutor;
@@ -129,7 +130,9 @@ public SonarQubeMcpServer(Map<String, String> environment) {
129130
this.transportProvider = new StdioServerTransportProvider(new ObjectMapper());
130131
}
131132

132-
initializeBasicServices();
133+
sonarQubeVersionChecker.failIfSonarQubeServerVersionIsNotSupported();
134+
135+
initializeBasicServicesAndTools();
133136
}
134137

135138
public void start() {
@@ -138,7 +141,6 @@ public void start() {
138141
httpServerManager.startServer().join();
139142
}
140143

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

@@ -168,10 +170,9 @@ public void start() {
168170
}
169171

170172
/**
171-
* Quick initialization - only creates basic services needed for configuration validation.
172-
* Heavy operations (version check, plugin download, backend init) are deferred to background.
173+
* Quick operations only - heavy operations (plugin download, backend init) are deferred to background.
173174
*/
174-
private void initializeBasicServices() {
175+
private void initializeBasicServicesAndTools() {
175176
this.backendService = new BackendService(mcpConfiguration);
176177
this.httpClientProvider = new HttpClientProvider(mcpConfiguration.getUserAgent());
177178
this.toolExecutor = new ToolExecutor(backendService, initializationFuture);
@@ -181,9 +182,11 @@ private void initializeBasicServices() {
181182
if (mcpConfiguration.isHttpEnabled()) {
182183
var initServerApi = createServerApiWithToken(mcpConfiguration.getSonarQubeToken());
183184
this.sonarQubeVersionChecker = new SonarQubeVersionChecker(initServerApi);
185+
loadBackendIndependentTools(initServerApi);
184186
} else {
185187
this.serverApi = initializeServerApi(mcpConfiguration);
186188
this.sonarQubeVersionChecker = new SonarQubeVersionChecker(serverApi);
189+
loadBackendIndependentTools(serverApi);
187190
}
188191
}
189192

@@ -192,10 +195,6 @@ private void initializeBasicServices() {
192195
*/
193196
private void initializeBackgroundServices() {
194197
try {
195-
sonarQubeVersionChecker.failIfSonarQubeServerVersionIsNotSupported();
196-
197-
loadBackendIndependentTools();
198-
199198
PluginsSynchronizer pluginsSynchronizer;
200199
if (mcpConfiguration.isHttpEnabled()) {
201200
var initServerApi = createServerApiWithToken(mcpConfiguration.getSonarQubeToken());
@@ -229,20 +228,19 @@ private void initializeBackgroundServices() {
229228
* These can be loaded BEFORE plugin synchronization (which is slow).
230229
* This makes most tools available to users within seconds instead of minutes.
231230
*/
232-
private void loadBackendIndependentTools() {
233-
var independentTools = new ArrayList<Tool>();
231+
private void loadBackendIndependentTools(ServerApi serverApi) {
234232
if (mcpConfiguration.isSonarCloud()) {
235-
independentTools.add(new ListEnterprisesTool(this));
233+
supportedTools.add(new ListEnterprisesTool(this));
236234
} else {
237-
independentTools.addAll(List.of(
235+
supportedTools.addAll(List.of(
238236
new SystemHealthTool(this),
239237
new SystemInfoTool(this),
240238
new SystemLogsTool(this),
241239
new SystemPingTool(this),
242240
new SystemStatusTool(this)));
243241
}
244242

245-
independentTools.addAll(List.of(
243+
supportedTools.addAll(List.of(
246244
new ChangeIssueStatusTool(this),
247245
new SearchMyProjectsTool(this),
248246
new SearchIssuesTool(this),
@@ -259,7 +257,11 @@ private void loadBackendIndependentTools() {
259257
new ListWebhooksTool(this),
260258
new ListPortfoliosTool(this, mcpConfiguration.isSonarCloud())));
261259

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

265267
/**
@@ -284,22 +286,24 @@ private void loadBackendDependentTools() {
284286
LOG.info("Standard analysis mode (no IDE bridge)");
285287
dependentTools.add(new AnalysisTool(backendService, this));
286288
}
287-
dependentTools.add(new SearchDependencyRisksTool(this, sonarQubeVersionChecker));
288289

289290
registerAndNotifyBatch(dependentTools);
290291
var filterReason = mcpConfiguration.isReadOnlyMode() ? "category and read-only filtering" : "category filtering";
291292
LOG.info("All tools loaded: " + this.supportedTools.size() + " tools after " + filterReason);
292293
}
293294

295+
private List<Tool> filterForEnabledTools(List<Tool> toolsToFilter) {
296+
return toolsToFilter.stream()
297+
.filter(tool -> mcpConfiguration.isToolCategoryEnabled(tool.getCategory()))
298+
.filter(tool -> !mcpConfiguration.isReadOnlyMode() || tool.definition().annotations().readOnlyHint())
299+
.toList();
300+
}
301+
294302
/**
295303
* Registers a batch of tools after filtering based on configuration.
296-
* Tools are filtered by category and read-only mode before being registered.
297304
*/
298305
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();
306+
var filteredTools = filterForEnabledTools(tools);
303307

304308
this.supportedTools.addAll(filteredTools);
305309

@@ -438,7 +442,7 @@ public SonarQubeMcpServer(McpServerTransportProviderBase transportProvider, @Nul
438442
this.mcpConfiguration = new McpServerLaunchConfiguration(environment);
439443
this.transportProvider = transportProvider;
440444
this.httpServerManager = httpServerManager;
441-
initializeBasicServices();
445+
initializeBasicServicesAndTools();
442446
}
443447

444448
// Package-private getters for testing

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)