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 2a821fd47497 CAMEL-26454: camel-jbang-mcp - chaos tools: 
camel_runtime_chaos_* and the chaos tool group (#29047)
2a821fd47497 is described below

commit 2a821fd474979091d981101a4f5cef152fb20793
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 3 22:35:33 2026 +0530

    CAMEL-26454: camel-jbang-mcp - chaos tools: camel_runtime_chaos_* and the 
chaos tool group (#29047)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       |  16 +-
 .../mcp/server/jbang/JbangDevMcpServer.java        |  13 +-
 components/camel-chaos/src/main/docs/chaos.adoc    |  16 +-
 design/chaos.adoc                                  |  28 +-
 .../modules/ROOT/attachments/security-model.yaml   |   2 +-
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  61 +++-
 .../ROOT/pages/camel-jbang-tui-local-models.adoc   |   7 +-
 .../modules/ROOT/pages/security-model.adoc         |  18 +-
 .../cli/connector/FileCliConnectorTransport.java   |   9 +-
 .../connector/FileCliConnectorTransportTest.java   |  12 +
 .../camel/dsl/jbang/core/commands/AskTools.java    |  43 ++-
 .../dsl/jbang/core/commands/ai/AppFeatures.java    |  20 ++
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |   2 +-
 .../dsl/jbang/core/commands/ai/ToolConfirms.java   |  26 +-
 .../dsl/jbang/core/commands/ai/ToolGroup.java      |   4 +-
 .../dsl/jbang/core/commands/ai/ToolGroups.java     |  47 +++-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   | 140 ++++++++-
 .../dsl/jbang/core/commands/AskToolsTest.java      |  49 ++++
 .../jbang/core/commands/ai/AppFeaturesTest.java    |  21 ++
 .../jbang/core/commands/ai/ToolConfirmsTest.java   |  20 ++
 .../dsl/jbang/core/commands/ai/ToolGroupsTest.java |  32 +++
 .../jbang/core/commands/ai/ToolRegistryTest.java   |  74 +++++
 .../core/commands/mcp/JbangDevMcpServerTest.java   |   5 +-
 dsl/camel-jbang/camel-jbang-mcp/pom.xml            |  12 +
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  87 +++++-
 .../mcp/RuntimeToolsChaosSecurityTest.java         | 111 ++++++++
 .../core/commands/mcp/RuntimeToolsChaosTest.java   | 313 +++++++++++++++++++++
 .../jbang/core/commands/mcp/RuntimeToolsTest.java  |   3 +-
 .../dsl/jbang/core/commands/tui/TuiToolGroups.java |   9 +-
 .../core/commands/tui/AiPanelToolGroupsTest.java   |  17 ++
 30 files changed, 1154 insertions(+), 63 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index 796940c47939..5875de042a65 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -125,6 +125,19 @@ switched on in the integration (see 
<<_switching_it_on,Switching it on>>): they
 The `chaos` developer console (`/q/dev/chaos` with `camel run --console`) 
shows the same rules and report, as text or
 JSON. It only reads: a rule cannot be added, changed or removed over HTTP.
 
+=== From an AI agent (MCP)
+
+The Camel MCP server (`camel mcp`, see xref:manual::camel-jbang-mcp.adoc[Camel 
MCP server]) has the same six operations
+as tools, for an AI agent that tries a route against failures: 
`camel_runtime_chaos_list`, `camel_runtime_chaos_report`,
+`camel_runtime_chaos_add`, `camel_runtime_chaos_change`, 
`camel_runtime_chaos_remove` and `camel_runtime_chaos_clear`.
+They take the same one-line rules: adding, changing, removing and clearing go 
through the same `chaos` action of the CLI
+connector, and the list and the report read the console from the status of the 
application. So chaos must be switched
+on, and a rule that is not valid is refused with the reason. Adding and 
changing a rule are marked destructive, so a
+client that honours `destructiveHint` asks before it runs them, and the 
`read-write` access level of the MCP security
+layer does not offer them; the list and the report are read-only. `camel ask` 
has the same tools, and asks before it
+adds or changes a rule. The development MCP tools that `camel run --mcp` 
(`camel.jbang.mcp=true`) serves over HTTP have
+only the list and the report.
+
 == Rules
 
 A rule is one line: the target, the fault and its parameters as `name=value`, 
then `at`, `rate` and `until`, and
@@ -209,7 +222,8 @@ default one. A rule whose class is not found, is not a 
`Throwable`, is abstract
 constructor taking a `String` nor a public no-argument one is rejected when it 
is added. Any such public class on the
 classpath of the application can be named, and its constructor runs when the 
fault fires, so a rule is development
 configuration of a trusted developer, like a route: it comes only from 
`camel.chaos.rules` (marked `insecure:dev`), Java
-and the CLI connector, never over HTTP, and chaos stays off with profile 
`prod`.
+and the CLI connector (which the CLI, `camel ask` and the MCP tools use), 
never over the HTTP server of the application,
+and chaos stays off with profile `prod`.
 
 With `status` (300 to 599) it is the `HttpOperationFailedException` of 
camel-http-base that the HTTP components (`http`,
 `https`, `undertow`, `vertx-http`) throw for that status, with the `header.*` 
values as the response headers and the
diff --git 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/jbang/JbangDevMcpServer.java
 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/jbang/JbangDevMcpServer.java
index c67836463ccc..260e9fffd2b7 100644
--- 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/jbang/JbangDevMcpServer.java
+++ 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/jbang/JbangDevMcpServer.java
@@ -39,8 +39,9 @@ import org.slf4j.LoggerFactory;
 
 /**
  * Dev/diagnostics MCP server on the management HTTP port, exposing shared 
JBang {@code ToolRegistry} tools through
- * {@link VertxMcpServerEngine}. JBang classes are resolved reflectively so 
{@code camel-jbang-core} is not required at
- * compile time.
+ * {@link VertxMcpServerEngine}: those of {@code ToolRegistry.httpTools()}, so 
not the chaos tools that change the rules
+ * (over HTTP chaos only lists and reports). JBang classes are resolved 
reflectively so {@code camel-jbang-core} is not
+ * required at compile time.
  */
 public class JbangDevMcpServer extends ServiceSupport implements 
CamelContextAware {
 
@@ -86,7 +87,7 @@ public class JbangDevMcpServer extends ServiceSupport 
implements CamelContextAwa
         engine.initialize(new McpServerInfo(SERVER_NAME, version, path));
         engine.start();
 
-        for (Object descriptor : allToolDescriptors()) {
+        for (Object descriptor : httpToolDescriptors()) {
             engine.toolAdded(toMcpTool(descriptor));
         }
     }
@@ -157,10 +158,10 @@ public class JbangDevMcpServer extends ServiceSupport 
implements CamelContextAwa
     }
 
     @SuppressWarnings("unchecked")
-    private List<Object> allToolDescriptors() throws 
ReflectiveOperationException {
+    private List<Object> httpToolDescriptors() throws 
ReflectiveOperationException {
         Class<?> registry = mandatoryClass(TOOL_REGISTRY);
-        Method allTools = registry.getMethod("allTools");
-        return (List<Object>) allTools.invoke(null);
+        Method httpTools = registry.getMethod("httpTools");
+        return (List<Object>) httpTools.invoke(null);
     }
 
     private Object executeTool(String name, Map<String, String> args) throws 
ReflectiveOperationException {
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 796940c47939..5875de042a65 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -125,6 +125,19 @@ switched on in the integration (see 
<<_switching_it_on,Switching it on>>): they
 The `chaos` developer console (`/q/dev/chaos` with `camel run --console`) 
shows the same rules and report, as text or
 JSON. It only reads: a rule cannot be added, changed or removed over HTTP.
 
+=== From an AI agent (MCP)
+
+The Camel MCP server (`camel mcp`, see xref:manual::camel-jbang-mcp.adoc[Camel 
MCP server]) has the same six operations
+as tools, for an AI agent that tries a route against failures: 
`camel_runtime_chaos_list`, `camel_runtime_chaos_report`,
+`camel_runtime_chaos_add`, `camel_runtime_chaos_change`, 
`camel_runtime_chaos_remove` and `camel_runtime_chaos_clear`.
+They take the same one-line rules: adding, changing, removing and clearing go 
through the same `chaos` action of the CLI
+connector, and the list and the report read the console from the status of the 
application. So chaos must be switched
+on, and a rule that is not valid is refused with the reason. Adding and 
changing a rule are marked destructive, so a
+client that honours `destructiveHint` asks before it runs them, and the 
`read-write` access level of the MCP security
+layer does not offer them; the list and the report are read-only. `camel ask` 
has the same tools, and asks before it
+adds or changes a rule. The development MCP tools that `camel run --mcp` 
(`camel.jbang.mcp=true`) serves over HTTP have
+only the list and the report.
+
 == Rules
 
 A rule is one line: the target, the fault and its parameters as `name=value`, 
then `at`, `rate` and `until`, and
@@ -209,7 +222,8 @@ default one. A rule whose class is not found, is not a 
`Throwable`, is abstract
 constructor taking a `String` nor a public no-argument one is rejected when it 
is added. Any such public class on the
 classpath of the application can be named, and its constructor runs when the 
fault fires, so a rule is development
 configuration of a trusted developer, like a route: it comes only from 
`camel.chaos.rules` (marked `insecure:dev`), Java
-and the CLI connector, never over HTTP, and chaos stays off with profile 
`prod`.
+and the CLI connector (which the CLI, `camel ask` and the MCP tools use), 
never over the HTTP server of the application,
+and chaos stays off with profile `prod`.
 
 With `status` (300 to 599) it is the `HttpOperationFailedException` of 
camel-http-base that the HTTP components (`http`,
 `https`, `undertow`, `vertx-http`) throw for that status, with the `header.*` 
values as the response headers and the
diff --git a/design/chaos.adoc b/design/chaos.adoc
index 7967645d8927..e97071b01a00 100644
--- a/design/chaos.adoc
+++ b/design/chaos.adoc
@@ -5,7 +5,7 @@ authors:
 reviewers: ["@davsclaus"]
 approvers: []
 creation-date: 2026-10-26
-last-updated: 2026-10-31
+last-updated: 2026-11-03
 status: proposed
 see-also:
   - "CAMEL-26330"
@@ -389,8 +389,9 @@ pending is not final.
   `chaos-change` and `chaos-remove` need it to match exactly one integration, 
as `camel cmd sql` does; `chaos-clear`
   and `camel get chaos` may omit it and then act on all that have chaos 
switched on, as `stop-route` does.
 - The camel-jbang MCP server gets a `chaos` runtime tool group next to `sql`, 
`tracing`, `resilience` and `http`,
-  offered only when the application has `camel-chaos`. The tools are in 
`RuntimeTools` (`@McpSecured`) and go through
-  the CLI connector, as the other runtime tools do; the shared `ToolRegistry` 
(also used by the TUI) has the same marks:
+  loaded when the status of the application has the chaos console 
(`camel-chaos` on its classpath). The tools are in
+  `RuntimeTools` (`@McpSecured`) and go through the CLI connector and the 
status, as the other runtime tools do; the
+  shared `ToolRegistry` (used by `camel ask`) has the same marks:
 +
 [cols="2,1,1"]
 |===
@@ -402,10 +403,18 @@ pending is not final.
 |===
 +
 With `camel.mcp.security.enabled=true` and 
`camel.mcp.security.access-level=read-write`, add and change are refused,
-as `camel_runtime_route_control`, `_sql`, `_send` and `_receive` are; 
`read-only` leaves list and report.
-- The TUI and `camel ask` ask the user before an AI adds or changes a fault, 
with the rule shown. Their AI tools ask
-  only for the calls in `ToolConfirms` (CAMEL-25676), not by the destructive 
flag, so the chaos PR adds these two
-  there and to "Developer tools" of the security model.
+as `camel_runtime_route_control`, `_sql`, `_send` and `_receive` are; 
`read-only` leaves list and report. Remove and
+clear are not destructive: they only take faults away, and the report keeps 
the removed rules, as starting and stopping
+the tracer (`camel_runtime_trace`) is not destructive either. The group loads 
when the status of the application has
+the chaos console, with chaos on or off; its guidance line says which. With 
chaos off it has only the list and the
+report, as the other four could only be refused (on or off is fixed when the 
application starts).
+- The development MCP tools of `camel run --mcp` (`camel.jbang.mcp=true`, 
`JbangDevMcpServer`) are served over HTTP
+  without hints or asking, on the management server or else the main one. They 
take `ToolRegistry.httpTools()`, which
+  leaves out the chaos tools that change rules, so there too chaos only lists 
and reports.
+- `camel ask` asks the user before an AI adds or changes a fault, with the 
rule shown. Its AI tools ask only for the
+  calls in `ToolConfirms` (CAMEL-25676), not by the destructive flag, so these 
two are there (`ToolConfirms.chaos`)
+  and in "Developer tools" of the security model. The AI panel of the TUI has 
no chaos tools: its `chaos` group adds
+  only a line that faults may be injected on purpose, so its model does not 
hunt for a bug behind them.
 - JMX is not needed for the first version.
 
 === Order of work
@@ -419,7 +428,7 @@ Each step is its own pull request after this document:
 . Faults on the messages that enter a route, and on consumer polls. Done.
 . camel-core: the step hook in the error handler of each step (CAMEL-26338), 
then the faults at steps. Done.
 . Route lifecycle faults. Done.
-. The MCP tool group.
+. The MCP tool group. Done.
 . A DST world that runs chaos rules in virtual time, as an example and a 
regression test.
 
 == Alternatives considered
@@ -452,7 +461,8 @@ per step, a development-time cost only. The new options are 
in a new group.
 
 - The JAR left on the classpath of a production build. Mitigations: the hooks 
fail closed (only with `dev` or
   `camel.chaos.enabled=true`, never with `prod`), a WARN at startup, and rules 
that expire. Rules come only from
-  `camel.chaos.rules` and the CLI connector, which the MCP tools use; over 
HTTP the dev console only lists and reports.
+  `camel.chaos.rules` and the CLI connector, which the MCP tools use, never 
over the HTTP server of the application:
+  there the dev console and the development MCP tools of `camel run --mcp` 
only list and report.
 - `camel.chaos.rules` is an `insecure:dev` option: `@Metadata(security = 
"insecure:dev")` on its field in
   `ChaosConfigurationProperties`, as on `devConsoleEnabled` in 
`DefaultConfigurationProperties`. Profile `prod` sets
   `camel.security.policy=fail`, so it refuses to start with rules configured; 
profile `dev` allows it, and with no
diff --git a/docs/user-manual/modules/ROOT/attachments/security-model.yaml 
b/docs/user-manual/modules/ROOT/attachments/security-model.yaml
index 944ee934d5c5..f3be686786fa 100644
--- a/docs/user-manual/modules/ROOT/attachments/security-model.yaml
+++ b/docs/user-manual/modules/ROOT/attachments/security-model.yaml
@@ -161,7 +161,7 @@ config_variants:
       default: false                 # CVE-2016-8749 (Jackson); the unmarshal 
type header selects the class
     - option: camel.chaos.enabled           # camel-chaos
       insecure_value: true
-      default: unset                 # on with profile dev, never with prod; 
rules from camel.chaos.rules (insecure:dev), Java or camel cmd chaos-*; 
exception= names any public Throwable class
+      default: unset                 # on with profile dev, never with prod; 
rules from camel.chaos.rules (insecure:dev), Java, camel cmd chaos-* or the 
chaos tools (camel ask, MCP); exception= names any public Throwable class
 
 properties_provided:
   # id, violation symptom, indicative severity tier (the range published at 
the advisory index since 2015)
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 d3a20847e436..98be74672942 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -273,6 +273,8 @@ Start a route with `camel run my-route.yaml`, then ask:
 * _"Send a test message to direct:start"_ — uses `camel_runtime_send`
 * _"Which processors are the slowest?"_ — uses `camel_runtime_top`
 * _"Browse messages in seda:numbers"_ — uses `camel_runtime_browse`
+* _"Fail one in ten sends to kafka for five minutes, then tell me what broke"_ 
— uses `camel_runtime_chaos_add` and
+  `camel_runtime_chaos_report` (needs `camel run --chaos`)
 
 === Prompts (structured workflows)
 
@@ -1012,7 +1014,8 @@ given, and a call that names its pid is refused.
   is refused, and redirects are not followed. `timeoutSeconds` bounds the wait 
(default 30, at most 120).
 
 | `camel_runtime_tool_groups`
-| Which runtime tool groups (`sql`, `tracing`, `resilience`, `http`) the 
application needs, from what its status shows.
+| Which runtime tool groups (`sql`, `tracing`, `resilience`, `http`, `chaos`) 
the application needs, from what its
+  status shows.
   See <<_tool_groups_for_local_models>>.
 
 | `camel_runtime_datasources`
@@ -1044,6 +1047,56 @@ given, and a call that names its pid is refused.
 | Compares the running route definitions with the source files to detect 
configuration drift.
 |===
 
+==== Chaos
+
+With `camel-chaos` in the application (`camel run --chaos`) and chaos switched 
on (profile `dev`, the default of
+`camel run`, or `camel.chaos.enabled=true`; never profile `prod`), these tools 
add faults while it runs and read what
+they did, the same as `camel cmd chaos-add`, `chaos-change`, `chaos-remove`, 
`chaos-clear` and `camel get chaos`. A
+rule is one line in the form of `camel.chaos.rules`, such as 
`endpoint=kafka:orders* error rate=0.1 until=5m`; see
+xref:next@components:others:chaos.adoc[Chaos]. Adding, changing, removing and 
clearing go through the `chaos` action of the
+CLI connector: a rule that is not valid is refused with the reason of the rule 
parser, and with chaos off every tool
+that changes rules is refused. The list and the report read the chaos console 
from the status of the application.
+
+[cols="2,1,1,3",options="header"]
+|===
+| Tool | `readOnlyHint` | `destructiveHint` | Description
+
+| `camel_runtime_chaos_list`
+| `true` | `false`
+| The current rules: id, version, the rule, state, where it fires, matches, 
faults fired, time left and the
+  expectation; or why chaos is off. Read from the status, which the 
application writes every second.
+
+| `camel_runtime_chaos_report`
+| `true` | `false`
+| The report: the verdict and whether it is final, the seed, every version of 
every rule, the expectations and the
+  counts per route.
+
+| `camel_runtime_chaos_add`
+| `false` | `true`
+| Adds a rule (`rule`) and returns its id (`r1`, `r2`, ...).
+
+| `camel_runtime_chaos_change`
+| `false` | `true`
+| Gives the rule with the `id` the new `rule` and one more version; the report 
keeps the version it replaces.
+
+| `camel_runtime_chaos_remove`
+| `false` | `false`
+| Removes the rule with the `id`; the report keeps what it did.
+
+| `camel_runtime_chaos_clear`
+| `false` | `false`
+| Removes all rules, and returns how many.
+|===
+
+Adding and changing a rule are marked destructive: a rule makes messages fail, 
wait or get lost, and its
+`exception=` can name any exception class. So a client that honours 
`destructiveHint` asks before it runs them, and
+the `read-write` access level of the security layer 
(`camel.mcp.security.access-level`) does not offer them, as it does
+not offer `camel_runtime_route_control` or `camel_runtime_sql`; `read-only` 
offers the list and the report. Removing
+and clearing only take faults away, and the report keeps the removed rules, so 
they are not destructive. `camel ask`
+has the same tools (`chaos_add` and the others), and asks the user before it 
adds or changes a rule. The development
+MCP tools that `camel run --mcp` serves over HTTP have only the list and the 
report: a rule is never added, changed or
+removed over the HTTP server of the application.
+
 ==== Tool groups for local models
 
 A client that drives a small local model pays for every tool schema on every 
request. Rather than listing every
@@ -1069,13 +1122,17 @@ runtime tool, it can offer the core tools plus the 
groups the selected applicati
 | `http`
 | Rest DSL services or platform-http endpoints, or a `platform-http`, `rest` 
or `rest-openapi` consumer
 | `camel_runtime_http_endpoints`, `camel_runtime_http_request`
+
+| `chaos`
+| `camel-chaos` (its console in the status), switched on or off; the guidance 
line says which
+| the six tools of <<_chaos>>; with chaos off only the list and the report
 |===
 
 The answer names the application and its pid, lists the `core` tools (the 
shared `camel_*` tools a small model
 should always get), each loaded group with its tools and one line of guidance 
for the system prompt (for example
 `SQL: datasource(s) orders (HikariCP), used by sql endpoints. Table names come 
from the SQL trace
 (camel_runtime_sql_trace); don't guess a schema.`), and the `signals` in the 
status that loaded each group. Its
-`fingerprint` stays the same as long as the groups, the datasources and the 
HTTP base URL do,
+`fingerprint` stays the same as long as the groups, the datasources, the HTTP 
base URL and chaos on or off do,
 so a client calls the tool when the user selects an application or the 
application reloads, and rebuilds its
 tool list and prompt only when the fingerprint changes; the prompt then stays 
the same and a local model keeps
 its prompt cache. A status written by an older Camel version may lack some of 
the keys, which just loads fewer
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc
index c2e13667d71c..6a8564782215 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-local-models.adoc
@@ -78,13 +78,18 @@ from the *SQL Trace* tab
 | `http` | Rest DSL services or platform-http endpoints, or a `platform-http`, 
`rest` or `rest-openapi` consumer
 | `tui_http_endpoints` (the served operations, optionally the OpenAPI 
contract) and `tui_http_request` (a request to
 the integration's own server on localhost, no other host), and a line with the 
base URL and the contract
+| `chaos` | `camel-chaos`, switched on or off
+| a line saying whether chaos is on, so failures and delays may be injected on 
purpose, with the rules in the `chaos`
+section of `tui_get_status`; the panel has no tool that changes the rules 
(`camel cmd chaos-add`, `camel ask` and the
+MCP server have them)
 |===
 
 Each loaded group adds one line at the end of the system prompt. The groups 
are read again only when you
 select another integration or the selected one reloads its routes (after a 
reload the groups only grow), so
 the tools and the prompt stay the same from question to question and Ollama 
keeps reusing the cached prompt.
 An integration without any group yet is read again on each question, since one 
that just started may not
-have written its status completely. With all four groups the static prefix 
grows by about 800 tokens.
+have written its status completely. With the `sql`, `tracing`, `resilience` 
and `http` groups the static prefix grows
+by about 800 tokens; the `chaos` group adds one line.
 
 The full tool set (`/tools full` and hosted providers) is not affected by the 
groups. `/tools` and `/context`
 show the loaded groups.
diff --git a/docs/user-manual/modules/ROOT/pages/security-model.adoc 
b/docs/user-manual/modules/ROOT/pages/security-model.adoc
index 155dd73b7ee8..322be0f6e27d 100644
--- a/docs/user-manual/modules/ROOT/pages/security-model.adoc
+++ b/docs/user-manual/modules/ROOT/pages/security-model.adoc
@@ -177,10 +177,10 @@ LLM. It keeps the same contract in a terminal:
   `camel ask` makes without a screen of the integration in front of the
   developer: stopping, killing or restarting the integration or its routes,
   running an integration, browsing an endpoint the integration does not have,
-  writing a heap dump, and a `camel` command that changes something, such as
-  `camel cmd chaos-add`, which turns on faults in the integration. The
-  commands that only read, such as `camel get` and `camel ps`, run without
-  asking.
+  writing a heap dump, adding or changing a chaos rule, and a `camel` command
+  that changes something, such as `camel cmd chaos-add`, which turns on faults
+  in the integration. The commands that only read, such as `camel get` and
+  `camel ps`, run without asking.
 * Without a terminal to ask on, such a call does not run, unless the
   developer passed `--yes`.
 
@@ -1836,10 +1836,12 @@ explicit-opt-in entry under _Out of scope_.
 | `camel.chaos.enabled=true` (`camel-chaos`)
 | unset (on with profile `dev`, never with profile `prod`)
 | Chaos is switched on, and its rules fail, delay, drop and corrupt messages 
on purpose. The rules come only from the
-  developer: `camel.chaos.rules` (marked `insecure:dev`), Java, and the `camel 
cmd chaos-*` commands over the CLI
-  connector; the `chaos` developer console only reads. The `exception=` of a 
rule names any public `Throwable` class
-  on the classpath, whose constructor runs when the fault fires. With profile 
`prod` chaos stays off, whatever
-  `camel.chaos.enabled` and `camel.chaos.rules` say.
+  developer: `camel.chaos.rules` (marked `insecure:dev`), Java, and the CLI 
connector, which the `camel cmd chaos-*`
+  commands and the chaos tools of `camel ask` and of the camel-jbang MCP 
server use. They are never added over the
+  HTTP server of the application: the `chaos` developer console and the 
development MCP tools of `camel run --mcp`
+  only list and report. The `exception=` of a rule names any public 
`Throwable` class on the classpath, whose
+  constructor runs when the fault fires. With profile `prod` chaos stays off, 
whatever `camel.chaos.enabled` and
+  `camel.chaos.rules` say.
 |===
 
 A machine-readable index of these variants, the entry-point trust levels, the
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
index 278ec3d11557..c4cd807ab021 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/FileCliConnectorTransport.java
@@ -18,6 +18,8 @@ package org.apache.camel.cli.connector;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
@@ -322,7 +324,11 @@ public class FileCliConnectorTransport extends 
ServiceSupport implements CliConn
             // even during termination then collect status as we want to see 
status changes during stopping
             JsonObject root = snapshots.status();
             LOG.trace("Updating status file: {}", statusFile);
-            IOHelper.writeText(root.toJson(), statusFile);
+            // written aside and moved in place: a reader must never find half 
a status
+            File temporary = new File(statusFile.getPath() + ".tmp");
+            IOHelper.writeText(root.toJson(), temporary);
+            Files.move(temporary.toPath(), statusFile.toPath(), 
StandardCopyOption.ATOMIC_MOVE,
+                    StandardCopyOption.REPLACE_EXISTING);
         } catch (Exception e) {
             // ignore
             LOG.trace("Error updating status file: {} due to: {}. This 
exception is ignored.",
@@ -454,6 +460,7 @@ public class FileCliConnectorTransport extends 
ServiceSupport implements CliConn
         }
         if (statusFile != null) {
             FileUtil.deleteFile(statusFile);
+            FileUtil.deleteFile(new File(statusFile.getPath() + ".tmp"));
         }
         if (actionFile != null) {
             FileUtil.deleteFile(actionFile);
diff --git 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportTest.java
 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportTest.java
index a76432df4f4b..2cfa85578d79 100644
--- 
a/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportTest.java
+++ 
b/dsl/camel-cli-connector/src/test/java/org/apache/camel/cli/connector/FileCliConnectorTransportTest.java
@@ -77,6 +77,18 @@ class FileCliConnectorTransportTest {
         System.setProperty("user.home", oldHome);
     }
 
+    @Test
+    void theStatusIsMovedInPlaceAndGoesWithItsTemporaryFile() {
+        File status = file(PID + "-status.json");
+        // written aside and moved in place, so every read finds a whole status
+        assertThat(awaitJson(status).toJson()).isEqualTo("{}");
+        assertThat(file(PID + "-status.json.tmp")).doesNotExist();
+
+        transport.stop();
+        assertThat(status).doesNotExist();
+        assertThat(file(PID + "-status.json.tmp")).doesNotExist();
+    }
+
     @Test
     void resultIsTheReply() {
         writeAction(action("result"), "r1");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
index 9e0aa0ce9da7..4cab9fb57077 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
@@ -59,16 +59,22 @@ public class AskTools {
             "catalog_component_doc", "catalog_eip_doc", 
"catalog_dataformat_doc", "catalog_language_doc");
 
     /** The shared tools that the tool groups (by their camel-jbang-mcp names) 
stand for. */
-    static final Map<String, String> GROUP_TOOLS = Map.of(
-            ToolGroups.SQL_TOOL, "execute_sql",
-            ToolGroups.DATASOURCES_TOOL, "get_datasources",
-            ToolGroups.SQL_TRACE_TOOL, "get_sql_trace",
-            ToolGroups.SPANS_TOOL, "get_spans",
-            ToolGroups.TRACE_TOOL, "trace_control",
-            ToolGroups.METRICS_TOOL, "get_metrics",
-            ToolGroups.CIRCUIT_BREAKERS_TOOL, "get_circuit_breakers",
-            ToolGroups.HTTP_ENDPOINTS_TOOL, "get_http_endpoints",
-            ToolGroups.HTTP_REQUEST_TOOL, "http_request");
+    static final Map<String, String> GROUP_TOOLS = Map.ofEntries(
+            Map.entry(ToolGroups.SQL_TOOL, "execute_sql"),
+            Map.entry(ToolGroups.DATASOURCES_TOOL, "get_datasources"),
+            Map.entry(ToolGroups.SQL_TRACE_TOOL, "get_sql_trace"),
+            Map.entry(ToolGroups.SPANS_TOOL, "get_spans"),
+            Map.entry(ToolGroups.TRACE_TOOL, "trace_control"),
+            Map.entry(ToolGroups.METRICS_TOOL, "get_metrics"),
+            Map.entry(ToolGroups.CIRCUIT_BREAKERS_TOOL, 
"get_circuit_breakers"),
+            Map.entry(ToolGroups.HTTP_ENDPOINTS_TOOL, "get_http_endpoints"),
+            Map.entry(ToolGroups.HTTP_REQUEST_TOOL, "http_request"),
+            Map.entry(ToolGroups.CHAOS_LIST_TOOL, "chaos_list"),
+            Map.entry(ToolGroups.CHAOS_REPORT_TOOL, "chaos_report"),
+            Map.entry(ToolGroups.CHAOS_ADD_TOOL, "chaos_add"),
+            Map.entry(ToolGroups.CHAOS_CHANGE_TOOL, "chaos_change"),
+            Map.entry(ToolGroups.CHAOS_REMOVE_TOOL, "chaos_remove"),
+            Map.entry(ToolGroups.CHAOS_CLEAR_TOOL, "chaos_clear"));
 
     private static final String CLI_LIST_COMMANDS = "cli_list_commands";
     private static final String CLI_COMMAND_HELP = "cli_command_help";
@@ -240,7 +246,9 @@ public class AskTools {
         return names;
     }
 
-    /** The shared tools of the tool groups the selected integration calls for 
(SQL, tracing, resilience, HTTP). */
+    /**
+     * The shared tools of the tool groups the selected integration calls for 
(SQL, tracing, resilience, HTTP, chaos).
+     */
     private List<String> groupTools() {
         return ToolGroups.select(features()).mcpTools().stream()
                 .map(GROUP_TOOLS::get)
@@ -255,7 +263,10 @@ public class AskTools {
 
     static String sharedToolNames(String text) {
         String answer = text;
-        for (Map.Entry<String, String> e : GROUP_TOOLS.entrySet()) {
+        // the longest names first: camel_runtime_sql is the start of 
camel_runtime_sql_trace
+        List<Map.Entry<String, String>> entries = new 
ArrayList<>(GROUP_TOOLS.entrySet());
+        entries.sort((a, b) -> b.getKey().length() - a.getKey().length());
+        for (Map.Entry<String, String> e : entries) {
             answer = answer.replace(e.getKey(), e.getValue());
         }
         return answer;
@@ -340,6 +351,8 @@ public class AskTools {
                     ? args.get("files") : args.getOrDefault("directory", 
ctx.defaultDirectory().toString()));
             case "write_heap_dump" -> ToolConfirms.heapDump(
                     args.get("name") != null ? args.get("name") : 
processName());
+            case "chaos_add", "chaos_change" -> 
ToolConfirms.chaos(name.substring("chaos_".length()),
+                    args.get("name") != null ? args.get("name") : 
processName(), args.get("id"), args.get("rule"));
             default -> null;
         };
     }
@@ -395,9 +408,9 @@ public class AskTools {
             sb.append(" and cli_command_help find a command, cli_exec runs 
it.\n\n");
         }
         sb.append("Some calls ask the user first: a file write (the user sees 
the change), a message to an endpoint the");
-        sb.append(" integration does not have, SQL that changes data, an 
expression that runs code, stopping or");
-        sb.append(" restarting the integration, and a camel command that 
changes something. A call the user did not");
-        sb.append(" confirm did not run.\n\n");
+        sb.append(" integration does not have, SQL that changes data, an 
expression that runs code, adding or changing");
+        sb.append(" a chaos rule, stopping or restarting the integration, and 
a camel command that changes something.");
+        sb.append(" A call the user did not confirm did not run.\n\n");
 
         sb.append("Guidelines:\n");
         AuthoringGuidelines.lines().forEach(line -> sb.append("- 
").append(line).append('\n'));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
index ab43a90256f7..d21a0926b678 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeatures.java
@@ -102,6 +102,20 @@ public record AppFeatures(
         return signals.containsKey("semantic-monitoring");
     }
 
+    /**
+     * Whether the integration has camel-chaos: the CLI connector puts the 
chaos console in its status, with chaos
+     * switched on or off.
+     */
+    public boolean chaos() {
+        return signals.containsKey("chaos");
+    }
+
+    /** Whether chaos is switched on in the integration: profile dev or 
camel.chaos.enabled=true, never profile prod. */
+    public boolean chaosOn() {
+        String chaos = signals.get("chaos");
+        return chaos != null && chaos.startsWith("on");
+    }
+
     /** Whether the integration serves HTTP: platform-http or Rest DSL 
endpoints. */
     public boolean httpServed() {
         return http != null;
@@ -282,6 +296,12 @@ public record AppFeatures(
             http.signals().forEach(signals::putIfAbsent);
         }
 
+        // camel-chaos: its console, switched on (with the current rules) or 
off
+        if (status.get("chaos") instanceof Map<?, ?> chaos) {
+            boolean on = Boolean.TRUE.equals(chaos.get("enabled")) || 
"true".equals(text(chaos.get("enabled")));
+            signals.put("chaos", on ? "on, " + list(chaos.get("rules")).size() 
+ " rule(s)" : "off");
+        }
+
         return new AppFeatures(
                 new ArrayList<>(dataSources.values()), new 
ArrayList<>(components), sqlTraced,
                 breakerProcessor || breakers, new ArrayList<>(breakerRoutes), 
otel, tracing, micrometer, signals, http);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 096fc24197be..884a3e7093c4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -2709,7 +2709,7 @@ public final class AuthoringTools {
         return null;
     }
 
-    private static String required(Map<String, String> args, String key) {
+    static String required(Map<String, String> args, String key) {
         String value = args.get(key);
         if (value == null || value.isBlank()) {
             throw new ToolExecutionException(key + " is required");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
index 019b506a08c2..855e184a08fe 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirms.java
@@ -43,7 +43,9 @@ import org.apache.camel.util.URISupport;
  * catalog marks internal (direct:, seda:, log:, mock: and the like) stay 
inside it. Also a message whose body the
  * integration reads from a file outside the project folder (a body that 
starts with file:);</li>
  * <li>a SQL statement that is not a single SELECT (or SHOW, DESCRIBE), and a 
row update;</li>
- * <li>an expression in a language that runs code, such as Groovy.</li>
+ * <li>an expression in a language that runs code, such as Groovy;</li>
+ * <li>a chaos rule added or changed (camel-chaos): it makes messages of the 
integration fail, wait or get lost, and can
+ * name any exception class. Removing a rule only takes faults away.</li>
  * </ul>
  * A file write is confirmed with its diff ({@link #write}). {@code camel 
ask}, which shows no screen of the integration
  * while it works, also asks before a call that stops or starts an 
integration, browses an endpoint the integration does
@@ -287,6 +289,28 @@ public final class ToolConfirms {
         };
     }
 
+    /**
+     * A chaos tool that adds or changes a rule (camel-chaos), with the rule; 
null for the others (list, report, remove,
+     * clear), which only read or take faults away, and without a rule (or the 
id of a change), which the tool refuses.
+     *
+     * @param command     add, change, remove or clear
+     * @param integration the name of the integration, or null for the 
selected one
+     * @param id          the id of the rule a change replaces
+     * @param rule        the rule in its one-line text form
+     */
+    public static Call chaos(String command, String integration, String id, 
String rule) {
+        String c = command == null || rule == null || rule.isBlank() ? "" : 
command.strip().toLowerCase(Locale.ROOT);
+        String where = integration != null && !integration.isBlank() ? 
integration : "the selected integration";
+        String reason = "While the rule is active, the integration's messages 
fail, wait or get lost; exception= can name"
+                        + " any exception class";
+        return switch (c) {
+            case "add" -> new Call("add a chaos rule to " + where, rule, 
reason);
+            case "change" -> id != null && !id.isBlank()
+                    ? new Call("change chaos rule " + id + " of " + where, 
rule, reason) : null;
+            default -> null;
+        };
+    }
+
     /** Running an integration on this machine. */
     public static Call run(String what) {
         return new Call("run an integration", what, "It starts an integration 
on this machine with the project's code");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
index 63314dd8686d..010fee0e3bc9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroup.java
@@ -31,7 +31,9 @@ public enum ToolGroup {
     /** The HTTP endpoints the integration serves, and a request to them 
(CAMEL-25307). */
     HTTP("http"),
     /** Retained semantic evaluation and route-decision evidence. */
-    SEMANTIC("semantic");
+    SEMANTIC("semantic"),
+    /** The rules and the report of camel-chaos, for an integration that has 
it (development only). */
+    CHAOS("chaos");
 
     private final String id;
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
index 9bc0cfe9c36c..6aaf456178f5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroups.java
@@ -22,10 +22,10 @@ import java.util.stream.Collectors;
 
 /**
  * Picks the runtime tool groups for an integration from its {@link 
AppFeatures} (CAMEL-24834). A small local model pays
- * for every tool schema on every request, so the SQL, tracing, resilience and 
HTTP tools only load when the integration
- * has a database, tracing, circuit breakers or HTTP endpoints; each loaded 
group adds one line of guidance that names
- * what the integration has and which tool reads it. The tool names and the 
wording here are those of the MCP server;
- * the TUI maps the same groups to its own tools.
+ * for every tool schema on every request, so the SQL, tracing, resilience, 
HTTP and chaos tools only load when the
+ * integration has a database, tracing, circuit breakers, HTTP endpoints or 
camel-chaos; each loaded group adds one line
+ * of guidance that names what the integration has and which tool reads it. 
The tool names and the wording here are
+ * those of the MCP server; the TUI maps the same groups to its own tools.
  */
 public final class ToolGroups {
 
@@ -38,6 +38,12 @@ public final class ToolGroups {
     public static final String CIRCUIT_BREAKERS_TOOL = 
"camel_runtime_circuit_breakers";
     public static final String HTTP_ENDPOINTS_TOOL = 
"camel_runtime_http_endpoints";
     public static final String HTTP_REQUEST_TOOL = 
"camel_runtime_http_request";
+    public static final String CHAOS_LIST_TOOL = "camel_runtime_chaos_list";
+    public static final String CHAOS_REPORT_TOOL = 
"camel_runtime_chaos_report";
+    public static final String CHAOS_ADD_TOOL = "camel_runtime_chaos_add";
+    public static final String CHAOS_CHANGE_TOOL = 
"camel_runtime_chaos_change";
+    public static final String CHAOS_REMOVE_TOOL = 
"camel_runtime_chaos_remove";
+    public static final String CHAOS_CLEAR_TOOL = "camel_runtime_chaos_clear";
 
     /**
      * A loaded group.
@@ -128,6 +134,14 @@ public final class ToolGroups {
                         group, List.of(HTTP_ENDPOINTS_TOOL, HTTP_REQUEST_TOOL),
                         "HTTP: " + describeHttp(f) + "; " + HTTP_REQUEST_TOOL 
+ " calls it, " + HTTP_ENDPOINTS_TOOL
                                                                                
 + " lists the operations."));
+                // with chaos off the other four can only be refused
+                case CHAOS -> groups.add(new Group(
+                        group,
+                        f.chaosOn()
+                                ? List.of(CHAOS_LIST_TOOL, CHAOS_REPORT_TOOL, 
CHAOS_ADD_TOOL, CHAOS_CHANGE_TOOL,
+                                        CHAOS_REMOVE_TOOL, CHAOS_CLEAR_TOOL)
+                                : List.of(CHAOS_LIST_TOOL, CHAOS_REPORT_TOOL),
+                        describeChaos(f)));
             }
         }
         return new Selection(groups, fingerprint(groups(f), f));
@@ -151,12 +165,15 @@ public final class ToolGroups {
         if (features.semanticAudit() || features.semanticMonitoring()) {
             groups.add(ToolGroup.SEMANTIC);
         }
+        if (features.chaos()) {
+            groups.add(ToolGroup.CHAOS);
+        }
         return groups;
     }
 
     /**
-     * Sorted group ids and datasource names, and the HTTP base URL when the 
integration serves HTTP: the same
-     * integration gives the same fingerprint, whatever order its status lists 
things in.
+     * Sorted group ids and datasource names, the HTTP base URL when the 
integration serves HTTP, and whether chaos is
+     * off: the same integration gives the same fingerprint, whatever order 
its status lists things in.
      */
     static String fingerprint(List<ToolGroup> groups, AppFeatures features) {
         String ids = 
groups.stream().map(ToolGroup::id).sorted().collect(Collectors.joining(","));
@@ -166,6 +183,10 @@ public final class ToolGroups {
             String url = features.http().baseUrl();
             fp += "|" + (url != null ? url : "");
         }
+        if (groups.contains(ToolGroup.CHAOS) && !features.chaosOn()) {
+            // fewer chaos tools
+            fp += "|chaos off";
+        }
         return fp;
     }
 
@@ -204,6 +225,20 @@ public final class ToolGroups {
         return sb.toString();
     }
 
+    /**
+     * The guidance of the chaos group: whether chaos is on, the tools that 
read the rules and the report, and the tool
+     * that adds a fault, with the rule form.
+     */
+    public static String describeChaos(AppFeatures features) {
+        if (!features.chaosOn()) {
+            return "Chaos: camel-chaos is off (profile dev or 
camel.chaos.enabled=true switches it on); "
+                   + CHAOS_LIST_TOOL + " says why.";
+        }
+        return "Chaos: camel-chaos is on, so failures may be injected on 
purpose; " + CHAOS_LIST_TOOL + " and "
+               + CHAOS_REPORT_TOOL + " show the rules and what they did; " + 
CHAOS_ADD_TOOL
+               + " takes a rule such as endpoint=kafka:orders* error rate=0.1 
until=5m.";
+    }
+
     /** {@code Circuit breakers in routes a, b}, or without the routes when 
they are not known. */
     public static String describeBreakers(AppFeatures features) {
         return features.circuitBreakerRoutes().isEmpty()
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index b6624e5d0dc1..9318c3de22ce 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -59,6 +59,7 @@ public final class ToolRegistry {
         registerRuntimeStatusTools();
         registerRuntimeActionTools();
         registerDevConsoleTools();
+        registerChaosTools();
         registerAnalysisTools();
         registerCatalogTools();
         registerExampleTools();
@@ -72,6 +73,15 @@ public final class ToolRegistry {
         return Collections.unmodifiableList(TOOLS);
     }
 
+    /**
+     * The tools that a server over HTTP offers: the development MCP server of 
{@code camel run --mcp}
+     * ({@code JbangDevMcpServer}), which has no hints and does not ask. All 
but the chaos tools that change the rules:
+     * over HTTP chaos only lists and reports, as its developer console does 
(design/chaos.adoc).
+     */
+    public static List<ToolDescriptor> httpTools() {
+        return TOOLS.stream().filter(t -> !t.name().startsWith("chaos_") || 
t.isReadOnly()).toList();
+    }
+
     public static ToolDescriptor findTool(String name) {
         return BY_NAME.get(name);
     }
@@ -483,9 +493,10 @@ public final class ToolRegistry {
                 }));
 
         register(tool("get_tool_groups",
-                "Which runtime tool groups (sql, tracing, resilience, http) 
the integration needs, from what it has: "
-                                         + "datasources and SQL endpoints, 
OpenTelemetry, message tracing, Micrometer, "
-                                         + "circuit breakers, HTTP endpoints. 
Returns the tools of each group with one line of guidance, "
+                "Which runtime tool groups (sql, tracing, resilience, http, 
chaos) the integration needs, from what it "
+                                         + "has: datasources and SQL 
endpoints, OpenTelemetry, message tracing, "
+                                         + "Micrometer, circuit breakers, HTTP 
endpoints, camel-chaos. Returns the tools of "
+                                         + "each group with one line of 
guidance, "
                                          + "and a fingerprint that changes 
only when the groups do.")
                 .param("name", "string", "Name or PID of the integration 
(default: the selected or only one)", false)
                 .executor((ctx, args) -> {
@@ -686,6 +697,129 @@ public final class ToolRegistry {
                 .executor((ctx, args) -> ctx.readStatus("micrometer")));
     }
 
+    // ---- camel-chaos: the rules, the report and the chaos action of the CLI 
connector (design/chaos.adoc) ----
+
+    private static final String CHAOS_NAME = "Name or PID of the integration 
(default: the selected or only one)";
+
+    /**
+     * The chaos tools, the six operations of camel cmd chaos-add, 
chaos-change, chaos-remove, chaos-clear and camel get
+     * chaos [--report]. The list and the report read the chaos console in the 
status of the integration; the others
+     * send the chaos action of the CLI connector, which works only when chaos 
is switched on and answers a rule that is
+     * not valid with the reason of the parser of camel.chaos.rules. Adding 
and changing a rule is destructive: a rule
+     * makes messages fail, wait or get lost, and exception= can name any 
exception class. Removing and clearing only
+     * take faults away, and the report keeps the removed rules.
+     */
+    private static void registerChaosTools() {
+        register(tool("chaos_list",
+                "List the chaos rules of the integration (camel-chaos, 
development only): id, version, the rule, state, "
+                                    + "where it fires, matches, faults fired, 
time left and the expectation; or why "
+                                    + "chaos is off.")
+                .param("name", "string", CHAOS_NAME, false)
+                .executor((ctx, args) -> {
+                    selectNamedProcess(ctx, args);
+                    return chaosSection(ctx, "rules").toJson();
+                }));
+
+        register(tool("chaos_report",
+                "Get the chaos report of the integration (camel-chaos): the 
verdict (PASS or FAIL) and whether it is "
+                                      + "final, the seed, every version of 
every rule with what it fired, the "
+                                      + "expectations and the counts per 
route; or why chaos is off.")
+                .param("name", "string", CHAOS_NAME, false)
+                .executor((ctx, args) -> {
+                    selectNamedProcess(ctx, args);
+                    return chaosSection(ctx, "report").toJson();
+                }));
+
+        register(tool("chaos_add",
+                "Add a chaos rule to the integration, in the one-line form of 
camel.chaos.rules, e.g. "
+                                   + "'endpoint=kafka:orders* error rate=0.1 
until=5m'. Returns the id of the rule. "
+                                   + "Chaos must be on in the integration; a 
rule that is not valid is refused with "
+                                   + "the reason.")
+                .param("rule", "string", "The rule in its one-line text form, 
as one entry of camel.chaos.rules", true)
+                .param("name", "string", CHAOS_NAME, false)
+                .readOnly(false).destructive(true)
+                .executor((ctx, args) -> {
+                    String rule = AuthoringTools.required(args, "rule");
+                    selectNamedProcess(ctx, args);
+                    return chaos(ctx, "add", null, rule);
+                }));
+
+        register(tool("chaos_change",
+                "Change a chaos rule of the integration: the rule with the id 
gets the new one-line rule and one more "
+                                      + "version; the report keeps the version 
it replaces. Returns the id and the new "
+                                      + "version.")
+                .param("id", "string", "The id of the rule, such as r1 
(chaos_list shows them)", true)
+                .param("rule", "string", "The new rule in its one-line text 
form", true)
+                .param("name", "string", CHAOS_NAME, false)
+                .readOnly(false).destructive(true)
+                .executor((ctx, args) -> {
+                    String id = AuthoringTools.required(args, "id");
+                    String rule = AuthoringTools.required(args, "rule");
+                    selectNamedProcess(ctx, args);
+                    return chaos(ctx, "change", id, rule);
+                }));
+
+        register(tool("chaos_remove",
+                "Remove a chaos rule of the integration: its faults stop, and 
the report keeps what it did.")
+                .param("id", "string", "The id of the rule, such as r1 
(chaos_list shows them)", true)
+                .param("name", "string", CHAOS_NAME, false)
+                .readOnly(false).destructive(false)
+                .executor((ctx, args) -> {
+                    String id = AuthoringTools.required(args, "id");
+                    selectNamedProcess(ctx, args);
+                    return chaos(ctx, "remove", id, null);
+                }));
+
+        register(tool("chaos_clear",
+                "Remove all chaos rules of the integration: no more faults, 
and the report keeps what they did. "
+                                     + "Returns how many were removed.")
+                .param("name", "string", CHAOS_NAME, false)
+                .readOnly(false).destructive(false)
+                .executor((ctx, args) -> {
+                    selectNamedProcess(ctx, args);
+                    return chaos(ctx, "clear", null, null);
+                }));
+    }
+
+    /** Sends the chaos action, and returns the answer of the integration; a 
refusal is an error with its reason. */
+    private static String chaos(ToolContext ctx, String command, String id, 
String rule) {
+        return ctx.executeAction("chaos", root -> {
+            root.put("command", command);
+            if (id != null) {
+                root.put("id", id);
+            }
+            if (rule != null) {
+                root.put("rule", rule);
+            }
+        });
+    }
+
+    /**
+     * Whether chaos is on, and the rules or the report from the chaos console 
in the status of the integration; or why
+     * chaos is off.
+     */
+    static JsonObject chaosSection(ToolContext ctx, String key) {
+        JsonObject status = ctx.readFullStatus();
+        if (status == null) {
+            throw new ToolExecutionException("No status from the integration 
with pid " + ctx.pid() + " yet");
+        }
+        if (!(status.get("chaos") instanceof Map<?, ?> chaos)) {
+            throw new ToolExecutionException(
+                    "camel-chaos is not on the classpath of the integration 
with pid " + ctx.pid()
+                                             + ": run it with camel run 
--chaos, or add the camel-chaos dependency");
+        }
+        JsonObject answer = new JsonObject();
+        boolean on = Boolean.TRUE.equals(chaos.get("enabled")) || 
"true".equals(String.valueOf(chaos.get("enabled")));
+        answer.put("enabled", on);
+        if (on) {
+            Object value = chaos.get(key);
+            answer.put(key, value != null ? value : "rules".equals(key) ? new 
JsonArray() : new JsonObject());
+        } else {
+            answer.put("reason", chaos.get("reason"));
+        }
+        return answer;
+    }
+
     @SuppressWarnings("unchecked")
     private static void collectCircuitBreakers(JsonObject root, String 
sectionKey, JsonArray target) {
         Object section = root.get(sectionKey);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
index 242fdbe6fe70..617516b29b66 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
@@ -23,7 +23,10 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolConfirms;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolGroups;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -207,4 +210,50 @@ class AskToolsTest {
                 .contains("Some calls ask the user first").contains("\nThe 
integration:\n- SQL: one datasource\n")
                 .doesNotContain("stop_route").doesNotContain("do not use 
markdown");
     }
+
+    @Test
+    void addingOrChangingAChaosRuleIsAskedFirst() {
+        List<ToolConfirms.Call> asked = new ArrayList<>();
+        AskTools tools = new AskTools(-1, call -> {
+            asked.add(call);
+            return false;
+        });
+
+        String answer = tools.executeTool("chaos_add",
+                new JsonObject(Map.of("rule", "endpoint=kafka:* error 
rate=0.5", "name", "orders")));
+
+        // not confirmed, so it did not run
+        assertThat(answer).contains("\"status\":\"rejected\"");
+        assertThat(asked).hasSize(1);
+        assertThat(asked.get(0).title()).isEqualTo("add a chaos rule to 
orders");
+        assertThat(asked.get(0).target()).isEqualTo("endpoint=kafka:* error 
rate=0.5");
+
+        ToolConfirms.Call change = tools.confirmation("chaos_change", 
Map.of("id", "r1", "rule", "endpoint=* drop"));
+        assertThat(change.title()).isEqualTo("change chaos rule r1 of the 
selected integration");
+        assertThat(change.target()).isEqualTo("endpoint=* drop");
+        // the others only read or take faults away
+        for (String name : List.of("chaos_remove", "chaos_clear", 
"chaos_list", "chaos_report")) {
+            assertThat(tools.confirmation(name, Map.of("id", 
"r1"))).as(name).isNull();
+        }
+    }
+
+    @Test
+    void theChaosGroupNamesTheSharedTools() {
+        AppFeatures chaos = AppFeatures.fromStatus(new 
JsonObject(Map.of("chaos", Map.of("enabled", true))));
+        for (String mcpTool : ToolGroups.select(chaos).mcpTools()) {
+            // camel_runtime_chaos_add is chaos_add, and so on
+            String shared = AskTools.GROUP_TOOLS.get(mcpTool);
+            
assertThat(shared).as(mcpTool).isEqualTo(mcpTool.substring("camel_runtime_".length()));
+            assertThat(ToolRegistry.findTool(shared)).as(shared).isNotNull();
+        }
+        assertThat(AskTools.sharedToolNames(ToolGroups.describeChaos(chaos)))
+                .contains("chaos_list and chaos_report show the 
rules").doesNotContain("camel_runtime_");
+    }
+
+    @Test
+    void aGroupToolThatStartsWithTheNameOfAnotherIsNamedRight() {
+        // camel_runtime_sql is the start of camel_runtime_sql_trace: the 
longer name is replaced first
+        assertThat(AskTools.sharedToolNames("camel_runtime_sql_trace and 
camel_runtime_sql"))
+                .isEqualTo("get_sql_trace and execute_sql");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
index 949f5464748d..7ef19a554dd0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AppFeaturesTest.java
@@ -177,4 +177,25 @@ class AppFeaturesTest {
         assertFalse(f.httpServed());
         assertTrue(ToolGroups.groups(f).isEmpty());
     }
+
+    @Test
+    void theChaosConsoleIsTheChaosGroupOnOrOff() throws Exception {
+        AppFeatures on = features("{'chaos': {'enabled': true, 'rules': 
[{'id': 'r1'}, {'id': 'r2'}], 'report': {}}}");
+        assertTrue(on.chaos());
+        assertTrue(on.chaosOn());
+        assertEquals("on, 2 rule(s)", on.signals().get("chaos"));
+        assertEquals(List.of(ToolGroup.CHAOS), ToolGroups.groups(on));
+
+        AppFeatures off = features("{'chaos': {'enabled': false, 'reason': 
'Chaos is off'}}");
+        assertTrue(off.chaos());
+        assertFalse(off.chaosOn());
+        assertEquals("off", off.signals().get("chaos"));
+        assertEquals(List.of(ToolGroup.CHAOS), ToolGroups.groups(off));
+
+        // without camel-chaos the connector writes no chaos console
+        assertFalse(features("{'context': {'name': 'shop'}}").chaos());
+        assertFalse(AppFeatures.none().chaos());
+        // the newer status wins: chaos switched off after a reload
+        assertFalse(on.merge(off).chaosOn());
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
index 95dc86b797df..63e11ca544ca 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolConfirmsTest.java
@@ -284,4 +284,24 @@ class ToolConfirmsTest {
         assertThat(ToolConfirms.browse("file:/Users/me/.ssh", 
ENDPOINTS).title())
                 .isEqualTo("browse an endpoint the integration does not have");
     }
+
+    @Test
+    void addingOrChangingAChaosRuleIsAskedWithTheRule() {
+        ToolConfirms.Call add = ToolConfirms.chaos("add", "orders", null, 
"endpoint=* error exception=java.sql.SQLException");
+        assertThat(add.title()).isEqualTo("add a chaos rule to orders");
+        assertThat(add.target()).isEqualTo("endpoint=* error 
exception=java.sql.SQLException");
+        assertThat(add.reason()).contains("fail, wait or get lost", 
"exception=");
+
+        ToolConfirms.Call change = ToolConfirms.chaos("change", null, "r1", 
"endpoint=* drop");
+        assertThat(change.title()).isEqualTo("change chaos rule r1 of the 
selected integration");
+        assertThat(change.target()).isEqualTo("endpoint=* drop");
+
+        // removing and clearing only take faults away
+        assertThat(ToolConfirms.chaos("remove", "orders", "r1", 
null)).isNull();
+        assertThat(ToolConfirms.chaos("clear", "orders", null, null)).isNull();
+        assertThat(ToolConfirms.chaos(null, "orders", null, null)).isNull();
+        // without a rule the tool refuses the call, so there is nothing to ask
+        assertThat(ToolConfirms.chaos("add", "orders", null, " ")).isNull();
+        assertThat(ToolConfirms.chaos("change", "orders", null, "endpoint=* 
drop")).isNull();
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
index 8570682716f6..f2830b7b4d4a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolGroupsTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -141,4 +142,35 @@ class ToolGroupsTest {
         assertEquals(List.of(ToolGroup.SQL, ToolGroup.TRACING, 
ToolGroup.RESILIENCE, ToolGroup.HTTP), s.toolGroups());
         
assertEquals("http,resilience,sql,tracing|audit,orders|http://localhost:8080/api";,
 s.fingerprint());
     }
+
+    @Test
+    void chaosLoadsItsSixToolsWhenOnAndOnlyTheReadsWhenOff() {
+        AppFeatures on = AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", true))));
+        ToolGroups.Selection s = ToolGroups.select(on);
+        assertEquals(List.of(ToolGroup.CHAOS), s.toolGroups());
+        assertEquals(List.of("camel_runtime_chaos_list", 
"camel_runtime_chaos_report", "camel_runtime_chaos_add",
+                "camel_runtime_chaos_change", "camel_runtime_chaos_remove", 
"camel_runtime_chaos_clear"), s.mcpTools());
+        assertEquals("Chaos: camel-chaos is on, so failures may be injected on 
purpose; camel_runtime_chaos_list and"
+                     + " camel_runtime_chaos_report show the rules and what 
they did; camel_runtime_chaos_add takes a rule"
+                     + " such as endpoint=kafka:orders* error rate=0.1 
until=5m.",
+                s.guidance().get(0));
+        assertEquals("chaos|", s.fingerprint());
+
+        AppFeatures off = AppFeatures.fromStatus(new 
JsonObject(Map.of("chaos", Map.of("enabled", false))));
+        ToolGroups.Selection o = ToolGroups.select(off);
+        assertEquals("Chaos: camel-chaos is off (profile dev or 
camel.chaos.enabled=true switches it on);"
+                     + " camel_runtime_chaos_list says why.",
+                o.guidance().get(0));
+        // chaos on or off is fixed when the integration starts: off, the 
other four could only be refused
+        assertEquals(List.of("camel_runtime_chaos_list", 
"camel_runtime_chaos_report"), o.mcpTools());
+        assertEquals("chaos||chaos off", o.fingerprint());
+    }
+
+    @Test
+    void chaosComesLastSoTheOtherGroupsKeepTheirPlace() {
+        AppFeatures chaos = AppFeatures.fromStatus(new 
JsonObject(Map.of("chaos", Map.of("enabled", true))));
+        ToolGroups.Selection s = 
ToolGroups.select(everything().withHttp(stockApi(8080)).merge(chaos));
+        assertEquals(List.of(ToolGroup.SQL, ToolGroup.TRACING, 
ToolGroup.RESILIENCE, ToolGroup.HTTP, ToolGroup.CHAOS),
+                s.toolGroups());
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
index 6ec6d1b27a44..10d7185c5b9e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
@@ -312,4 +312,78 @@ class ToolRegistryTest {
         assertEquals(3, step.get("bodySize"));
         assertNull(step.get("message"), "no bodies, headers or properties in 
the summary");
     }
+
+    @Test
+    void theChaosToolsHaveTheMarksOfTheMcpTools() {
+        // the same marks as camel_runtime_chaos_* (design/chaos.adoc, 
"Control")
+        for (String name : List.of("chaos_list", "chaos_report")) {
+            ToolDescriptor t = ToolRegistry.findTool(name);
+            assertTrue(t.isReadOnly(), name);
+            assertFalse(t.isDestructive(), name);
+        }
+        for (String name : List.of("chaos_add", "chaos_change")) {
+            ToolDescriptor t = ToolRegistry.findTool(name);
+            assertFalse(t.isReadOnly(), name);
+            assertTrue(t.isDestructive(), name);
+        }
+        for (String name : List.of("chaos_remove", "chaos_clear")) {
+            ToolDescriptor t = ToolRegistry.findTool(name);
+            assertFalse(t.isReadOnly(), name);
+            assertFalse(t.isDestructive(), name);
+        }
+    }
+
+    @Test
+    void theChaosToolsCheckTheirArgumentsBeforeTheyReachAnIntegration() {
+        // nothing is selected: an argument that is missing is said first
+        assertEquals("rule is required", 
assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("chaos_add", new ToolContext(), 
Map.of())).getMessage());
+        assertEquals("id is required", 
assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("chaos_change", new ToolContext(), 
Map.of("rule", "endpoint=* drop")))
+                .getMessage());
+        assertEquals("rule is required", 
assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("chaos_change", new ToolContext(), 
Map.of("id", "r1", "rule", " ")))
+                .getMessage());
+        assertEquals("id is required", 
assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("chaos_remove", new ToolContext(), 
Map.of())).getMessage());
+    }
+
+    @Test
+    void theChaosListAndReportReadTheChaosConsoleOfTheStatus() throws 
Exception {
+        JsonObject[] status = new JsonObject[1];
+        ToolContext ctx = new ToolContext() {
+            @Override
+            public JsonObject readFullStatus() {
+                return status[0];
+            }
+        };
+        ctx.selectProcess(4711);
+
+        status[0] = (JsonObject) 
Jsoner.deserialize("{\"context\":{\"name\":\"shop\"}}");
+        assertTrue(assertThrows(ToolExecutionException.class, () -> 
ToolRegistry.chaosSection(ctx, "rules"))
+                .getMessage().startsWith("camel-chaos is not on the classpath 
of the integration with pid 4711"));
+
+        status[0] = (JsonObject) 
Jsoner.deserialize("{\"chaos\":{\"enabled\":false,\"reason\":\"Chaos is 
off\"}}");
+        assertEquals("{\"enabled\":false,\"reason\":\"Chaos is off\"}", 
ToolRegistry.chaosSection(ctx, "report").toJson());
+
+        status[0] = (JsonObject) Jsoner.deserialize(
+                
"{\"chaos\":{\"enabled\":true,\"rules\":[{\"id\":\"r1\"}],\"report\":{\"verdict\":\"PASS\"}}}");
+        assertEquals("{\"enabled\":true,\"rules\":[{\"id\":\"r1\"}]}", 
ToolRegistry.chaosSection(ctx, "rules").toJson());
+        assertEquals("{\"enabled\":true,\"report\":{\"verdict\":\"PASS\"}}",
+                ToolRegistry.chaosSection(ctx, "report").toJson());
+
+        status[0] = null;
+        assertTrue(assertThrows(ToolExecutionException.class, () -> 
ToolRegistry.chaosSection(ctx, "rules"))
+                .getMessage().startsWith("No status from the integration with 
pid 4711"));
+    }
+
+    @Test
+    void overHttpChaosOnlyListsAndReports() {
+        List<String> http = 
ToolRegistry.httpTools().stream().map(ToolDescriptor::name).toList();
+        List<String> all = 
ToolRegistry.allTools().stream().map(ToolDescriptor::name).toList();
+        List<String> left = all.stream().filter(n -> 
!http.contains(n)).toList();
+        // only the chaos tools that change the rules stay off HTTP; the other 
tools keep what they offered
+        assertEquals(List.of("chaos_add", "chaos_change", "chaos_remove", 
"chaos_clear"), left);
+        assertTrue(http.containsAll(List.of("chaos_list", "chaos_report", 
"stop_route", "execute_sql")));
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/JbangDevMcpServerTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/JbangDevMcpServerTest.java
index 36a65375ce00..99e8e6590248 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/JbangDevMcpServerTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/JbangDevMcpServerTest.java
@@ -74,7 +74,10 @@ class JbangDevMcpServerTest {
 
             assertThat(client.listTools().tools())
                     .extracting(McpSchema.Tool::name)
-                    .contains(ToolRegistry.allTools().get(0).name());
+                    .contains(ToolRegistry.allTools().get(0).name())
+                    // over HTTP chaos only lists and reports: a rule is never 
added, changed or removed over HTTP
+                    .contains("chaos_list", "chaos_report")
+                    .doesNotContain("chaos_add", "chaos_change", 
"chaos_remove", "chaos_clear");
 
             McpSchema.Tool parameterizedTool = 
client.listTools().tools().stream()
                     .filter(t -> "select_process".equals(t.name()))
diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml 
b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
index 1f8e127949e1..19240eb3ca0a 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml
@@ -166,11 +166,23 @@
             <artifactId>camel-xml-io-dsl</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <!-- the chaos tools against a running integration: camel-chaos 
with the CLI connector -->
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-chaos</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-junit</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 55397fa7fb88..bd875f235af4 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -359,9 +359,10 @@ public class RuntimeTools {
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Which runtime tool groups the Camel application needs, from 
what it has: sql (datasources, \
-                  SQL endpoints), tracing (OpenTelemetry, message tracing, 
Micrometer) and resilience (circuit \
-                  breakers). Returns the core tools, each group's tools with 
one line of guidance, and a fingerprint \
-                  that changes only when the groups do. A client for a small 
model offers the core tools plus these.""")
+                  SQL endpoints), tracing (OpenTelemetry, message tracing, 
Micrometer), resilience (circuit \
+                  breakers), http (the endpoints it serves) and chaos 
(camel-chaos). Returns the core tools, each \
+                  group's tools with one line of guidance, and a fingerprint 
that changes only when the groups do. \
+                  A client for a small model offers the core tools plus 
these.""")
     public JsonObject camel_runtime_tool_groups(
             @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid) {
         return delegateToRegistry("get_tool_groups", nameOrPid, Map.of());
@@ -1061,4 +1062,84 @@ public class RuntimeTools {
         }
         return delegateToRegistry("browse_endpoint", nameOrPid, args);
     }
+
+    // ---- camel-chaos (the chaos tool group): the rules and the report, and 
the chaos action of the CLI connector ----
+    // add and change are destructive (a rule makes messages fail, wait or get 
lost) and the clients ask first
+    // (ToolConfirms.chaos); remove and clear change the integration only back 
towards normal, and the report keeps
+    // the removed rules, so they are not destructive, as trace start and stop 
are not
+
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
+          description = """
+                  List the chaos rules of the Camel application (camel-chaos, 
development only): id, version, the \
+                  rule, state (active or ended), where it fires, matches, 
faults fired, time left and the expectation; \
+                  or why chaos is off. The rules come from the status of the 
application, written every second.""")
+    public JsonObject camel_runtime_chaos_list(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid) {
+        return delegateToRegistry("chaos_list", nameOrPid, Map.of());
+    }
+
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
+          description = """
+                  Get the chaos report of the Camel application: the verdict 
(PASS or FAIL) and whether it is final, \
+                  the seed, every version of every rule with what it fired, 
the expectations (a pending one is not \
+                  decided yet) and the counts per route; or why chaos is 
off.""")
+    public JsonObject camel_runtime_chaos_report(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid) {
+        return delegateToRegistry("chaos_report", nameOrPid, Map.of());
+    }
+
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
+          description = """
+                  Add a chaos rule to the Camel application, in the one-line 
form of camel.chaos.rules, e.g. \
+                  'endpoint=kafka:orders* error rate=0.1 until=5m'. Returns 
the id of the rule. Chaos must be on in \
+                  the application; a rule that is not valid is refused with 
the reason.""")
+    public JsonObject camel_runtime_chaos_add(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid,
+            @ToolArg(description = "The rule in its one-line text form, as one 
entry of camel.chaos.rules") String rule) {
+        requireArg("rule", rule);
+        return delegateToRegistry("chaos_add", nameOrPid, Map.of("rule", 
rule));
+    }
+
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
+          description = """
+                  Change a chaos rule of the Camel application: the rule with 
the id gets the new one-line rule and \
+                  one more version; the report keeps the version it replaces. 
Returns the id and the new version.""")
+    public JsonObject camel_runtime_chaos_change(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid,
+            @ToolArg(description = "The id of the rule, such as r1 
(camel_runtime_chaos_list shows them)") String id,
+            @ToolArg(description = "The new rule in its one-line text form") 
String rule) {
+        requireArg("id", id);
+        requireArg("rule", rule);
+        return delegateToRegistry("chaos_change", nameOrPid, Map.of("id", id, 
"rule", rule));
+    }
+
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
+          description = "Remove a chaos rule of the Camel application: its 
faults stop, and the report keeps what it did.")
+    public JsonObject camel_runtime_chaos_remove(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid,
+            @ToolArg(description = "The id of the rule, such as r1 
(camel_runtime_chaos_list shows them)") String id) {
+        requireArg("id", id);
+        return delegateToRegistry("chaos_remove", nameOrPid, Map.of("id", id));
+    }
+
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
+          description = """
+                  Remove all chaos rules of the Camel application: no more 
faults, and the report keeps what they \
+                  did. Returns how many were removed.""")
+    public JsonObject camel_runtime_chaos_clear(
+            @ToolArg(description = NAME_OR_PID_DESC, required = false) String 
nameOrPid) {
+        return delegateToRegistry("chaos_clear", nameOrPid, Map.of());
+    }
+
+    private static void requireArg(String name, String value) {
+        if (value == null || value.isBlank()) {
+            throw new ToolCallException(name + " is required", null);
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsChaosSecurityTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsChaosSecurityTest.java
new file mode 100644
index 000000000000..faa54e1810f3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsChaosSecurityTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.quarkiverse.mcp.server.Tool;
+import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolConfirms;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolGroups;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The marks of the chaos tools and what they mean: adding and changing a rule 
is destructive and asked first
+ * ({@link ToolConfirms}), the list and the report only read, and each access 
level of the security layer
+ * ({@link McpAccessFilter} decides from the annotations) offers the tools by 
their marks.
+ */
+class RuntimeToolsChaosSecurityTest {
+
+    /** The tools of the chaos group, with readOnlyHint and destructiveHint as 
agreed for the group. */
+    private static final Map<String, boolean[]> MARKS = new LinkedHashMap<>();
+
+    static {
+        MARKS.put("camel_runtime_chaos_list", new boolean[] { true, false });
+        MARKS.put("camel_runtime_chaos_report", new boolean[] { true, false });
+        MARKS.put("camel_runtime_chaos_add", new boolean[] { false, true });
+        MARKS.put("camel_runtime_chaos_change", new boolean[] { false, true });
+        MARKS.put("camel_runtime_chaos_remove", new boolean[] { false, false 
});
+        MARKS.put("camel_runtime_chaos_clear", new boolean[] { false, false });
+    }
+
+    private static Tool.Annotations annotations(String name) {
+        return Arrays.stream(RuntimeTools.class.getDeclaredMethods())
+                .filter(m -> m.getName().equals(name) && 
m.isAnnotationPresent(Tool.class))
+                .findFirst().orElseThrow(() -> new AssertionError("No tool " + 
name))
+                .getAnnotation(Tool.class).annotations();
+    }
+
+    private static List<String> offered(McpSecurityConfig.AccessLevel level) {
+        return MARKS.keySet().stream()
+                .filter(name -> 
level.permits(annotations(name).readOnlyHint(), 
annotations(name).destructiveHint()))
+                .toList();
+    }
+
+    @Test
+    void everyToolOfTheGroupHasItsMarks() {
+        assertThat(ToolGroups.select(AppFeatures.fromStatus(new 
JsonObject(Map.of("chaos", Map.of("enabled", true)))))
+                .mcpTools())
+                .containsExactlyElementsOf(MARKS.keySet());
+        MARKS.forEach((name, marks) -> {
+            Tool.Annotations a = annotations(name);
+            assertThat(a.readOnlyHint()).as(name + " 
readOnlyHint").isEqualTo(marks[0]);
+            assertThat(a.destructiveHint()).as(name + " 
destructiveHint").isEqualTo(marks[1]);
+            assertThat(a.openWorldHint()).as(name + " 
openWorldHint").isFalse();
+            // the shared registry, which camel ask uses, has the same marks
+            ToolDescriptor shared = 
ToolRegistry.findTool(name.substring("camel_runtime_".length()));
+            assertThat(shared).as(name).isNotNull();
+            
assertThat(shared.isReadOnly()).as(shared.name()).isEqualTo(marks[0]);
+            
assertThat(shared.isDestructive()).as(shared.name()).isEqualTo(marks[1]);
+        });
+    }
+
+    @Test
+    void exactlyTheDestructiveToolsAreInToolConfirms() {
+        MARKS.forEach((name, marks) -> {
+            String command = name.substring("camel_runtime_chaos_".length());
+            ToolConfirms.Call call = ToolConfirms.chaos(command, "shop", "r1", 
"endpoint=kafka:* error rate=0.1");
+            assertThat(call != null).as(name + " is asked 
first").isEqualTo(marks[1]);
+            if (call != null) {
+                // the user sees the rule
+                assertThat(call.target()).isEqualTo("endpoint=kafka:* error 
rate=0.1");
+            }
+        });
+    }
+
+    @Test
+    void theAccessLevelsOfferTheToolsByTheirMarks() {
+        assertThat(offered(McpSecurityConfig.AccessLevel.READ_ONLY))
+                .containsExactly("camel_runtime_chaos_list", 
"camel_runtime_chaos_report");
+        // as for route_control, sql, send and receive: no destructive tool at 
read-write
+        
assertThat(offered(McpSecurityConfig.AccessLevel.READ_WRITE)).containsExactly(
+                "camel_runtime_chaos_list", "camel_runtime_chaos_report", 
"camel_runtime_chaos_remove",
+                "camel_runtime_chaos_clear");
+        
assertThat(offered(McpSecurityConfig.AccessLevel.ADMIN)).containsExactlyElementsOf(MARKS.keySet());
+        // with the security layer on, the interceptor of @McpSecured audits 
each call (and redacts the answer when
+        // camel.mcp.security.redaction.enabled=true)
+        
assertThat(RuntimeTools.class.isAnnotationPresent(McpSecured.class)).isTrue();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsChaosTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsChaosTest.java
new file mode 100644
index 000000000000..c2b3e8fe9598
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsChaosTest.java
@@ -0,0 +1,313 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.CamelContext;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.cli.connector.DefaultCliConnectorFactory;
+import org.apache.camel.cli.connector.LocalCliConnector;
+import org.apache.camel.component.chaos.ChaosManager;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.CliConnectorFactory;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assumptions.assumeThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * The chaos tools against a running integration: a CamelContext with 
camel-chaos and the CLI connector in this JVM,
+ * reached over the files of the CLI connector as the MCP server reaches any 
integration. Only the lookup of the process
+ * is faked, as the real one never takes this JVM for an integration. So a 
rule goes through the chaos action of the
+ * connector, the ChaosManager and the parser of camel.chaos.rules, and the 
list and the report come from the chaos
+ * console in the status the connector writes.
+ * <p>
+ * Isolated because it changes {@code user.home}, where the connector and the 
tools exchange their files.
+ */
+@Isolated
+class RuntimeToolsChaosTest {
+
+    private static final long PID = ProcessHandle.current().pid();
+    private static final String RULE_ERROR = "endpoint=direct:target error 
rate=1";
+
+    @TempDir
+    Path home;
+
+    private String userHome;
+    private Path cliHome;
+    private CamelContext context;
+    private LocalCliConnector connector;
+    private ProducerTemplate template;
+    private final RuntimeTools tools = new RuntimeTools();
+
+    @BeforeEach
+    void useTheTemporaryHome() {
+        userHome = System.getProperty("user.home");
+        cliHome = CommandLineHelper.getHomeDir();
+        System.setProperty("user.home", home.toString());
+        CommandLineHelper.useHomeDir(home.toString());
+        tools.runtimeService = new RuntimeService() {
+            @Override
+            public ProcessInfo findSingleProcess(String nameOrPid) {
+                // the integration of the test runs in this JVM, which the 
real lookup never finds
+                return "shop".equals(nameOrPid) ? new ProcessInfo(PID, "shop", 
"shop") : super.findSingleProcess(nameOrPid);
+            }
+        };
+    }
+
+    @AfterEach
+    void stop() {
+        try {
+            if (template != null) {
+                template.stop();
+            }
+            if (connector != null) {
+                connector.stop();
+            }
+            if (context != null) {
+                context.stop();
+            }
+        } finally {
+            System.setProperty("user.home", userHome);
+            CommandLineHelper.useHomeDir(cliHome.toString());
+        }
+    }
+
+    private void start(String profile, Map<String, String> properties) throws 
Exception {
+        context = new DefaultCamelContext();
+        // only the connector of the test, which polls faster
+        DefaultCliConnectorFactory disabled = new DefaultCliConnectorFactory();
+        disabled.setEnabled(false);
+        
context.getCamelContextExtension().addContextPlugin(CliConnectorFactory.class, 
disabled);
+        context.getCamelContextExtension().setProfile(profile);
+        
properties.forEach(context.getPropertiesComponent()::addInitialProperty);
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("start").to("direct:target");
+                
from("direct:target").routeId("target").setBody(constant("done"));
+            }
+        });
+        context.start();
+        template = context.createProducerTemplate();
+        connector = new LocalCliConnector(new DefaultCliConnectorFactory());
+        connector.setCamelContext(context);
+        connector.start();
+        connector.updateDelay(100);
+        // the tools read the status the connector writes
+        awaitThat("the status with the chaos console", this::statusHasChaos);
+    }
+
+    private boolean statusHasChaos() {
+        try {
+            return 
tools.camel_runtime_chaos_list("shop").containsKey("enabled");
+        } catch (ToolCallException e) {
+            return false;
+        }
+    }
+
+    private static void awaitThat(String what, Callable<Boolean> condition) {
+        await(what).atMost(20, TimeUnit.SECONDS).pollInterval(50, 
TimeUnit.MILLISECONDS).until(condition);
+    }
+
+    /** The status is written every tick: the list once it shows what the 
condition wants. */
+    private JsonObject awaitList(Predicate<JsonObject> condition) {
+        JsonObject[] last = new JsonObject[1];
+        awaitThat("the rules", () -> condition.test(last[0] = 
tools.camel_runtime_chaos_list("shop")));
+        return last[0];
+    }
+
+    private static List<String> rules(JsonObject list) {
+        return list.<List<Map<String, Object>>> getCollection("rules").stream()
+                .map(r -> r.get("id") + " v" + r.get("version") + " " + 
r.get("rule")).toList();
+    }
+
+    private Exception send() {
+        return template.send("direct:start", e -> 
e.getMessage().setBody("order")).getException();
+    }
+
+    @Test
+    void everyToolAgainstARunningIntegration() throws Exception {
+        start(null, Map.of("camel.chaos.enabled", "true"));
+        assertThat(awaitList(l -> l.getBoolean("enabled")).<List<Object>> 
getCollection("rules")).isEmpty();
+
+        JsonObject added = tools.camel_runtime_chaos_add("shop", RULE_ERROR);
+        assertThat(added.getString("id")).isEqualTo("r1");
+        assertThat(added.getInteger("version")).isEqualTo(1);
+        // the fault is in place
+        assertThat(send()).isNotNull();
+
+        JsonObject changed = tools.camel_runtime_chaos_change("shop", "r1", 
"endpoint=direct:target delay duration=1ms");
+        assertThat(changed.getString("id")).isEqualTo("r1");
+        assertThat(changed.getInteger("version")).isEqualTo(2);
+        assertThat(send()).isNull();
+
+        assertThat(tools.camel_runtime_chaos_add("shop", "step=* delay 
duration=1ms").getString("id")).isEqualTo("r2");
+        assertThat(rules(awaitList(l -> rules(l).size() == 2)))
+                .containsExactly("r1 v2 endpoint=direct:target delay 
duration=1ms", "r2 v1 step=* delay duration=1ms");
+
+        JsonObject removed = tools.camel_runtime_chaos_remove("shop", "r2");
+        assertThat(removed.getString("id")).isEqualTo("r2");
+        assertThat(removed.getInteger("removed")).isEqualTo(1);
+        assertThat(rules(awaitList(l -> rules(l).size() == 1)))
+                .containsExactly("r1 v2 endpoint=direct:target delay 
duration=1ms");
+
+        
assertThat(tools.camel_runtime_chaos_clear("shop").getInteger("removed")).isEqualTo(1);
+        assertThat(rules(awaitList(l -> rules(l).isEmpty()))).isEmpty();
+
+        // the report keeps every version of every rule
+        JsonObject[] report = new JsonObject[1];
+        awaitThat("the report with the three rules", () -> {
+            report[0] = tools.camel_runtime_chaos_report("shop");
+            return report[0].getMap("report").get("rules") instanceof List<?> 
l && l.size() == 3;
+        });
+        assertThat(report[0].getBoolean("enabled")).isTrue();
+        assertThat(report[0].<Map<String, Object>> 
getMap("report")).containsKey("verdict");
+        assertThat(((List<?>) 
report[0].getMap("report").get("rules")).stream().map(Map.class::cast)
+                .map(r -> r.get("id") + " v" + r.get("version") + " " + 
r.get("state")))
+                .containsExactly("r1 v1 replaced", "r2 v1 removed", "r1 v2 
removed");
+    }
+
+    @Test
+    void aRuleThatIsNotValidIsRefusedWithTheReasonOfTheParser() throws 
Exception {
+        start(null, Map.of("camel.chaos.enabled", "true"));
+        String rule = "endpoint=direct:target explode rate=2";
+        // what the parser of camel.chaos.rules says about it
+        String reason = parserReason(rule);
+        assertThat(reason).isNotBlank();
+
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_add("shop", rule))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessage(reason);
+        tools.camel_runtime_chaos_add("shop", RULE_ERROR);
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_change("shop", 
"r1", rule))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessage(reason);
+        // nothing changed
+        assertThat(rules(awaitList(l -> 
!rules(l).isEmpty()))).containsExactly("r1 v1 " + RULE_ERROR);
+    }
+
+    private String parserReason(String rule) {
+        try {
+            ChaosManager.get(context).addRule(rule);
+            return null;
+        } catch (IllegalArgumentException e) {
+            return e.getMessage();
+        }
+    }
+
+    @Test
+    void anIdThatDoesNotExistIsSaid() throws Exception {
+        start(null, Map.of("camel.chaos.enabled", "true"));
+
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_change("shop", 
"r9", RULE_ERROR))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessage("There is no chaos rule r9");
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_remove("shop", 
"r9"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessage("There is no chaos rule r9");
+    }
+
+    @Test
+    void withChaosOffEveryChangeIsRefusedAndTheListSaysWhy() throws Exception {
+        // camel-chaos is on the classpath, but without profile dev or 
camel.chaos.enabled=true chaos is off
+        start(null, Map.of());
+
+        JsonObject list = tools.camel_runtime_chaos_list("shop");
+        assertThat(list.getBoolean("enabled")).isFalse();
+        assertThat(list.getString("reason")).startsWith("Chaos is off");
+        assertThat(list).doesNotContainKey("rules");
+        
assertThat(tools.camel_runtime_chaos_report("shop").getString("reason")).startsWith("Chaos
 is off");
+
+        for (Runnable change : List.<Runnable> of(
+                () -> tools.camel_runtime_chaos_add("shop", RULE_ERROR),
+                () -> tools.camel_runtime_chaos_change("shop", "r1", 
RULE_ERROR),
+                () -> tools.camel_runtime_chaos_remove("shop", "r1"),
+                () -> tools.camel_runtime_chaos_clear("shop"))) {
+            assertThatThrownBy(change::run)
+                    .isInstanceOf(ToolCallException.class)
+                    .hasMessageStartingWith("Chaos is off in this 
integration");
+        }
+        assertThat(send()).isNull();
+    }
+
+    @Test
+    void profileProdKeepsChaosOff() throws Exception {
+        start("prod", Map.of("camel.chaos.enabled", "true"));
+
+        assertThat(tools.camel_runtime_chaos_list("shop").getString("reason"))
+                .isEqualTo("Chaos is off: it stays off with profile prod");
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_add("shop", 
RULE_ERROR))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageStartingWith("Chaos is off in this integration");
+    }
+
+    @Test
+    void anArgumentThatIsMissingIsSaidBeforeAnIntegrationIsLookedUp() {
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_add("no-such-app", 
" "))
+                .isInstanceOf(ToolCallException.class).hasMessage("rule is 
required");
+        assertThatThrownBy(() -> 
tools.camel_runtime_chaos_change("no-such-app", null, RULE_ERROR))
+                .isInstanceOf(ToolCallException.class).hasMessage("id is 
required");
+        assertThatThrownBy(() -> 
tools.camel_runtime_chaos_change("no-such-app", "r1", ""))
+                .isInstanceOf(ToolCallException.class).hasMessage("rule is 
required");
+        assertThatThrownBy(() -> 
tools.camel_runtime_chaos_remove("no-such-app", null))
+                .isInstanceOf(ToolCallException.class).hasMessage("id is 
required");
+    }
+
+    @Test
+    void anIntegrationThatIsNotFoundOrNotTheOnlyOneIsRefused() throws 
Exception {
+        // two integrations, as the real lookup finds them: a status file of a 
live process each
+        ProcessHandle parent = ProcessHandle.current().parent().orElse(null);
+        ProcessHandle grandparent = parent != null ? 
parent.parent().orElse(null) : null;
+        assumeThat(grandparent).as("two live processes to stand for two 
integrations").isNotNull();
+        Path dir = CommandLineHelper.getCamelDir();
+        Files.createDirectories(dir);
+        Files.writeString(dir.resolve(parent.pid() + "-status.json"),
+                "{\"context\":{\"name\":\"orders-a\",\"state\":\"Started\"}}");
+        Files.writeString(dir.resolve(grandparent.pid() + "-status.json"),
+                "{\"context\":{\"name\":\"orders-b\",\"state\":\"Started\"}}");
+        assumeThat(new RuntimeService().discoverProcesses()).as("the two 
integrations are found").hasSize(2);
+
+        // rule ids are per integration: a change must reach exactly one
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_add("orders", 
RULE_ERROR))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageStartingWith("No unique Camel process found 
matching 'orders'");
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_clear(null))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageStartingWith("Multiple Camel processes running");
+        assertThatThrownBy(() -> tools.camel_runtime_chaos_list("billing"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageStartingWith("No unique Camel process found 
matching 'billing'");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
index 66bc21f02de8..ce032ccd2524 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeToolsTest.java
@@ -124,7 +124,8 @@ class RuntimeToolsTest {
         for (String name : List.of("execute_sql", "get_tool_groups", 
"get_http_endpoints", "http_request", "get_datasources",
                 "get_sql_trace",
                 "get_circuit_breakers", "get_metrics",
-                "get_eip_stats", "get_spans", "get_startup_steps", 
"get_route_analysis", "detect_config_drift")) {
+                "get_eip_stats", "get_spans", "get_startup_steps", 
"get_route_analysis", "detect_config_drift",
+                "chaos_list", "chaos_report", "chaos_add", "chaos_change", 
"chaos_remove", "chaos_clear")) {
             assertThat(ToolRegistry.findTool(name)).as(name).isNotNull();
         }
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
index 04c5367e075e..b13294f2b517 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolGroups.java
@@ -28,7 +28,8 @@ import org.apache.camel.dsl.jbang.core.commands.ai.ToolGroups;
  * The AI panel's side of {@link ToolGroups} (CAMEL-24834): the tui_* tools 
each group adds to the core set, and the
  * guidance line in the panel's words. Only SQL and HTTP need tools of their 
own; the spans, the traced messages, the
  * metrics and the circuit breakers are read with core tools (tui_get_spans, 
tui_get_history, tui_get_table), so those
- * groups only add the line that tells a small model where to look.
+ * groups only add the line that tells a small model where to look. The chaos 
group adds no tool either: its line says
+ * whether camel-chaos may be injecting faults.
  */
 final class TuiToolGroups {
 
@@ -118,6 +119,12 @@ final class TuiToolGroups {
                                + " rate; OPEN means the fallback runs.";
             case HTTP -> "HTTP: " + ToolGroups.describeHttp(f) + "; " + 
HTTP_REQUEST_TOOL + " calls it, "
                          + HTTP_ENDPOINTS_TOOL + " lists the operations.";
+            // the panel has no chaos tools (camel ask and camel-jbang-mcp 
have them): it only learns that faults may be
+            // injected on purpose, so it does not hunt for a bug behind them
+            case CHAOS -> f.chaosOn()
+                    ? "Chaos: camel-chaos is on, so some failures and delays 
may be injected on purpose; tui_get_status"
+                      + " section 'chaos' has the rules."
+                    : "Chaos: camel-chaos is in the integration but off, so it 
injects no faults.";
         };
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
index bd772e1ba236..838ed0a3d0d0 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelToolGroupsTest.java
@@ -234,4 +234,21 @@ class AiPanelToolGroupsTest {
         assertEquals(toolNames(plain), toolNames(panel));
         assertTrue(toolNames(panel).contains("tui_update_row"));
     }
+
+    @Test
+    void theChaosGroupAddsOnlyALine() {
+        // the panel has no chaos tools; its model learns that faults may be 
injected on purpose
+        TuiToolGroups.Selection on = TuiToolGroups.select(
+                AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", true)))));
+        assertEquals(List.of(ToolGroup.CHAOS), on.groups());
+        assertTrue(on.tools().isEmpty(), on.tools().toString());
+        assertEquals(List.of("Chaos: camel-chaos is on, so some failures and 
delays may be injected on purpose;"
+                             + " tui_get_status section 'chaos' has the 
rules."),
+                on.guidance());
+
+        TuiToolGroups.Selection off = TuiToolGroups.select(
+                AppFeatures.fromStatus(new JsonObject(Map.of("chaos", 
Map.of("enabled", false)))));
+        assertEquals(List.of("Chaos: camel-chaos is in the integration but 
off, so it injects no faults."),
+                off.guidance());
+    }
 }

Reply via email to