From 81ad15b28b04f60db1755e3dcd73c8ae1004ecba Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sun, 12 Apr 2026 00:04:54 +0300 Subject: [PATCH] Fixed MCP config not working on Windows due to quote stripping in CreateProcess (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/13) Resolves: #13 Write --mcp-config JSON to a temp file instead of passing inline JSON, avoiding Windows CreateProcess quote-stripping. The temp file is deleted when the session stops. Also fixed error panel cleanup: hang detection now calls killOnly() instead of stop(), so settings.local.json and the temp MCP config file are preserved until the error panel is dismissed (stopProcess() on panel close triggers full file cleanup). Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../controller/ClaudeSessionController.java | 41 ++++++++++++++- .../process/ClaudeProcess.java | 51 +++++++++++++++++-- .../process/ClaudeProcessTest.java | 21 ++++++++ 5 files changed, 111 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d74fb390..3515a80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed MCP config not working on Windows due to quote stripping in CreateProcess (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/13) - Improved error panel on process start failure: shows Working Directory and CLAUDE_CONFIG_DIR fields (before Command and Error) for easier diagnosis; all fields have a right-click Copy menu; fixed "Restart Advanced…" not dismissing the error panel - Added Advanced tab in Tools → Options → Claude Code with Debug mode, Hang timeout, and a new "Enable MCP integration" toggle (disabling it skips the --mcp-config flag while hooks remain active) - Fixed process start errors and hang timeouts now logged to the IDE log file at SEVERE level diff --git a/pom.xml b/pom.xml index d806dc45..7a1e19f9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbclaudecodegui netbeans-claude-code-gui - 0.21.38-SNAPSHOT + 0.21.39-SNAPSHOT nbm Netbeans Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java index dfe80c14..3bfb3228 100644 --- a/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java +++ b/src/main/java/io/github/nbclaudecodegui/controller/ClaudeSessionController.java @@ -974,6 +974,45 @@ public void setHangCallback(java.util.function.BiConsumer callba this.hangCallback = callback; } + /** + * Kills the PTY process and resets timers/state, but does NOT call + * {@link ClaudeProcess#stop()} — so settings.local.json and the temp MCP + * config file are preserved until the error panel is dismissed and + * {@link #stopProcess()} is called. + * + *

