feat: add MCP configuration form

This commit is contained in:
Carl-Robert Linnupuu 2025-11-18 19:47:35 +00:00
parent 1502cb5a4a
commit 4e126c14bc
12 changed files with 1675 additions and 0 deletions

View file

@ -0,0 +1,154 @@
package ee.carlrobert.codegpt.settings.mcp
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.thisLogger
import ee.carlrobert.codegpt.mcp.McpCommandValidator
import io.modelcontextprotocol.client.McpClient
import io.modelcontextprotocol.client.McpSyncClient
import io.modelcontextprotocol.client.transport.ServerParameters
import io.modelcontextprotocol.client.transport.StdioClientTransport
import java.time.Duration
import java.util.concurrent.TimeoutException
@Service
class McpClientManager {
private val logger = thisLogger()
fun createClient(serverDetails: McpServerDetailsState): McpSyncClient? {
return try {
val command = serverDetails.command ?: "npx"
val resolvedCommand = McpCommandValidator.resolveCommand(command)
?: throw IllegalStateException(McpCommandValidator.getCommandNotFoundMessage(command))
val connectionParams = ServerParameters.builder(resolvedCommand)
.args(*serverDetails.arguments.toTypedArray())
.env(serverDetails.environmentVariables)
.build()
McpClient.sync(StdioClientTransport(connectionParams))
.requestTimeout(Duration.ofSeconds(30))
.loggingConsumer { notification ->
logger.info("MCP Server '${serverDetails.name}': ${notification.data()}")
}
.build()
} catch (e: Exception) {
logger.warn("Failed to create MCP client for server '${serverDetails.name}'", e)
null
}
}
data class ToolInfo(val name: String, val description: String)
data class ConnectionTestResult(
val success: Boolean,
val serverName: String? = null,
val serverVersion: String? = null,
val tools: List<ToolInfo> = emptyList(),
val toolDescriptions: Map<String, String> = emptyMap(),
val resources: List<String> = emptyList(),
val errorMessage: String? = null
)
fun testConnection(serverDetails: McpServerDetailsState): ConnectionTestResult {
val startTime = System.currentTimeMillis()
return try {
val command = serverDetails.command ?: "npx"
val resolvedCommand = McpCommandValidator.resolveCommand(command)
if (resolvedCommand == null) {
logger.warn("Command not found for '${serverDetails.name}': $command")
return ConnectionTestResult(
success = false,
errorMessage = McpCommandValidator.getCommandNotFoundMessage(command)
)
}
val client = createClient(serverDetails)
if (client == null) {
logger.warn("Failed to create client for '${serverDetails.name}'")
return ConnectionTestResult(
success = false,
errorMessage = "Failed to create client - check command and arguments"
)
}
client.use { mcpClient ->
logger.info("Client created, attempting to initialize connection...")
val initResult = mcpClient.initialize()
val serverInfo = initResult.serverInfo
logger.info("Connection initialized successfully")
val toolsResult = try {
logger.debug("Listing tools...")
mcpClient.listTools()
} catch (e: Exception) {
logger.debug("Failed to list tools: ${e.message}")
null
}
val resourcesResult = try {
logger.debug("Listing resources...")
mcpClient.listResources()
} catch (e: Exception) {
logger.debug("Failed to list resources: ${e.message}")
null
}
val tools = toolsResult?.tools?.map {
ToolInfo(it.name, it.description ?: "No description available")
} ?: emptyList()
val toolDescriptions = toolsResult?.tools?.associate {
it.name to (it.description ?: "No description available")
} ?: emptyMap()
val resources = resourcesResult?.resources?.map { it.name } ?: emptyList()
val duration = System.currentTimeMillis() - startTime
logger.info("Connection test successful for '${serverDetails.name}': ${serverInfo.name} (took ${duration}ms)")
ConnectionTestResult(
success = true,
serverName = serverInfo.name,
serverVersion = serverInfo.version,
tools = tools,
toolDescriptions = toolDescriptions,
resources = resources
)
}
} catch (e: TimeoutException) {
val duration = System.currentTimeMillis() - startTime
logger.warn(
"Connection test timed out for '${serverDetails.name}' after ${duration}ms",
e
)
ConnectionTestResult(
success = false,
errorMessage = "Connection timed out after ${duration / 1000} seconds. The server may be unavailable or slow to respond."
)
} catch (e: java.io.IOException) {
val duration = System.currentTimeMillis() - startTime
logger.warn(
"IO error during connection test for '${serverDetails.name}' after ${duration}ms",
e
)
ConnectionTestResult(
success = false,
errorMessage = "Network error: ${e.message}. Check if the server command is correct and accessible."
)
} catch (e: Exception) {
val duration = System.currentTimeMillis() - startTime
logger.warn("Connection test failed for '${serverDetails.name}' after ${duration}ms", e)
ConnectionTestResult(
success = false,
errorMessage = when {
e.message?.contains("No such file") == true -> "Command not found: '${serverDetails.command}'. Make sure it's installed and in PATH."
e.message?.contains("Permission denied") == true -> "Permission denied. Check file permissions and execution rights."
e.message?.contains("Connection refused") == true -> "Connection refused. The server may not be running or accessible."
e.message?.contains("timeout") == true || e.message?.contains("timed out") == true -> "Connection timed out. The server may be slow to respond or unavailable."
else -> e.message ?: "Unknown connection error"
}
)
}
}
}

View file

