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;
+    }
+}

Reply via email to