This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch sql-query-tui
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 572219b2e651f544946a2146a290820abe2ed9fc
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jun 28 10:56:41 2026 +0200

    chore: camel-jbang - Add SQL query dev-console, CLI command, TUI tab, and 
MCP tool
    
    Adds end-to-end SQL query support: a new sql-query dev-console that
    executes queries against DataSource beans, a `camel cmd sql` CLI command,
    a TUI SQL Query tab with query history (reusable InputHistory class),
    and a tui_execute_sql MCP tool for AI agents.
    
    Co-Authored-By: Claude <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../impl/console/SqlQueryDevConsoleConfigurer.java |  77 +++
 .../org/apache/camel/dev-console/sql-query.json    |  15 +
 ...rg.apache.camel.impl.console.SqlQueryDevConsole |   2 +
 .../org/apache/camel/dev-console/sql-query         |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../camel/impl/console/SqlQueryDevConsole.java     | 312 ++++++++++
 .../pages/jbang-commands/camel-jbang-cmd-sql.adoc  |  29 +
 .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc |   1 +
 .../camel/cli/connector/LocalCliConnector.java     |  25 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../core/commands/action/CamelSqlQueryAction.java  | 173 ++++++
 .../camel/dsl/jbang/core/common/RuntimeHelper.java |  25 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |  61 +-
 .../dsl/jbang/core/commands/tui/InputHistory.java  | 153 +++++
 .../dsl/jbang/core/commands/tui/SqlQueryTab.java   | 654 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  |  30 +
 17 files changed, 1545 insertions(+), 19 deletions(-)

diff --git 
a/core/camel-console/src/generated/java/org/apache/camel/impl/console/SqlQueryDevConsoleConfigurer.java
 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/SqlQueryDevConsoleConfigurer.java