@ -0,0 +1,29 @@
package ee.carlrobert.codegpt.settings.mcp
import com.intellij.openapi.options.Configurable
import ee.carlrobert.codegpt.settings.mcp.form.McpForm
import javax.swing.JComponent
class McpConfigurable : Configurable {
private lateinit var component: McpForm
override fun getDisplayName(): String {
return "ProxyAI: MCP Servers"
}
override fun createComponent(): JComponent {
component = McpForm()
return component.createPanel()
}
override fun isModified(): Boolean = component.isModified()
override fun apply() {
component.applyChanges()
}
override fun reset() {
component.resetChanges()
}
}

View file

@ -0,0 +1,48 @@
package ee.carlrobert.codegpt.settings.mcp
import com.intellij.openapi.components.*
import ee.carlrobert.codegpt.util.ApplicationUtil
@Service
@State(
name = "CodeGPT_McpSettings",
storages = [Storage("CodeGPT_McpSettings.xml")]
)
class McpSettings : SimplePersistentStateComponent<McpSettingsState>(McpSettingsState())
class McpSettingsState : BaseState() {
var servers by list<McpServerDetailsState>()
init {
servers.add(McpServerDetailsState().apply {
id = 1L
name = "Everything Server"
command = "npx"
arguments = mutableListOf("-y", "@modelcontextprotocol/server-everything")
})
servers.add(McpServerDetailsState().apply {
id = 2L
name = "File System Server"
command = "npx"
arguments = mutableListOf(
"-y",
"@modelcontextprotocol/server-filesystem",
ApplicationUtil.findCurrentProject()?.basePath ?: "/"
)
})
servers.add(McpServerDetailsState().apply {
id = 3L
name = "Git Server"
command = "npx"
arguments = mutableListOf("-y", "@modelcontextprotocol/server-git")
})
}
}
class McpServerDetailsState : BaseState() {
var id by property(1L)
var name by string("New MCP Server")
var command by string("npx")
var arguments by list<String>()
var environmentVariables by map<String, String>()
}

View file

@ -0,0 +1,323 @@
package ee.carlrobert.codegpt.settings.mcp.form
import com.intellij.icons.AllIcons
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBList
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.dsl.builder.*
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.settings.mcp.McpClientManager
import java.awt.Dimension
import java.awt.Font
import javax.swing.JComponent
import javax.swing.JList
import javax.swing.JProgressBar
import javax.swing.ListSelectionModel
class McpConnectionTestResultDialog(
private val serverName: String,
private var result: McpClientManager.ConnectionTestResult?,
private val serverCommand: String? = null
) : DialogWrapper(true) {
private var centerPanel: JComponent? = null
init {
title = "Testing MCP Connection"
init()
}
override fun createCenterPanel(): JComponent {
centerPanel = when {
result == null -> createLoadingPanel()
result!!.success -> createSuccessPanel()
else -> createErrorPanel()
}
return centerPanel!!
}
fun updateResult(newResult: McpClientManager.ConnectionTestResult) {
result = newResult
title = if (newResult.success) "Connection Test Successful" else "Connection Test Failed"
val newPanel = if (newResult.success) {
createSuccessPanel()
} else {
createErrorPanel()
}
val contentPane = contentPanel
contentPane.removeAll()
contentPane.add(newPanel)
contentPane.revalidate()
contentPane.repaint()
pack()
}
override fun createActions() = arrayOf(okAction)
private fun createLoadingPanel(): JComponent {
return panel {
row {
icon(AnimatedIcon.Default()).gap(RightGap.SMALL)
label("Testing connection to '${serverName}'...")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD)
}
}.bottomGap(BottomGap.NONE)
serverInformation()
group("Connection Status") {
row {
val progressBar = JProgressBar().apply {
isIndeterminate = true
preferredSize = Dimension(400, 20)
}
cell(progressBar)
.align(Align.FILL)
}
row {
label("Initializing connection and discovering capabilities...")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.ITALIC)
}
}
}
row {
icon(AllIcons.General.Information)
cell(
JBLabel(
"""
<html>
This may take a few seconds while the server starts up and responds.
</html>
""".trimIndent()
)
)
.applyToComponent {
font = JBUI.Fonts.label()
}
}.topGap(TopGap.SMALL)
}.apply {
preferredSize = Dimension(650, 300)
minimumSize = Dimension(650, 250)
maximumSize = Dimension(650, 350)
}
}
private fun createSuccessPanel(): JComponent {
return panel {
row {
icon(AllIcons.General.InspectionsOK).gap(RightGap.SMALL)
label("Connection test successful!")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD)
}
}.bottomGap(BottomGap.SMALL)
serverInformation()
if (result!!.tools.isNotEmpty()) {
group("Available Tools (${result!!.tools.size})") {
row {
val toolsWithDescriptions = result!!.tools.map { tool ->
val description =
result!!.toolDescriptions[tool.name] ?: "No description available"
ToolInfo(tool.name, description)
}
val toolsList = JBList(CollectionListModel(toolsWithDescriptions)).apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION
cellRenderer = object : ColoredListCellRenderer<ToolInfo>() {
override fun customizeCellRenderer(
list: JList<out ToolInfo>,
value: ToolInfo,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
icon = AllIcons.Nodes.Function
append(value.name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
if (value.description.isNotBlank() && value.description != "No description available") {
append(" - ", SimpleTextAttributes.GRAYED_ATTRIBUTES)
val truncatedDesc = if (value.description.length > 80) {
value.description.take(77) + "..."
} else {
value.description
}
append(
truncatedDesc,
SimpleTextAttributes.GRAYED_ATTRIBUTES
)
}
if (value.description.length > 80) {
toolTipText =
"<html><b>${value.name}</b><br/>${value.description}</html>"
}
}
}
visibleRowCount = minOf(6, result!!.tools.size)
fixedCellWidth = 550
}
cell(JBScrollPane(toolsList))
.align(Align.FILL)
.resizableColumn()
}.resizableRow()
.rowComment("These tools will be available for use with the AI assistant")
}
}
if (result!!.resources.isNotEmpty()) {
group("Available Resources (${result!!.resources.size})") {
row {
val resourcesList = JBList(CollectionListModel(result!!.resources)).apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION
cellRenderer = object : ColoredListCellRenderer<String>() {
override fun customizeCellRenderer(
list: JList<out String>,
value: String,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
icon = AllIcons.Nodes.DataTables
append(value, SimpleTextAttributes.REGULAR_ATTRIBUTES)
}
}
visibleRowCount = minOf(6, result!!.resources.size)
}
cell(JBScrollPane(resourcesList))
.align(Align.FILL)
.resizableColumn()
}.resizableRow()
.rowComment("These resources can be accessed by the AI assistant")
}
}
if (result!!.tools.isEmpty() && result!!.resources.isEmpty()) {
group("Server Capabilities") {
row {
icon(AllIcons.General.Information)
label("No tools or resources were reported by this server.")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.ITALIC)
}
}
row {
comment("The server may still provide other capabilities not listed here.")
}
}
}
}.apply {
preferredSize = Dimension(650, 450)
minimumSize = Dimension(650, 400)
maximumSize = Dimension(650, 600)
}
}
private data class ToolInfo(val name: String, val description: String)
private fun Panel.serverInformation() {
group("Server Information") {
row("Server Name:") {
label(serverName)
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD)
}
}
result?.serverName?.let { name ->
row("MCP Server:") {
label("$name${result?.serverVersion?.let { " (v$it)" } ?: ""}")
}
}
serverCommand?.let { command ->
row("Command:") {
label(command)
}
}
}.bottomGap(BottomGap.SMALL).topGap(TopGap.NONE)
}
private fun createErrorPanel(): JComponent {
return panel {
row {
icon(AllIcons.General.Error).gap(RightGap.SMALL)
label("Connection test failed")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD)
}
}.bottomGap(BottomGap.NONE)
serverInformation()
result!!.errorMessage?.let { error ->
group("Error Details") {
row {
val errorTextArea = com.intellij.ui.components.JBTextArea(error).apply {
isEditable = false
rows = 3
columns = 50
lineWrap = true
wrapStyleWord = true
}
cell(JBScrollPane(errorTextArea))
.align(Align.FILL)
.resizableColumn()
}.resizableRow()
}
}
group("Troubleshooting Tips") {
val tips = listOf(
"Verify the command exists and is accessible" + (serverCommand?.let { ": '$it'" }
?: ""),
"Check that all command arguments are correct",
"Ensure required environment variables are properly set",
"Try running the command manually in your terminal"
)
tips.forEach { tip ->
row {
icon(AllIcons.General.BalloonInformation)
label(tip)
}
}
}
row {
icon(AllIcons.General.ContextHelp)
cell(
JBLabel(
"""
<html>
<b>Need more help?</b> Check the MCP server documentation or test the command in your terminal.
</html>
""".trimIndent()
)
)
.applyToComponent {
font = JBUI.Fonts.label()
}
}.topGap(TopGap.SMALL)
}.apply {
preferredSize = Dimension(650, 400)
minimumSize = Dimension(650, 350)
maximumSize = Dimension(650, 500)
}
}
}

