[mcp server] IJPL-230584 refactor codex support to avoid toml regexp parsing

GitOrigin-RevId: f7d1b2dd6fef5e83607c5f2b0953711be0469e7f
This commit is contained in:
Dmitry Batkovich
2026-01-30 13:00:39 +00:00
committed by intellij-monorepo-bot
parent 5dcb960f09
commit 7cd16cea1d
8 changed files with 205 additions and 117 deletions
+15
View File
@@ -16368,6 +16368,21 @@ jvm_import(
source_jar = "@com_typesafe-config-1_4_3-sources_http//file"
)
copy_file(
name = "com.moandjiezana.toml/toml4j-0.7.2.jar_copy",
src = "@com_moandjiezana_toml-toml4j-0_7_2_http//file",
out = "com.moandjiezana.toml/toml4j-0.7.2.jar",
allow_symlink = True,
visibility = ["//visibility:public"]
)
jvm_import(
name = "mcpserver-toml4j",
jar = "@com_moandjiezana_toml-toml4j-0_7_2_http//file",
source_jar = "@com_moandjiezana_toml-toml4j-0_7_2-sources_http//file",
visibility = ["//visibility:public"]
)
copy_file(
name = "com.github.marschall/memoryfilesystem-2.8.2.jar_copy",
src = "@com_github_marschall-memoryfilesystem-2_8_2_http//file",
+14
View File
@@ -12998,6 +12998,20 @@ http_file(
downloaded_file_path = "config-1.4.3-sources.jar"
)
http_file(
name = "com_moandjiezana_toml-toml4j-0_7_2_http",
url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/com/moandjiezana/toml/toml4j/0.7.2/toml4j-0.7.2.jar",
sha256 = "f5475e63e7e89e5db62223489aec7a56bd303543772077a17c2cb54c19ca3a20",
downloaded_file_path = "toml4j-0.7.2.jar"
)
http_file(
name = "com_moandjiezana_toml-toml4j-0_7_2-sources_http",
url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/com/moandjiezana/toml/toml4j/0.7.2/toml4j-0.7.2-sources.jar",
sha256 = "fc9c72fb6a7abda2a65c037af622471da897f84e0a95643210b1c41328c83c5d",
downloaded_file_path = "toml4j-0.7.2-sources.jar"
)
http_file(
name = "com_github_marschall-memoryfilesystem-2_8_2_http",
url = "https://cache-redirector.jetbrains.com/repo1.maven.org/maven2/com/github/marschall/memoryfilesystem/2.8.2/memoryfilesystem-2.8.2.jar",
@@ -1270,6 +1270,10 @@ object CommunityLibraryLicenses {
LibraryLicense("thriftpy2", version = "0.4.13", attachedTo = "intellij.python", url = "https://github.com/Thriftpy/thriftpy2/")
.mit("https://github.com/Thriftpy/thriftpy2/blob/master/LICENSE"),
LibraryLicense("toml4j", libraryName = "toml4j", url = "https://github.com/mwanji/toml4j")
.mit("https://github.com/mwanji/toml4j/blob/master/LICENSE")
.suppliedByPersons("Moandji Ezana"),
// for traceprocessor-proto module library in intellij.android.profilersAndroid
LibraryLicense("Trang", libraryName = "trang-core.jar", version = LibraryLicense.CUSTOM_REVISION, url = "https://relaxng.org/jclark/trang.html")
.newBsd("https://opensource.org/license/bsd-3-clause/"),
+1
View File
@@ -14,6 +14,7 @@ jvm_library(
srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
resources = [":mcpserver_resources"],
deps = [
"@lib//:mcpserver-toml4j",
"@lib//:jetbrains-annotations",
"@lib//:kotlin-stdlib",
"//libraries/kotlinx/coroutines/core",
+18
View File
@@ -29,6 +29,24 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module-library">
<library name="toml4j" type="repository">
<properties include-transitive-deps="false" maven-id="com.moandjiezana.toml:toml4j:0.7.2">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/com/moandjiezana/toml/toml4j/0.7.2/toml4j-0.7.2.jar">
<sha256sum>f5475e63e7e89e5db62223489aec7a56bd303543772077a17c2cb54c19ca3a20</sha256sum>
</artifact>
</verification>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/moandjiezana/toml/toml4j/0.7.2/toml4j-0.7.2.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/moandjiezana/toml/toml4j/0.7.2/toml4j-0.7.2-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
+6 -1
View File
@@ -42,4 +42,9 @@
reason: <- intellij.mcpserver
- name: lib/mcpserver.jar
modules:
- name: intellij.mcpserver
- name: intellij.mcpserver
- name: lib/toml4j.jar
library: toml4j
module: intellij.mcpserver
files:
- name: $MAVEN_REPOSITORY$/com/moandjiezana/toml/toml4j/0/toml4j-0.jar
@@ -6,6 +6,8 @@ import com.intellij.mcpserver.clients.configs.CodexStreamableHttpConfig
import com.intellij.mcpserver.clients.configs.ExistingConfig
import com.intellij.mcpserver.clients.configs.ServerConfig
import com.intellij.util.io.createParentDirectories
import com.moandjiezana.toml.Toml
import com.moandjiezana.toml.TomlWriter
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.readText
@@ -32,33 +34,47 @@ open class CodexClient(scope: McpClientInfo.Scope, configPath: Path) : McpClient
val existingContent = if (configPath.exists()) configPath.readText() else ""
val productServerKey = productSpecificServerKey()
var updatedContent = existingContent
LEGACY_SERVER_KEYS.forEach { legacyKey ->
if (legacyKey != productServerKey) {
updatedContent = removeCodexSection(updatedContent, legacyKey)
}
}
updatedContent = updateCodexConfig(updatedContent, productServerKey, streamableHttpUrl)
val updatedContent = updateCodexConfig(
existing = existingContent,
productServerKey = productServerKey,
legacyKeys = LEGACY_SERVER_KEYS,
url = streamableHttpUrl
)
configPath.parent?.createParentDirectories()
configPath.writeText(updatedContent)
}
companion object {
private val SERVER_SECTION_REGEX = Regex("(?mis)^\\s*\\[mcp_servers\\.([^]]+)]\\s*(.*?)(?=^\\s*\\[|\\z)")
private const val MCP_SERVERS = "mcp_servers"
private fun parseCodexServers(content: String): Map<String, ExistingConfig> {
val servers = mutableMapOf<String, ExistingConfig>()
SERVER_SECTION_REGEX.findAll(content).forEach { matchResult ->
val serverName = matchResult.groupValues[1].trim()
val body = matchResult.groupValues[2]
val command = extractTomlString(body, "command")
val type = extractTomlString(body, "type") ?: extractTomlString(body, "transport")
val url = extractTomlString(body, "url") ?: extractTomlString(body, "serverUrl")
val args = extractTomlStringArray(body, "args")
val env = extractTomlInlineTable(body, "env")
servers[serverName] = ExistingConfig(
val root = Toml().read(content).toMap()
val serversAny = root[MCP_SERVERS] as? Map<*, *> ?: return emptyMap()
val result = LinkedHashMap<String, ExistingConfig>(serversAny.size)
for ((k, v) in serversAny) {
val serverName = (k as? String)?.trim().orEmpty()
if (serverName.isEmpty()) continue
val table = v as? Map<*, *> ?: continue
val command = table["command"] as? String
val type = (table["type"] as? String) ?: (table["transport"] as? String)
val url = (table["url"] as? String) ?: (table["serverUrl"] as? String)
val args = (table["args"] as? List<*>)?.filterIsInstance<String>()?.ifEmpty { null }
val env = (table["env"] as? Map<*, *>)?.entries
?.mapNotNull { (ek, ev) ->
val key = ek as? String ?: return@mapNotNull null
val value = ev as? String ?: return@mapNotNull null
key to value
}
?.toMap()
?.ifEmpty { null }
result[serverName] = ExistingConfig(
command = command,
args = args,
env = env,
@@ -66,111 +82,34 @@ open class CodexClient(scope: McpClientInfo.Scope, configPath: Path) : McpClient
type = type,
)
}
return servers
return result
}
private fun extractTomlString(body: String, key: String): String? {
val regex = Regex("(?mis)^\\s*${Regex.escape(key)}\\s*=\\s*\"((?:\\\\.|[^\"])*)\"")
val match = regex.find(body) ?: return null
return unescapeTomlString(match.groupValues[1])
}
private fun updateCodexConfig(
existing: String,
productServerKey: String,
legacyKeys: Set<String>,
url: String
): String {
val root = Toml().read(existing).toMap()
private fun extractTomlStringArray(body: String, key: String): List<String>? {
val regex = Regex("(?mis)^\\s*${Regex.escape(key)}\\s*=\\s*\\[(.*?)]")
val match = regex.find(body) ?: return null
val inner = match.groupValues[1]
val valueMatches = Regex("\"((?:\\\\.|[^\"])*)\"").findAll(inner)
val values = valueMatches.map { unescapeTomlString(it.groupValues[1]) }.toList()
return values.ifEmpty { null }
}
val existingServers = (root[MCP_SERVERS] as? Map<*, *>) ?: emptyMap<Any, Any>()
private fun extractTomlInlineTable(body: String, key: String): Map<String, String>? {
val regex = Regex("(?mis)^\\s*${Regex.escape(key)}\\s*=\\s*\\{(.*?)}")
val match = regex.find(body) ?: return null
val inner = match.groupValues[1]
val pairs = Regex("([A-Za-z0-9_.\\-]+)\\s*=\\s*\"((?:\\\\.|[^\"])*)\"").findAll(inner)
val map = pairs.associate { it.groupValues[1] to unescapeTomlString(it.groupValues[2]) }
return map.ifEmpty { null }
}
private fun unescapeTomlString(value: String): String {
val result = StringBuilder()
var index = 0
while (index < value.length) {
val ch = value[index]
if (ch == '\\' && index + 1 < value.length) {
val next = value[index + 1]
when (next) {
'\\' -> result.append('\\')
'"' -> result.append('"')
'n' -> result.append('\n')
'r' -> result.append('\r')
't' -> result.append('\t')
'b' -> result.append('\b')
else -> result.append(next)
}
index += 2
}
else {
result.append(ch)
index++
}
}
return result.toString()
}
private fun escapeTomlString(value: String): String {
val result = StringBuilder()
value.forEach { ch ->
when (ch) {
'\\' -> result.append("\\\\")
'"' -> result.append("\\\"")
'\n' -> result.append("\\n")
'\r' -> result.append("\\r")
'\t' -> result.append("\\t")
'\b' -> result.append("\\b")
else -> result.append(ch)
}
}
return result.toString()
}
private fun removeCodexSection(existing: String, serverKey: String): String {
val sectionRegex = codexSectionRegex(serverKey)
return sectionRegex.replace(existing, "")
}
private fun updateCodexConfig(existing: String, serverKey: String, url: String): String {
val sectionRegex = codexSectionRegex(serverKey)
val newSection = buildCodexSection(serverKey, url)
if (sectionRegex.containsMatchIn(existing)) {
return sectionRegex.replace(existing, newSection)
val serversWithoutLegacy = existingServers.filterKeys { key ->
key !in legacyKeys || key == productServerKey
}
if (existing.isBlank()) {
return newSection
}
val existingProductTable = (serversWithoutLegacy[productServerKey] as? Map<*, *>) ?: emptyMap<Any, Any>()
val updatedProductTable = existingProductTable + ("url" to url)
val updatedServers = serversWithoutLegacy + (productServerKey to updatedProductTable)
val builder = StringBuilder(existing)
val endsWithSingleNewline = existing.endsWith("\n")
val endsWithDoubleNewline = existing.endsWith("\n\n") || existing.endsWith("\r\n\r\n")
if (!endsWithSingleNewline) {
builder.append('\n')
}
if (!endsWithDoubleNewline) {
builder.append('\n')
}
builder.append(newSection)
return builder.toString()
}
val updatedRoot = root + (MCP_SERVERS to updatedServers)
private fun codexSectionRegex(serverKey: String): Regex {
val escapedHeader = Regex.escape("mcp_servers.$serverKey")
return Regex("(?mis)^\\s*\\[$escapedHeader]\\s*(.*?)(?=^\\s*\\[|\\z)")
}
val writer = TomlWriter()
val rendered = writer.write(updatedRoot)
private fun buildCodexSection(serverKey: String, url: String): String {
return "[mcp_servers.$serverKey]\nurl = \"${escapeTomlString(url)}\"\n\n"
return if (rendered.endsWith("\n")) rendered else "$rendered\n"
}
}
}
@@ -12,7 +12,6 @@ import kotlin.io.path.readText
import kotlin.io.path.writeText
class CodexClientTest {
@TempDir
lateinit var tempDir: Path
@@ -97,6 +96,99 @@ class CodexClientTest {
val occurrences = result.split(expectedUrl).size - 1
assertTrue(1 == occurrences)
}
@Test
fun `isConfigured returns false when config file does not exist`() {
val configPath = tempDir.resolve("missing.toml")
val client = CodexClient(McpClientInfo.Scope.GLOBAL, configPath)
assertFalse(client.isConfigured() == true)
}
@Test
fun `isConfigured returns false for non-stream url`() {
val configPath = tempDir.resolve("config.toml")
configPath.writeText(
"""
[mcp_servers.test]
url = "http://localhost:8123/api"
""".trimIndent()
)
val client = CodexClient(McpClientInfo.Scope.GLOBAL, configPath)
assertFalse(client.isConfigured() == true)
}
@Test
fun `isConfigured returns false for malformed url`() {
val configPath = tempDir.resolve("config.toml")
configPath.writeText(
"""
[mcp_servers.test]
url = "not-a-url"
""".trimIndent()
)
val client = CodexClient(McpClientInfo.Scope.GLOBAL, configPath)
assertFalse(client.isConfigured() == true)
}
@Test
fun `isConfigured returns false when mcp_servers section is missing`() {
val configPath = tempDir.resolve("config.toml")
configPath.writeText(
"""
[other_section]
url = "http://localhost:8123/stream"
""".trimIndent()
)
val client = CodexClient(McpClientInfo.Scope.GLOBAL, configPath)
assertFalse(client.isConfigured() == true)
}
@Test
fun `configure overwrites existing product-specific section`() {
val configPath = tempDir.resolve("config.toml")
configPath.writeText(
"""
[mcp_servers.codextest]
url = "http://localhost:1111/stream"
""".trimIndent()
)
McpClient.overrideProductSpecificServerKeyForTests("codextest")
McpClient.overrideWriteLegacyForTests(false)
val client = TestCodexClient(McpClientInfo.Scope.GLOBAL, configPath, "http://localhost:2222/stream")
client.configure()
val result = configPath.readText()
assertTrue(result.contains("""url = "http://localhost:2222/stream""""))
assertFalse(result.contains("""url = "http://localhost:1111/stream""""))
}
@Test
fun `configure preserves unrelated content`() {
val configPath = tempDir.resolve("config.toml")
configPath.writeText(
"""
# comment
[random.section]
value = 42
""".trimIndent()
)
McpClient.overrideProductSpecificServerKeyForTests("codextest")
McpClient.overrideWriteLegacyForTests(false)
val client = TestCodexClient(McpClientInfo.Scope.GLOBAL, configPath, "http://localhost:3333/stream")
client.configure()
val result = configPath.readText()
assertTrue(result.contains("[random.section]"))
assertTrue(result.contains("value = 42"))
assertTrue(result.contains("[mcp_servers.codextest]"))
}
}
private class TestCodexClient(