Skip to content

Commit 7427624

Browse files
committed
fix(idea): Implement fully async MCP preloading to prevent EDT blocking
- Refactor startMcpPreloading() from suspend fun to regular fun - Launch preloading coroutine on Dispatchers.IO immediately (no EDT blocking) - Replace blocking while loop with McpToolConfigManager.waitForPreloading() - Add separate monitor coroutine for real-time status updates - Use proper Job.join() instead of polling with delay() - Cancel monitor job when preloading completes This is the long-term fix for the plugin freeze issue. The previous fix temporarily disabled MCP preloading, which worked but removed functionality. The new implementation: - Never blocks EDT (launches coroutine immediately and returns) - Uses proper suspend functions (waitForPreloading uses Job.join internally) - Monitors status in parallel without interfering with preloading - Provides real-time UI updates via StateFlow - Handles cancellation and errors properly Testing shows the plugin starts successfully with MCP preloading enabled, and the UI remains responsive during the entire preloading process.
1 parent 55cc87a commit 7427624

File tree

1 file changed

+74
-69
lines changed

1 file changed

+74
-69
lines changed

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt

Lines changed: 74 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,8 @@ class IdeaAgentViewModel(
197197
)
198198
vmLogger.warn("LLM service created successfully")
199199

200-
// ⚠️ TEMPORARILY DISABLED - MCP preloading may cause freeze
201-
// Start MCP preloading after LLM service is created
202-
// startMcpPreloading()
203-
vmLogger.warn("MCP preloading SKIPPED (temporarily disabled for debugging)")
200+
// Start MCP preloading after LLM service is created (fully async, non-blocking)
201+
startMcpPreloading()
204202
}
205203
vmLogger.warn("loadConfiguration() completed successfully")
206204
} catch (e: Exception) {
@@ -215,78 +213,85 @@ class IdeaAgentViewModel(
215213

216214
/**
217215
* Start MCP servers preloading in background.
218-
* Aligned with CodingAgentViewModel's startMcpPreloading().
219216
*
220-
* ⚠️ WARNING: This method contains a blocking while loop that can freeze the EDT
221-
* if MCP servers are unresponsive. Currently disabled for debugging.
217+
* This method is fully asynchronous and non-blocking:
218+
* 1. Launches a background coroutine on Dispatchers.IO
219+
* 2. Initializes MCP servers via McpToolConfigManager.init()
220+
* 3. Monitors preloading status by waiting for the preloading job to complete
221+
* 4. Updates UI state via StateFlow (no EDT blocking)
222+
*
223+
* The key difference from the old implementation:
224+
* - OLD: Used a while loop with delay() that could block for up to 60 seconds
225+
* - NEW: Uses waitForPreloading() which suspends without blocking EDT
222226
*/
223-
private suspend fun startMcpPreloading() {
224-
vmLogger.warn("startMcpPreloading() called")
225-
try {
226-
vmLogger.warn("Loading MCP servers configuration...")
227-
_mcpPreloadingMessage.value = "Loading MCP servers configuration..."
228-
229-
// Use IdeaToolConfigService to get and cache tool config
230-
vmLogger.warn("Getting IdeaToolConfigService instance...")
231-
val toolConfigService = IdeaToolConfigService.getInstance(project)
232-
vmLogger.warn("Reloading tool config...")
233-
toolConfigService.reloadConfig()
234-
vmLogger.warn("Getting tool config...")
235-
val toolConfig = toolConfigService.getToolConfig()
236-
cachedToolConfig = toolConfig
237-
vmLogger.warn("Tool config loaded - ${toolConfig.mcpServers.size} MCP servers configured")
238-
239-
if (toolConfig.mcpServers.isEmpty()) {
240-
vmLogger.warn("No MCP servers configured - skipping preloading")
241-
_mcpPreloadingMessage.value = "No MCP servers configured"
242-
return
243-
}
227+
private fun startMcpPreloading() {
228+
vmLogger.warn("startMcpPreloading() called - launching background coroutine")
244229

245-
vmLogger.warn("Initializing ${toolConfig.mcpServers.size} MCP servers...")
246-
_mcpPreloadingMessage.value = "Initializing ${toolConfig.mcpServers.size} MCP servers..."
247-
248-
// Initialize MCP servers (this will start background preloading)
249-
vmLogger.warn("Calling McpToolConfigManager.init()...")
250-
McpToolConfigManager.init(toolConfig)
251-
vmLogger.warn("McpToolConfigManager.init() returned")
252-
253-
// Monitor preloading status with timeout to prevent infinite loop
254-
val timeoutMs = 60_000L // 60 seconds max
255-
val startTime = System.currentTimeMillis()
256-
vmLogger.warn("Starting MCP preloading monitor loop (timeout: ${timeoutMs}ms)...")
257-
var loopCount = 0
258-
while (McpToolConfigManager.isPreloading() &&
259-
(System.currentTimeMillis() - startTime) < timeoutMs
260-
) {
261-
loopCount++
262-
val elapsed = System.currentTimeMillis() - startTime
263-
vmLogger.warn("MCP preloading loop iteration $loopCount (elapsed: ${elapsed}ms)")
264-
_mcpPreloadingStatus.value = McpToolConfigManager.getPreloadingStatus()
265-
_mcpPreloadingMessage.value =
266-
"Loading MCP servers... (${_mcpPreloadingStatus.value.preloadedServers.size} completed)"
267-
delay(500)
268-
}
269-
vmLogger.warn("MCP preloading loop exited after $loopCount iterations")
230+
// Launch on IO dispatcher to avoid blocking EDT
231+
coroutineScope.launch(Dispatchers.IO) {
232+
try {
233+
vmLogger.warn("MCP preloading coroutine started on ${Thread.currentThread().name}")
234+
_mcpPreloadingMessage.value = "Loading MCP servers configuration..."
235+
236+
// Use IdeaToolConfigService to get and cache tool config
237+
val toolConfigService = IdeaToolConfigService.getInstance(project)
238+
toolConfigService.reloadConfig()
239+
val toolConfig = toolConfigService.getToolConfig()
240+
cachedToolConfig = toolConfig
241+
vmLogger.warn("Tool config loaded - ${toolConfig.mcpServers.size} MCP servers configured")
242+
243+
if (toolConfig.mcpServers.isEmpty()) {
244+
vmLogger.warn("No MCP servers configured - skipping preloading")
245+
_mcpPreloadingMessage.value = "No MCP servers configured"
246+
return@launch
247+
}
248+
249+
val enabledCount = toolConfig.mcpServers.filter { !it.value.disabled }.size
250+
vmLogger.warn("Initializing $enabledCount enabled MCP servers...")
251+
_mcpPreloadingMessage.value = "Initializing $enabledCount MCP servers..."
252+
253+
// Initialize MCP servers (this starts background preloading in McpToolConfigManager)
254+
McpToolConfigManager.init(toolConfig)
255+
vmLogger.warn("McpToolConfigManager.init() returned - preloading started in background")
256+
257+
// Launch a separate coroutine to monitor preloading status
258+
// This doesn't block - it just updates the UI state periodically
259+
val monitorJob = launch {
260+
while (McpToolConfigManager.isPreloading()) {
261+
_mcpPreloadingStatus.value = McpToolConfigManager.getPreloadingStatus()
262+
val preloadedCount = _mcpPreloadingStatus.value.preloadedServers.size
263+
_mcpPreloadingMessage.value = "Loading MCP servers... ($preloadedCount/$enabledCount completed)"
264+
delay(500) // Update UI every 500ms
265+
}
266+
}
270267

271-
// Final status update
272-
_mcpPreloadingStatus.value = McpToolConfigManager.getPreloadingStatus()
268+
// Wait for preloading to complete (non-blocking suspend)
269+
// This uses Job.join() internally, which is a proper suspend function
270+
vmLogger.warn("Waiting for MCP preloading to complete...")
271+
McpToolConfigManager.waitForPreloading()
272+
vmLogger.warn("MCP preloading job completed")
273273

274-
val preloadedCount = _mcpPreloadingStatus.value.preloadedServers.size
275-
val totalCount = toolConfig.mcpServers.filter { !it.value.disabled }.size
274+
// Cancel the monitor job since preloading is done
275+
monitorJob.cancel()
276276

277-
vmLogger.warn("MCP preloading complete - $preloadedCount/$totalCount servers loaded")
278-
_mcpPreloadingMessage.value = if (preloadedCount > 0) {
279-
"MCP servers loaded successfully ($preloadedCount/$totalCount servers)"
280-
} else {
281-
"MCP servers initialization completed (no tools loaded)"
277+
// Final status update
278+
_mcpPreloadingStatus.value = McpToolConfigManager.getPreloadingStatus()
279+
val preloadedCount = _mcpPreloadingStatus.value.preloadedServers.size
280+
281+
vmLogger.warn("MCP preloading complete - $preloadedCount/$enabledCount servers loaded")
282+
_mcpPreloadingMessage.value = if (preloadedCount > 0) {
283+
"MCP servers loaded successfully ($preloadedCount/$enabledCount servers)"
284+
} else {
285+
"MCP servers initialization completed (no tools loaded)"
286+
}
287+
} catch (e: CancellationException) {
288+
vmLogger.warn("MCP preloading cancelled", e)
289+
// Cancellation is expected when configuration is reloaded, don't log as error
290+
throw e
291+
} catch (e: Exception) {
292+
vmLogger.error("MCP preloading failed with exception", e)
293+
_mcpPreloadingMessage.value = "Failed to load MCP servers: ${e.message}"
282294
}
283-
} catch (e: CancellationException) {
284-
vmLogger.warn("MCP preloading cancelled", e)
285-
// Cancellation is expected when configuration is reloaded, don't log as error
286-
throw e
287-
} catch (e: Exception) {
288-
vmLogger.error("MCP preloading failed with exception", e)
289-
_mcpPreloadingMessage.value = "Failed to load MCP servers: ${e.message}"
290295
}
291296
}
292297

0 commit comments

Comments
 (0)