View file

@ -0,0 +1,32 @@
package ee.carlrobert.codegpt.settings.mcp.form
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import ee.carlrobert.codegpt.settings.mcp.McpSettingsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileWriter
class McpFileProvider {
private val objectMapper: ObjectMapper = ObjectMapper()
.registerModule(Jdk8Module())
.registerModule(JavaTimeModule())
.registerModule(KotlinModule.Builder().build())
.apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) }
suspend fun writeSettings(path: String, data: McpSettingsState) = withContext(Dispatchers.IO) {
val serializedData = objectMapper.writeValueAsString(data)
FileWriter(path).use {
it.write(serializedData)
}
}
fun readFromFile(path: String): McpSettingsState =
objectMapper.readValue<McpSettingsState>(File(path))
}

View file

@ -0,0 +1,416 @@
package ee.carlrobert.codegpt.settings.mcp.form
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.DialogWrapper.OK_EXIT_CODE
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.ui.TextBrowseFolderListener
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.render.LabelBasedRenderer
import com.intellij.ui.treeStructure.SimpleTree
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.components.BorderLayoutPanel
import ee.carlrobert.codegpt.settings.mcp.McpClientManager
import ee.carlrobert.codegpt.settings.mcp.McpSettings
import ee.carlrobert.codegpt.settings.mcp.McpSettingsState
import ee.carlrobert.codegpt.settings.mcp.form.McpFormUtil.toState
import ee.carlrobert.codegpt.settings.mcp.form.details.McpServerDetails
import ee.carlrobert.codegpt.settings.mcp.form.details.McpServerDetailsPanel
import ee.carlrobert.codegpt.ui.OverlayUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.awt.Component
import java.awt.Font
import javax.swing.JComponent
import javax.swing.JTree
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreePath
import javax.swing.tree.TreeSelectionModel
class McpServerTreeNode(val details: McpServerDetails) : DefaultMutableTreeNode() {
override fun toString(): String = details.name
}
class McpForm {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val serverDetailsPanel = McpServerDetailsPanel()
private val serversNode = DefaultMutableTreeNode("MCP Servers")
private val root = DefaultMutableTreeNode("Root").apply { add(serversNode) }
private val treeModel = DefaultTreeModel(root)
private val tree = SimpleTree(treeModel).apply {
isRootVisible = false
selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
cellRenderer = McpTreeCellRenderer()
setupChildNodes()
addTreeSelectionListener { e ->
val node = e.newLeadSelectionPath?.lastPathComponent as? McpServerTreeNode
node?.let {
serverDetailsPanel.updateData(it.details)
}
}
}
private val project = ProjectManager.getInstance().defaultProject
private val mcpFileProvider = McpFileProvider()
init {
runInEdt(ModalityState.any()) {
expandAll()
selectFirstServer()
}
}
private fun createHeaderPanel() = com.intellij.ui.dsl.builder.panel {
row {
label("MCP Servers")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD, 16f)
}
.gap(com.intellij.ui.dsl.builder.RightGap.COLUMNS)
button("Import from JSON") { importFromJson() }
.gap(com.intellij.ui.dsl.builder.RightGap.SMALL)
button("Import from File") { importSettingsFromFile() }
.gap(com.intellij.ui.dsl.builder.RightGap.SMALL)
button("Export") { exportSettingsToFile() }
}
row {
text("Connect AI assistants to external tools and data sources")
.applyToComponent {
foreground = JBUI.CurrentTheme.ContextHelp.FOREGROUND
font = JBUI.Fonts.smallFont()
}
}.bottomGap(BottomGap.MEDIUM)
}
fun createPanel(): JComponent {
return BorderLayoutPanel(8, 0)
.addToTop(createHeaderPanel())
.addToLeft(createToolbarDecorator().createPanel())
.addToCenter(serverDetailsPanel.getPanel())
}
fun isModified(): Boolean {
val settings = service<McpSettings>().state
val formServers = getFormServers()
if (settings.servers.size != formServers.size) return true
return !settings.servers.zip(formServers).all { (state, form) ->
state.id == form.id &&
state.name == form.name &&
state.command == form.command &&
state.arguments == form.arguments &&
state.environmentVariables == form.environmentVariables
}
}
fun applyChanges() {
val settings = service<McpSettings>().state
settings.servers = getFormServers().map { it.toState() }.toMutableList()
}
fun resetChanges() {
removeAllChildNodes()
setupChildNodes()
reloadTreeView()
}
private fun getFormServers(): List<McpServerDetails> {
return serversNode.children().toList()
.filterIsInstance<McpServerTreeNode>()
.map { it.details }
}
private fun setupChildNodes() {
service<McpSettings>().state.servers.forEach {
serversNode.add(McpServerTreeNode(McpServerDetails(it)))
}
}
private fun createToolbarDecorator(): ToolbarDecorator =
ToolbarDecorator.createDecorator(tree)
.setPreferredSize(java.awt.Dimension(300, 0))
.setAddAction { handleAddAction() }
.setRemoveAction { handleRemoveAction() }
.setRemoveActionUpdater {
tree.selectionPath?.lastPathComponent is McpServerTreeNode
}
.addExtraAction(object :
AnAction("Test Connection", "Test server connection", AllIcons.Actions.Execute) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
override fun update(e: AnActionEvent) {
val node = tree.selectionPath?.lastPathComponent as? McpServerTreeNode
e.presentation.isEnabled = node != null
}
override fun actionPerformed(e: AnActionEvent) {
handleTestConnection()
}
})
.addExtraAction(object : AnAction(
"View JSON",
"View configuration as JSON",
AllIcons.FileTypes.Json
) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
override fun update(e: AnActionEvent) {
e.presentation.isEnabled =
tree.selectionPath?.lastPathComponent is McpServerTreeNode
}
override fun actionPerformed(e: AnActionEvent) {
viewServerAsJson()
}
})
.disableUpDownActions()
private fun handleAddAction() {
val nextId = (getFormServers().maxOfOrNull { it.id } ?: 0) + 1
val newServer = McpServerDetails(
id = nextId,
name = "New MCP Server",
command = "npx",
arguments = mutableListOf(),
environmentVariables = mutableMapOf()
)
val newNode = McpServerTreeNode(newServer)
insertAndSelectNode(newNode)
}
private fun handleRemoveAction() {
val selectedNode = tree.selectionPath?.lastPathComponent as? McpServerTreeNode ?: return
treeModel.removeNodeFromParent(selectedNode)
serverDetailsPanel.remove(selectedNode.details)
}
private fun handleTestConnection() {
val selectedNode = tree.selectionPath?.lastPathComponent as? McpServerTreeNode ?: return
val server = selectedNode.details
val dialog = McpConnectionTestResultDialog(server.name, null, server.command)
coroutineScope.launch(Dispatchers.IO) {
try {
val serverState = server.toState()
val testResult = service<McpClientManager>().testConnection(serverState)
runInEdt(ModalityState.any()) { dialog.updateResult(testResult) }
} catch (e: Exception) {
val testResult = McpClientManager.ConnectionTestResult(
success = false,
errorMessage = e.message ?: "Unknown error occurred"
)
runInEdt(ModalityState.any()) {
dialog.updateResult(testResult)
}
}
}
dialog.show()
}
private fun insertAndSelectNode(newNode: McpServerTreeNode) {
treeModel.insertNodeInto(newNode, serversNode, serversNode.childCount)
tree.selectionPath = TreePath(newNode.path)
}
private fun removeAllChildNodes() {
serversNode.removeAllChildren()
}
private fun reloadTreeView() {
treeModel.reload()
expandAll()
selectFirstServer()
}
private fun expandAll() {
tree.expandPath(TreePath(serversNode.path))
}
private fun selectFirstServer() {
val firstServer = serversNode.getFirstChild() as? McpServerTreeNode
firstServer?.let {
tree.selectionPath = TreePath(it.path)
}
}
private fun exportSettingsToFile() {
val defaultFileName = "mcp-servers.json"
val settings = service<McpSettings>().state
val fileNameTextField = JBTextField(defaultFileName).apply { columns = 20 }
val fileChooserDescriptor =
FileChooserDescriptorFactory.createSingleFolderDescriptor().apply {
isForcedToUseIdeaFileChooser = true
}
val textFieldWithBrowseButton = TextFieldWithBrowseButton().apply {
text = project.basePath ?: System.getProperty("user.home")
addBrowseFolderListener(TextBrowseFolderListener(fileChooserDescriptor, project))
}
val result = createExportDialog(fileNameTextField, textFieldWithBrowseButton).show()
val fileName = fileNameTextField.text.ifEmpty { defaultFileName }
val filePath = textFieldWithBrowseButton.text
if (result == OK_EXIT_CODE) {
val fullFilePath = "$filePath/$fileName"
coroutineScope.launch {
runCatching {
mcpFileProvider.writeSettings(fullFilePath, settings)
}.onFailure {
showExportErrorMessage()
}
}
}
}
private fun importSettingsFromFile() {
val fileChooserDescriptor = FileChooserDescriptorFactory
.createSingleFileDescriptor("json")
.apply { isForcedToUseIdeaFileChooser = true }
FileChooser.chooseFile(fileChooserDescriptor, project, null)?.let { file ->
ReadAction.nonBlocking<McpSettingsState> {
file.canonicalPath?.let { mcpFileProvider.readFromFile(it) }
}
.inSmartMode(project)
.finishOnUiThread(ModalityState.defaultModalityState()) { settings ->
insertServers(settings.servers)
reloadTreeView()
}
.submit(AppExecutorUtil.getAppExecutorService())
.onError { showImportErrorMessage() }
}
}
private fun insertServers(servers: List<ee.carlrobert.codegpt.settings.mcp.McpServerDetailsState>) {
serversNode.removeAllChildren()
servers.forEachIndexed { index, server ->
val node = McpServerTreeNode(McpServerDetails(server))
treeModel.insertNodeInto(node, serversNode, index)
}
}
private fun createExportDialog(
fileNameTextField: JBTextField,
filePathButton: TextFieldWithBrowseButton
): DialogBuilder {
val form = FormBuilder.createFormBuilder()
.addLabeledComponent("File name:", fileNameTextField)
.addLabeledComponent("Save to:", filePathButton)
.panel
return DialogBuilder().apply {
setTitle("Export MCP Settings")
centerPanel(form)
addOkAction()
addCancelAction()
}
}
private fun showExportErrorMessage() {
OverlayUtil.showBalloon(
"Failed to export MCP settings",
MessageType.ERROR,
tree
)
}
private fun showImportErrorMessage() {
OverlayUtil.showBalloon(
"Failed to import MCP settings",
MessageType.ERROR,
tree
)
}
private fun importFromJson() {
val dialog = McpJsonImportDialog()
if (dialog.showAndGet()) {
val importedServers = dialog.importedServers
if (importedServers.isNotEmpty()) {
importedServers.forEach { server ->
val newNode = McpServerTreeNode(server)
insertAndSelectNode(newNode)
}
val message = if (importedServers.size == 1) {
"Successfully imported 1 MCP server"
} else {
"Successfully imported ${importedServers.size} MCP servers"
}
OverlayUtil.showBalloon(
message,
MessageType.INFO,
tree
)
if (importedServers.isNotEmpty()) {
val firstImportedNode = serversNode.children().toList()
.filterIsInstance<McpServerTreeNode>()
.find { it.details.id == importedServers.first().id }
firstImportedNode?.let {
tree.selectionPath = TreePath(it.path)
}
}
}
}
}
private fun viewServerAsJson() {
val selectedNode = tree.selectionPath?.lastPathComponent as? McpServerTreeNode ?: return
val server = selectedNode.details
val dialog = McpJsonViewDialog(server)
dialog.show()
}
private class McpTreeCellRenderer : LabelBasedRenderer.Tree() {
override fun getTreeCellRendererComponent(
tree: JTree,
value: Any?,
selected: Boolean,
expanded: Boolean,
leaf: Boolean,
row: Int,
focused: Boolean
): Component {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, focused)
if (value is McpServerTreeNode) {
icon = AllIcons.Nodes.Services
iconTextGap = 6
} else {
icon = AllIcons.Nodes.Folder
}
return this
}
}
}

