1717package org .sonarsource .sonarqube .mcp ;
1818
1919import com .fasterxml .jackson .databind .ObjectMapper ;
20+ import com .google .common .annotations .VisibleForTesting ;
2021import io .modelcontextprotocol .server .McpServer ;
2122import io .modelcontextprotocol .server .McpServerFeatures ;
2223import io .modelcontextprotocol .server .McpSyncServer ;
2829import java .util .ArrayList ;
2930import java .util .List ;
3031import java .util .Map ;
32+ import java .util .Objects ;
33+ import java .util .concurrent .CompletableFuture ;
34+ import java .util .concurrent .ExecutionException ;
3135import java .util .function .Function ;
3236import javax .annotation .Nullable ;
3337import org .sonarsource .sonarqube .mcp .bridge .SonarQubeIdeBridgeClient ;
@@ -96,6 +100,7 @@ public class SonarQubeMcpServer implements ServerApiProvider {
96100 private McpSyncServer syncServer ;
97101 private volatile boolean isShutdown = false ;
98102 private boolean logFileLocationLogged ;
103+ private final CompletableFuture <Void > initializationFuture = new CompletableFuture <>();
99104
100105 public static void main (String [] args ) {
101106 new SonarQubeMcpServer (System .getenv ()).start ();
@@ -124,31 +129,122 @@ public SonarQubeMcpServer(Map<String, String> environment) {
124129 this .transportProvider = new StdioServerTransportProvider (new ObjectMapper ());
125130 }
126131
127- initializeServices ();
132+ initializeBasicServices ();
128133 }
129134
130- private void initializeServices () {
135+
136+
137+ public void start () {
138+ // Start HTTP server if enabled
139+ if (httpServerManager != null ) {
140+ LOG .info ("Starting HTTP server on " + mcpConfiguration .getHttpHost () + ":" + mcpConfiguration .getHttpPort () + "..." );
141+ httpServerManager .startServer ().join ();
142+ LOG .info ("HTTP server started" );
143+ }
144+
145+ // Build and start MCP server immediately with NO tools
146+ // Tools will be added dynamically once background initialization completes
147+ Function <Object , McpSyncServer > serverBuilder = provider -> {
148+ var builder = switch (provider ) {
149+ case McpServerTransportProvider p -> McpServer .sync (p );
150+ case McpStreamableServerTransportProvider p -> McpServer .sync (p );
151+ default -> throw new IllegalArgumentException ("Unsupported transport provider type: " + provider .getClass ().getName ());
152+ };
153+ return builder
154+ .serverInfo (new McpSchema .Implementation (SONARQUBE_MCP_SERVER_NAME , mcpConfiguration .getAppVersion ()))
155+ .instructions ("Transform your code quality workflow with SonarQube integration. " +
156+ "Analyze code, monitor project health, investigate issues, and understand quality gates. " +
157+ "Note: Tools are being loaded in the background and will be available shortly." )
158+ .capabilities (McpSchema .ServerCapabilities .builder ().tools (true ).logging ().build ())
159+ // Start with no tools - they will be added dynamically after initialization
160+ .build ();
161+ };
162+
163+ syncServer = serverBuilder .apply (transportProvider );
164+
165+ Runtime .getRuntime ().addShutdownHook (new Thread (this ::shutdown ));
166+
167+ logInitialization ();
168+
169+ // Start background initialization in a separate thread
170+ CompletableFuture .runAsync (this ::initializeBackgroundServices )
171+ .exceptionally (ex -> {
172+ LOG .error ("Fatal error during background initialization" , ex );
173+ return null ;
174+ });
175+ }
176+
177+ /**
178+ * Quick initialization - only creates basic services needed for configuration validation.
179+ * Heavy operations (version check, plugin download, backend init) are deferred to background.
180+ */
181+ private void initializeBasicServices () {
131182 this .backendService = new BackendService (mcpConfiguration );
132183 this .httpClientProvider = new HttpClientProvider (mcpConfiguration .getUserAgent ());
133- this .toolExecutor = new ToolExecutor (backendService );
184+ this .toolExecutor = new ToolExecutor (backendService , initializationFuture );
134185
135- PluginsSynchronizer pluginsSynchronizer ;
186+ // Create ServerApi and SonarQubeVersionChecker early (doesn't make network calls yet)
187+ // This allows initTools() to reference sonarQubeVersionChecker before background init completes
136188 if (mcpConfiguration .isHttpEnabled ()) {
137189 var initServerApi = createServerApiWithToken (mcpConfiguration .getSonarQubeToken ());
138190 this .sonarQubeVersionChecker = new SonarQubeVersionChecker (initServerApi );
139- pluginsSynchronizer = new PluginsSynchronizer (initServerApi , mcpConfiguration .getStoragePath ());
140191 } else {
141192 this .serverApi = initializeServerApi (mcpConfiguration );
142193 this .sonarQubeVersionChecker = new SonarQubeVersionChecker (serverApi );
143- pluginsSynchronizer = new PluginsSynchronizer (serverApi , mcpConfiguration .getStoragePath ());
144194 }
145- sonarQubeVersionChecker .failIfSonarQubeServerVersionIsNotSupported ();
146- var analyzers = pluginsSynchronizer .synchronizeAnalyzers ();
147- backendService .initialize (analyzers );
148- backendService .notifyTransportModeUsed ();
195+ }
196+
197+ /**
198+ * Heavy initialization that runs in background after the server has started.
199+ * Downloads analyzers, checks version, initializes the backend, and dynamically loads tools.
200+ */
201+ private void initializeBackgroundServices () {
202+ try {
203+ sonarQubeVersionChecker .failIfSonarQubeServerVersionIsNotSupported ();
204+
205+ PluginsSynchronizer pluginsSynchronizer ;
206+ if (mcpConfiguration .isHttpEnabled ()) {
207+ var initServerApi = createServerApiWithToken (mcpConfiguration .getSonarQubeToken ());
208+ pluginsSynchronizer = new PluginsSynchronizer (initServerApi , mcpConfiguration .getStoragePath ());
209+ } else {
210+ pluginsSynchronizer = new PluginsSynchronizer (Objects .requireNonNull (serverApi ), mcpConfiguration .getStoragePath ());
211+ }
212+ var analyzers = pluginsSynchronizer .synchronizeAnalyzers ();
213+
214+ backendService .initialize (analyzers );
215+ backendService .notifyTransportModeUsed ();
216+
217+ // Load and register tools dynamically
218+ LOG .info ("Loading tools..." );
219+ initTools ();
220+ notifyToolsLoaded ();
149221
150- LOG .info ("Startup initialization completed" );
222+ initializationFuture .complete (null );
223+ LOG .info ("Background initialization completed successfully" );
224+ } catch (Exception e ) {
225+ LOG .error ("Background initialization failed" , e );
226+ initializationFuture .completeExceptionally (e );
227+ throw e ;
228+ }
151229 }
230+
231+ /**
232+ * Notifies connected clients about newly loaded tools.
233+ * This may fail if no client is connected yet (common during startup or in tests).
234+ * Failure is acceptable as clients will discover tools via listTools() when they connect.
235+ */
236+ private void notifyToolsLoaded () {
237+ try {
238+ for (var tool : supportedTools ) {
239+ syncServer .addTool (toSpec (tool ));
240+ }
241+ syncServer .notifyToolsListChanged ();
242+ LOG .info (supportedTools .size () + " tools loaded and registered" );
243+ } catch (Exception e ) {
244+ LOG .info (supportedTools .size () + " tools loaded (client notification deferred)" );
245+ }
246+ }
247+
152248 private void initTools () {
153249 var allTools = new ArrayList <Tool >();
154250
@@ -213,37 +309,6 @@ private void initTools() {
213309 LOG .info ("Loaded " + this .supportedTools .size () + " tools after " + filterReason );
214310 }
215311
216- public void start () {
217- initTools ();
218-
219- if (httpServerManager != null ) {
220- LOG .info ("Starting HTTP server on " + mcpConfiguration .getHttpHost () + ":" + mcpConfiguration .getHttpPort () + "..." );
221- httpServerManager .startServer ().join ();
222- LOG .info ("HTTP server started" );
223- }
224-
225- Function <Object , McpSyncServer > serverBuilder = provider -> {
226- var builder = switch (provider ) {
227- case McpServerTransportProvider p -> McpServer .sync (p );
228- case McpStreamableServerTransportProvider p -> McpServer .sync (p );
229- default -> throw new IllegalArgumentException ("Unsupported transport provider type: " + provider .getClass ().getName ());
230- };
231- return builder
232- .serverInfo (new McpSchema .Implementation (SONARQUBE_MCP_SERVER_NAME , mcpConfiguration .getAppVersion ()))
233- .instructions ("Transform your code quality workflow with SonarQube integration. " +
234- "Analyze code, monitor project health, investigate issues, and understand quality gates." )
235- .capabilities (McpSchema .ServerCapabilities .builder ().tools (true ).logging ().build ())
236- .tools (supportedTools .stream ().map (this ::toSpec ).toArray (McpServerFeatures .SyncToolSpecification []::new ))
237- .build ();
238- };
239-
240- syncServer = serverBuilder .apply (transportProvider );
241-
242- Runtime .getRuntime ().addShutdownHook (new Thread (this ::shutdown ));
243-
244- logInitialization ();
245- }
246-
247312 private McpServerFeatures .SyncToolSpecification toSpec (Tool tool ) {
248313 return new McpServerFeatures .SyncToolSpecification .Builder ()
249314 .tool (tool .definition ())
@@ -259,7 +324,7 @@ private void logInitialization() {
259324 var sonarQubeType = mcpConfiguration .isSonarCloud () ? "SonarQube Cloud" : "SonarQube Server" ;
260325
261326 LOG .info ("========================================" );
262- LOG .info ("SonarQube MCP Server Startup Configuration :" );
327+ LOG .info ("SonarQube MCP Server Started :" );
263328 LOG .info ("Transport: " + transportType +
264329 (mcpConfiguration .isHttpEnabled () ? (" (" + mcpConfiguration .getHttpHost () + ":" + mcpConfiguration .getHttpPort () + ")" ) : "" ));
265330 LOG .info ("Instance: " + sonarQubeType );
@@ -270,7 +335,7 @@ private void logInitialization() {
270335 if (mcpConfiguration .isReadOnlyMode ()) {
271336 LOG .info ("Mode: READ-ONLY (write operations disabled)" );
272337 }
273- LOG .info ("Tools loaded: " + supportedTools . size () );
338+ LOG .info ("Status: Server ready - tools loading in background" );
274339 LOG .info ("========================================" );
275340 }
276341
@@ -365,7 +430,7 @@ public SonarQubeMcpServer(McpServerTransportProviderBase transportProvider, @Nul
365430 this .mcpConfiguration = new McpServerLaunchConfiguration (environment );
366431 this .transportProvider = transportProvider ;
367432 this .httpServerManager = httpServerManager ;
368- initializeServices ();
433+ initializeBasicServices ();
369434 }
370435
371436 // Package-private getters for testing
@@ -377,4 +442,13 @@ public List<Tool> getSupportedTools() {
377442 return List .copyOf (supportedTools );
378443 }
379444
445+ /**
446+ * For testing: wait for background initialization to complete.
447+ * This ensures tools are fully loaded before tests proceed.
448+ */
449+ @ VisibleForTesting
450+ public void waitForInitialization () throws ExecutionException , InterruptedException {
451+ initializationFuture .get ();
452+ }
453+
380454}
0 commit comments