@@ -18,6 +18,14 @@ import kotlinx.serialization.encodeToString
18
18
import kotlinx.serialization.json.jsonObject
19
19
import java.util.concurrent.CompletableFuture
20
20
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
21
29
22
30
@Service(Service .Level .PROJECT )
23
31
class CustomMcpServerManager (val project : Project ) {
@@ -26,52 +34,44 @@ class CustomMcpServerManager(val project: Project) {
26
34
27
35
suspend fun collectServerInfos (): List <Tool > {
28
36
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]!!
37
39
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()
63
69
}
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
71
70
}
72
-
73
- tools
74
- }.flatten()
71
+ } catch (e: Exception ) {
72
+ logger<CustomMcpServerManager >().warn(" Timeout or error during collecting server infos: $e " )
73
+ emptyList()
74
+ }
75
75
76
76
cached[mcpServerConfig] = tools
77
77
return tools
@@ -116,3 +116,55 @@ class CustomMcpServerManager(val project: Project) {
116
116
}
117
117
}
118
118
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