View file

@ -0,0 +1,16 @@
package ee.carlrobert.codegpt.settings.mcp.form
import ee.carlrobert.codegpt.settings.mcp.McpServerDetailsState
import ee.carlrobert.codegpt.settings.mcp.form.details.McpServerDetails
object McpFormUtil {
fun McpServerDetails.toState(): McpServerDetailsState {
val state = McpServerDetailsState()
state.id = this.id
state.name = this.name
state.command = this.command
state.arguments = this.arguments.toMutableList()
state.environmentVariables = this.environmentVariables.toMutableMap()
return state
}
}

View file

@ -0,0 +1,189 @@
package ee.carlrobert.codegpt.settings.mcp.form
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.EditorTextField
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import ee.carlrobert.codegpt.settings.mcp.form.details.McpServerDetails
import java.awt.Dimension
import java.awt.Font
import javax.swing.JComponent
class McpJsonImportDialog : DialogWrapper(true) {
private val project = ProjectManager.getInstance().defaultProject
private val jsonEditor = EditorTextField(
"",
project,
FileTypeManager.getInstance().getFileTypeByExtension("json")
).apply {
setOneLineMode(false)
preferredSize = Dimension(580, 250)
addSettingsProvider { editor ->
val settings = editor.settings
settings.isLineNumbersShown = true
settings.isAutoCodeFoldingEnabled = true
settings.isFoldingOutlineShown = true
settings.isAllowSingleLogicalLineFolding = true
settings.isRightMarginShown = false
settings.isUseSoftWraps = false
settings.isWhitespacesShown = false
editor.colorsScheme.apply {
editorFontSize = JBUI.Fonts.label().size
editorFontName = JBUI.Fonts.label().fontName
}
if (editor is EditorEx) {
editor.isViewer = false
editor.setVerticalScrollbarVisible(true)
editor.setHorizontalScrollbarVisible(true)
editor.backgroundColor = editor.colorsScheme.defaultBackground
}
}
text = """
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"]
}
}
}
""".trimIndent()
addSettingsProvider { editor ->
editor.caretModel.moveToOffset(0)
editor.scrollingModel.scrollToCaret(com.intellij.openapi.editor.ScrollType.RELATIVE)
}
}
private val objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
var importedServers: List<McpServerDetails> = emptyList()
private set
init {
title = "Import MCP Servers from JSON"
init()
UIUtil.invokeLaterIfNeeded {
jsonEditor.editor?.let { editor ->
editor.caretModel.moveToOffset(0)
editor.scrollingModel.scrollToCaret(com.intellij.openapi.editor.ScrollType.MAKE_VISIBLE)
}
}
}
override fun createCenterPanel(): JComponent {
return panel {
row {
label("Paste your MCP server configuration:")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD)
}
}.bottomGap(BottomGap.SMALL)
row {
scrollCell(jsonEditor)
.align(Align.FILL)
.resizableColumn()
}.resizableRow()
}.apply {
preferredSize = Dimension(600, 400)
minimumSize = Dimension(550, 350)
}
}
override fun doValidate(): ValidationInfo? {
val jsonText = jsonEditor.text.trim()
if (jsonText.isEmpty()) {
return ValidationInfo("JSON content cannot be empty", jsonEditor)
}
return try {
val parsedServers = parseJsonToServers(jsonText)
if (parsedServers.isEmpty()) {
ValidationInfo("No valid MCP servers found in the JSON", jsonEditor)
} else {
importedServers = parsedServers
null
}
} catch (e: Exception) {
ValidationInfo("Invalid JSON format: ${e.message}", jsonEditor)
}
}
private fun parseJsonToServers(jsonText: String): List<McpServerDetails> {
val jsonNode = objectMapper.readTree(jsonText)
val servers = mutableListOf<McpServerDetails>()
var nextId = System.currentTimeMillis()
when {
jsonNode.has("mcpServers") -> {
val mcpServers = jsonNode.get("mcpServers")
mcpServers.fields().forEach { (name, config) ->
servers.add(createServerFromConfig(nextId++, name, config))
}
}
jsonNode.has("command") -> {
servers.add(createServerFromConfig(nextId++, "Imported Server", jsonNode))
}
jsonNode.isArray -> {
jsonNode.forEach { serverNode ->
val name =
serverNode.get("name")?.asText() ?: "Imported Server ${servers.size + 1}"
servers.add(createServerFromConfig(nextId++, name, serverNode))
}
}
else -> {
jsonNode.fields().forEach { (name, config) ->
if (config.isObject) {
servers.add(createServerFromConfig(nextId++, name, config))
}
}
}
}
return servers
}
private fun createServerFromConfig(id: Long, name: String, config: JsonNode): McpServerDetails {
val command = config.get("command")?.asText() ?: "npx"
val args = config.get("args")?.map { it.asText() }?.toMutableList()
?: config.get("arguments")?.map { it.asText() }?.toMutableList()
?: mutableListOf()
val env = mutableMapOf<String, String>()
config.get("env")?.fields()?.forEach { (key, value) ->
env[key] = value.asText()
}
config.get("environment")?.fields()?.forEach { (key, value) ->
env[key] = value.asText()
}
return McpServerDetails(
id = id,
name = name,
command = command,
arguments = args,
environmentVariables = env
)
}
}

