Skip to content

Commit baec5c4

Browse files
committed
Use temp file for --mcp-config instead of inline JSON
Avoids shell escaping issues with complex MCP configurations. Temp file is cleaned up on transport close.
1 parent 893878e commit baec5c4

2 files changed

Lines changed: 95 additions & 122 deletions

File tree

claude-code-sdk/src/main/java/org/springaicommunity/claude/agent/sdk/transport/StreamingTransport.java

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import java.io.*;
4242
import java.nio.charset.StandardCharsets;
43+
import java.nio.file.Files;
4344
import java.nio.file.Path;
4445
import java.time.Duration;
4546
import java.util.ArrayList;
@@ -54,8 +55,8 @@
5455
import java.util.function.Consumer;
5556

5657
/**
57-
* Streaming transport for Claude CLI communication. Manages the subprocess lifecycle
58-
* and handles JSON message streaming via stdin/stdout.
58+
* Streaming transport for Claude CLI communication. Manages the subprocess lifecycle and
59+
* handles JSON message streaming via stdin/stdout.
5960
*
6061
* <p>
6162
* Key features:
@@ -126,6 +127,9 @@ public class StreamingTransport implements AutoCloseable {
126127
/** Flag for clean shutdown - volatile for visibility across threads (MCP pattern) */
127128
private volatile boolean isClosing = false;
128129

130+
/** Temp file for MCP config — written before session start, deleted on close. */
131+
private volatile Path mcpConfigFile;
132+
129133
/** Stderr handler for the current session (may be null if using default logging). */
130134
private volatile StderrHandler currentStderrHandler;
131135

@@ -215,8 +219,10 @@ public StreamingTransport(Path workingDirectory, Duration defaultTimeout, String
215219
.fromExecutorService(Executors.newSingleThreadExecutor(r -> new Thread(r, "claude-error")), "error");
216220

217221
// Initialize sinks with backpressure
218-
// Use replay() to buffer messages for late subscribers in multi-turn conversations
219-
// This ensures messages aren't lost between turns when there's no active subscriber
222+
// Use replay() to buffer messages for late subscribers in multi-turn
223+
// conversations
224+
// This ensures messages aren't lost between turns when there's no active
225+
// subscriber
220226
this.inboundSink = Sinks.many().replay().all();
221227
this.outboundSink = Sinks.many().unicast().onBackpressureBuffer();
222228
this.serverInfoSink = Sinks.one();
@@ -405,7 +411,8 @@ List<String> buildStreamingCommand(CLIOptions options) {
405411
command.add("stream-json");
406412
// NOTE: --permission-prompt-tool is NOT added unconditionally
407413
// This matches Python SDK behavior where it's only added if explicitly configured
408-
// Adding it unconditionally may affect how --allowedTools restrictions are enforced
414+
// Adding it unconditionally may affect how --allowedTools restrictions are
415+
// enforced
409416
command.add("--verbose");
410417

411418
// Standard options
@@ -503,18 +510,21 @@ List<String> buildStreamingCommand(CLIOptions options) {
503510
}
504511
}
505512

506-
// Add MCP server configuration
513+
// Add MCP server configuration via temp file (avoids shell escaping issues)
507514
if (options.getMcpServers() != null && !options.getMcpServers().isEmpty()) {
508515
try {
509516
Map<String, Object> serversForCli = buildMcpConfigForCli(options.getMcpServers());
510517
if (!serversForCli.isEmpty()) {
511518
String mcpConfigJson = objectMapper.writeValueAsString(Map.of("mcpServers", serversForCli));
519+
this.mcpConfigFile = Files.createTempFile("claude-mcp-", ".json");
520+
Files.writeString(this.mcpConfigFile, mcpConfigJson);
512521
command.add("--mcp-config");
513-
command.add(mcpConfigJson);
522+
command.add(this.mcpConfigFile.toString());
523+
logger.debug("Wrote MCP config to temp file: {}", this.mcpConfigFile);
514524
}
515525
}
516-
catch (JsonProcessingException e) {
517-
logger.warn("Failed to serialize MCP config, skipping --mcp-config flag", e);
526+
catch (IOException e) {
527+
logger.warn("Failed to write MCP config file, skipping --mcp-config flag", e);
518528
}
519529
}
520530

@@ -567,7 +577,8 @@ List<String> buildStreamingCommand(CLIOptions options) {
567577
}
568578

569579
// Permission prompt tool - matches Python SDK auto-detection pattern
570-
// Python SDK (client.py lines 68-69): Automatically sets permission_prompt_tool_name="stdio"
580+
// Python SDK (client.py lines 68-69): Automatically sets
581+
// permission_prompt_tool_name="stdio"
571582
// when a can_use_tool callback is configured
572583
String permissionPromptTool = options.getPermissionPromptToolName();
573584
if (permissionPromptTool == null && options.getToolPermissionCallback() != null) {
@@ -785,9 +796,11 @@ else if (parsed.isControlResponse()) {
785796
// Log why loop ended
786797
if (isClosing) {
787798
logger.debug("Message processing loop ended: isClosing=true");
788-
} else if (process != null && !process.isAlive()) {
799+
}
800+
else if (process != null && !process.isAlive()) {
789801
logger.debug("Message processing loop ended: process exited with code {}", process.exitValue());
790-
} else {
802+
}
803+
else {
791804
logger.debug("Message processing loop ended: stdout closed");
792805
}
793806
}
@@ -1154,6 +1167,18 @@ public void close() {
11541167
}
11551168
}
11561169

1170+
// Clean up MCP config temp file
1171+
if (mcpConfigFile != null) {
1172+
try {
1173+
Files.deleteIfExists(mcpConfigFile);
1174+
logger.debug("Deleted MCP config temp file: {}", mcpConfigFile);
1175+
}
1176+
catch (IOException e) {
1177+
logger.debug("Failed to delete MCP config temp file: {}", mcpConfigFile, e);
1178+
}
1179+
mcpConfigFile = null;
1180+
}
1181+
11571182
// Shutdown schedulers
11581183
inboundScheduler.dispose();
11591184
outboundScheduler.dispose();

0 commit comments

Comments
 (0)