Skip to content

Commit 6bd52a0

Browse files
committed
feat(mcp): add MCP tools configuration UI
Add MCP configuration action and popup for selecting tools: - Create McpConfigAction with toolbar button integration - Implement McpConfigPopup with searchable tree UI - Add McpConfigService for managing tool selection state - Support server-grouped tool display and filtering - Integrate configuration button into SketchToolWindow toolbar
1 parent 11f6db2 commit 6bd52a0

File tree

5 files changed

+360
-0
lines changed

5 files changed

+360
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package cc.unitmesh.devti.gui.toolbar
2+
3+
import cc.unitmesh.devti.AutoDevIcons
4+
import cc.unitmesh.devti.mcp.ui.McpConfigPopup
5+
import cc.unitmesh.devti.mcp.ui.McpConfigService
6+
import com.intellij.openapi.actionSystem.*
7+
import com.intellij.openapi.actionSystem.ex.CustomComponentAction
8+
import com.intellij.openapi.project.Project
9+
import com.intellij.ui.components.panels.Wrapper
10+
import com.intellij.util.ui.JBInsets
11+
import com.intellij.util.ui.JBUI
12+
import javax.swing.JButton
13+
import javax.swing.JComponent
14+
15+
class McpConfigAction : AnAction("MCP Config", "Configure MCP tools", AutoDevIcons.MCP),
16+
CustomComponentAction {
17+
18+
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
19+
20+
override fun actionPerformed(e: AnActionEvent) {
21+
val project = e.project ?: return
22+
showConfigPopup(e.inputEvent?.component as? JComponent, project)
23+
}
24+
25+
override fun createCustomComponent(presentation: Presentation, place: String): JComponent {
26+
val button: JButton = object : JButton() {
27+
init {
28+
putClientProperty("ActionToolbar.smallVariant", true)
29+
putClientProperty("customButtonInsets", JBInsets(1, 1, 1, 1).asUIResource())
30+
31+
icon = AutoDevIcons.MCP
32+
toolTipText = "Configure MCP Tools"
33+
setOpaque(false)
34+
35+
addActionListener {
36+
val project = ActionToolbar.getDataContextFor(this).getData(CommonDataKeys.PROJECT)
37+
if (project != null) {
38+
showConfigPopup(this, project)
39+
}
40+
}
41+
}
42+
}
43+
44+
return Wrapper(button).also {
45+
it.setBorder(JBUI.Borders.empty(0, 10))
46+
}
47+
}
48+
49+
override fun update(e: AnActionEvent) {
50+
val project = e.project
51+
e.presentation.isEnabled = project != null
52+
53+
if (project != null) {
54+
val configService = project.getService(McpConfigService::class.java)
55+
val selectedCount = configService.getSelectedToolsCount()
56+
57+
e.presentation.text = if (selectedCount > 0) {
58+
"MCP Config ($selectedCount tools selected)"
59+
} else {
60+
"MCP Config"
61+
}
62+
}
63+
}
64+
65+
private fun showConfigPopup(component: JComponent?, project: Project) {
66+
McpConfigPopup.show(component, project)
67+
}
68+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package cc.unitmesh.devti.mcp.ui
2+
3+
import com.intellij.openapi.project.Project
4+
import com.intellij.openapi.ui.popup.JBPopupFactory
5+
import com.intellij.ui.CheckboxTree
6+
import com.intellij.ui.CheckedTreeNode
7+
import com.intellij.ui.SimpleTextAttributes
8+
import com.intellij.ui.components.JBScrollPane
9+
import com.intellij.ui.components.JBTextField
10+
import com.intellij.util.ui.JBUI
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.launch
14+
import java.awt.BorderLayout
15+
import java.awt.Dimension
16+
import java.awt.event.KeyAdapter
17+
import java.awt.event.KeyEvent
18+
import javax.swing.*
19+
import javax.swing.tree.DefaultTreeModel
20+
import javax.swing.tree.TreePath
21+
22+
class McpConfigPopup {
23+
companion object {
24+
fun show(component: JComponent?, project: Project) {
25+
val configService = project.getService(McpConfigService::class.java)
26+
val popup = McpConfigPopup()
27+
popup.createAndShow(component, project, configService)
28+
}
29+
}
30+
31+
private fun createAndShow(component: JComponent?, project: Project, configService: McpConfigService) {
32+
val mainPanel = JPanel(BorderLayout()).apply {
33+
preferredSize = Dimension(400, 500)
34+
border = JBUI.Borders.empty(8)
35+
}
36+
37+
// Search field
38+
val searchField = JBTextField().apply {
39+
emptyText.text = "Search tools..."
40+
border = JBUI.Borders.empty(4)
41+
}
42+
43+
// Tree for tool selection
44+
val rootNode = CheckedTreeNode("MCP Tools")
45+
val treeModel = DefaultTreeModel(rootNode)
46+
val tree = CheckboxTree(object : CheckboxTree.CheckboxTreeCellRenderer() {
47+
override fun customizeRenderer(
48+
tree: JTree?,
49+
value: Any?,
50+
selected: Boolean,
51+
expanded: Boolean,
52+
leaf: Boolean,
53+
row: Int,
54+
hasFocus: Boolean
55+
) {
56+
if (value is ToolTreeNode) {
57+
textRenderer.append(value.tool.name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
58+
value.tool.description?.let { desc ->
59+
if (desc.isNotEmpty()) {
60+
textRenderer.append(" - $desc", SimpleTextAttributes.GRAYED_ATTRIBUTES)
61+
}
62+
}
63+
} else if (value is ServerTreeNode) {
64+
textRenderer.append(value.serverName, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES)
65+
} else {
66+
textRenderer.append(value.toString(), SimpleTextAttributes.REGULAR_ATTRIBUTES)
67+
}
68+
}
69+
}, rootNode).apply {
70+
isRootVisible = false
71+
showsRootHandles = true
72+
}
73+
74+
val scrollPane = JBScrollPane(tree).apply {
75+
preferredSize = Dimension(380, 350)
76+
}
77+
78+
// Button panel
79+
val buttonPanel = JPanel().apply {
80+
layout = BoxLayout(this, BoxLayout.X_AXIS)
81+
add(Box.createHorizontalGlue())
82+
83+
val applyButton = JButton("Apply").apply {
84+
addActionListener {
85+
saveSelectedTools(tree, configService)
86+
// Close popup - this will be handled by the popup framework
87+
}
88+
}
89+
90+
val cancelButton = JButton("Cancel")
91+
92+
add(cancelButton)
93+
add(Box.createHorizontalStrut(8))
94+
add(applyButton)
95+
}
96+
97+
mainPanel.add(searchField, BorderLayout.NORTH)
98+
mainPanel.add(scrollPane, BorderLayout.CENTER)
99+
mainPanel.add(buttonPanel, BorderLayout.SOUTH)
100+
101+
// Load tools asynchronously
102+
loadToolsIntoTree(project, configService, rootNode, treeModel, tree)
103+
104+
// Search functionality
105+
searchField.addKeyListener(object : KeyAdapter() {
106+
override fun keyReleased(e: KeyEvent) {
107+
filterTree(tree, rootNode, searchField.text)
108+
}
109+
})
110+
111+
val popup = JBPopupFactory.getInstance()
112+
.createComponentPopupBuilder(mainPanel, searchField)
113+
.setTitle("Configure MCP Tools")
114+
.setResizable(true)
115+
.setMovable(true)
116+
.setRequestFocus(true)
117+
.createPopup()
118+
119+
if (component != null) {
120+
popup.showUnderneathOf(component)
121+
} else {
122+
popup.showCenteredInCurrentWindow(project)
123+
}
124+
}
125+
126+
private fun loadToolsIntoTree(
127+
project: Project,
128+
configService: McpConfigService,
129+
rootNode: CheckedTreeNode,
130+
treeModel: DefaultTreeModel,
131+
tree: CheckboxTree
132+
) {
133+
CoroutineScope(Dispatchers.IO).launch {
134+
// Use empty content for now, or you can pass actual content based on context
135+
val allTools = configService.getAllAvailableTools("")
136+
val selectedTools = configService.getSelectedTools()
137+
138+
CoroutineScope(Dispatchers.IO).launch {
139+
rootNode.removeAllChildren()
140+
141+
allTools.forEach { (serverName, tools) ->
142+
val serverNode = ServerTreeNode(serverName)
143+
rootNode.add(serverNode)
144+
145+
tools.forEach { tool ->
146+
val toolNode = ToolTreeNode(serverName, tool)
147+
val isSelected = selectedTools[serverName]?.contains(tool.name) == true
148+
toolNode.isChecked = isSelected
149+
serverNode.add(toolNode)
150+
}
151+
}
152+
153+
treeModel.reload()
154+
expandAllNodes(tree)
155+
}
156+
}
157+
}
158+
159+
private fun saveSelectedTools(tree: CheckboxTree, configService: McpConfigService) {
160+
val selectedTools = mutableMapOf<String, MutableSet<String>>()
161+
162+
val root = tree.model.root as CheckedTreeNode
163+
for (i in 0 until root.childCount) {
164+
val serverNode = root.getChildAt(i) as ServerTreeNode
165+
val serverName = serverNode.serverName
166+
167+
for (j in 0 until serverNode.childCount) {
168+
val toolNode = serverNode.getChildAt(j) as ToolTreeNode
169+
if (toolNode.isChecked) {
170+
selectedTools.computeIfAbsent(serverName) { mutableSetOf() }
171+
.add(toolNode.tool.name)
172+
}
173+
}
174+
}
175+
176+
configService.setSelectedTools(selectedTools)
177+
}
178+
179+
private fun filterTree(tree: CheckboxTree, rootNode: CheckedTreeNode, searchText: String) {
180+
// Simple implementation - in a real scenario, you might want more sophisticated filtering
181+
tree.expandPath(TreePath(rootNode.path))
182+
}
183+
184+
private fun expandAllNodes(tree: CheckboxTree) {
185+
for (i in 0 until tree.rowCount) {
186+
tree.expandRow(i)
187+
}
188+
}
189+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package cc.unitmesh.devti.mcp.ui
2+
3+
import cc.unitmesh.devti.mcp.client.CustomMcpServerManager
4+
import com.intellij.openapi.components.Service
5+
import com.intellij.openapi.project.Project
6+
import io.modelcontextprotocol.kotlin.sdk.Tool
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.withContext
9+
10+
@Service(Service.Level.PROJECT)
11+
class McpConfigService(private val project: Project) {
12+
private val selectedTools = mutableMapOf<String, MutableSet<String>>()
13+
14+
companion object {
15+
fun getInstance(project: Project): McpConfigService {
16+
return project.getService(McpConfigService::class.java)
17+
}
18+
}
19+
20+
fun addSelectedTool(serverName: String, toolName: String) {
21+
selectedTools.getOrPut(serverName) { mutableSetOf() }.add(toolName)
22+
}
23+
24+
fun removeSelectedTool(serverName: String, toolName: String) {
25+
selectedTools[serverName]?.remove(toolName)
26+
if (selectedTools[serverName]?.isEmpty() == true) {
27+
selectedTools.remove(serverName)
28+
}
29+
}
30+
31+
fun isToolSelected(serverName: String, toolName: String): Boolean {
32+
return selectedTools[serverName]?.contains(toolName) ?: false
33+
}
34+
35+
fun getSelectedTools(): Map<String, Set<String>> {
36+
return selectedTools.mapValues { it.value.toSet() }
37+
}
38+
39+
fun clearSelectedTools() {
40+
selectedTools.clear()
41+
}
42+
43+
fun setSelectedTools(tools: Map<String, MutableSet<String>>) {
44+
selectedTools.clear()
45+
selectedTools.putAll(tools)
46+
}
47+
48+
/**
49+
* Get all available tools from MCP servers
50+
* @param content The content context for server configuration
51+
* Returns a map of server name to list of tools
52+
*/
53+
suspend fun getAllAvailableTools(content: String = ""): Map<String, List<Tool>> = withContext(Dispatchers.IO) {
54+
val mcpServerManager = CustomMcpServerManager.instance(project)
55+
val allTools = mutableMapOf<String, List<Tool>>()
56+
57+
val serverConfigs = mcpServerManager.getEnabledServers(content)
58+
59+
if (serverConfigs.isNullOrEmpty()) {
60+
return@withContext emptyMap()
61+
}
62+
63+
serverConfigs.forEach { (serverName, serverConfig) ->
64+
try {
65+
val tools = mcpServerManager.collectServerInfo(serverName, serverConfig)
66+
allTools[serverName] = tools
67+
} catch (e: Exception) {
68+
// Log error but continue with other servers
69+
allTools[serverName] = emptyList()
70+
}
71+
}
72+
73+
allTools
74+
}
75+
76+
/**
77+
* Get the total count of selected tools across all servers
78+
*/
79+
fun getSelectedToolsCount(): Int {
80+
return selectedTools.values.sumOf { it.size }
81+
}
82+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cc.unitmesh.devti.mcp.ui
2+
3+
import com.intellij.ui.CheckedTreeNode
4+
import io.modelcontextprotocol.kotlin.sdk.Tool
5+
6+
class ServerTreeNode(val serverName: String) : CheckedTreeNode(serverName) {
7+
init {
8+
allowsChildren = true
9+
}
10+
}
11+
12+
class ToolTreeNode(val serverName: String, val tool: Tool) : CheckedTreeNode(tool.name) {
13+
init {
14+
allowsChildren = false
15+
userObject = tool.name
16+
}
17+
18+
override fun toString(): String = tool.name
19+
}

core/src/main/kotlin/cc/unitmesh/devti/sketch/SketchToolWindow.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import cc.unitmesh.devti.gui.chat.view.MessageView
99
import cc.unitmesh.devti.gui.toolbar.CopyAllMessagesAction
1010
import cc.unitmesh.devti.gui.toolbar.NewSketchAction
1111
import cc.unitmesh.devti.gui.toolbar.SummaryMessagesAction
12+
import cc.unitmesh.devti.gui.toolbar.McpConfigAction
1213
import cc.unitmesh.devti.inline.AutoDevInlineChatService
1314
import cc.unitmesh.devti.inline.fullHeight
1415
import cc.unitmesh.devti.inline.fullWidth
@@ -160,6 +161,7 @@ open class SketchToolWindow(
160161
buttonBox.add(createActionButton(NewSketchAction()))
161162
buttonBox.add(createActionButton(CopyAllMessagesAction()))
162163
buttonBox.add(createActionButton(SummaryMessagesAction()))
164+
buttonBox.add(createActionButton(McpConfigAction()))
163165
cell(buttonBox).alignRight()
164166
}
165167
}

0 commit comments

Comments
 (0)