Skip to content

Commit 04d54a3

Browse files
committed
fix(devti): improve MCP server connection and tool listing
- Refactor collectServerInfos() for better error handling and timeout support - Add resolveCommand() function to find correct command paths, especially for Windows and npx - Update client version to "0.3.0" - Improve logging for error cases
1 parent f701f81 commit 04d54a3

File tree

1 file changed

+95
-43
lines changed

1 file changed

+95
-43
lines changed

core/src/main/kotlin/cc/unitmesh/devti/mcp/client/CustomMcpServerManager.kt

Lines changed: 95 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ import kotlinx.serialization.encodeToString
1818
import kotlinx.serialization.json.jsonObject
1919
import java.util.concurrent.CompletableFuture
2020
import java.util.concurrent.TimeUnit
21+
import com.intellij.openapi.util.SystemInfo
22+
import java.io.File
23+
import java.io.BufferedReader
24+
import java.io.InputStreamReader
25+
import kotlinx.coroutines.async
26+
import kotlinx.coroutines.awaitAll
27+
import kotlinx.coroutines.coroutineScope
28+
import kotlinx.coroutines.withTimeout
2129

2230
@Service(Service.Level.PROJECT)
2331
class CustomMcpServerManager(val project: Project) {
@@ -26,52 +34,44 @@ class CustomMcpServerManager(val project: Project) {
2634

2735
suspend fun collectServerInfos(): List<Tool> {
2836
val mcpServerConfig = project.customizeSetting.mcpServerConfig
29-
if (mcpServerConfig.isEmpty()) {
30-
return emptyList()
31-
}
32-
33-
if (cached.containsKey(mcpServerConfig)) {
34-
return cached[mcpServerConfig]!!
35-
}
36-
37+
if (mcpServerConfig.isEmpty()) return emptyList()
38+
if (cached.containsKey(mcpServerConfig)) return cached[mcpServerConfig]!!
3739
val mcpConfig = McpServer.load(mcpServerConfig)
38-
if (mcpConfig == null) {
39-
return emptyList()
40-
}
41-
42-
val tools: List<Tool> = mcpConfig.mcpServers.mapNotNull { it: Map.Entry<String, McpServer> ->
43-
if (it.value.disabled == true) {
44-
return@mapNotNull null
45-
}
46-
47-
val client = Client(clientInfo = Implementation(name = it.key, version = "1.0.0"))
48-
49-
val processBuilder = ProcessBuilder(it.value.command, *it.value.args.toTypedArray())
50-
val process = processBuilder.start()
51-
52-
val input = process.inputStream.asSource().buffered()
53-
val output = process.outputStream.asSink().buffered()
54-
55-
val transport = StdioClientTransport(input, output)
56-
var tools = listOf<Tool>()
57-
58-
try {
59-
client.connect(transport)
60-
val listTools = client.listTools()
61-
if (listTools?.tools != null) {
62-
tools = listTools.tools
40+
if (mcpConfig == null) return emptyList()
41+
42+
val tools: List<Tool> = try {
43+
withTimeout(30_000L) {
44+
coroutineScope {
45+
mcpConfig.mcpServers.map { entry ->
46+
async {
47+
if (entry.value.disabled == true) return@async emptyList<Tool>()
48+
val resolvedCommand = resolveCommand(entry.value.command)
49+
val client = Client(clientInfo = Implementation(name = entry.key, version = "0.3.0"))
50+
val processBuilder = ProcessBuilder(resolvedCommand, *entry.value.args.toTypedArray())
51+
val process = processBuilder.start()
52+
val input = process.inputStream.asSource().buffered()
53+
val output = process.outputStream.asSink().buffered()
54+
val transport = StdioClientTransport(input, output)
55+
56+
try {
57+
client.connect(transport)
58+
val listTools = client.listTools()
59+
listTools?.tools?.forEach { tool ->
60+
toolClientMap[tool] = client
61+
}
62+
listTools?.tools ?: emptyList()
63+
} catch (e: Exception) {
64+
logger<CustomMcpServerManager>().warn("Failed to list tools from ${entry.key}: $e")
65+
emptyList<Tool>()
66+
}
67+
}
68+
}.awaitAll().flatten()
6369
}
64-
65-
listTools?.tools?.map {
66-
toolClientMap[it] = client
67-
}
68-
} catch (e: java.lang.Error) {
69-
logger<CustomMcpServerManager>().warn("Failed to list tools from ${it.key}: $e")
70-
null
7170
}
72-
73-
tools
74-
}.flatten()
71+
} catch (e: Exception) {
72+
logger<CustomMcpServerManager>().warn("Timeout or error during collecting server infos: $e")
73+
emptyList()
74+
}
7575

7676
cached[mcpServerConfig] = tools
7777
return tools
@@ -116,3 +116,55 @@ class CustomMcpServerManager(val project: Project) {
116116
}
117117
}
118118

119+
120+
fun resolveCommand(command: String): String {
121+
if (SystemInfo.isWindows) {
122+
try {
123+
val pb = ProcessBuilder("where", command)
124+
val process = pb.start()
125+
val reader = BufferedReader(InputStreamReader(process.inputStream))
126+
val resolved = reader.readLine() // take first non-null output
127+
if (!resolved.isNullOrBlank()) return resolved.trim()
128+
} catch (e: Exception) {
129+
logger<CustomMcpServerManager>().warn("Failed to resolve command using where: $e")
130+
}
131+
} else {
132+
val homeDir = System.getProperty("user.home")
133+
if (command == "npx") {
134+
val knownPaths = listOf(
135+
"/opt/homebrew/bin/npx",
136+
"/usr/local/bin/npx",
137+
"/usr/bin/npx",
138+
"$homeDir/.volta/bin/npx",
139+
"$homeDir/.nvm/current/bin/npx",
140+
"$homeDir/.npm-global/bin/npx"
141+
)
142+
knownPaths.forEach { path ->
143+
if (File(path).exists()) return path
144+
}
145+
}
146+
try {
147+
val pb = ProcessBuilder("which", command)
148+
val currentPath = System.getenv("PATH") ?: ""
149+
val additionalPaths = if (command == "npx") {
150+
listOf(
151+
"/opt/homebrew/bin",
152+
"/opt/homebrew/sbin",
153+
"/usr/local/bin",
154+
"$homeDir/.volta/bin",
155+
"$homeDir/.nvm/current/bin",
156+
"$homeDir/.npm-global/bin"
157+
).joinToString(":")
158+
} else ""
159+
pb.environment()["PATH"] =
160+
if (additionalPaths.isNotBlank()) "$additionalPaths:$currentPath" else currentPath
161+
val process = pb.start()
162+
val reader = BufferedReader(InputStreamReader(process.inputStream))
163+
val resolved = reader.readLine()
164+
if (!resolved.isNullOrBlank()) return resolved.trim()
165+
} catch (e: Exception) {
166+
logger<CustomMcpServerManager>().warn("Failed to resolve command using which: $e")
167+
}
168+
}
169+
return command
170+
}

0 commit comments

Comments
 (0)