1
+ // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2
+ package cc.unitmesh.devti.mcp
3
+
4
+ import cc.unitmesh.devti.mcp.claude.ClaudeConfigManager
5
+ import cc.unitmesh.devti.settings.coder.coderSetting
6
+ import com.intellij.execution.configurations.GeneralCommandLine
7
+ import com.intellij.execution.process.OSProcessHandler
8
+ import com.intellij.execution.process.ProcessAdapter
9
+ import com.intellij.execution.process.ProcessEvent
10
+ import com.intellij.execution.process.ProcessOutputTypes
11
+ import com.intellij.ide.BrowserUtil
12
+ import com.intellij.notification.NotificationAction
13
+ import com.intellij.notification.NotificationGroupManager
14
+ import com.intellij.notification.NotificationType
15
+ import com.intellij.openapi.diagnostic.logger
16
+ import com.intellij.openapi.project.Project
17
+ import com.intellij.openapi.startup.ProjectActivity
18
+ import com.intellij.openapi.util.Key
19
+ import com.intellij.openapi.util.SystemInfo
20
+ import java.io.File
21
+
22
+ internal class MCPServerStartupValidator : ProjectActivity {
23
+ private val GROUP_ID = " UnitMesh.MCPServer"
24
+
25
+ val logger by lazy { logger<MCPServerStartupValidator >() }
26
+
27
+ fun isNpxInstalled (): Boolean {
28
+ return try {
29
+ logger.info(" Starting npx installation check" )
30
+ if (SystemInfo .isWindows) {
31
+ logger.info(" Detected Windows OS, using 'where' command" )
32
+ checkNpxWindows()
33
+ } else {
34
+ logger.info(" Detected non-Windows OS, checking known locations" )
35
+ checkNpxUnix()
36
+ }
37
+ } catch (e: Exception ) {
38
+ logger.error(" Failed to check npx installation" , e)
39
+ logger.error(" Exception details - Class: ${e.javaClass.name} , Message: ${e.message} " )
40
+ false
41
+ }
42
+ }
43
+
44
+ private fun checkNpxWindows (): Boolean {
45
+ val commandLine = GeneralCommandLine (" where" , " npx" )
46
+ logger.info(" Windows - Environment PATH: ${commandLine.environment[" PATH" ]} " )
47
+
48
+ val handler = OSProcessHandler (commandLine)
49
+ val output = StringBuilder ()
50
+ val error = StringBuilder ()
51
+
52
+ handler.addProcessListener(object : ProcessAdapter () {
53
+ override fun onTextAvailable (event : ProcessEvent , outputType : Key <* >) {
54
+ when (outputType) {
55
+ ProcessOutputTypes .STDOUT -> output.append(event.text)
56
+ ProcessOutputTypes .STDERR -> error.append(event.text)
57
+ }
58
+ }
59
+ })
60
+
61
+ handler.startNotify()
62
+ val completed = handler.waitFor(5000 )
63
+
64
+ logger.info(" Windows - where npx completed with success: $completed " )
65
+ if (output.isNotBlank()) logger.info(" Windows - Output: $output " )
66
+ if (error.isNotBlank()) logger.warn(" Windows - Error: $error " )
67
+
68
+ return completed && handler.exitCode == 0
69
+ }
70
+
71
+ private fun checkNpxUnix (): Boolean {
72
+ // First try checking known locations including user-specific installations
73
+ val homeDir = System .getProperty(" user.home" )
74
+ val knownPaths = listOf (
75
+ " /opt/homebrew/bin/npx" ,
76
+ " /usr/local/bin/npx" ,
77
+ " /usr/bin/npx" ,
78
+ " $homeDir /.volta/bin/npx" , // Volta installation
79
+ " $homeDir /.nvm/current/bin/npx" , // NVM installation
80
+ " $homeDir /.npm-global/bin/npx" // NPM global installation
81
+ )
82
+
83
+ logger.info(" Unix - Checking known npx locations: ${knownPaths.joinToString(" , " )} " )
84
+
85
+ val existingPath = knownPaths.find { path ->
86
+ File (path).also {
87
+ logger.info(" Unix - Checking path: $path exists: ${it.exists()} " )
88
+ }.exists()
89
+ }
90
+
91
+ if (existingPath != null ) {
92
+ logger.info(" Unix - Found npx at: $existingPath " )
93
+ return true
94
+ }
95
+
96
+ // Fallback to which command with extended PATH
97
+ logger.info(" Unix - No npx found in known locations, trying which command" )
98
+ val commandLine = GeneralCommandLine (" which" , " npx" )
99
+
100
+ // Add all potential paths to PATH
101
+ val currentPath = System .getenv(" PATH" ) ? : " "
102
+ val additionalPaths = listOf (
103
+ " /opt/homebrew/bin" ,
104
+ " /opt/homebrew/sbin" ,
105
+ " /usr/local/bin" ,
106
+ " $homeDir /.volta/bin" ,
107
+ " $homeDir /.nvm/current/bin" ,
108
+ " $homeDir /.npm-global/bin"
109
+ ).joinToString(" :" )
110
+ commandLine.environment[" PATH" ] = " $additionalPaths :$currentPath "
111
+ logger.info(" Unix - Modified PATH for which command: ${commandLine.environment[" PATH" ]} " )
112
+
113
+ val handler = OSProcessHandler (commandLine)
114
+ val output = StringBuilder ()
115
+ val error = StringBuilder ()
116
+
117
+ handler.addProcessListener(object : ProcessAdapter () {
118
+ override fun onTextAvailable (event : ProcessEvent , outputType : Key <* >) {
119
+ when (outputType) {
120
+ ProcessOutputTypes .STDOUT -> output.append(event.text)
121
+ ProcessOutputTypes .STDERR -> error.append(event.text)
122
+ }
123
+ }
124
+ })
125
+
126
+ handler.startNotify()
127
+ val completed = handler.waitFor(5000 )
128
+
129
+ logger.info(" Unix - which npx completed with success: $completed " )
130
+ logger.info(" Unix - which npx completed with code: ${handler.exitCode} " )
131
+ if (output.isNotBlank()) logger.info(" Unix - Output: $output " )
132
+ if (error.isNotBlank()) logger.warn(" Unix - Error: $error " )
133
+
134
+ return completed && handler.exitCode == 0
135
+ }
136
+
137
+ override suspend fun execute (project : Project ) {
138
+ val notificationGroup = NotificationGroupManager .getInstance().getNotificationGroup(GROUP_ID )
139
+ if (SystemInfo .isLinux) {
140
+ logger.info(" No Claude Client on Linux, skipping validation" )
141
+ return
142
+ }
143
+ if (! project.coderSetting.state.enableMcpServer) {
144
+ logger.info(" MCP Server is disabled, skipping validation" )
145
+ return
146
+ }
147
+
148
+ if (! ClaudeConfigManager .isClaudeClientInstalled()) {
149
+ val notification = notificationGroup.createNotification(
150
+ " Claude Client is not installed" ,
151
+ NotificationType .INFORMATION
152
+ )
153
+ notification.addAction(NotificationAction .createSimpleExpiring(" Open Installation Instruction" ) {
154
+ BrowserUtil .open(" https://claude.ai/download" )
155
+ })
156
+ notification.notify(project)
157
+ }
158
+
159
+ val npxInstalled = isNpxInstalled()
160
+ if (! npxInstalled) {
161
+ val notification = notificationGroup.createNotification(
162
+ " Node is not installed" ,
163
+ " MCP Server Proxy requires Node.js to be installed" ,
164
+ NotificationType .INFORMATION
165
+ )
166
+ notification.addAction(NotificationAction .createSimpleExpiring(" Open Installation Instruction" ) {
167
+ BrowserUtil .open(" https://nodejs.org/en/download/package-manager" )
168
+ })
169
+
170
+ notification.notify(project)
171
+ }
172
+
173
+ if (ClaudeConfigManager .isClaudeClientInstalled() && npxInstalled && ! ClaudeConfigManager .isProxyConfigured()) {
174
+ val notification = notificationGroup.createNotification(
175
+ " MCP Server Proxy is not configured" ,
176
+ NotificationType .INFORMATION
177
+ )
178
+ notification.addAction(NotificationAction .createSimpleExpiring(" Install MCP Server Proxy" ) {
179
+ ClaudeConfigManager .modifyClaudeSettings()
180
+ })
181
+ notification.notify(project)
182
+ }
183
+ }
184
+ }
0 commit comments