new file mode 100644
index 000000000000..61bc950601a3
--- /dev/null
+++ 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/SqlQueryDevConsoleConfigurer.java
@@ -0,0 +1,77 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.impl.console;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.impl.console.SqlQueryDevConsole;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class SqlQueryDevConsoleConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter {
+
+    private static final Map<String, Object> ALL_OPTIONS;
+    static {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("CamelContext", org.apache.camel.CamelContext.class);
+        map.put("DefaultMaxRows", int.class);
+        map.put("DefaultQueryTimeout", int.class);
+        ALL_OPTIONS = map;
+    }
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.impl.console.SqlQueryDevConsole target = 
(org.apache.camel.impl.console.SqlQueryDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": target.setCamelContext(property(camelContext, 
org.apache.camel.CamelContext.class, value)); return true;
+        case "defaultmaxrows":
+        case "defaultMaxRows": target.setDefaultMaxRows(property(camelContext, 
int.class, value)); return true;
+        case "defaultquerytimeout":
+        case "defaultQueryTimeout": 
target.setDefaultQueryTimeout(property(camelContext, int.class, value)); return 
true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getAllOptions(Object target) {
+        return ALL_OPTIONS;
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return org.apache.camel.CamelContext.class;
+        case "defaultmaxrows":
+        case "defaultMaxRows": return int.class;
+        case "defaultquerytimeout":
+        case "defaultQueryTimeout": return int.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.impl.console.SqlQueryDevConsole target = 
(org.apache.camel.impl.console.SqlQueryDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return target.getCamelContext();
+        case "defaultmaxrows":
+        case "defaultMaxRows": return target.getDefaultMaxRows();
+        case "defaultquerytimeout":
+        case "defaultQueryTimeout": return target.getDefaultQueryTimeout();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/sql-query.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/sql-query.json
new file mode 100644
index 000000000000..1b98f12a4690
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/sql-query.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "sql-query",
+    "title": "SQL Query",
+    "description": "Execute SQL queries on DataSource beans",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.SqlQueryDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.SqlQueryDevConsole
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.SqlQueryDevConsole
new file mode 100644
index 000000000000..cf682ab4c486
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.SqlQueryDevConsole
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.SqlQueryDevConsoleConfigurer
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/sql-query
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/sql-query
new file mode 100644
index 000000000000..9ddcee3835ca
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/sql-query
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.SqlQueryDevConsole
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index e25def6b5f32..40b26b866b52 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source 
startup-recorder system-properties thread top trace transformers 
type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source sql-query 
startup-recorder system-properties thread top trace transformers 
type-converters variables
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.21.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlQueryDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlQueryDevConsole.java
new file mode 100644
index 000000000000..81ecc1b3c460
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlQueryDevConsole.java
@@ -0,0 +1,312 @@
+/*
+ * 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.impl.console;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.Statement;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "sql-query", displayName = "SQL Query", description = 
"Execute SQL queries on DataSource beans")
+@Configurer(extended = true)
+public class SqlQueryDevConsole extends AbstractDevConsole {
+
+    /**
+     * The SQL query to execute
+     */
+    public static final String SQL = "sql";
+
+    /**
+     * Name of the DataSource bean in the registry (auto-detected if only one 
exists)
+     */
+    public static final String DATASOURCE = "datasource";
+
+    /**
+     * Maximum number of rows to return
+     */
+    public static final String MAX_ROWS = "maxRows";
+
+    /**
+     * Query timeout in seconds
+     */
+    public static final String QUERY_TIMEOUT = "queryTimeout";
+
+    @Metadata(defaultValue = "100",
+              description = "Maximum number of rows to return from a query")
+    private int defaultMaxRows = 100;
+
+    @Metadata(defaultValue = "30",
+              description = "Default query timeout in seconds")
+    private int defaultQueryTimeout = 30;
+
+    public SqlQueryDevConsole() {
+        super("camel", "sql-query", "SQL Query", "Execute SQL queries on 
DataSource beans");
+    }
+
+    public int getDefaultMaxRows() {
+        return defaultMaxRows;
+    }
+
+    public void setDefaultMaxRows(int defaultMaxRows) {
+        this.defaultMaxRows = defaultMaxRows;
+    }
+
+    public int getDefaultQueryTimeout() {
+        return defaultQueryTimeout;
+    }
+
+    public void setDefaultQueryTimeout(int defaultQueryTimeout) {
+        this.defaultQueryTimeout = defaultQueryTimeout;
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        StringBuilder sb = new StringBuilder();
+
+        String sql = (String) options.get(SQL);
+        if (sql == null || sql.isBlank()) {
+            sb.append("No SQL query provided\n");
+            return sb.toString();
+        }
+
+        String dsName = (String) options.get(DATASOURCE);
+        int maxRows = parseIntOption(options, MAX_ROWS, defaultMaxRows);
+        int queryTimeout = parseIntOption(options, QUERY_TIMEOUT, 
defaultQueryTimeout);
+
+        DataSource ds;
+        String resolvedName;
+        if (dsName != null && !dsName.isBlank()) {
+            ds = getCamelContext().getRegistry().lookupByNameAndType(dsName, 
DataSource.class);
+            resolvedName = dsName;
+            if (ds == null) {
+                sb.append(String.format("DataSource '%s' not found in 
registry%n", dsName));
+                return sb.toString();
+            }
+        } else {
+            Map<String, DataSource> all = 
getCamelContext().getRegistry().findByTypeWithName(DataSource.class);
+            if (all.isEmpty()) {
+                sb.append("No DataSource found in registry\n");
+                return sb.toString();
+            }
+            if (all.size() > 1) {
+                sb.append(String.format("Multiple DataSources found: %s. 
Specify one with --datasource%n",
+                        String.join(", ", all.keySet())));
+                return sb.toString();
+            }
+            Map.Entry<String, DataSource> single = 
all.entrySet().iterator().next();
+            ds = single.getValue();
+            resolvedName = single.getKey();
+        }
+
+        StopWatch watch = new StopWatch();
+        try (Connection conn = ds.getConnection();
+             Statement stmt = conn.createStatement()) {
+
+            stmt.setMaxRows(maxRows);
+            stmt.setQueryTimeout(queryTimeout);
+
+            boolean hasResultSet = stmt.execute(sql);
+            long elapsed = watch.taken();
+
+            if (hasResultSet) {
+                try (ResultSet rs = stmt.getResultSet()) {
+                    ResultSetMetaData meta = rs.getMetaData();
+                    int colCount = meta.getColumnCount();
+
+                    sb.append(String.format("DataSource: %s%n", resolvedName));
+                    sb.append(String.format("Elapsed: %s%n%n", 
TimeUtils.printDuration(elapsed)));
+
+                    // column headers
+                    for (int i = 1; i <= colCount; i++) {
+                        if (i > 1) {
+                            sb.append(" | ");
+                        }
+                        sb.append(meta.getColumnLabel(i));
+                    }
+                    sb.append("\n");
+
+                    int rowCount = 0;
+                    while (rs.next()) {
+                        for (int i = 1; i <= colCount; i++) {
+                            if (i > 1) {
+                                sb.append(" | ");
+                            }
+                            Object val = rs.getObject(i);
+                            sb.append(val != null ? String.valueOf(val) : 
"null");
+                        }
+                        sb.append("\n");
+                        rowCount++;
+                    }
+                    sb.append(String.format("%n%d row(s)%n", rowCount));
+                    if (rowCount >= maxRows) {
+                        sb.append("(truncated)\n");
+                    }
+                }
+            } else {
+                int updateCount = stmt.getUpdateCount();
+                sb.append(String.format("DataSource: %s%n", resolvedName));
+                sb.append(String.format("Update count: %d%n", updateCount));
+                sb.append(String.format("Elapsed: %s%n", 
TimeUtils.printDuration(elapsed)));
+            }
+        } catch (Exception e) {
+            long elapsed = watch.taken();
+            sb.append(String.format("Error: %s%n", e.getMessage()));
+            sb.append(String.format("Elapsed: %s%n", 
TimeUtils.printDuration(elapsed)));
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        String sql = (String) options.get(SQL);
+        if (sql == null || sql.isBlank()) {
+            root.put("status", "error");
+            root.put("message", "No SQL query provided");
+            return root;
+        }
+
+        String dsName = (String) options.get(DATASOURCE);
+        int maxRows = parseIntOption(options, MAX_ROWS, defaultMaxRows);
+        int queryTimeout = parseIntOption(options, QUERY_TIMEOUT, 
defaultQueryTimeout);
+
+        DataSource ds;
+        String resolvedName;
+        if (dsName != null && !dsName.isBlank()) {
+            ds = getCamelContext().getRegistry().lookupByNameAndType(dsName, 
DataSource.class);
+            resolvedName = dsName;
+            if (ds == null) {
+                root.put("status", "error");
+                root.put("message", String.format("DataSource '%s' not found 
in registry", dsName));
+                return root;
+            }
+        } else {
+            Map<String, DataSource> all = 
getCamelContext().getRegistry().findByTypeWithName(DataSource.class);
+            if (all.isEmpty()) {
+                root.put("status", "error");
+                root.put("message", "No DataSource found in registry");
+                return root;
+            }
+            if (all.size() > 1) {
+                root.put("status", "error");
+                root.put("message", "Multiple DataSources found, specify one: 
" + String.join(", ", all.keySet()));
+                // include available names for the caller
+                JsonArray names = new JsonArray();
+                all.keySet().forEach(names::add);
+                root.put("availableDataSources", names);
+                return root;
+            }
+            Map.Entry<String, DataSource> single = 
all.entrySet().iterator().next();
+            ds = single.getValue();
+            resolvedName = single.getKey();
+        }
+
+        StopWatch watch = new StopWatch();
+        try (Connection conn = ds.getConnection();
+             Statement stmt = conn.createStatement()) {
+
+            stmt.setMaxRows(maxRows);
+            stmt.setQueryTimeout(queryTimeout);
+
+            boolean hasResultSet = stmt.execute(sql);
+            long elapsed = watch.taken();
+
+            root.put("status", "success");
+            root.put("elapsed", elapsed);
+            root.put("datasource", resolvedName);
+
+            if (hasResultSet) {
+                try (ResultSet rs = stmt.getResultSet()) {
+                    ResultSetMetaData meta = rs.getMetaData();
+                    int colCount = meta.getColumnCount();
+
+                    JsonArray columns = new JsonArray();
+                    for (int i = 1; i <= colCount; i++) {
+                        JsonObject col = new JsonObject();
+                        col.put("name", meta.getColumnLabel(i));
+                        col.put("type", meta.getColumnTypeName(i));
+                        columns.add(col);
+                    }
+                    root.put("columns", columns);
+
+                    JsonArray rows = new JsonArray();
+                    int rowCount = 0;
+                    while (rs.next()) {
+                        JsonObject row = new JsonObject();
+                        for (int i = 1; i <= colCount; i++) {
+                            String colName = meta.getColumnLabel(i);
+                            Object val = rs.getObject(i);
+                            if (val == null) {
+                                row.put(colName, null);
+                            } else if (val instanceof Number n) {
+                                row.put(colName, n);
+                            } else if (val instanceof Boolean b) {
+                                row.put(colName, b);
+                            } else {
+                                row.put(colName, String.valueOf(val));
+                            }
+                        }
+                        rows.add(row);
+                        rowCount++;
+                    }
+                    root.put("rows", rows);
+                    root.put("rowCount", rowCount);
+                    root.put("truncated", rowCount >= maxRows);
+                }
+            } else {
+                int updateCount = stmt.getUpdateCount();
+                root.put("updateCount", updateCount);
+            }
+        } catch (Exception e) {
+            long elapsed = watch.taken();
+            root.put("status", "error");
+            root.put("elapsed", elapsed);
+            root.put("message", e.getMessage());
+        }
+
+        return root;
+    }
+
+    private static int parseIntOption(Map<String, Object> options, String key, 
int defaultValue) {
+        Object val = options.get(key);
+        if (val instanceof Number n) {
+            return n.intValue();
+        }
+        if (val instanceof String s && !s.isBlank()) {
+            try {
+                return Integer.parseInt(s);
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+        return defaultValue;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-sql.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-sql.adoc
new file mode 100644
index 000000000000..8c72e6b72140
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-sql.adoc
@@ -0,0 +1,29 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd sql
+
+Execute SQL query on a DataSource
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd sql [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--datasource` | Name of the DataSource bean (auto-detected if only one 
exists) |  | String
+| `--max-rows` | Maximum number of rows to return | 100 | int
+| `--query,--sql` | The SQL query to execute |  | String
+| `--query-timeout` | Query timeout in seconds | 30 | int
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
index b3e799143a74..4e261d317b2d 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
@@ -34,6 +34,7 @@ camel cmd [options]
 | xref:jbang-commands/camel-jbang-cmd-route-topology.adoc[route-topology] | 
Display inter-route topology connections
 | xref:jbang-commands/camel-jbang-cmd-send.adoc[send] | Send messages to 
endpoints
 | xref:jbang-commands/camel-jbang-cmd-span.adoc[span] | Display OpenTelemetry 
spans from running Camel integrations
+| xref:jbang-commands/camel-jbang-cmd-sql.adoc[sql] | Execute SQL query on a 
DataSource
 | xref:jbang-commands/camel-jbang-cmd-start-group.adoc[start-group] | Start 
Camel route groups
 | xref:jbang-commands/camel-jbang-cmd-start-route.adoc[start-route] | Start 
Camel routes
 | xref:jbang-commands/camel-jbang-cmd-stop-group.adoc[stop-group] | Stop Camel 
route groups
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 21688dde8ca6..ae14b3906620 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -371,6 +371,8 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionReceiveTask(root);
             } else if ("readme".equals(action)) {
                 doActionReadmeTask(root);
+            } else if ("sql-query".equals(action)) {
+                doActionSqlQueryTask(root);
             } else if ("cli-debug".equals(action)) {
                 doActionCliDebug(root);
             }
@@ -1012,6 +1014,29 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionSqlQueryTask(JsonObject root) throws Exception {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("sql-query");
+        if (dc != null) {
+            String sql = root.getStringOrDefault("sql", "");
+            String datasource = root.getString("datasource");
+            int maxRows = root.getIntegerOrDefault("maxRows", 100);
+            int queryTimeout = root.getIntegerOrDefault("queryTimeout", 30);
+            Map<String, Object> args = new HashMap<>();
+            args.put("sql", sql);
+            if (datasource != null) {
+                args.put("datasource", datasource);
+            }
+            args.put("maxRows", String.valueOf(maxRows));
+            args.put("queryTimeout", String.valueOf(queryTimeout));
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
args);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionLoadTask(JsonObject root) throws Exception {
         List<String> files = root.getCollection("source");
         boolean restart = root.getBooleanOrDefault("restart", false);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 19c7b6505642..06b36670a7a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 5113540c571f..da28f9ad87bb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -121,6 +121,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("route-structure", new CommandLine(new 
CamelRouteStructureAction(this)))
                         .addSubcommand("route-topology", new CommandLine(new 
CamelRouteTopologyAction(this)))
                         .addSubcommand("send", new CommandLine(new 
CamelSendAction(this)))
+                        .addSubcommand("sql", new CommandLine(new 
CamelSqlQueryAction(this)))
                         .addSubcommand("span", new CommandLine(new 
CamelSpanAction(this)))
                         .addSubcommand("start-group", new CommandLine(new 
CamelRouteGroupStartAction(this)))
                         .addSubcommand("start-route", new CommandLine(new 
CamelRouteStartAction(this)))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryAction.java
new file mode 100644
index 000000000000..e383fd9a531b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryAction.java
@@ -0,0 +1,173 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import picocli.CommandLine;
+
[email protected](name = "sql",
+                     description = "Execute SQL query on a DataSource", 
sortOptions = false,
+                     showDefaultValues = true,
+                     footer = {
+                             "%nExamples:",
+                             "  camel cmd sql myApp --query=\"SELECT * FROM 
orders\"",
+                             "  camel cmd sql myApp --query=\"SELECT * FROM 
users\" --datasource=myDS --max-rows=50" })
+public class CamelSqlQueryAction extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration",
+                            arity = "1")
+    String name;
+
+    @CommandLine.Option(names = { "--query", "--sql" }, required = true,
+                        description = "The SQL query to execute")
+    String query;
+
+    @CommandLine.Option(names = { "--datasource" },
+                        description = "Name of the DataSource bean 
(auto-detected if only one exists)")
+    String datasource;
+
+    @CommandLine.Option(names = { "--max-rows" }, defaultValue = "100",
+                        description = "Maximum number of rows to return")
+    int maxRows = 100;
+
+    @CommandLine.Option(names = { "--query-timeout" }, defaultValue = "30",
+                        description = "Query timeout in seconds")
+    int queryTimeout = 30;
+
+    public CamelSqlQueryAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        List<Long> pids = findPids(name);
+        if (pids.size() != 1) {
+            printer().println("Name or pid " + name + " matches " + pids.size()
+                              + " running Camel integrations. Specify a name 
or PID that matches exactly one.");
+            return 1;
+        }
+
+        long pid = pids.get(0);
+
+        Path outputFile = getOutputFile(Long.toString(pid));
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "sql-query");
+        root.put("sql", query);
+        if (datasource != null) {
+            root.put("datasource", datasource);
+        }
+        root.put("maxRows", maxRows);
+        root.put("queryTimeout", queryTimeout);
+
+        Path actionFile = getActionFile(Long.toString(pid));
+        Files.writeString(actionFile, root.toJson());
+
+        JsonObject jo = getJsonObject(outputFile, (queryTimeout + 10) * 1000L);
+        try {
+            if (jo == null) {
+                printer().println("Timeout waiting for SQL query response");
+                return 1;
+            }
+
+            String status = jo.getString("status");
+            if ("error".equals(status)) {
+                printer().println("Error: " + jo.getString("message"));
+                return 1;
+            }
+
+            if (jo.containsKey("updateCount")) {
+                int updateCount = jo.getInteger("updateCount");
+                long elapsed = jo.getLongOrDefault("elapsed", 0);
+                printer().println("Update count: " + updateCount);
+                printer().println("Elapsed: " + elapsed + "ms");
+                return 0;
+            }
+
+            JsonArray columns = jo.getCollection("columns");
+            JsonArray rows = jo.getCollection("rows");
+            if (columns == null || rows == null) {
+                printer().println("No result data");
+                return 0;
+            }
+
+            // compute column widths
+            String[] colNames = new String[columns.size()];
+            int[] widths = new int[columns.size()];
+            for (int i = 0; i < columns.size(); i++) {
+                JsonObject col = (JsonObject) columns.get(i);
+                colNames[i] = col.getString("name");
+                widths[i] = colNames[i].length();
+            }
+            for (Object rowObj : rows) {
+                JsonObject row = (JsonObject) rowObj;
+                for (int i = 0; i < colNames.length; i++) {
+                    Object val = row.get(colNames[i]);
+                    int len = val != null ? String.valueOf(val).length() : 4;
+                    widths[i] = Math.max(widths[i], len);
+                }
+            }
+
+            // print header
+            StringBuilder header = new StringBuilder();
+            StringBuilder separator = new StringBuilder();
+            for (int i = 0; i < colNames.length; i++) {
+                if (i > 0) {
+                    header.append(" | ");
+                    separator.append("-+-");
+                }
+                header.append(String.format("%-" + widths[i] + "s", 
colNames[i]));
+                separator.append("-".repeat(widths[i]));
+            }
+            printer().println(header.toString());
+            printer().println(separator.toString());
+
+            // print rows
+            for (Object rowObj : rows) {
+                JsonObject row = (JsonObject) rowObj;
+                StringBuilder line = new StringBuilder();
+                for (int i = 0; i < colNames.length; i++) {
+                    if (i > 0) {
+                        line.append(" | ");
+                    }
+                    Object val = row.get(colNames[i]);
+                    String s = val != null ? String.valueOf(val) : "null";
+                    line.append(String.format("%-" + widths[i] + "s", s));
+                }
+                printer().println(line.toString());
+            }
+
+            int rowCount = jo.getIntegerOrDefault("rowCount", rows.size());
+            boolean truncated = jo.getBooleanOrDefault("truncated", false);
+            long elapsed = jo.getLongOrDefault("elapsed", 0);
+            printer().println();
+            printer().println(rowCount + " row(s)" + (truncated ? " 
(truncated)" : "") + " in " + elapsed + "ms");
+        } finally {
+            PathUtils.deleteFile(outputFile);
+        }
+
+        return 0;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
index a23378060634..91caf955c9f4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -183,6 +183,10 @@ public final class RuntimeHelper {
      * @return           the raw response string, or a timeout message if no 
response was received
      */
     public static String executeAction(long pid, String action, 
Consumer<JsonObject> configure) {
+        return executeAction(pid, action, configure, ACTION_TIMEOUT_MS);
+    }
+
+    public static String executeAction(long pid, String action, 
Consumer<JsonObject> configure, long timeoutMs) {
         String requestId = UUID.randomUUID().toString().substring(0, 8);
         Path camelDir = CommandLineHelper.getCamelDir();
         Path outputFile = camelDir.resolve(pid + "-output-" + requestId + 
".json");
@@ -199,7 +203,7 @@ public final class RuntimeHelper {
 
         try {
             StopWatch watch = new StopWatch();
-            while (watch.taken() < ACTION_TIMEOUT_MS) {
+            while (watch.taken() < timeoutMs) {
                 try {
                     Thread.sleep(POLL_INTERVAL_MS);
                     if (Files.exists(outputFile) && 
outputFile.toFile().length() > 0) {
@@ -272,6 +276,25 @@ public final class RuntimeHelper {
         }
     }
 
+    public static JsonObject executeSqlQuery(long pid, String sql, String 
datasource, int maxRows, int queryTimeout) {
+        long timeout = (queryTimeout + 10) * 1000L;
+        String result = executeAction(pid, "sql-query", root -> {
+            root.put("sql", sql);
+            if (datasource != null) {
+                root.put("datasource", datasource);
+            }
+            root.put("maxRows", maxRows);
+            root.put("queryTimeout", queryTimeout);
+        }, timeout);
+        try {
+            return (JsonObject) Jsoner.deserialize(result);
+        } catch (Exception e) {
+            JsonObject wrapper = new JsonObject();
+            wrapper.put("result", result);
+            return wrapper;
+        }
+    }
+
     public static JsonObject readStatusFromFile(Path path) {
         try {
             if (Files.exists(path) && path.toFile().length() > 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index f9985158d512..bd4b5c97243d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -225,6 +225,7 @@ public class CamelMonitor extends CamelCommand {
     private ProcessTab processTab;
     private OverviewTab overviewTab;
     private DataSourceTab dataSourceTab;
+    private SqlQueryTab sqlQueryTab;
 
     // "Switch integration" popup state
     private boolean showSwitchPopup;
@@ -279,6 +280,7 @@ public class CamelMonitor extends CamelCommand {
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
         dataSourceTab = new DataSourceTab(ctx);
+        sqlQueryTab = new SqlQueryTab(ctx);
         endpointsTab = new EndpointsTab(ctx, metrics);
         httpTab = new HttpTab(ctx);
         healthTab = new HealthTab(ctx);
@@ -438,7 +440,7 @@ public class CamelMonitor extends CamelCommand {
                 return true;
             }
             if (ke.isDown()) {
-                morePopupState.selectNext(14);
+                morePopupState.selectNext(15);
                 return true;
             }
             int shortcutSel = morePopupShortcut(ke);
@@ -461,10 +463,11 @@ public class CamelMonitor extends CamelCommand {
                         case 7 -> inflightTab;
                         case 8 -> memoryTab;
                         case 9 -> metricsTab;
-                        case 10 -> spansTab;
-                        case 11 -> processTab;
-                        case 12 -> startupTab;
-                        case 13 -> threadsTab;
+                        case 10 -> sqlQueryTab;
+                        case 11 -> spansTab;
+                        case 12 -> processTab;
+                        case 13 -> startupTab;
+                        case 14 -> threadsTab;
                         default -> null;
                     };
                     if (activeMoreTab != null) {
@@ -540,7 +543,9 @@ public class CamelMonitor extends CamelCommand {
         boolean logSearchActive = tabsState.selected() == TAB_LOG && 
logTab.isSearchInputActive();
         boolean spanFilterActive = tabsState.selected() == TAB_MORE && 
activeMoreTab == spansTab
                 && spansTab.isFilterInputActive();
-        boolean textEditing = probeEditing || logSearchActive || 
spanFilterActive;
+        boolean sqlInputActive = tabsState.selected() == TAB_MORE && 
activeMoreTab == sqlQueryTab
+                && sqlQueryTab.isInputActive();
+        boolean textEditing = probeEditing || logSearchActive || 
spanFilterActive || sqlInputActive;
         if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
             runner.quit();
             return true;
@@ -777,6 +782,10 @@ public class CamelMonitor extends CamelCommand {
             filesBrowser.handlePaste(pe.text());
             return true;
         }
+        if (activeMoreTab == sqlQueryTab && sqlQueryTab.isInputActive()) {
+            sqlQueryTab.handlePaste(pe.text());
+            return true;
+        }
         return false;
     }
 
@@ -942,6 +951,7 @@ public class CamelMonitor extends CamelCommand {
         configurationTab.onIntegrationChanged();
         consumersTab.onIntegrationChanged();
         dataSourceTab.onIntegrationChanged();
+        sqlQueryTab.onIntegrationChanged();
         circuitBreakerTab.onIntegrationChanged();
         inflightTab.onIntegrationChanged();
         spansTab.onIntegrationChanged();
@@ -1244,7 +1254,7 @@ public class CamelMonitor extends CamelCommand {
 
     private void renderMorePopup(Frame frame, Rect area) {
         int popupW = 22;
-        int popupH = 16;
+        int popupH = 17;
         // Position just below the "0 More▾" tab label
         int dividerW = CharWidth.of(" | ");
         int tabBarX = 0;
@@ -1276,6 +1286,7 @@ public class CamelMonitor extends CamelCommand {
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("I", 
keyStyle), Span.raw("nflight"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("M", 
keyStyle), Span.raw("emory"))),
                 ListItem.from(Line.from(Span.raw("  M"), Span.styled("e", 
keyStyle), Span.raw("trics"))),
+                ListItem.from(Line.from(Span.raw("  S"), Span.styled("Q", 
keyStyle), Span.raw("L Query"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("O", 
keyStyle), Span.raw("Tel Spans"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("P", 
keyStyle), Span.raw("rocess"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("S", 
keyStyle), Span.raw("tartup"))),
@@ -1387,18 +1398,21 @@ public class CamelMonitor extends CamelCommand {
         if (ke.isChar('e')) {
             return 9;
         }
-        if (ke.isChar('o')) {
+        if (ke.isChar('q')) {
             return 10;
         }
-        if (ke.isChar('p')) {
+        if (ke.isChar('o')) {
             return 11;
         }
-        if (ke.isChar('s')) {
+        if (ke.isChar('p')) {
             return 12;
         }
-        if (ke.isChar('t')) {
+        if (ke.isChar('s')) {
             return 13;
         }
+        if (ke.isChar('t')) {
+            return 14;
+        }
         return -1;
     }
 
@@ -2478,7 +2492,8 @@ public class CamelMonitor extends CamelCommand {
 
     private static final String[] MORE_TAB_NAMES = {
             "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
-            "Consumers", "DataSource", "Inflight", "Memory", "Metrics", 
"Spans", "Process", "Startup", "Threads"
+            "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "SQL 
Query", "Spans", "Process", "Startup",
+            "Threads"
     };
 
     String navigateToTab(String tabName) {
@@ -2504,10 +2519,11 @@ public class CamelMonitor extends CamelCommand {
                     case 7 -> inflightTab;
                     case 8 -> memoryTab;
                     case 9 -> metricsTab;
-                    case 10 -> spansTab;
-                    case 11 -> processTab;
-                    case 12 -> startupTab;
-                    case 13 -> threadsTab;
+                    case 10 -> sqlQueryTab;
+                    case 11 -> spansTab;
+                    case 12 -> processTab;
+                    case 13 -> startupTab;
+                    case 14 -> threadsTab;
                     default -> null;
                 };
                 if (activeMoreTab != null) {
@@ -2988,6 +3004,19 @@ public class CamelMonitor extends CamelCommand {
         return RuntimeHelper.sendMessage(pid, endpoint, body, headers);
     }
 
+    JsonObject executeSql(String sql, String datasource, int maxRows, int 
queryTimeout) {
+        if (ctx.selectedPid == null) {
+            return null;
+        }
+        long pid;
+        try {
+            pid = Long.parseLong(ctx.selectedPid);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+        return RuntimeHelper.executeSqlQuery(pid, sql, datasource, maxRows, 
queryTimeout);
+    }
+
     String controlIntegration(String action) {
         if (action == null || action.isBlank()) {
             return "Error: action is required";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InputHistory.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InputHistory.java
new file mode 100644
index 000000000000..d132557af381
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InputHistory.java
@@ -0,0 +1,153 @@
+/*
+ * 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.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+
+/**
+ * Reusable input history with popup selector. Stores string entries and 
renders a centered popup overlay for selection.
+ */
+class InputHistory {
+
+    private static final int MAX_ENTRIES = 20;
+
+    private final List<String> entries = new ArrayList<>();
+    private final ListState listState = new ListState();
+    private boolean popupVisible;
+    private String selected;
+
+    void add(String entry) {
+        if (entry == null || entry.isBlank()) {
+            return;
+        }
+        entries.remove(entry);
+        entries.add(0, entry);
+        if (entries.size() > MAX_ENTRIES) {
+            entries.remove(entries.size() - 1);
+        }
+    }
+
+    boolean isEmpty() {
+        return entries.isEmpty();
+    }
+
+    boolean isPopupVisible() {
+        return popupVisible;
+    }
+
+    void showPopup() {
+        popupVisible = true;
+        listState.select(0);
+        selected = null;
+    }
+
+    void hidePopup() {
+        popupVisible = false;
+        selected = null;
+    }
+
+    /**
+     * Returns the entry selected on Enter, then resets. Returns null if 
nothing was selected.
+     */
+    String takeSelected() {
+        String s = selected;
+        selected = null;
+        return s;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!popupVisible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            hidePopup();
+            return true;
+        }
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            return true;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(entries.size());
+            return true;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = listState.selected();
+            if (sel != null && sel >= 0 && sel < entries.size()) {
+                selected = entries.get(sel);
+            }
+            popupVisible = false;
+            return true;
+        }
+        return true;
+    }
+
+    void renderPopup(Frame frame, Rect area, String title) {
+        if (!popupVisible || entries.isEmpty()) {
+            return;
+        }
+
+        int maxLabelLen = 0;
+        for (String q : entries) {
+            String oneLine = q.replace('\n', ' ');
+            maxLabelLen = Math.max(maxLabelLen, oneLine.length() + 4);
+        }
+        int popupW = Math.min(area.width() - 4, Math.max(30, maxLabelLen + 4));
+        int popupH = Math.min(area.height() - 4, entries.size() + 2);
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        ListItem[] items = new ListItem[entries.size()];
+        for (int i = 0; i < entries.size(); i++) {
+            String label = entries.get(i).replace('\n', ' ');
+            if (label.length() > popupW - 6) {
+                label = label.substring(0, popupW - 9) + "...";
+            }
+            items[i] = ListItem.from(Line.from(Span.raw("  " + label)));
+        }
+
+        ListWidget list = ListWidget.builder()
+                .items(items)
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(Title.from(Line.from(Span.styled(" " + title + 
" ", Style.EMPTY.fg(Color.YELLOW).bold()))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, listState);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
new file mode 100644
index 000000000000..a6e9d06591c7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
@@ -0,0 +1,654 @@
+/*
+ * 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.tui;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextArea;
+import dev.tamboui.widgets.input.TextAreaState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class SqlQueryTab implements MonitorTab {
+
+    private final MonitorContext ctx;
+    private final TextAreaState sqlInput = new TextAreaState();
+    private final TableState tableState = new TableState();
+    private final AtomicBoolean executing = new AtomicBoolean();
+    private final InputHistory sqlHistory = new InputHistory();
+
+    // datasource selection
+    private List<String> dsNames = new ArrayList<>();
+    private int selectedDs;
+    private boolean focusOnInput = true;
+
+    // results
+    private String[] columnNames;
+    private List<JsonObject> resultRows;
+    private int rowCount;
+    private boolean truncated;
+    private long elapsed;
+    private String errorMessage;
+    private Integer updateCount;
+
+    SqlQueryTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    boolean isInputActive() {
+        return focusOnInput;
+    }
+
+    void handlePaste(String text) {
+        if (focusOnInput) {
+            sqlInput.insert(text);
+        }
+    }
+
+    @Override
+    public void onIntegrationChanged() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        dsNames.clear();
+        selectedDs = 0;
+        if (info != null) {
+            for (DataSourceInfo ds : info.dataSources) {
+                dsNames.add(ds.name);
+            }
+        }
+        clearResults();
+    }
+
+    @Override
+    public void onTabSelected() {
+        focusOnInput = true;
+        onIntegrationChanged();
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (executing.get()) {
+            return true;
+        }
+
+        // history popup takes priority
+        if (sqlHistory.isPopupVisible()) {
+            sqlHistory.handleKeyEvent(ke);
+            String selected = sqlHistory.takeSelected();
+            if (selected != null) {
+                sqlInput.clear();
+                sqlInput.insert(selected);
+            }
+            return true;
+        }
+
+        if (ke.isCancel()) {
+            if (!focusOnInput && resultRows != null) {
+                focusOnInput = true;
+                return true;
+            }
+            return false;
+        }
+
+        // F5 to execute query
+        if (focusOnInput && ke.code() == KeyCode.F5) {
+            executeQuery();
+            return true;
+        }
+
+        // Tab to toggle focus between input and results
+        if (ke.code() == KeyCode.TAB && resultRows != null && 
!resultRows.isEmpty()) {
+            focusOnInput = !focusOnInput;
+            return true;
+        }
+
+        // Ctrl+E to open history popup
+        if (focusOnInput && ke.hasCtrl() && ke.isCharIgnoreCase('e') && 
!sqlHistory.isEmpty()) {
+            sqlHistory.showPopup();
+            return true;
+        }
+
+        // Enter adds newline in input mode
+        if (ke.isConfirm()) {
+            if (focusOnInput) {
+                sqlInput.insert('\n');
+                return true;
+            }
+            return true;
+        }
+
+        if (focusOnInput) {
+            // datasource cycling with Ctrl+Left/Right
+            if (ke.hasCtrl() && ke.isLeft() && dsNames.size() > 1) {
+                selectedDs = (selectedDs - 1 + dsNames.size()) % 
dsNames.size();
+                return true;
+            }
+            if (ke.hasCtrl() && ke.isRight() && dsNames.size() > 1) {
+                selectedDs = (selectedDs + 1) % dsNames.size();
+                return true;
+            }
+
+            // cursor movement
+            if (ke.isUp()) {
+                sqlInput.moveCursorUp();
+                return true;
+            }
+            if (ke.isDown()) {
+                sqlInput.moveCursorDown();
+                return true;
+            }
+            if (ke.isLeft()) {
+                sqlInput.moveCursorLeft();
+                return true;
+            }
+            if (ke.isRight()) {
+                sqlInput.moveCursorRight();
+                return true;
+            }
+            if (ke.isHome()) {
+                sqlInput.moveCursorToLineStart();
+                return true;
+            }
+            if (ke.isEnd()) {
+                sqlInput.moveCursorToLineEnd();
+                return true;
+            }
+            if (ke.isDeleteBackward()) {
+                sqlInput.deleteBackward();
+                return true;
+            }
+            if (ke.isDeleteForward()) {
+                sqlInput.deleteForward();
+                return true;
+            }
+
+            // typed character
+            if (ke.code() == KeyCode.CHAR) {
+                sqlInput.insert(ke.character());
+                return true;
+            }
+
+            return true;
+        }
+
+        // results navigation
+        if (ke.isUp()) {
+            navigateUp();
+            return true;
+        }
+        if (ke.isDown()) {
+            navigateDown();
+            return true;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean handleEscape() {
+        if (sqlHistory.isPopupVisible()) {
+            sqlHistory.hidePopup();
+            return true;
+        }
+        if (!focusOnInput && resultRows != null) {
+            focusOnInput = true;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        if (!focusOnInput) {
+            tableState.selectPrevious();
+        }
+    }
+
+    @Override
+    public void navigateDown() {
+        if (!focusOnInput && resultRows != null) {
+            tableState.selectNext(resultRows.size());
+        }
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        if (area.height() < 4) {
+            return;
+        }
+
+        // layout: datasource bar (1 line) + SQL input (5 lines) + results 
(rest)
+        int inputH = 5;
+        int dsBarH = dsNames.size() > 1 ? 1 : 0;
+        int topH = dsBarH + inputH;
+
+        List<Rect> parts = Layout.vertical()
+                .constraints(Constraint.length(topH), Constraint.min(3))
+                .split(area);
+
+        renderInputArea(frame, parts.get(0), dsBarH);
+        renderResults(frame, parts.get(1));
+
+        sqlHistory.renderPopup(frame, area, "Query History");
+    }
+
+    private void renderInputArea(Frame frame, Rect area, int dsBarH) {
+        if (dsBarH > 0 && area.height() > 0) {
+            Rect dsBar = new Rect(area.x(), area.y(), area.width(), 1);
+            StringBuilder sb = new StringBuilder(" DataSource: ");
+            for (int i = 0; i < dsNames.size(); i++) {
+                if (i == selectedDs) {
+                    sb.append("[").append(dsNames.get(i)).append("]");
+                } else {
+                    sb.append(" ").append(dsNames.get(i)).append(" ");
+                }
+                if (i < dsNames.size() - 1) {
+                    sb.append("  ");
+                }
+            }
+            Style style = focusOnInput ? Style.EMPTY.fg(Color.CYAN) : 
Style.EMPTY.fg(Color.DARK_GRAY);
+            Paragraph dsLabel = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(sb.toString(), 
style))))
+                    .build();
+            frame.renderWidget(dsLabel, dsBar);
+        }
+
+        Rect inputRect = new Rect(area.x(), area.y() + dsBarH, area.width(), 
area.height() - dsBarH);
+
+        String title;
+        if (executing.get()) {
+            title = " Executing... ";
+        } else if (errorMessage != null) {
+            title = " SQL Query (error) ";
+        } else if (updateCount != null) {
+            title = String.format(" SQL Query (%d updated, %dms) ", 
updateCount, elapsed);
+        } else if (resultRows != null) {
+            title = String.format(" SQL Query (%d row(s), %dms) ", rowCount, 
elapsed);
+        } else {
+            title = " SQL Query (F5 to execute) ";
+        }
+        Style borderStyle = focusOnInput ? Style.EMPTY.fg(Color.CYAN) : 
Style.EMPTY.fg(Color.DARK_GRAY);
+        Block inputBlock = Block.builder()
+                .title(Title.from(title))
+                .borderType(BorderType.ROUNDED)
+                .borderStyle(borderStyle)
+                .build();
+        Rect inner = inputBlock.inner(inputRect);
+        frame.renderWidget(inputBlock, inputRect);
+
+        TextArea textArea = TextArea.builder()
+                .cursorStyle(Style.EMPTY.reversed())
+                .placeholder("Type SQL query here...")
+                .build();
+        if (focusOnInput) {
+            textArea.renderWithCursor(inner, frame.buffer(), sqlInput, frame);
+        } else {
+            textArea.render(inner, frame.buffer(), sqlInput);
+        }
+    }
+
+    private void renderResults(Frame frame, Rect area) {
+        if (errorMessage != null) {
+            Block errBlock = Block.builder()
+                    .title(Title.from(" Error "))
+                    .borderType(BorderType.ROUNDED)
+                    .borderStyle(Style.EMPTY.fg(Color.RED))
+                    .build();
+            Rect inner = errBlock.inner(area);
+            frame.renderWidget(errBlock, area);
+            Paragraph errText = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(errorMessage, 
Style.EMPTY.fg(Color.RED)))))
+                    .build();
+            frame.renderWidget(errText, inner);
+            return;
+        }
+
+        if (updateCount != null) {
+            Block ucBlock = Block.builder()
+                    .title(Title.from(" Result "))
+                    .borderType(BorderType.ROUNDED)
+                    .borderStyle(Style.EMPTY.fg(Color.GREEN))
+                    .build();
+            Rect inner = ucBlock.inner(area);
+            frame.renderWidget(ucBlock, area);
+            String msg = String.format("Update count: %d  (%dms)", 
updateCount, elapsed);
+            Paragraph ucText = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(msg, 
Style.EMPTY.fg(Color.GREEN)))))
+                    .build();
+            frame.renderWidget(ucText, inner);
+            return;
+        }
+
+        if (columnNames == null || resultRows == null) {
+            Block emptyBlock = Block.builder()
+                    .title(Title.from(" Results "))
+                    .borderType(BorderType.ROUNDED)
+                    .borderStyle(Style.EMPTY.fg(Color.DARK_GRAY))
+                    .build();
+            Rect inner = emptyBlock.inner(area);
+            frame.renderWidget(emptyBlock, area);
+
+            String hint = dsNames.isEmpty()
+                    ? "No DataSource available"
+                    : "Type a SQL query and press F5 to execute";
+            Paragraph hintText = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(hint, 
Style.EMPTY.fg(Color.DARK_GRAY)))))
+                    .build();
+            frame.renderWidget(hintText, inner);
+            return;
+        }
+
+        // build result table
+        String resultTitle = String.format(" %d row(s)%s  %dms ",
+                rowCount, truncated ? " (truncated)" : "", elapsed);
+        Style tableBorderStyle = !focusOnInput ? Style.EMPTY.fg(Color.CYAN) : 
Style.EMPTY.fg(Color.DARK_GRAY);
+        Block tableBlock = Block.builder()
+                .title(Title.from(resultTitle))
+                .borderType(BorderType.ROUNDED)
+                .borderStyle(tableBorderStyle)
+                .build();
+
+        int[] widths = computeColumnWidths(area.width() - 2);
+
+        Row header = Row.from(buildHeaderCells());
+        header.style(Style.EMPTY.fg(Color.YELLOW));
+
+        List<Row> dataRows = new ArrayList<>();
+        for (JsonObject row : resultRows) {
+            List<Cell> cells = new ArrayList<>();
+            for (String col : columnNames) {
+                Object val = row.get(col);
+                String s = val != null ? String.valueOf(val) : "null";
+                Style style = val == null ? Style.EMPTY.fg(Color.DARK_GRAY) : 
Style.EMPTY.fg(Color.WHITE);
+                cells.add(Cell.from(Span.styled(s, style)));
+            }
+            dataRows.add(Row.from(cells));
+        }
+
+        Constraint[] colConstraints = new Constraint[widths.length];
+        for (int i = 0; i < widths.length; i++) {
+            colConstraints[i] = Constraint.length(widths[i]);
+        }
+
+        Table table = Table.builder()
+                .header(header)
+                .rows(dataRows)
+                .widths(colConstraints)
+                .block(tableBlock)
+                .highlightStyle(Style.EMPTY.bg(Color.DARK_GRAY))
+                .build();
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private Cell[] buildHeaderCells() {
+        Cell[] cells = new Cell[columnNames.length];
+        for (int i = 0; i < columnNames.length; i++) {
+            cells[i] = Cell.from(Span.styled(columnNames[i], 
Style.EMPTY.fg(Color.YELLOW)));
+        }
+        return cells;
+    }
+
+    private int[] computeColumnWidths(int availableWidth) {
+        int colCount = columnNames.length;
+        int[] widths = new int[colCount];
+
+        for (int i = 0; i < colCount; i++) {
+            widths[i] = columnNames[i].length();
+        }
+        if (resultRows != null) {
+            for (JsonObject row : resultRows) {
+                for (int i = 0; i < colCount; i++) {
+                    Object val = row.get(columnNames[i]);
+                    int len = val != null ? String.valueOf(val).length() : 4;
+                    widths[i] = Math.max(widths[i], len);
+                }
+            }
+        }
+
+        // cap each column to reasonable max
+        int maxColWidth = Math.max(10, availableWidth / Math.max(1, colCount));
+        for (int i = 0; i < colCount; i++) {
+            widths[i] = Math.min(widths[i] + 2, maxColWidth);
+        }
+        return widths;
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        if (focusOnInput) {
+            spans.add(Span.styled(" F5", Style.EMPTY.fg(Color.YELLOW)));
+            spans.add(Span.styled("=Execute ", 
Style.EMPTY.fg(Color.DARK_GRAY)));
+            if (!sqlHistory.isEmpty()) {
+                spans.add(Span.styled(" C-e", Style.EMPTY.fg(Color.YELLOW)));
+                spans.add(Span.styled("=History ", 
Style.EMPTY.fg(Color.DARK_GRAY)));
+            }
+            if (dsNames.size() > 1) {
+                spans.add(Span.styled(" C-←→", Style.EMPTY.fg(Color.YELLOW)));
+                spans.add(Span.styled("=DataSource ", 
Style.EMPTY.fg(Color.DARK_GRAY)));
+            }
+            if (resultRows != null && !resultRows.isEmpty()) {
+                spans.add(Span.styled(" Tab", Style.EMPTY.fg(Color.YELLOW)));
+                spans.add(Span.styled("=Results ", 
Style.EMPTY.fg(Color.DARK_GRAY)));
+            }
+        } else {
+            spans.add(Span.styled(" Tab", Style.EMPTY.fg(Color.YELLOW)));
+            spans.add(Span.styled("=Input ", Style.EMPTY.fg(Color.DARK_GRAY)));
+            spans.add(Span.styled(" ↑↓", Style.EMPTY.fg(Color.YELLOW)));
+            spans.add(Span.styled("=Navigate ", 
Style.EMPTY.fg(Color.DARK_GRAY)));
+        }
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # SQL Query
+
+                Execute SQL queries against DataSource beans registered in the 
Camel application.
+
+                ## Usage
+                - Type a SQL query in the input field and press **F5** to 
execute
+                - Use **Enter** for new lines in the query
+                - Use **Up/Down** arrows to move cursor within the query
+                - Paste multi-line queries from clipboard
+                - Use **Ctrl+E** to open query history (select with Enter, 
dismiss with Esc)
+                - Use **Tab** to toggle focus between input and results table
+                - Use **Esc** to return focus to the input field from results
+                - Use **Ctrl+Left/Right** to switch between DataSources (when 
multiple exist)
+
+                ## Supported Queries
+                - SELECT queries return a result table
+                - INSERT, UPDATE, DELETE return an update count
+                - Any valid SQL supported by the underlying database
+
+                ## Safety
+                - Results are limited to 100 rows by default
+                - Query timeout is 30 seconds by default
+                - This feature is only available when dev console is enabled 
(dev profile)
+                """;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        JsonObject root = new JsonObject();
+        if (columnNames != null && resultRows != null) {
+            JsonArray cols = new JsonArray();
+            for (String col : columnNames) {
+                cols.add(col);
+            }
+            root.put("columns", cols);
+            root.put("rows", new JsonArray(resultRows));
+            root.put("rowCount", rowCount);
+            root.put("truncated", truncated);
+            root.put("elapsed", elapsed);
+        }
+        if (errorMessage != null) {
+            root.put("error", errorMessage);
+        }
+        if (updateCount != null) {
+            root.put("updateCount", updateCount);
+        }
+        return root;
+    }
+
+    private void executeQuery() {
+        String sql = sqlInput.text().trim();
+        if (sql.isEmpty() || ctx.selectedPid == null || ctx.runner == null) {
+            return;
+        }
+        if (!executing.compareAndSet(false, true)) {
+            return;
+        }
+
+        clearResults();
+
+        sqlHistory.add(sql);
+        String pid = ctx.selectedPid;
+        String dsName = dsNames.isEmpty() ? null : dsNames.get(selectedDs);
+
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                executeInBackground(pid, sql, dsName);
+            } finally {
+                executing.set(false);
+            }
+        });
+    }
+
+    private void executeInBackground(String pid, String sql, String dsName) {
+        Path outputFile = ctx.getOutputFile(pid);
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "sql-query");
+        root.put("sql", sql);
+        if (dsName != null) {
+            root.put("datasource", dsName);
+        }
+        root.put("maxRows", 100);
+        root.put("queryTimeout", 30);
+
+        Path actionFile = ctx.getActionFile(pid);
+        PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+        JsonObject jo = pollJsonResponse(outputFile, 35000);
+        PathUtils.deleteFile(outputFile);
+
+        if (jo == null) {
+            if (ctx.runner != null) {
+                ctx.runner.runOnRenderThread(() -> {
+                    errorMessage = "Timeout waiting for query response";
+                });
+            }
+            return;
+        }
+
+        String status = jo.getString("status");
+        if ("error".equals(status)) {
+            String msg = jo.getString("message");
+            if (ctx.runner != null) {
+                ctx.runner.runOnRenderThread(() -> {
+                    errorMessage = msg;
+                    elapsed = jo.getLongOrDefault("elapsed", 0);
+                });
+            }
+            return;
+        }
+
+        // update count result
+        if (jo.containsKey("updateCount")) {
+            int uc = jo.getInteger("updateCount");
+            long el = jo.getLongOrDefault("elapsed", 0);
+            if (ctx.runner != null) {
+                ctx.runner.runOnRenderThread(() -> {
+                    updateCount = uc;
+                    elapsed = el;
+                    focusOnInput = true;
+                });
+            }
+            return;
+        }
+
+        // SELECT result
+        JsonArray columns = jo.getCollection("columns");
+        JsonArray rows = jo.getCollection("rows");
+        if (columns == null || rows == null) {
+            return;
+        }
+
+        String[] cols = new String[columns.size()];
+        for (int i = 0; i < columns.size(); i++) {
+            JsonObject col = (JsonObject) columns.get(i);
+            cols[i] = col.getString("name");
+        }
+
+        List<JsonObject> parsedRows = new ArrayList<>();
+        for (Object rowObj : rows) {
+            parsedRows.add((JsonObject) rowObj);
+        }
+
+        int rc = jo.getIntegerOrDefault("rowCount", parsedRows.size());
+        boolean trunc = jo.getBooleanOrDefault("truncated", false);
+        long el = jo.getLongOrDefault("elapsed", 0);
+
+        if (ctx.runner != null) {
+            ctx.runner.runOnRenderThread(() -> {
+                columnNames = cols;
+                resultRows = parsedRows;
+                rowCount = rc;
+                truncated = trunc;
+                elapsed = el;
+                tableState.select(0);
+                focusOnInput = true;
+            });
+        }
+    }
+
+    private void clearResults() {
+        columnNames = null;
+        resultRows = null;
+        rowCount = 0;
+        truncated = false;
+        elapsed = 0;
+        errorMessage = null;
+        updateCount = null;
+        tableState.select(0);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index fb871183452f..d3921228b7e6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -443,6 +443,20 @@ class TuiMcpServer {
                         "body", propDef("string", "Message body to send"),
                         "headers", propDef("string", "Message headers as 
key=value pairs separated by newlines")),
                 List.of("endpoint")));
+        toolList.add(toolDef(
+                "tui_execute_sql",
+                "Executes a SQL query against a DataSource in the selected 
integration. "
+                                   + "Returns structured JSON with columns, 
rows, and metadata for SELECT queries, "
+                                   + "or an update count for 
INSERT/UPDATE/DELETE. "
+                                   + "Requires dev console to be enabled in 
the running application.",
+                Map.of("query", propDef("string", "The SQL query to execute"),
+                        "datasource", propDef("string",
+                                "Name of the DataSource bean (auto-detected if 
only one exists)"),
+                        "maxRows", propDef("integer",
+                                "Maximum number of rows to return (default 
100)"),
+                        "queryTimeout", propDef("integer",
+                                "Query timeout in seconds (default 30)")),
+                List.of("query")));
         toolList.add(toolDef(
                 "tui_set_log_level",
                 "Changes the runtime log level of the selected integration. "
@@ -565,6 +579,7 @@ class TuiMcpServer {
                 case "tui_get_history" -> callGetHistory(args);
                 case "tui_get_topology" -> callGetTopology();
                 case "tui_send_message" -> callSendMessage(args);
+                case "tui_execute_sql" -> callExecuteSql(args);
                 case "tui_set_log_level" -> callSetLogLevel(args);
                 case "tui_filter" -> callFilter(args);
                 case "tui_toggle_trace_display" -> 
callToggleTraceDisplay(args);
@@ -1186,6 +1201,21 @@ class TuiMcpServer {
         return Jsoner.serialize(response);
     }
 
+    private String callExecuteSql(Map<String, Object> args) {
+        String query = (String) args.get("query");
+        if (query == null || query.isBlank()) {
+            return "Error: query is required";
+        }
+        String datasource = args.get("datasource") instanceof String s ? s : 
null;
+        int maxRows = args.get("maxRows") instanceof Number n ? n.intValue() : 
100;
+        int queryTimeout = args.get("queryTimeout") instanceof Number n ? 
n.intValue() : 30;
+        JsonObject response = monitor.executeSql(query, datasource, maxRows, 
queryTimeout);
+        if (response == null) {
+            return "Error: no integration selected or PID unavailable";
+        }
+        return Jsoner.serialize(response);
+    }
+
     private String callSetLogLevel(Map<String, Object> args) {
         String level = (String) args.get("level");
         if (level == null || level.isBlank()) {

Reply via email to