This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new adaefd1ae61e CAMEL-26298: camel-mcp-server - camel run --mcp-stdio
starts, and stdout carries the MCP protocol only (#28803)
adaefd1ae61e is described below
commit adaefd1ae61ecef1cd0b387cb0c1be8e36057286
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 00:03:48 2026 +0100
CAMEL-26298: camel-mcp-server - camel run --mcp-stdio starts, and stdout
carries the MCP protocol only (#28803)
* CAMEL-26298: camel-mcp-server - camel run --mcp-stdio starts, and std…
…out carries the MCP protocol only
The stdio engine looked up the MCP JSON mapper in initialize(), while Camel
Main configures the
CamelContext. The MCP SDK finds it with the service loader of the thread
context class loader,
which in Camel CLI cannot see the downloaded mcp-json-jackson2 jar then (it
is only the
application class loader of CamelContext while the context starts), so
camel run --mcp-stdio
failed with "No McpJsonMapperSupplier available". The mapper is now looked
up with the
application context class loader of CamelContext, when the engine needs it.
Also for stdio mode:
- System.out points at stderr while the engine runs and the transport
writes to the original
stdout, so a stream:out endpoint or a println in a bean does not break
the MCP stream
- the engine stops the CamelContext when the client closes stdin, so the
process exits
- camel run does not print the configuration values of camel config to
stdout with --mcp-stdio
* CAMEL-26298: camel-mcp-server - Test that stream:out stays out of the…
… MCP stream
* Merge remote-tracking branch 'origin/main' into CAMEL-26298-mcp-stdio…
…-startup
# Conflicts:
#
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/docs/mcp-server.adoc | 11 ++-
components/camel-ai/camel-mcp-server/pom.xml | 5 ++
.../camel-mcp-server/src/main/docs/mcp-server.adoc | 11 ++-
.../mcp/server/stdio/StdioMcpServerEngine.java | 95 ++++++++++++++++++++--
.../mcp/server/stdio/McpStdioHarnessMain.java | 40 +++++++++
.../mcp/server/stdio/StdioMcpServerProcessIT.java | 64 ++++++++++++++-
.../apache/camel/dsl/jbang/core/commands/Run.java | 9 +-
.../commands/RunMcpStdioConfigurationTest.java | 58 +++++++++++++
8 files changed, 276 insertions(+), 17 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
index 8a4e562991e9..e7f46982a45f 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mcp-server.adoc
@@ -220,7 +220,10 @@ JSON-RPC only. Camel JBang configures a stderr Log4j2
appender automatically
when `--mcp-stdio` is used or when `camel.server.mcp-transport=stdio` is set
in the profile. On Camel Main, configure logging to stderr yourself (for
example using the `log4j2-mcp-stdio.properties` resource shipped in
-`camel-mcp-server`). Configure your MCP client (for example in `mcp.json`) to
launch
+`camel-mcp-server`). Anything else written to `System.out` while the MCP
server runs, such as
+a `stream:out` endpoint or a `System.out.println` in a bean, goes to stderr as
well.
+When the MCP client closes stdin, Camel stops and the process exits.
+Configure your MCP client (for example in `mcp.json`) to launch
the Camel process as a subprocess:
[source,json]
@@ -346,8 +349,10 @@ subscriptions (`resources/subscribe`) are not offered: the
server advertises
The stdio engine uses the MCP Java SDK `StdioServerTransportProvider`. MCP
JSON-RPC is read from stdin and written to stdout. Logging and startup banners
must not appear on stdout — Camel Main/JBang configure Log4j2 to write logs to
-stderr in stdio mode. No HTTP port is opened and no session management headers
-apply.
+stderr in stdio mode. While the engine runs, `System.out` points at stderr,
and the
+transport writes to the original stdout. When stdin reaches its end (the
client went
+away), the engine stops the CamelContext. No HTTP port is opened and no session
+management headers apply.
== Security
diff --git a/components/camel-ai/camel-mcp-server/pom.xml
b/components/camel-ai/camel-mcp-server/pom.xml
index 2faf453d725a..eb827fc23b7c 100644
--- a/components/camel-ai/camel-mcp-server/pom.xml
+++ b/components/camel-ai/camel-mcp-server/pom.xml
@@ -95,6 +95,11 @@
<artifactId>camel-openai</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-stream</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-infra-ollama</artifactId>
diff --git a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
index 8a4e562991e9..e7f46982a45f 100644
--- a/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
+++ b/components/camel-ai/camel-mcp-server/src/main/docs/mcp-server.adoc
@@ -220,7 +220,10 @@ JSON-RPC only. Camel JBang configures a stderr Log4j2
appender automatically
when `--mcp-stdio` is used or when `camel.server.mcp-transport=stdio` is set
in the profile. On Camel Main, configure logging to stderr yourself (for
example using the `log4j2-mcp-stdio.properties` resource shipped in
-`camel-mcp-server`). Configure your MCP client (for example in `mcp.json`) to
launch
+`camel-mcp-server`). Anything else written to `System.out` while the MCP
server runs, such as
+a `stream:out` endpoint or a `System.out.println` in a bean, goes to stderr as
well.
+When the MCP client closes stdin, Camel stops and the process exits.
+Configure your MCP client (for example in `mcp.json`) to launch
the Camel process as a subprocess:
[source,json]
@@ -346,8 +349,10 @@ subscriptions (`resources/subscribe`) are not offered: the
server advertises
The stdio engine uses the MCP Java SDK `StdioServerTransportProvider`. MCP
JSON-RPC is read from stdin and written to stdout. Logging and startup banners
must not appear on stdout — Camel Main/JBang configure Log4j2 to write logs to
-stderr in stdio mode. No HTTP port is opened and no session management headers
-apply.
+stderr in stdio mode. While the engine runs, `System.out` points at stderr,
and the
+transport writes to the original stdout. When stdin reaches its end (the
client went
+away), the engine stops the CamelContext. No HTTP port is opened and no session
+management headers apply.
== Security
diff --git
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerEngine.java
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerEngine.java
index 6db97cf5f055..b20a1e79bf17 100644
---
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerEngine.java
+++
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerEngine.java
@@ -16,12 +16,16 @@
*/
package org.apache.camel.component.mcp.server.stdio;
+import java.io.FilterInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.json.McpJsonMapper;
@@ -67,6 +71,7 @@ public class StdioMcpServerEngine extends ServiceSupport
implements McpServerEng
private McpSyncServer server;
private InputStream inputStream;
private OutputStream outputStream;
+ private PrintStream originalOut;
private final List<McpServerFeatures.SyncToolSpecification> pendingTools =
new ArrayList<>();
@Override
@@ -82,7 +87,31 @@ public class StdioMcpServerEngine extends ServiceSupport
implements McpServerEng
@Override
public void initialize(McpServerInfo info) {
this.info = info;
- this.jsonMapper = McpJsonDefaults.getMapper();
+ }
+
+ /**
+ * The JSON mapper of the MCP SDK, found with the service loader of the
application class loader of Camel.
+ * <p>
+ * The MCP SDK finds the mapper with the thread context class loader.
Camel CLI (camel run) loads the downloaded
+ * jars, such as mcp-json-jackson2, in the application context class
loader of CamelContext, which is the thread
+ * context class loader while CamelContext starts, but not while Camel
Main configures it and initializes this
+ * engine.
+ */
+ private McpJsonMapper jsonMapper() {
+ if (jsonMapper == null) {
+ ClassLoader cl = camelContext != null ?
camelContext.getApplicationContextClassLoader() : null;
+ if (cl == null) {
+ cl = StdioMcpServerEngine.class.getClassLoader();
+ }
+ ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(cl);
+ jsonMapper = McpJsonDefaults.getMapper();
+ } finally {
+ Thread.currentThread().setContextClassLoader(tccl);
+ }
+ }
+ return jsonMapper;
}
/**
@@ -101,13 +130,15 @@ public class StdioMcpServerEngine extends ServiceSupport
implements McpServerEng
@Override
protected void doStart() throws Exception {
- if (jsonMapper == null) {
- jsonMapper = McpJsonDefaults.getMapper();
- }
+ McpJsonMapper mapper = jsonMapper();
if (inputStream != null && outputStream != null) {
- transport = new StdioServerTransportProvider(jsonMapper,
inputStream, outputStream);
+ transport = new StdioServerTransportProvider(mapper, inputStream,
outputStream);
} else {
- transport = new StdioServerTransportProvider(jsonMapper);
+ // stdout carries the MCP protocol only: keep it for the
transport, and send anything else written to
+ // System.out (a stream:out endpoint, a println in a bean, a
banner of a library) to stderr
+ originalOut = System.out;
+ System.setOut(System.err);
+ transport = new StdioServerTransportProvider(mapper, new
StopOnEndOfInput(System.in), originalOut);
}
server = McpServer.sync(transport)
.serverInfo(info.serverName(), info.version())
@@ -129,10 +160,60 @@ public class StdioMcpServerEngine extends ServiceSupport
implements McpServerEng
server.closeGracefully();
server = null;
}
+ if (originalOut != null) {
+ System.setOut(originalOut);
+ originalOut = null;
+ }
pendingTools.clear();
transport = null;
}
+ private void onEndOfInput() {
+ if (camelContext == null || isStoppingOrStopped()) {
+ return;
+ }
+ // the client closed stdin: the MCP stdio transport expects the server
to exit then, so stop Camel (which
+ // also ends Camel Main), from another thread as this is the thread
that reads stdin
+ LOG.info("MCP client closed stdin, stopping {}",
camelContext.getName());
+ Thread thread = new Thread(() -> {
+ try {
+ camelContext.stop();
+ } catch (Exception e) {
+ LOG.warn("Failed to stop {} after the MCP client closed stdin:
{}", camelContext.getName(), e.getMessage());
+ }
+ }, "CamelMcpStdioShutdown");
+ thread.start();
+ }
+
+ /**
+ * Calls {@link #onEndOfInput()} when the input reaches its end.
+ */
+ private final class StopOnEndOfInput extends FilterInputStream {
+
+ private final AtomicBoolean ended = new AtomicBoolean();
+
+ StopOnEndOfInput(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public int read() throws IOException {
+ return checkEnd(super.read());
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return checkEnd(super.read(b, off, len));
+ }
+
+ private int checkEnd(int n) {
+ if (n == -1 && ended.compareAndSet(false, true)) {
+ onEndOfInput();
+ }
+ return n;
+ }
+ }
+
@Override
public void toolAdded(McpServerTool tool) {
McpServerFeatures.SyncToolSpecification spec =
buildToolSpecification(tool);
@@ -197,7 +278,7 @@ public class StdioMcpServerEngine extends ServiceSupport
implements McpServerEng
private McpSchema.Tool buildMcpTool(McpServerTool tool) {
String schema = tool.inputSchemaJson() != null ?
tool.inputSchemaJson() : EMPTY_OBJECT_SCHEMA;
- McpSchema.Tool.Builder builder = McpSchema.Tool.builder(tool.name(),
jsonMapper, schema)
+ McpSchema.Tool.Builder builder = McpSchema.Tool.builder(tool.name(),
jsonMapper(), schema)
.description(tool.description());
applyAnnotations(builder, tool.annotations());
return builder.build();
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/McpStdioHarnessMain.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/McpStdioHarnessMain.java
index 6c09847fae04..39b621ffe8e0 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/McpStdioHarnessMain.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/McpStdioHarnessMain.java
@@ -16,6 +16,11 @@
*/
package org.apache.camel.component.mcp.server.stdio;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+
import org.apache.camel.StartupSummaryLevel;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
@@ -32,6 +37,11 @@ public final class McpStdioHarnessMain {
public static void main(String[] args) throws Exception {
System.setProperty("log4j2.configurationFile",
"classpath:log4j2-mcp-stdio.properties");
String tags = args.length > 0 ? args[0] : "harness";
+ if (args.length > 1 && "cli".equals(args[1])) {
+ // as Camel CLI (camel run): the thread context class loader
cannot see the downloaded jars, such as
+ // mcp-json-jackson2, while Camel Main configures the CamelContext
+ Thread.currentThread().setContextClassLoader(new
HidingClassLoader(McpStdioHarnessMain.class.getClassLoader()));
+ }
Main main = new Main();
main.configure().setStartupSummaryLevel(StartupSummaryLevel.Off);
@@ -46,10 +56,40 @@ public final class McpStdioHarnessMain {
.routeId("say-hello-route")
.setBody(simple("Hello ${header.name}"));
+ // writes to System.out, which in stdio mode must not reach
the MCP stream
+ from("ai-tool:print_tool?tags=" + tags + "&description=Print
to System.out")
+ .process(e -> System.out.println("printed by a route"))
+ .setBody(constant("printed by stream:out"))
+ .to("stream:out")
+ .setBody(constant("printed"));
+
from("ai-tool:hidden_tool?description=Untagged tool, must not
be exposed")
.setBody(constant("hidden"));
}
});
main.run();
}
+
+ /**
+ * Hides the service file of the MCP JSON mapper, as the class loader of
Camel CLI does not have the downloaded
+ * jars.
+ */
+ private static final class HidingClassLoader extends ClassLoader {
+
+ private static final String HIDDEN =
"META-INF/services/io.modelcontextprotocol.json.McpJsonMapperSupplier";
+
+ HidingClassLoader(ClassLoader parent) {
+ super(parent);
+ }
+
+ @Override
+ public URL getResource(String name) {
+ return HIDDEN.equals(name) ? null : super.getResource(name);
+ }
+
+ @Override
+ public Enumeration<URL> getResources(String name) throws IOException {
+ return HIDDEN.equals(name) ? Collections.emptyEnumeration() :
super.getResources(name);
+ }
+ }
}
diff --git
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerProcessIT.java
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerProcessIT.java
index 261741a98c30..c5cd3e37ea0c 100644
---
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerProcessIT.java
+++
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/stdio/StdioMcpServerProcessIT.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.mcp.server.stdio;
import java.time.Duration;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import io.modelcontextprotocol.client.McpClient;
@@ -80,8 +81,64 @@ class StdioMcpServerProcessIT {
}
}
+ @Test
+ void
testStdioMcpServerStartsWhenTheContextClassLoaderCannotSeeTheJsonMapper() {
+ McpSyncClient client = null;
+ try {
+ client = createClient("cli");
+ client.initialize();
+
+ McpSchema.CallToolResult result
+ = client.callTool(new
McpSchema.CallToolRequest("say_hello", Map.of("name", "Camel")));
+ assertThat(textOf(result)).isEqualTo("Hello Camel");
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ }
+ }
+
+ @Test
+ void testStdioMcpServerKeepsSystemOutOfTheMcpStream() {
+ McpSyncClient client = null;
+ try {
+ client = createClient();
+ client.initialize();
+
+ McpSchema.CallToolResult printed = client.callTool(new
McpSchema.CallToolRequest("print_tool", Map.of()));
+ assertThat(textOf(printed)).isEqualTo("printed");
+ // the printed line went to stderr, so the next call still works
+ McpSchema.CallToolResult result
+ = client.callTool(new
McpSchema.CallToolRequest("say_hello", Map.of("name", "Camel")));
+ assertThat(textOf(result)).isEqualTo("Hello Camel");
+ } finally {
+ if (client != null) {
+ client.closeGracefully();
+ }
+ }
+ }
+
+ @Test
+ void testStdioMcpServerExitsWhenTheClientClosesStdin() throws Exception {
+ List<String> command = harnessCommand("default");
+ Process process = new ProcessBuilder(command)
+ .redirectError(ProcessBuilder.Redirect.DISCARD)
+ .redirectOutput(ProcessBuilder.Redirect.DISCARD)
+ .start();
+ try {
+ process.getOutputStream().close();
+ assertThat(process.waitFor(60, TimeUnit.SECONDS)).as("the server
exits when stdin is closed").isTrue();
+ } finally {
+ process.destroyForcibly();
+ }
+ }
+
private static McpSyncClient createClient() {
- List<String> command = harnessCommand();
+ return createClient("default");
+ }
+
+ private static McpSyncClient createClient(String mode) {
+ List<String> command = harnessCommand(mode);
ServerParameters parameters = ServerParameters.builder(command.get(0))
.args(command.subList(1, command.size()))
.build();
@@ -91,7 +148,7 @@ class StdioMcpServerProcessIT {
.build();
}
- private static List<String> harnessCommand() {
+ private static List<String> harnessCommand(String mode) {
String javaHome = System.getProperty("java.home");
String javaBin = javaHome + "/bin/java";
String classpath = System.getProperty("java.class.path");
@@ -101,7 +158,8 @@ class StdioMcpServerProcessIT {
"-cp",
classpath,
McpStdioHarnessMain.class.getName(),
- HARNESS_TAG);
+ HARNESS_TAG,
+ mode);
}
private static String textOf(McpSchema.CallToolResult result) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index bdbbee658209..b43b7981b4a9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -349,11 +349,18 @@ public class Run extends CamelCommand {
checkFilesExist();
skipCitrusTests();
- printConfigurationValues("Running integration with the following
configuration:");
+ printRunConfiguration();
// run
return run();
}
+ void printRunConfiguration() {
+ // the printer of this command is quiet with MCP over stdio, already
for the configuration values printed
+ // before the run is prepared, as stdout carries the MCP protocol only
+ mcpStdioMode = serverOptions.mcpStdio;
+ printConfigurationValues("Running integration with the following
configuration:");
+ }
+
/**
* Fails a local file that does not exist with exit code 66, before
anything is prepared or printed. Remote files
* (such as github: and https:) and wildcards are checked when they are
read.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunMcpStdioConfigurationTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunMcpStdioConfigurationTest.java
new file mode 100644
index 000000000000..a46c214b0feb
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunMcpStdioConfigurationTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import org.apache.camel.dsl.jbang.core.commands.config.BaseConfigTestSupport;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.run.common.StringPrinter;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * With MCP over stdio, stdout carries the MCP protocol only, so the
configuration values of camel config are not
+ * printed there (CAMEL-26298).
+ */
+class RunMcpStdioConfigurationTest extends BaseConfigTestSupport {
+
+ @Test
+ void configurationValuesArePrinted() throws Exception {
+ UserConfigHelper.createUserConfig("dev=true");
+
+ assertThat(printRunConfiguration("tools.yaml"))
+ .contains("Running integration with the following
configuration:")
+ .contains("--reload=true");
+ }
+
+ @Test
+ void configurationValuesAreNotPrintedWithMcpStdio() throws Exception {
+ UserConfigHelper.createUserConfig("dev=true");
+
+ assertThat(printRunConfiguration("tools.yaml", "--mcp-stdio",
"--mcp-tags=demo")).isEmpty();
+ }
+
+ private static String printRunConfiguration(String... args) {
+ StringPrinter out = new StringPrinter();
+ Run run = new Run(new CamelJBangMain().withPrinter(out));
+ CommandLine commandLine = new CommandLine(run);
+ CommandLineHelper.augmentWithUserConfiguration(commandLine);
+ commandLine.parseArgs(args);
+ run.printRunConfiguration();
+ return out.getStdout();
+ }
+}