View file

@ -0,0 +1,133 @@
package ee.carlrobert.codegpt.settings.mcp.form
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.intellij.icons.AllIcons
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.MessageType
import com.intellij.ui.EditorTextField
import com.intellij.ui.components.JBLabel
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground
import ee.carlrobert.codegpt.settings.mcp.form.details.McpServerDetails
import ee.carlrobert.codegpt.ui.OverlayUtil
import java.awt.Dimension
import java.awt.Font
import java.awt.datatransfer.StringSelection
import javax.swing.JComponent
class McpJsonViewDialog(private val server: McpServerDetails) : DialogWrapper(true) {
private val objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
private val project = ProjectManager.getInstance().defaultProject
private val jsonEditor = EditorTextField(
"",
project,
FileTypeManager.getInstance().getFileTypeByExtension("json")
).apply {
setOneLineMode(false)
preferredSize = Dimension(700, 450)
addSettingsProvider { editor ->
val settings = editor.settings
settings.isLineNumbersShown = true
settings.isAutoCodeFoldingEnabled = true
settings.isFoldingOutlineShown = true
settings.isAllowSingleLogicalLineFolding = true
settings.isRightMarginShown = false
settings.isUseSoftWraps = false
settings.isWhitespacesShown = false
editor.colorsScheme.apply {
editorFontSize = JBUI.Fonts.label().size
editorFontName = JBUI.Fonts.label().fontName
}
editor.isViewer = true
editor.backgroundColor = editor.colorsScheme.defaultBackground
}
text = generateServerJson()
}
init {
title = "JSON Configuration - ${server.name}"
init()
}
override fun createCenterPanel(): JComponent {
return panel {
row {
icon(AllIcons.FileTypes.Json)
label("JSON configuration for '${server.name}':")
.applyToComponent {
font = JBUI.Fonts.label().deriveFont(Font.BOLD)
}
}.bottomGap(BottomGap.MEDIUM)
row {
cell(com.intellij.ui.components.JBScrollPane(jsonEditor).apply {
preferredSize = Dimension(700, 450)
border = JBUI.Borders.customLine(separatorForeground())
})
.align(Align.FILL)
.resizableColumn()
}.resizableRow()
separator()
row {
button("Copy to Clipboard") {
CopyPasteManager.getInstance().setContents(StringSelection(jsonEditor.text))
OverlayUtil.showBalloon(
"JSON configuration copied to clipboard",
MessageType.INFO,
it.source as JComponent
)
}.applyToComponent {
icon = AllIcons.Actions.Copy
}
}.topGap(TopGap.NONE)
}.apply {
preferredSize = Dimension(750, 600)
minimumSize = Dimension(700, 500)
}
}
override fun createActions() = arrayOf(okAction)
private fun generateServerJson(): String {
val serverConfig = objectMapper.createObjectNode().apply {
put("command", server.command)
if (server.arguments.isNotEmpty()) {
val argsArray = putArray("args")
server.arguments.forEach { argsArray.add(it) }
}
if (server.environmentVariables.isNotEmpty()) {
val envObject = putObject("env")
server.environmentVariables.forEach { (key, value) ->
envObject.put(key, value)
}
}
}
val mcpServers = objectMapper.createObjectNode()
mcpServers.set<ObjectNode>(server.name, serverConfig)
val rootObject = objectMapper.createObjectNode()
rootObject.set<ObjectNode>("mcpServers", mcpServers)
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootObject)
}
}

