This is an automated email from the ASF dual-hosted git repository.
Croway 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 ee3d46fbd17d CAMEL-25646: camel-jbang-mcp - the initialize response
carries server instructions: the rules an agent needs before its first call
(#27816)
ee3d46fbd17d is described below
commit ee3d46fbd17d1b0bd0e7a5577328f9d1c5c28076
Author: Federico Mariani <[email protected]>
AuthorDate: Wed Oct 14 14:03:36 2026 +0200
CAMEL-25646: camel-jbang-mcp - the initialize response carries server
instructions: the rules an agent needs before its first call (#27816)
* CAMEL-25646: camel-jbang-mcp - the initialize response carries server
instructions: the rules an agent needs before its first call
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25646: the camel run rules match main (CAMEL-25528, CAMEL-25533),
shorter host note in the docs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25646: the handled example uses the canonical YAML form, as the doc
hints of CAMEL-25539 do
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 26 ++++
.../core/commands/mcp/McpServerInstructions.java | 117 +++++++++++++++++
.../commands/mcp/McpServerInstructionsTest.java | 139 +++++++++++++++++++++
3 files changed, 282 insertions(+)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 95f4d04e752a..889424d6b4f1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -87,6 +87,32 @@ java -Dquarkus.http.host-enabled=true
-Dquarkus.http.port=8080 -jar camel-jbang-
IMPORTANT: The MCP server does **not** expose REST endpoints — all
communication uses JSON-RPC over the MCP protocol.
+== Server instructions
+
+The `initialize` response carries `instructions`: a short plain-text guide
(under 4096 characters) that the agent
+host gives the model at session start, before any tool call and before any
tool description is loaded. It answers the
+questions that no tool answers and that agents otherwise go hunting for:
+
+* The first line is a one-sentence summary with the Camel version of the
server: look components up by URI scheme
+ and validate with `camel_validate_source` before finishing.
+* The rules: components by URI scheme (`smtp`, not `mail`; `platform-http` for
an HTTP server); `onException`,
+ `errorHandler`, `routeConfiguration`, `intercept` and `onCompletion` are
top-level entries, not steps, and
+ `handled`/`continued` are expressions; under `camel run`,
`camel.server.port` (or `--port`) sets the HTTP port, and
+ extra jars go in a `//DEPS` line, `camel.jbang.dependencies` in
`application.properties`, or `--dep` (the
+ `camel_run` tool has no dependencies argument); the mail Subject, From and
To headers are ignored unless
+ `useHeaderSubject`, `useHeaderFrom` and `useHeaderRecipients` are `true`;
remove the `Camel*` headers you did not
+ set yourself before an http or mail producer; `xslt` gets the headers and
variables as stylesheet parameters; a
+ source that passes the schema can still fail at runtime, so run it and read
the log.
+* Which tool answers which question, by tool name: `camel_catalog_doc`,
`camel_catalog_find`,
+ `camel_catalog_sample`, `camel_catalog_examples`,
`camel_dependency_for_class`, `camel_validate_source`,
+ `camel_run`, `camel_get_log`, `camel_get_errors` and
`camel_project_overview`.
+
+The first line is self-contained, as some hosts show only that line.
+
+To replace the text, set `quarkus.mcp.server.server-info.instructions`, for
example
+`-Dquarkus.mcp.server.server-info.instructions="..."` with `java -jar`. When
that property is set, the server sends
+its value instead of the built-in text.
+
[[Setup]]
== Setup
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
new file mode 100644
index 000000000000..f6f8feeae963
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
@@ -0,0 +1,117 @@
+/*
+ * 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.mcp;
+
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.quarkiverse.mcp.server.InitialResponseInfo;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+/**
+ * The {@code instructions} of the MCP {@code initialize} response: the rules
an agent needs before its first tool call,
+ * and which tool answers which question.
+ * <p>
+ * Hosts that defer tool schemas (Claude Code, Codex, OpenCode, Pi) give the
model this text at session start, before
+ * any tool description. Pi shows only the first line and Codex weighs the
first 512 characters most, so the first line
+ * is a one-sentence summary with the Camel version. The whole text stays
under 4096 characters (what Claude Code
+ * loads).
+ * <p>
+ * A value set in {@code quarkus.mcp.server.server-info.instructions} replaces
this text: Quarkus asks the
+ * {@link InitialResponseInfo} beans before it reads the property, so this
bean steps aside when the property is set.
+ */
+@ApplicationScoped
+public class McpServerInstructions implements InitialResponseInfo {
+
+ /** The length the hosts take in full (Claude Code loads up to this many
characters). */
+ static final int MAX_LENGTH = 4096;
+
+ /** The tool names the text maps questions to; a test checks that each one
is an {@code @Tool} method. */
+ static final List<String> MAPPED_TOOLS = List.of(
+ "camel_catalog_doc", "camel_catalog_find", "camel_catalog_sample",
"camel_catalog_examples",
+ "camel_dependency_for_class", "camel_validate_source",
"camel_run", "camel_get_log", "camel_get_errors",
+ "camel_project_overview");
+
+ /** The text, with the Camel version as its only parameter. Kept here so
the whole text reviews in one place. */
+ private static final String TEMPLATE = """
+ Apache Camel %s catalog, validation and run tools: look components
up by URI scheme (smtp, \
+ platform-http) and validate with camel_validate_source before
finishing.
+
+ Rules:
+ - Components are named by URI scheme: smtp/smtps/imap/pop3 (not
mail), platform-http for an HTTP server, \
+ http/https for an HTTP client. camel_catalog_find maps a protocol
or product to the scheme.
+ - YAML DSL: onException, errorHandler, routeConfiguration,
intercept and onCompletion are top-level \
+ entries beside route (or inside routeConfiguration), never steps.
handled and continued are \
+ expressions: handled: {constant: {expression: "true"}}.
+ - camel run: camel.server.port (or camel run --port) sets the HTTP
port. Extra jars: a //DEPS line in a \
+ source file, camel.jbang.dependencies=group:artifact:version in
application.properties, or --dep on the \
+ command line (camel_run has no dependencies argument, so use one
of the first two).
+ - mail producer: the Subject, From and To headers are ignored
unless useHeaderSubject=true, \
+ useHeaderFrom=true and useHeaderRecipients=true; otherwise use the
subject, from and to options.
+ - Before an http or mail producer, remove the Camel* headers you
did not set yourself \
+ (removeHeaders: {pattern: "Camel*"} when none are yours), so
inbound CamelHttpPath, CamelHttpQuery or \
+ CamelHttpUri do not redirect the call and no header leaks into the
mail.
+ - xslt receives the message headers and variables as stylesheet
parameters (xsl:param).
+ - Passing the YAML schema is not passing at runtime: run the app
(camel_run) and read its log before \
+ calling it done.
+
+ Which tool:
+ - options, URI syntax, headers of a component, EIP, data format or
language: camel_catalog_doc
+ - which component for a protocol or product: camel_catalog_find
+ - a validated YAML sample and where it goes: camel_catalog_sample
+ - complete example projects: camel_catalog_examples
+ - Maven coordinates (the --dep value) of a Java class:
camel_dependency_for_class
+ - validate YAML/Java/XML/properties before writing:
camel_validate_source
+ - run an integration: camel_run; its log: camel_get_log; its
errors: camel_get_errors
+ - routes, endpoints and mistakes of an existing project:
camel_project_overview
+ """;
+
+ @ConfigProperty(name = "quarkus.mcp.server.server-info.instructions")
+ Optional<String> configured = Optional.empty();
+
+ private final String text;
+
+ public McpServerInstructions() {
+ this(new DefaultCamelCatalog().getCatalogVersion());
+ }
+
+ McpServerInstructions(String camelVersion) {
+ this.text = build(camelVersion);
+ }
+
+ /** The instructions text for the given Camel version. */
+ static String build(String camelVersion) {
+ String version = camelVersion == null || camelVersion.isBlank() ?
"(unknown version)" : camelVersion.trim();
+ return TEMPLATE.formatted(version).strip();
+ }
+
+ String text() {
+ return text;
+ }
+
+ @Override
+ public Optional<String> instructions(String serverName) {
+ if (configured != null && configured.filter(s ->
!s.isBlank()).isPresent()) {
+ // an explicit quarkus.mcp.server.server-info.instructions wins:
empty lets Quarkus fall back to it
+ return Optional.empty();
+ }
+ return Optional.of(text);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
new file mode 100644
index 000000000000..1ebd193fca08
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.mcp;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import io.quarkiverse.mcp.server.Tool;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The server instructions sent in the MCP initialize response: their size,
their first line and the tool names they
+ * point the agent to.
+ */
+class McpServerInstructionsTest {
+
+ private static final Pattern TOOL_NAME =
Pattern.compile("\\bcamel_[a-z0-9_]+\\b");
+
+ @Test
+ void theTextFitsTheHostLimitAndStartsWithTheVersionedSummary() {
+ String version = new DefaultCamelCatalog().getCatalogVersion();
+ String text = new McpServerInstructions().text();
+ String firstLine = text.lines().findFirst().orElseThrow();
+
+ assertThat(text).hasSizeLessThan(McpServerInstructions.MAX_LENGTH);
+ // Pi shows only the first line, Codex weighs the first 512 characters
most
+ assertThat(firstLine).hasSizeLessThan(512).startsWith("Apache Camel "
+ version + " ")
+ .contains("URI scheme", "camel_validate_source");
+ assertThat(McpServerInstructions.build("9.9.9")).startsWith("Apache
Camel 9.9.9 ");
+ assertThat(McpServerInstructions.build(null)).startsWith("Apache Camel
(unknown version) ");
+ }
+
+ @Test
+ void theRulesComeInTheOrderOfTheProposal() {
+ String text = new McpServerInstructions().text();
+
+ assertThat(text).containsSubsequence(
+ "URI scheme: smtp", "platform-http",
+ "top-level", "handled: {constant: {expression: \"true\"}}",
+ "camel.server.port", "//DEPS", "camel.jbang.dependencies",
"--dep",
+ "useHeaderSubject=true", "useHeaderFrom=true",
"useHeaderRecipients=true",
+ "removeHeaders", "Camel*",
+ "xslt", "headers and variables",
+ "run the app",
+ "Which tool:");
+ }
+
+ @Test
+ void everyMappedToolIsMentionedAndIsAToolOfThisServer() throws IOException
{
+ String text = new McpServerInstructions().text();
+ Set<String> toolNames = toolNames();
+
+ assertThat(toolNames).as("@Tool methods found").hasSizeGreaterThan(10);
+ // the text names exactly the mapped tools, and each is a tool of this
server (not renamed or removed)
+ Matcher m = TOOL_NAME.matcher(text);
+ Set<String> named = new HashSet<>();
+ while (m.find()) {
+ named.add(m.group());
+ }
+ assertThat(toolNames).containsAll(named);
+
assertThat(McpServerInstructions.MAPPED_TOOLS).containsExactlyInAnyOrderElementsOf(named);
+ }
+
+ @Test
+ void anExplicitPropertyReplacesTheText() {
+ McpServerInstructions bean = new McpServerInstructions("4.0.0");
+
+
assertThat(bean.instructions("camel-mcp-server")).contains(bean.text());
+
+ bean.configured = Optional.of("my own instructions");
+ assertThat(bean.instructions("camel-mcp-server")).isEmpty();
+
+ bean.configured = Optional.of(" ");
+
assertThat(bean.instructions("camel-mcp-server")).contains(bean.text());
+ }
+
+ /** The names of the {@code @Tool} methods of every class in the package
of the MCP server. */
+ private static Set<String> toolNames() throws IOException {
+ Path classes;
+ try {
+ classes =
Path.of(McpServerInstructions.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ String pkg = McpServerInstructions.class.getPackageName();
+ Path dir = classes.resolve(pkg.replace('.', '/'));
+ Set<String> names = new HashSet<>();
+ List<String> classNames;
+ try (Stream<Path> files = Files.list(dir)) {
+ classNames = files.map(p -> p.getFileName().toString())
+ .filter(n -> n.endsWith(".class"))
+ .map(n -> pkg + "." + n.substring(0, n.length() -
".class".length()))
+ .toList();
+ }
+ for (String className : classNames) {
+ Class<?> type;
+ try {
+ type = Class.forName(className, false,
McpServerInstructions.class.getClassLoader());
+ } catch (ClassNotFoundException | LinkageError e) {
+ continue;
+ }
+ for (Method method : type.getDeclaredMethods()) {
+ Tool tool = method.getAnnotation(Tool.class);
+ if (tool != null) {
+ names.add(Tool.ELEMENT_NAME.equals(tool.name())
+ ? method.getName() : tool.name());
+ }
+ }
+ }
+ return names;
+ }
+}