{@code claudeProcess} is intentionally kept non-null so that the + * subsequent {@code stopProcess()} on panel dismiss can still call + * {@code claudeProcess.stop()} and clean up files. + */ + private void killProcessForErrorPanel() { + if (statusTimer != null) { + statusTimer.stop(); + statusTimer = null; + } + promptFlushTimer.stop(); + if (connector != null) { + connector.setLineListener(null); + } + if (claudeProcess != null) { + claudeProcess.killOnly(); + } + connector = null; + // claudeProcess kept — file cleanup deferred to stopProcess() on panel dismiss + modelComboPopulated = false; + modelDiscoveryInProgress = false; + modelDiscoveryAttempts = 0; + customModelIds = List.of(); + standardModelCount = 0; + synchronized (recentPtyLines) { recentPtyLines.clear(); } + processStartedAt = 0; + firstOutputReceived = false; + + model.setModelList(List.of(), -1); + model.clearChoiceMenu(); + model.clearEditModeRegistry(); + model.setLifecycle(SessionLifecycle.STARTING); + } + /** * Called from {@link #pollScreenState()} when the hang watchdog fires. * Stops the process and notifies the registered callback on the EDT. @@ -983,7 +1022,7 @@ public void setHangCallback(java.util.function.BiConsumer callba private void handleHang(int timeoutSec) { LOG.severe("Hang detected in " + workingDir + ": no PTY output received within " + timeoutSec + " s — killing process"); String command = claudeProcess != null ? claudeProcess.getLastCommand() : ""; - stopProcess(); + killProcessForErrorPanel(); // kill PTY; file cleanup deferred to panel dismiss java.util.function.BiConsumer cb = hangCallback; if (cb != null) { String msg = "No output received within " + timeoutSec diff --git a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java index 813dae4b..84b32fec 100644 --- a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java +++ b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java @@ -69,6 +69,9 @@ public ClaudeProcess() {} /** Working directory of the current session; {@code null} when stopped. */ private volatile String workingDir; + /** Temp file holding the --mcp-config JSON; {@code null} when not in use. */ + private volatile Path mcpConfigTempFile; + /** * Starts a Claude CLI PTY process in the given working directory using * the Default profile (no extra env vars injected). @@ -166,9 +169,19 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl if (io.github.nbclaudecodegui.settings.ClaudeCodePreferences.isMcpEnabled()) { // Pass MCP server config via --mcp-config (Claude 2.x no longer reads // mcpServers from settings.local.json; --mcp-config works in TUI mode). + // On Windows, inline JSON gets quote-stripped by CreateProcess, so we + // write to a temp file and pass the file path instead. cmd.add("--mcp-config"); - cmd.add(buildMcpConfigJson(port)); - LOG.info("Passing --mcp-config with netbeans SSE server on port " + port); + try { + Path tmpCfg = writeMcpConfigTempFile(port); + mcpConfigTempFile = tmpCfg; + cmd.add(tmpCfg.toAbsolutePath().toString()); + LOG.info("Passing --mcp-config via temp file: " + tmpCfg); + } catch (IOException e) { + LOG.warning("Could not write --mcp-config temp file, falling back to inline JSON: " + e.getMessage()); + cmd.add(buildMcpConfigJson(port)); + LOG.info("Passing --mcp-config with netbeans SSE server on port " + port); + } } else { LOG.info("MCP integration disabled by user preference; skipping --mcp-config"); } @@ -264,7 +277,14 @@ void appendSessionFlags(List cmd, String workingDir, Map * {@value #OUR_HOOK_MATCHER}). User-provided keys are left untouched. * If the file becomes empty after cleanup it is deleted. */ - public void stop() { + /** + * Kills the PTY process only — does NOT delete settings.local.json or the + * temp MCP config file. File cleanup is deferred to {@link #stop()}. + * + *

Use this when the error panel is about to be shown and files must stay + * alive until the user dismisses the panel. + */ + public void killOnly() { PtyProcess p = ptyProcess; if (p != null && p.isAlive()) { p.destroy(); @@ -278,6 +298,20 @@ public void stop() { } } ptyProcess = null; + // workingDir and mcpConfigTempFile intentionally NOT cleared — deferred to stop() + } + + public void stop() { + killOnly(); + + Path tmp = mcpConfigTempFile; + mcpConfigTempFile = null; + if (tmp != null) { + try { + Files.deleteIfExists(tmp); + LOG.fine("Deleted --mcp-config temp file: " + tmp); + } catch (IOException e) { /* ignore */ } + } String dir = workingDir; workingDir = null; @@ -651,6 +685,17 @@ static String toShellCommand(List cmd, boolean windows) { return sb.toString(); } + /** + * Writes the MCP config JSON to a temp file and returns its path. + * The file is registered for deletion on JVM exit as a safety net. + */ + static Path writeMcpConfigTempFile(int port) throws IOException { + Path tmp = Files.createTempFile("claude-mcp-config-", ".json"); + tmp.toFile().deleteOnExit(); + Files.writeString(tmp, buildMcpConfigJson(port), StandardCharsets.UTF_8); + return tmp; + } + /** * Builds the JSON string to pass as {@code --mcp-config} to the Claude CLI. */ diff --git a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java index 849ef7ac..2d1b2bf4 100644 --- a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java +++ b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java @@ -217,6 +217,27 @@ void testToShellCommandQuotesJsonArgWindows() { assertTrue(inner.contains("\"\""), "Windows quoting must double double-quotes"); } + // ------------------------------------------------------------------------- + // writeMcpConfigTempFile + // ------------------------------------------------------------------------- + + @Test + void testWriteMcpConfigTempFile() throws Exception { + Path tmp = ClaudeProcess.writeMcpConfigTempFile(28991); + try { + assertTrue(Files.exists(tmp), "temp file should be created"); + String content = Files.readString(tmp, StandardCharsets.UTF_8); + assertTrue(content.contains("netbeans"), "temp file should contain netbeans key"); + assertTrue(content.contains("28991"), "temp file should contain port"); + assertTrue(content.startsWith("{"), "temp file should be valid JSON object"); + // File path should contain no double-quotes (safe to pass as process arg) + assertFalse(tmp.toAbsolutePath().toString().contains("\""), + "temp file path should contain no double-quotes"); + } finally { + Files.deleteIfExists(tmp); + } + } + // ------------------------------------------------------------------------- // buildMcpConfigJson // -------------------------------------------------------------------------