Skip to content

Comments

feat: add support for codex cli client#8158

Open
kaplan-shaked wants to merge 1 commit intoscalameta:mainfrom
kaplan-shaked:feature-support-codex-client
Open

feat: add support for codex cli client#8158
kaplan-shaked wants to merge 1 commit intoscalameta:mainfrom
kaplan-shaked:feature-support-codex-client

Conversation

@kaplan-shaked
Copy link
Contributor

@kaplan-shaked kaplan-shaked commented Feb 1, 2026

Summary by CodeRabbit

  • New Features

    • Added TOML-based MCP server configuration support alongside existing JSON configuration, providing an alternative configuration method
    • Introduced new Codex client with native TOML configuration file support
  • Tests

    • Added unit test suite for TOML configuration functionality

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Introduces TOML-based configuration support for MCP servers alongside existing JSON handling. Adds a new TomlConfig utility for TOML manipulation (upsert/remove servers, read ports), integrates TOML logic into McpConfig methods, and introduces a new Codex client using TOML configuration.

Changes

Cohort / File(s) Summary
TOML Configuration Utility
metals/src/main/scala/scala/meta/internal/metals/mcp/TomlConfig.scala
New TomlConfig object providing three public methods: upsertServer (add/update server entries), removeServer (delete server entries), and readPort (extract port from TOML). Includes private helpers for section splitting and TOML generation.
MCP Configuration with TOML Support
metals/src/main/scala/scala/meta/internal/metals/mcp/McpConfig.scala
Extended writeConfig, deleteConfig, rewriteOldEndpointIfNeeded, and readPort methods to conditionally handle TOML files via TomlConfig. Added new Codex client extending Client with TOML-based configuration. Updated Client.allClients list to include Codex.
TOML Configuration Tests
tests/unit/src/test/scala/tests/mcp/TomlConfigSuite.scala
New unit test suite with five tests covering upsertServer (new and existing entries), removeServer (with and without other sections), and readPort functionality.

Poem

🐰 With TOML now in place so neat,
The config files march to a new beat,
Codex hops in with style and grace,
While servers find their proper space,
A whisker twitch of joy complete! 🌿✨

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add support for codex cli client' clearly and directly describes the main change: introducing support for a new Codex CLI client with TOML-based configuration. It is concise, specific, and accurately reflects the primary objective of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@metals/src/main/scala/scala/meta/internal/metals/mcp/McpConfig.scala`:
- Around line 57-68: The deletion uses the raw projectName while writeConfig
writes the server under the transformed name produced by
client.projectName(projectName), so TomlConfig.removeServer should be called
with the same transformed key; update the delete path to pass
client.projectName(projectName) (the same symbol used in writeConfig) into
TomlConfig.removeServer so the TOML key matches and removal succeeds, and keep
the rest of the logic (configFile.readText, writeText/delete) unchanged.

In `@metals/src/main/scala/scala/meta/internal/metals/mcp/TomlConfig.scala`:
- Around line 47-52: The URL extraction logic in
section.find(_.trim.startsWith("url =")) and the subsequent line.split("=")(1)
truncates values containing '=' and misses `url=value` style entries; update the
predicate to find lines where l.trim.startsWith("url") && l.contains("=") (or
equivalent) to allow optional whitespace, then extract the RHS by splitting only
on the first '=' (limit=2) or by taking substring after the first '='; finally
trim the result and strip surrounding quotes before normalizing (preserve query
strings) and continue parsing the port as before (e.g., using
normalizedUrl.split(":").last.toInt).
🧹 Nitpick comments (2)
metals/src/main/scala/scala/meta/internal/metals/mcp/TomlConfig.scala (1)

58-69: Consider escaping special characters in TOML string values.

The current implementation wraps values in quotes without escaping. If a value contains " or \, the generated TOML will be malformed. While current usage appears to be URLs only, this could cause issues if the API is used more broadly.

♻️ Proposed fix to escape special characters
   private def generateSection(
       tableName: String,
       serverName: String,
       properties: Map[String, String],
   ): List[String] = {
     val header = s"[$tableName.$serverName]"
     val props = properties.map { case (k, v) =>
-      s"""$k = "$v""""
+      val escaped = v.replace("\\", "\\\\").replace("\"", "\\\"")
+      s"""$k = "$escaped""""
     }.toList
     // Add a blank line before if needed? relying on split logic to handle spacing
     List(header) ++ props
   }
tests/unit/src/test/scala/tests/mcp/TomlConfigSuite.scala (1)

69-79: Consider adding edge case tests for readPort.