View file

@ -0,0 +1,83 @@
package ee.carlrobert.codegpt.settings.mcp.form.details
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.*
import javax.swing.JComponent
class SingleInputDialog(
title: String,
private val label: String,
initialValue: String
) : DialogWrapper(true) {
private val inputField = JBTextField(initialValue)
val inputValue: String
get() = inputField.text
init {
this.title = title
init()
}
override fun createCenterPanel(): JComponent {
return panel {
row(label) {
cell(inputField)
.columns(COLUMNS_LARGE)
.focused()
}
}
}
override fun doValidate(): ValidationInfo? {
return if (inputField.text.trim().isEmpty()) {
ValidationInfo("Value cannot be empty", inputField)
} else null
}
}
class EnvironmentVariableDialog(
title: String,
initialName: String,
initialValue: String
) : DialogWrapper(true) {
private val nameField = JBTextField(initialName)
private val valueField = JBTextField(initialValue)
val variableName: String
get() = nameField.text
val variableValue: String
get() = valueField.text
init {
this.title = title
init()
}
override fun createCenterPanel(): JComponent {
return panel {
row("Variable name:") {
cell(nameField)
.columns(COLUMNS_MEDIUM)
.focused()
}
row("Value:") {
cell(valueField)
.columns(COLUMNS_MEDIUM)
}
}
}
override fun doValidate(): ValidationInfo? {
return when {
nameField.text.trim().isEmpty() -> ValidationInfo("Variable name cannot be empty", nameField)
nameField.text.contains('=') -> ValidationInfo("Variable name cannot contain '=' character", nameField)
else -> null
}
}
}

View file

@ -0,0 +1,26 @@
package ee.carlrobert.codegpt.settings.mcp.form.details
import ee.carlrobert.codegpt.settings.mcp.McpServerDetailsState
import javax.swing.JComponent
interface McpDetailsPanel {
fun getPanel(): JComponent
fun updateData(details: McpServerDetails)
fun remove(details: McpServerDetails)
}
data class McpServerDetails(
val id: Long,
var name: String,
var command: String,
var arguments: MutableList<String>,
var environmentVariables: MutableMap<String, String>
) {
constructor(state: McpServerDetailsState) : this(
id = state.id,
name = state.name ?: "New MCP Server",
command = state.command ?: "npx",
arguments = state.arguments.toMutableList(),
environmentVariables = state.environmentVariables.toMutableMap()
)
}

View file

@ -0,0 +1,226 @@
package ee.carlrobert.codegpt.settings.mcp.form.details
import com.intellij.icons.AllIcons
import com.intellij.openapi.components.service
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.CardLayoutPanel
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBList
import com.intellij.ui.dsl.builder.*
import com.intellij.util.ui.JBUI
import ee.carlrobert.codegpt.settings.mcp.McpSettings
import java.awt.Component
import javax.swing.DefaultListCellRenderer
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JList
class McpServerDetailsPanel : McpDetailsPanel {
private val cardLayoutPanel =
object : CardLayoutPanel<McpServerDetails, McpServerDetails, JComponent>() {
override fun prepare(key: McpServerDetails): McpServerDetails = key
override fun create(details: McpServerDetails): JComponent {
return ServerEditorPanel(details).getPanel()
}
}
init {
service<McpSettings>().state.servers.forEach {
cardLayoutPanel.select(McpServerDetails(it), true)
}
}
override fun getPanel() = cardLayoutPanel
override fun updateData(details: McpServerDetails) {
cardLayoutPanel.select(details, true)
}
override fun remove(details: McpServerDetails) {
cardLayoutPanel.remove(cardLayoutPanel.getValue(details, false))
}
private class ServerEditorPanel(private val details: McpServerDetails) {
private val argumentsListModel = CollectionListModel(details.arguments)
private val argumentsList = JBList(argumentsListModel).apply {
cellRenderer = ServerEditorListCellRenderer(AllIcons.Nodes.Parameter)
emptyText.text = "No arguments configured"
}
private val envVarsListModel = CollectionListModel(
details.environmentVariables.map { "${it.key}=${it.value}" }.toMutableList()
)
private val envVarsList = JBList(envVarsListModel).apply {
cellRenderer = ServerEditorListCellRenderer(AllIcons.Nodes.Variable)
emptyText.text = "No environment variables configured"
}
fun getPanel(): DialogPanel = panel {
row {
label("Configure your MCP server settings below:")
}.bottomGap(BottomGap.MEDIUM)
group("Basic Configuration") {
row("Name:") {
textField()
.bindText({ details.name }, { details.name = it })
.columns(COLUMNS_MEDIUM)
.validationOnApply {
if (it.text.isBlank()) ValidationInfo("Server name is required", it)
else null
}
}.rowComment("A descriptive name for this MCP server")
row("Command:") {
textField()
.bindText({ details.command }, { details.command = it })
.columns(COLUMNS_MEDIUM)
.validationOnApply {
if (it.text.isBlank()) ValidationInfo("Command is required", it)
else null
}
}.rowComment("Executable command (e.g., npx, python, node)")
}
group("Command Arguments") {
row {
cell(createArgumentsPanel()).align(Align.FILL)
}.resizableRow()
.rowComment("Arguments passed to the server command")
}
group("Environment Variables") {
row {
cell(createEnvironmentPanel()).align(Align.FILL)
}.resizableRow()
.rowComment("Environment variables for the server process")
}
}
private fun createArgumentsPanel(): JComponent {
return ToolbarDecorator.createDecorator(argumentsList)
.setAddAction { addArgument() }
.setRemoveAction { removeSelectedArgument() }
.setEditAction { editSelectedArgument() }
.setAddActionName("Add Argument")
.setRemoveActionName("Remove Argument")
.setEditActionName("Edit Argument")
.setPreferredSize(JBUI.size(400, 120))
.createPanel()
}
private fun createEnvironmentPanel(): JComponent {
return ToolbarDecorator.createDecorator(envVarsList)
.setAddAction { addEnvironmentVariable() }
.setRemoveAction { removeSelectedEnvironmentVariable() }
.setEditAction { editSelectedEnvironmentVariable() }
.setAddActionName("Add Variable")
.setRemoveActionName("Remove Variable")
.setEditActionName("Edit Variable")
.setPreferredSize(JBUI.size(400, 120))
.createPanel()
}
private fun addArgument() {
val dialog = SingleInputDialog("Add Argument", "Argument:", "")
if (dialog.showAndGet()) {
val argument = dialog.inputValue.trim()
if (argument.isNotEmpty()) {
details.arguments.add(argument)
argumentsListModel.add(argument)
}
}
}
private fun removeSelectedArgument() {
val selectedIndex = argumentsList.selectedIndex
if (selectedIndex >= 0) {
val removed = details.arguments.removeAt(selectedIndex)
argumentsListModel.remove(removed)
}
}
private fun editSelectedArgument() {
val selectedIndex = argumentsList.selectedIndex
if (selectedIndex >= 0) {
val currentValue = details.arguments[selectedIndex]
val dialog = SingleInputDialog("Edit Argument", "Argument:", currentValue)
if (dialog.showAndGet()) {
val newValue = dialog.inputValue.trim()
if (newValue.isNotEmpty() && newValue != currentValue) {
details.arguments[selectedIndex] = newValue
argumentsListModel.setElementAt(newValue, selectedIndex)
}
}
}
}
private fun addEnvironmentVariable() {
val dialog = EnvironmentVariableDialog("Add Environment Variable", "", "")
if (dialog.showAndGet()) {
val name = dialog.variableName.trim()
val value = dialog.variableValue.trim()
if (name.isNotEmpty()) {
details.environmentVariables[name] = value
envVarsListModel.add("$name=$value")
}
}
}
private fun removeSelectedEnvironmentVariable() {
val selectedIndex = envVarsList.selectedIndex
if (selectedIndex >= 0) {
val selectedItem = envVarsListModel.getElementAt(selectedIndex)
val key = selectedItem.substringBefore('=')
details.environmentVariables.remove(key)
envVarsListModel.remove(selectedIndex)
}
}
private fun editSelectedEnvironmentVariable() {
val selectedIndex = envVarsList.selectedIndex
if (selectedIndex >= 0) {
val selectedItem = envVarsListModel.getElementAt(selectedIndex)
val parts = selectedItem.split('=', limit = 2)
val currentName = parts[0]
val currentValue = if (parts.size > 1) parts[1] else ""
val dialog = EnvironmentVariableDialog(
"Edit Environment Variable",
currentName,
currentValue
)
if (dialog.showAndGet()) {
val newName = dialog.variableName.trim()
val newValue = dialog.variableValue.trim()
if (newName.isNotEmpty()) {
details.environmentVariables.remove(currentName)
details.environmentVariables[newName] = newValue
envVarsListModel.setElementAt("$newName=$newValue", selectedIndex)
}
}
}
}
private class ServerEditorListCellRenderer(private val cellIcon: Icon) : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<*>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean
): Component {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
icon = cellIcon
text = value?.toString() ?: ""
return this
}
}
}
}