The current test covers the happy path. Given the URL parsing fragility noted in TomlConfig.scala, consider adding tests for:

  • URLs with query parameters containing =
  • URLs without the expected suffix
  • Malformed URLs
test("read-port-with-query-params") {
  val toml =
    """
      |[mcpServers.my-project-metals]
      |url = "http://localhost:8080/mcp?key=value"
    """.stripMargin

  val port =
    TomlConfig.readPort(toml, "mcpServers", "my-project-metals", "/mcp")
  assertEquals(port, Some(8080))
}

test("read-port-not-found") {
  val toml =
    """
      |[mcpServers.other]
      |url = "http://localhost:8080/mcp"
    """.stripMargin

  val port =
    TomlConfig.readPort(toml, "mcpServers", "my-project-metals", "/mcp")
  assertEquals(port, None)
}

Comment on lines +57 to +68
if (configFile.toFile.getName.endsWith(".toml")) {
val configContent = configFile.readText
val updatedConfig = TomlConfig.removeServer(
configContent,
client.serverField,
projectName,
)
if (updatedConfig.trim.isEmpty) {
configFile.delete()
} else {
configFile.writeText(updatedConfig)
}
Copy link

@coderabbitai coderabbitai bot Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Server name mismatch in TOML deletion will cause deletion to fail.

In writeConfig (line 31), the server is written using client.projectName(projectName) which transforms the name (e.g., adds -metals suffix for default clients). However, in deleteConfig (line 62), the raw projectName is passed to TomlConfig.removeServer.

This inconsistency means deletion will look for a different key than what was written, causing silent failure to remove the server entry.

🐛 Proposed fix
       if (configFile.toFile.getName.endsWith(".toml")) {
         val configContent = configFile.readText
         val updatedConfig = TomlConfig.removeServer(
           configContent,
           client.serverField,
-          projectName,
+          client.projectName(projectName),
         )
🤖 Prompt for AI Agents
In `@metals/src/main/scala/scala/meta/internal/metals/mcp/McpConfig.scala` around
lines 57 - 68, The deletion uses the raw projectName while writeConfig writes
the server under the transformed name produced by
client.projectName(projectName), so TomlConfig.removeServer should be called
with the same transformed key; update the delete path to pass
client.projectName(projectName) (the same symbol used in writeConfig) into
TomlConfig.removeServer so the TOML key matches and removal succeeds, and keep
the rest of the logic (configFile.readText, writeText/delete) unchanged.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a legit issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgodzik, thank you for confirming! The mismatch between client.projectName(projectName) in the write path and raw projectName in the delete path will indeed prevent proper cleanup of TOML server entries.

If you'd like, I can help create a plan to fix this issue across the codebase, or if you prefer to address it yourself, the fix is straightforward: use client.projectName(projectName) consistently in the deleteConfig method's TOML branch as shown in the bot's proposed diff.

@igor-ramazanov
Copy link

There're so many agentic tools and favouring one after another is not right, especially a commercial one.

@kaplan-shaked
Copy link
Contributor Author

@igor-ramazanov How is it different from supporting claude? or cursor?
It's an opt in feature you need to pass in the client. so why not?

@tgodzik
Copy link
Contributor

tgodzik commented Feb 8, 2026

This is just an improvement, we can add more of them. It should no be an issue.

@tgodzik
Copy link
Contributor

tgodzik commented Feb 10, 2026

I am adding #8184 to make the configuration useful. This means that we can add really any number of clients and make it easy for the user to set up

Copy link
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on it! And sorry for being late to get back to you

val configFile = projectPath.resolve(s"${client.settingsPath}$filename")
if (configFile.exists) {
// TOML does not support the old endpoint, so we don't need to rewrite it
if (configFile.exists && !filename.endsWith(".toml")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth extracting filename.endsWith(".toml") isTomlFile method, we have similar isScal, isJava methods

configFile.writeText(newConfig)
val config = if (configFile.exists) configFile.readText else ""
if (filename.endsWith(".toml")) {
val url = s"http://localhost:$port${MetalsMcpServer.mcpEndpoint}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth to move the whole link to MetalsMcpServer and reuse everywhere.

Comment on lines +57 to +68
if (configFile.toFile.getName.endsWith(".toml")) {
val configContent = configFile.readText
val updatedConfig = TomlConfig.removeServer(
configContent,
client.serverField,
projectName,
)
if (updatedConfig.trim.isEmpty) {
configFile.delete()
} else {
configFile.writeText(updatedConfig)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a legit issue.

"metals",
Map("url" -> "http://localhost:123"),
)
assert(updated.contains("[mcpServers.metals]"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use assertNoDiff with the full file output for the tests?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants