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 63a049b286c7 CAMEL-24798: camel-jbang - camel_get_files understands 
the Maven project layout
63a049b286c7 is described below

commit 63a049b286c7f1ecb43c360653571c45d8c59d68
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 17 20:50:16 2026 +0200

    CAMEL-24798: camel-jbang - camel_get_files understands the Maven project 
layout
    
    A local model asking "what's the name of the source file that has the
    route" against camel run --runtime=spring-boot cost 26 requests and a
    wrong answer, because camel_get_files listed one directory level and
    never reached src/main/resources/camel.
    
    The shared tool (camel mcp, camel tui --mcp, F8 panel) now lists the
    project recursively with relative paths, skipping build and VCS
    directories, and says whether it is a maven or flat layout. routeFiles
    and configFiles name the route and configuration files up front, with
    a kind per entry. Each running route's source (nested jar entry,
    classpath:, file: or Java class) is mapped onto the project file and
    line, or reported missing. file and camel_write_file take relative
    paths; absolute or escaping paths are refused with a clear message, and
    a missing file names the listing to call. In the TUI, the integration's
    name works as the directory. The TUI's own listing/reading code is
    replaced by the shared implementation.
    
    Docs updated on the MCP and TUI pages; AuthoringToolsTest,
    McpFacadeGetFilesTest and McpFacadeWriteFileTest cover the layout,
    path safety and route-source mapping.
    
    Closes #26558
    
    Co-Authored-By: Claude <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   7 +-
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   7 +
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 320 +++++++++++++++++++--
 .../jbang/core/commands/ai/AuthoringToolsTest.java | 106 ++++++-
 .../jbang/core/commands/mcp/AuthoringTools.java    |  14 +-
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  95 +++---
 .../dsl/jbang/core/commands/tui/RouteInfo.java     |   2 +
 .../dsl/jbang/core/commands/tui/StatusParser.java  |   1 +
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  12 +-
 .../core/commands/tui/McpFacadeGetFilesTest.java   | 138 +++++++++
 .../core/commands/tui/McpFacadeWriteFileTest.java  |   9 +-
 11 files changed, 624 insertions(+), 87 deletions(-)

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 6b272ccadbd8..99637a52a42b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -315,7 +315,12 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   read from its `camel-yaml-dsl` jar; without it the CLI's own, with nothing 
to download.
 
 | `camel_get_files`
-| The source files of a project directory (name, size, type), or the content 
of one of them.
+| The source files of a project directory, subdirectories included (`target`, 
`.git` and the like skipped). The
+  list says whether the directory is a Maven project or a flat folder, names 
the route files and the configuration
+  files up front (`routeFiles`, `configFiles`) and, when an integration is 
selected, which file and line each of its
+  routes was loaded from, mapped from the runtime's location (a jar entry of 
an exported project, a classpath or
+  file resource) onto the source under `src/main`. With `file`, a path 
relative to the directory as the list names
+  it, the content of that file.
 
 | `camel_write_file`
 | Writes the complete content of a file in the project directory. YAML and 
`.properties` content is validated
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index accb6284a792..3cc8ee23226b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -1274,6 +1274,13 @@ sense: `devMode` (changes are reloaded automatically), 
`temporary` (a copy that
 integration stops) and an `editing` hint. `camel_write_file` then writes the 
complete new content of
 a file in that directory.
 
+An integration started with `--runtime=spring-boot` or `--runtime=quarkus` 
runs from an exported Maven
+project, where the routes sit under `src/main/resources/camel` rather than 
next to a `pom.xml`. The
+listing understands that layout: it names the route and configuration files 
first, lists every file
+with its path relative to the project (build output skipped), and maps each 
running route to its file
+and line, so the agent reads the right file in one call instead of guessing 
names. File paths given to
+`camel_get_files` and `camel_write_file` are relative to that directory and 
may name a subdirectory.
+
 Every write is confirmed in the TUI first: a dialog names the file, the 
directory and the size of the
 change (`+3 -1` lines); press *d* to see the change as a unified diff, with 
removed lines on red and
 added lines on green like the Source tab's *F7* diff, and scroll it with the 
arrow keys; *Enter*, *Esc* or
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 f63629556da2..fcb10cf3b2ec 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
@@ -18,14 +18,23 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Consumer;
-import java.util.stream.Stream;
+import java.util.regex.Pattern;
 
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.util.json.JsonArray;
@@ -53,7 +62,18 @@ public final class AuthoringTools {
     static final String DIRECTORY_DESC = "Project directory with the source 
files (default: the selected integration's)";
 
     /** Files listed and read by the file tools; more than that and a 
directory is not an integration's sources. */
+    static final String FILE_PATH_DESC = "File path relative to the directory, 
e.g. src/main/resources/camel/foo.camel.yaml";
     private static final int MAX_FILES = 99;
+    /** How many files a listing looks at before it stops; the route and 
configuration files are found among them. */
+    private static final int SCAN_LIMIT = 2000;
+    private static final int MAX_DEPTH = 8;
+    /** Build output, tooling and VCS directories: never sources. */
+    private static final Set<String> SKIPPED_DIRS = Set.of(
+            "target", "build", "out", "node_modules", ".git", ".mvn", ".idea", 
".vscode", ".gradle", ".settings",
+            ".camel-jbang");
+    private static final Pattern YAML_ROUTE = Pattern.compile(
+            
"(?m)^\\s*-\\s*(route|from|rest|routeTemplate|route-template|templatedRoute|templated-route"
+                                                              + 
"|routeConfiguration|route-configuration|kamelet)\\s*:");
 
     private AuthoringTools() {
     }
@@ -145,11 +165,12 @@ public final class AuthoringTools {
                 }));
 
         registry.accept(tool("camel_get_files",
-                "The source files of a project directory: without file the 
list (name, size, type), with file its "
-                                                + "content. Use before editing 
to see the routes, configuration and other "
-                                                + "files of the integration.")
+                "The source files of a project directory, subdirectories 
included: without file the list, with "
+                                                + "the route and configuration 
files named first (routeFiles, configFiles) "
+                                                + "and, for a running 
integration, which file and line each route comes "
+                                                + "from; with file (a path 
relative to the directory, as listed) its content.")
                 .param("directory", "string", DIRECTORY_DESC, false)
-                .param("file", "string", "File name to read; omitted lists the 
files", false)
+                .param("file", "string", FILE_PATH_DESC + " to read; omitted 
lists the files", false)
                 .core(true)
                 .executor((ctx, args) -> {
                     Path dir = ctx.resolveDirectory(args.get("directory"));
@@ -157,7 +178,17 @@ public final class AuthoringTools {
                     if (file != null && !file.isBlank()) {
                         return readFile(dir, file).toJson();
                     }
-                    return listFiles(dir).toJson();
+                    JsonObject result = listFiles(dir);
+                    if (ctx.hasProcess()) {
+                        // the selected integration's routes, when they come 
from this directory
+                        JsonObject status = ctx.readFullStatus();
+                        JsonArray routes = routeSources(
+                                status != null && status.get("routes") 
instanceof Collection<?> c ? c : null, dir);
+                        if (routes.stream().anyMatch(r -> !((JsonObject) 
r).containsKey("missing"))) {
+                            result.put("routes", routes);
+                        }
+                    }
+                    return result.toJson();
                 }));
 
         registry.accept(tool("camel_write_file",
@@ -166,7 +197,7 @@ public final class AuthoringTools {
                                                  + "An integration running in 
dev mode reloads the change, otherwise restart it "
                                                  + "with camel_control.")
                 .param("directory", "string", DIRECTORY_DESC, false)
-                .param("file", "string", "File name, no path", true)
+                .param("file", "string", FILE_PATH_DESC + " (subdirectories 
are created)", true)
                 .param("content", "string", "The complete new content", true)
                 .param("validate", "boolean", "Validate before writing 
(default true)", false)
                 .param("camelVersion", "string", VERSION_DESC, false)
@@ -323,6 +354,7 @@ public final class AuthoringTools {
             }
         }
         try {
+            Files.createDirectories(path.getParent());
             Files.writeString(path, content, StandardCharsets.UTF_8);
         } catch (IOException e) {
             throw new ToolExecutionException("Failed to write " + path + ": " 
+ e.getMessage());
@@ -338,29 +370,55 @@ public final class AuthoringTools {
         return result;
     }
 
-    /** The files of a project directory, as {@code camel_get_files} lists 
them. */
+    /**
+     * The files of a project directory, subdirectories included, as {@code 
camel_get_files} lists them. A human answers
+     * "which file has the route" with one {@code ls -R}; this gives a model 
the same in one call: the layout
+     * ({@code maven} when the directory is a Maven project, else {@code 
flat}), the route files and the configuration
+     * files named up front, then every file with its path relative to the 
directory (build output and tooling
+     * directories skipped), capped at {@value #MAX_FILES} entries.
+     */
     public static JsonObject listFiles(Path dir) {
+        List<Path> all = projectFiles(dir);
+        boolean maven = Files.isRegularFile(dir.resolve("pom.xml")) && 
Files.isDirectory(dir.resolve("src/main"));
+        JsonArray routeFiles = new JsonArray();
+        JsonArray configFiles = new JsonArray();
         JsonArray files = new JsonArray();
-        try (Stream<Path> stream = Files.list(dir)) {
-            stream.filter(Files::isRegularFile)
-                    .sorted((a, b) -> 
a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
-                    .limit(MAX_FILES)
-                    .forEach(p -> {
-                        JsonObject entry = new JsonObject();
-                        entry.put("name", p.getFileName().toString());
-                        entry.put("size", formatSize(size(p)));
-                        entry.put("type", 
fileType(p.getFileName().toString()));
-                        files.add(entry);
-                    });
-        } catch (IOException e) {
-            throw new ToolExecutionException("Cannot list " + dir + ": " + 
e.getMessage());
+        for (Path p : all) {
+            String rel = relativePath(dir, p);
+            String type = fileType(rel);
+            String kind = fileKind(p, rel, type);
+            if ("route".equals(kind)) {
+                routeFiles.add(rel);
+            } else if ("config".equals(kind)) {
+                configFiles.add(rel);
+            }
+            if (files.size() < MAX_FILES) {
+                JsonObject entry = new JsonObject();
+                entry.put("name", rel);
+                entry.put("size", formatSize(size(p)));
+                entry.put("type", type);
+                if (kind != null) {
+                    entry.put("kind", kind);
+                }
+                files.add(entry);
+            }
         }
         JsonObject result = new JsonObject();
         result.put("directory", dir.toString());
+        result.put("layout", maven ? "maven" : "flat");
+        result.put("routeFiles", routeFiles);
+        result.put("configFiles", configFiles);
         result.put("files", files);
-        result.put("totalFiles", files.size());
-        if (files.isEmpty()) {
+        result.put("totalFiles", all.size());
+        if (all.isEmpty()) {
             result.put("message", "The directory has no files");
+        } else if (all.size() > MAX_FILES) {
+            result.put("message", "Listing the first " + MAX_FILES + " of " + 
all.size()
+                                  + " files; routeFiles and configFiles name 
every route and configuration file");
+        }
+        if (maven) {
+            result.put("hint", "Maven project: routes live under 
src/main/resources/camel or src/main/java and the"
+                               + " configuration under src/main/resources; 
pass file as the path listed here");
         }
         return result;
     }
@@ -369,7 +427,9 @@ public final class AuthoringTools {
     public static JsonObject readFile(Path dir, String file) {
         Path path = resolveFile(dir, file);
         if (!Files.isRegularFile(path)) {
-            throw new ToolExecutionException("No such file in the directory: " 
+ file);
+            throw new ToolExecutionException(
+                    "No such file: " + file + " in " + dir
+                                             + "; call camel_get_files without 
file to list them (routeFiles names the routes)");
         }
         JsonObject result = new JsonObject();
         result.put("file", file);
@@ -380,14 +440,216 @@ public final class AuthoringTools {
         return result;
     }
 
-    /** A plain file name inside the directory; anything else (a path, a 
parent reference) is refused. */
-    static Path resolveFile(Path dir, String file) {
+    /**
+     * Where each route of a running integration comes from, mapped onto the 
files of the project directory. The status
+     * document's {@code routes[].source} names what the runtime loaded: a jar 
entry for an exported project
+     * ({@code 
nested:.../target/app.jar/!BOOT-INF/classes/!/camel/foo.camel.yaml:4}), a 
{@code classpath:} or
+     * {@code file:} resource, or a Java class; the answer is the path a model 
can read and edit
+     * ({@code src/main/resources/camel/foo.camel.yaml}) with the line, or the 
location as given with {@code missing}
+     * when no such file exists under the directory.
+     *
+     * @param routes the status document's routes (maps with {@code routeId} 
and {@code source}), may be null
+     */
+    public static JsonArray routeSources(Collection<?> routes, Path dir) {
+        JsonArray out = new JsonArray();
+        if (routes == null) {
+            return out;
+        }
+        for (Object o : routes) {
+            if (!(o instanceof Map<?, ?> r) || !(r.get("source") instanceof 
String source) || source.isBlank()) {
+                continue;
+            }
+            SourceLocation loc = sourceLocation(source, dir);
+            JsonObject e = new JsonObject();
+            if (r.get("routeId") != null) {
+                e.put("routeId", String.valueOf(r.get("routeId")));
+            }
+            e.put("file", loc.file());
+            if (loc.line() > 0) {
+                e.put("line", loc.line());
+            }
+            if (!loc.exists()) {
+                e.put("missing", true);
+            }
+            out.add(e);
+        }
+        return out;
+    }
+
+    record SourceLocation(String file, int line, boolean exists) {
+    }
+
+    /** Maps one route source location onto a file under the directory; see 
{@link #routeSources}. */
+    static SourceLocation sourceLocation(String source, Path dir) {
+        String s = source.trim();
+        int line = 0;
+        int colon = s.lastIndexOf(':');
+        if (colon > 0 && colon < s.length() - 1) {
+            String suffix = s.substring(colon + 1);
+            if (suffix.chars().allMatch(Character::isDigit)) {
+                try {
+                    line = Integer.parseInt(suffix);
+                    s = s.substring(0, colon);
+                } catch (NumberFormatException e) {
+                    // too many digits for a line number: leave the location 
as it is
+                }
+            }
+        }
+        String rel;
+        int bang = s.lastIndexOf("!/");
+        if (bang >= 0) {
+            rel = s.substring(bang + 2);
+        } else if (s.startsWith("classpath:")) {
+            rel = s.substring("classpath:".length());
+        } else if (s.startsWith("file:")) {
+            rel = s.substring("file:".length());
+        } else {
+            rel = s;
+        }
+        rel = rel.replace('\\', '/');
+        try {
+            Path abs = Path.of(rel);
+            if (abs.isAbsolute() && Files.isRegularFile(abs)) {
+                return new SourceLocation(abs.startsWith(dir) ? 
relativePath(dir, abs) : abs.toString(), line, true);
+            }
+        } catch (InvalidPathException e) {
+            // not a path on this system; try it as a relative location below
+        }
+        while (rel.startsWith("/")) {
+            rel = rel.substring(1);
+        }
+        List<String> candidates = new ArrayList<>();
+        candidates.add("src/main/resources/" + rel);
+        candidates.add("src/main/java/" + rel);
+        if (!rel.contains("/") && !rel.contains(".java") && rel.contains(".")) 
{
+            // a Java route named by its class: org.acme.MyRoute
+            candidates.add("src/main/java/" + rel.replace('.', '/') + ".java");
+        }
+        candidates.add(rel);
+        for (String c : candidates) {
+            try {
+                Path p = dir.resolve(c).normalize();
+                if (p.startsWith(dir) && Files.isRegularFile(p)) {
+                    return new SourceLocation(c, line, true);
+                }
+            } catch (InvalidPathException e) {
+                // skip
+            }
+        }
+        // last resort: a file of that name anywhere in the project (a flat 
layout, a file that moved)
+        String leaf = rel.substring(rel.lastIndexOf('/') + 1);
+        if (!leaf.isEmpty()) {
+            for (Path p : projectFiles(dir)) {
+                if (p.getFileName().toString().equals(leaf)) {
+                    return new SourceLocation(relativePath(dir, p), line, 
true);
+                }
+            }
+        }
+        return new SourceLocation(rel, line, false);
+    }
+
+    /** The regular files under the directory, sorted by path, build and 
tooling directories skipped. */
+    private static List<Path> projectFiles(Path dir) {
+        List<Path> files = new ArrayList<>();
+        try {
+            Files.walkFileTree(dir, EnumSet.noneOf(FileVisitOption.class), 
MAX_DEPTH, new SimpleFileVisitor<>() {
+                @Override
+                public FileVisitResult preVisitDirectory(Path d, 
BasicFileAttributes attrs) {
+                    if (d.equals(dir)) {
+                        return FileVisitResult.CONTINUE;
+                    }
+                    String name = d.getFileName().toString();
+                    return SKIPPED_DIRS.contains(name) || name.startsWith(".")
+                            ? FileVisitResult.SKIP_SUBTREE : 
FileVisitResult.CONTINUE;
+                }
+
+                @Override
+                public FileVisitResult visitFile(Path f, BasicFileAttributes 
attrs) {
+                    if (attrs.isRegularFile() && 
!f.getFileName().toString().startsWith(".")) {
+                        files.add(f);
+                    }
+                    return files.size() >= SCAN_LIMIT ? 
FileVisitResult.TERMINATE : FileVisitResult.CONTINUE;
+                }
+
+                @Override
+                public FileVisitResult visitFileFailed(Path f, IOException e) {
+                    return FileVisitResult.CONTINUE;
+                }
+            });
+        } catch (IOException e) {
+            throw new ToolExecutionException("Cannot list " + dir + ": " + 
e.getMessage());
+        }
+        files.sort(Comparator.comparing((Path p) -> relativePath(dir, p), 
String.CASE_INSENSITIVE_ORDER));
+        return files;
+    }
+
+    static String relativePath(Path dir, Path p) {
+        return dir.relativize(p).toString().replace('\\', '/');
+    }
+
+    /**
+     * What a file is to Camel: {@code route} (a YAML, XML or Java file that 
defines routes, judged by its first lines),
+     * {@code config} ({@code application*.properties} or {@code .yaml}), 
{@code pom}, or null for anything else.
+     */
+    static String fileKind(Path p, String rel, String type) {
+        String leaf = rel.substring(rel.lastIndexOf('/') + 
1).toLowerCase(Locale.ROOT);
+        if (leaf.equals("pom.xml")) {
+            return "pom";
+        }
+        if (leaf.startsWith("application") && ("properties".equals(type) || 
"yaml".equals(type))) {
+            return "config";
+        }
+        if (leaf.endsWith(".camel.yaml") || leaf.endsWith(".camel.xml")) {
+            return "route";
+        }
+        if ("yaml".equals(type) || "xml".equals(type) || "java".equals(type)) {
+            String head = head(p);
+            if (head != null && isRouteSource(type, head)) {
+                return "route";
+            }
+        }
+        return null;
+    }
+
+    static boolean isRouteSource(String type, String head) {
+        return switch (type) {
+            case "yaml" -> YAML_ROUTE.matcher(head).find();
+            case "xml" -> head.contains("<routes") || head.contains("<route ") 
|| head.contains("<route>")
+                    || head.contains("<camelContext") || 
head.contains("<routeTemplates") || head.contains("<rests")
+                    || head.contains("<routeConfigurations");
+            case "java" -> head.contains("RouteBuilder");
+            default -> false;
+        };
+    }
+
+    /** The first bytes of a file, enough to tell what it defines; null when 
it cannot be read. */
+    private static String head(Path p) {
+        try (var in = Files.newInputStream(p)) {
+            return new String(in.readNBytes(8192), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    /**
+     * A file path relative to the directory (subdirectories allowed, as 
{@code camel_get_files} lists them); an
+     * absolute path or one that escapes the directory is refused.
+     */
+    public static Path resolveFile(Path dir, String file) {
         if (file == null || file.isBlank()) {
             throw new ToolExecutionException("file is required");
         }
-        Path path = dir.resolve(file).normalize();
-        if (!path.startsWith(dir) || !dir.equals(path.getParent())) {
-            throw new ToolExecutionException("file must be a plain file name 
in the directory " + dir);
+        Path path;
+        try {
+            if (Path.of(file).isAbsolute()) {
+                throw new ToolExecutionException("file must be a path relative 
to the directory " + dir + ", not " + file);
+            }
+            path = dir.resolve(file).normalize();
+        } catch (InvalidPathException e) {
+            throw new ToolExecutionException("Not a valid file path: " + file);
+        }
+        if (!path.startsWith(dir) || path.equals(dir)) {
+            throw new ToolExecutionException("file must stay inside the 
directory " + dir + ": " + file);
         }
         return path;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
index 5408d5b14286..8a48d3a9ea53 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
@@ -20,10 +20,12 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
@@ -119,9 +121,13 @@ class AuthoringToolsTest {
         Files.createDirectory(dir.resolve("sub"));
         JsonObject list = call("camel_get_files", new ToolContext(), 
Map.of("directory", dir.toString()));
         assertEquals(2, list.getInteger("totalFiles"), "directories are not 
listed");
+        assertEquals("flat", list.getString("layout"));
+        assertEquals(List.of("demo.camel.yaml"), 
list.getCollection("routeFiles"));
+        assertEquals(List.of("application.properties"), 
list.getCollection("configFiles"));
         JsonObject first = (JsonObject) 
list.getCollection("files").iterator().next();
         assertEquals("application.properties", first.getString("name"));
         assertEquals("properties", first.getString("type"));
+        assertEquals("config", first.getString("kind"));
         JsonObject file = call("camel_get_files", new ToolContext(),
                 Map.of("directory", dir.toString(), "file", 
"demo.camel.yaml"));
         assertEquals(VALID_ROUTE, file.getString("content"));
@@ -165,13 +171,107 @@ class AuthoringToolsTest {
     }
 
     @Test
-    void fileNamesStayInsideTheDirectory(@TempDir Path dir) {
-        for (String bad : List.of("../etc/passwd", "sub/x.yaml", 
"/tmp/x.yaml")) {
+    void filePathsStayInsideTheDirectoryButMayNameASubdirectory(@TempDir Path 
dir) throws IOException {
+        for (String bad : List.of("../etc/passwd", "/tmp/x.yaml", 
"sub/../../x.yaml")) {
             ToolExecutionException e = 
assertThrows(ToolExecutionException.class,
                     () -> ToolRegistry.execute("camel_write_file", new 
ToolContext(),
                             Map.of("directory", dir.toString(), "file", bad, 
"content", "x")));
-            assertTrue(e.getMessage().contains("plain file name"), bad + ": " 
+ e.getMessage());
+            assertTrue(e.getMessage().contains("inside the directory") || 
e.getMessage().contains("relative to"),
+                    bad + ": " + e.getMessage());
         }
+        // a relative path is fine, and its directories are created
+        JsonObject written = call("camel_write_file", new ToolContext(),
+                Map.of("directory", dir.toString(), "file", 
"src/main/resources/camel/x.camel.yaml", "content",
+                        VALID_ROUTE));
+        assertEquals("created", written.getString("status"));
+        assertEquals(VALID_ROUTE, 
Files.readString(dir.resolve("src/main/resources/camel/x.camel.yaml")));
+    }
+
+    @Test
+    void aMavenProjectListsItsRouteFilesFirstAndReadsByRelativePath(@TempDir 
Path dir) throws IOException {
+        Files.writeString(dir.resolve("pom.xml"), "<project/>");
+        Files.createDirectories(dir.resolve("src/main/resources/camel"));
+        Files.createDirectories(dir.resolve("src/main/java/org/acme"));
+        Files.createDirectories(dir.resolve("target/classes/camel"));
+        Files.createDirectories(dir.resolve(".mvn"));
+        
Files.writeString(dir.resolve("src/main/resources/camel/timer-log.camel.yaml"), 
VALID_ROUTE);
+        
Files.writeString(dir.resolve("src/main/resources/application.properties"), 
"camel.main.name=timer-log\n");
+        Files.writeString(dir.resolve("src/main/resources/log4j2.properties"), 
"rootLogger.level=info\n");
+        Files.writeString(dir.resolve("src/main/java/org/acme/MyRoute.java"),
+                "package org.acme;\nimport 
org.apache.camel.builder.RouteBuilder;\n"
+                                                                              
+ "public class MyRoute extends RouteBuilder { public void configure() { } 
}\n");
+        Files.writeString(dir.resolve("src/main/java/org/acme/Helper.java"), 
"package org.acme;\nclass Helper { }\n");
+        
Files.writeString(dir.resolve("target/classes/camel/timer-log.camel.yaml"), 
VALID_ROUTE);
+        Files.writeString(dir.resolve(".mvn/maven.config"), "-T1\n");
+        Files.writeString(dir.resolve("README.md"), "# demo\n");
+
+        JsonObject list = call("camel_get_files", new ToolContext(), 
Map.of("directory", dir.toString()));
+        assertEquals("maven", list.getString("layout"));
+        assertEquals(List.of("src/main/java/org/acme/MyRoute.java", 
"src/main/resources/camel/timer-log.camel.yaml"),
+                list.getCollection("routeFiles"));
+        assertEquals(List.of("src/main/resources/application.properties"), 
list.getCollection("configFiles"));
+        assertEquals(7, list.getInteger("totalFiles"), "target and .mvn are 
skipped");
+        
assertTrue(list.getString("hint").contains("src/main/resources/camel"), 
list.getString("hint"));
+        List<String> names = new ArrayList<>();
+        for (Object o : list.getCollection("files")) {
+            names.add(((JsonObject) o).getString("name"));
+        }
+        assertTrue(names.contains("pom.xml") && 
names.contains("src/main/resources/log4j2.properties"), names.toString());
+        assertTrue(names.stream().noneMatch(n -> n.startsWith("target/") || 
n.startsWith(".mvn/")), names.toString());
+
+        JsonObject file = call("camel_get_files", new ToolContext(),
+                Map.of("directory", dir.toString(), "file", 
"src/main/resources/camel/timer-log.camel.yaml"));
+        assertEquals(VALID_ROUTE, file.getString("content"));
+        ToolExecutionException e = assertThrows(ToolExecutionException.class,
+                () -> ToolRegistry.execute("camel_get_files", new 
ToolContext(),
+                        Map.of("directory", dir.toString(), "file", 
"camel/timer-log.camel.yaml")));
+        assertTrue(e.getMessage().contains("routeFiles"), "the error says how 
to find the file: " + e.getMessage());
+    }
+
+    @Test
+    void routeSourcesMapTheRuntimesLocationsOntoProjectFiles(@TempDir Path 
dir) throws IOException {
+        Files.createDirectories(dir.resolve("src/main/resources/camel"));
+        Files.createDirectories(dir.resolve("src/main/java/org/acme"));
+        
Files.writeString(dir.resolve("src/main/resources/camel/timer-log.camel.yaml"), 
VALID_ROUTE);
+        Files.writeString(dir.resolve("src/main/java/org/acme/MyRoute.java"), 
"class MyRoute {}");
+        Files.writeString(dir.resolve("flat.camel.yaml"), VALID_ROUTE);
+
+        // Spring Boot fat jar, Quarkus / classpath, a plain file, a Java 
class, a moved file and an unknown one
+        List<Map<String, String>> routes = List.of(
+                Map.of("routeId", "boot", "source", "nested:" + dir + 
"/target/app-1.0.jar/!BOOT-INF/classes/!/camel/"
+                                                    + 
"timer-log.camel.yaml:4"),
+                Map.of("routeId", "cp", "source", 
"classpath:camel/timer-log.camel.yaml:7"),
+                Map.of("routeId", "abs", "source", "file:" + 
dir.resolve("flat.camel.yaml") + ":1"),
+                Map.of("routeId", "java", "source", "org.acme.MyRoute:12"),
+                Map.of("routeId", "moved", "source", 
"file:old/place/flat.camel.yaml"),
+                Map.of("routeId", "gone", "source", 
"classpath:camel/nowhere.yaml:3"));
+        JsonArray mapped = AuthoringTools.routeSources(routes, dir);
+
+        assertEquals(6, mapped.size());
+        JsonObject boot = (JsonObject) mapped.get(0);
+        assertEquals("src/main/resources/camel/timer-log.camel.yaml", 
boot.getString("file"));
+        assertEquals(4, boot.getInteger("line"));
+        assertEquals(null, boot.get("missing"));
+        assertEquals("src/main/resources/camel/timer-log.camel.yaml", 
((JsonObject) mapped.get(1)).getString("file"));
+        assertEquals("flat.camel.yaml", ((JsonObject) 
mapped.get(2)).getString("file"));
+        assertEquals("src/main/java/org/acme/MyRoute.java", ((JsonObject) 
mapped.get(3)).getString("file"));
+        assertEquals(12, ((JsonObject) mapped.get(3)).getInteger("line"));
+        assertEquals("flat.camel.yaml", ((JsonObject) 
mapped.get(4)).getString("file"), "found by name");
+        JsonObject gone = (JsonObject) mapped.get(5);
+        assertEquals("camel/nowhere.yaml", gone.getString("file"));
+        assertEquals(Boolean.TRUE, gone.get("missing"));
+        assertTrue(AuthoringTools.routeSources(null, dir).isEmpty());
+        // a route without an id is listed by its file alone, not as "null"
+        JsonObject anonymous = (JsonObject) AuthoringTools.routeSources(
+                List.of(Map.of("source", "file:" + 
dir.resolve("flat.camel.yaml"))), dir).get(0);
+        assertEquals("flat.camel.yaml", anonymous.getString("file"));
+        assertFalse(anonymous.containsKey("routeId"));
+        // a digit suffix too long for a line number is not one: no exception, 
no line
+        JsonObject overflow = (JsonObject) AuthoringTools.routeSources(
+                List.of(Map.of("routeId", "big", "source", 
"file:flat.camel.yaml:2147483648")), dir).get(0);
+        assertEquals("flat.camel.yaml:2147483648", overflow.getString("file"));
+        assertFalse(overflow.containsKey("line"));
+        assertEquals(Boolean.TRUE, overflow.get("missing"));
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index 64ba31dfa8b5..d689b1acbb5c 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -120,12 +120,15 @@ public class AuthoringTools {
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "The source files of a project directory: without file 
the list (name, size, type), with "
-                        + "file its content. Use before editing to see the 
routes, configuration and other files of "
-                        + "the integration.")
+          description = "The source files of a project directory, 
subdirectories included: without file the list, "
+                        + "with the route and configuration files named first 
(routeFiles, configFiles) and, for a "
+                        + "running integration, which file and line each route 
comes from; with file (a path "
+                        + "relative to the directory, as listed) its content.")
     public JsonObject camel_get_files(
             @ToolArg(description = DIRECTORY_DESC, required = false) String 
directory,
-            @ToolArg(description = "File name to read; omitted lists the 
files", required = false) String file) {
+            @ToolArg(description = "File path relative to the directory, e.g. 
src/main/resources/camel/foo.camel.yaml,"
+                                   + " to read; omitted lists the files",
+                     required = false) String file) {
         return call("camel_get_files", args("directory", directory, "file", 
file));
     }
 
@@ -136,7 +139,8 @@ public class AuthoringTools {
                         + "camel_control.")
     public JsonObject camel_write_file(
             @ToolArg(description = DIRECTORY_DESC, required = false) String 
directory,
-            @ToolArg(description = "File name, no path", required = true) 
String file,
+            @ToolArg(description = "File path relative to the directory 
(subdirectories are created)",
+                     required = true) String file,
             @ToolArg(description = "The complete new content", required = 
true) String content,
             @ToolArg(description = "Validate before writing (default true)", 
required = false) Boolean validate,
             @ToolArg(description = VERSION_DESC, required = false) String 
camelVersion) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 095b3dfc467d..b44bf2d9c51f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -37,7 +37,9 @@ import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
 import dev.tamboui.widgets.tabs.TabsState;
+import org.apache.camel.dsl.jbang.core.commands.ai.AuthoringTools;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
 import org.apache.camel.util.json.JsonArray;
@@ -288,6 +290,11 @@ class McpFacade {
         return ctx != null ? ctx.selectedPid : null;
     }
 
+    /** Whether a running integration goes by this name (a model often passes 
it where a directory is asked for). */
+    boolean hasIntegration(String name) {
+        return name != null && !name.isEmpty() && findIntegration(name) != 
null;
+    }
+
     String getSelectedIntegrationName() {
         if (ctx == null) {
             return null;
@@ -978,8 +985,9 @@ class McpFacade {
         }
         if (content == null) {
             JsonObject existing = getFiles(name, file);
-            if (existing == null) {
-                return writeError("No such file in the source directory: " + 
file);
+            if (existing == null || existing.getString("content") == null) {
+                String error = existing != null ? existing.getString("error") 
: null;
+                return writeError(error != null ? error : "No such file in the 
source directory: " + file);
             }
             content = existing.getString("content");
         }
@@ -1026,6 +1034,12 @@ class McpFacade {
         result.put("editing", editing);
     }
 
+    /**
+     * The shared {@code camel_get_files} on the integration's source 
directory, with what only the TUI knows: whether
+     * the directory is editable and, for the listing, which file and line 
each running route comes from. A missing file
+     * or a bad path comes back as {@code error} with the directory, so the 
model corrects the path instead of
+     * concluding the sources are gone; null only when there is no integration 
or no source directory.
+     */
     JsonObject getFiles(String name, String file) {
         IntegrationInfo target = findIntegration(name);
         if (target == null) {
@@ -1035,56 +1049,40 @@ class McpFacade {
         if (dir == null || !Files.isDirectory(dir)) {
             return null;
         }
-        if (file != null && !file.isEmpty()) {
-            Path filePath = dir.resolve(file).normalize();
-            if (!filePath.startsWith(dir) || !Files.isRegularFile(filePath)) {
-                return null;
+        JsonObject result;
+        try {
+            result = file != null && !file.isEmpty()
+                    ? AuthoringTools.readFile(dir, file) : 
AuthoringTools.listFiles(dir);
+        } catch (ToolExecutionException e) {
+            JsonObject error = new JsonObject();
+            error.put("error", e.getMessage());
+            describeSourceDirectory(target, dir, error);
+            return error;
+        }
+        describeSourceDirectory(target, dir, result);
+        if (file == null || file.isEmpty()) {
+            JsonArray routes = new JsonArray();
+            for (RouteInfo r : target.routes) {
+                if (r.source != null && !r.source.isBlank()) {
+                    JsonObject j = new JsonObject();
+                    if (r.routeId != null) {
+                        j.put("routeId", r.routeId);
+                    }
+                    j.put("source", r.source);
+                    routes.add(j);
+                }
             }
-            try {
-                String content = Files.readString(filePath, 
StandardCharsets.UTF_8);
-                JsonObject result = new JsonObject();
-                result.put("file", file);
-                describeSourceDirectory(target, dir, result);
-                result.put("size", 
FilesBrowser.formatFileSize(Files.size(filePath)));
-                result.put("type", FilesBrowser.fileType(filePath));
-                result.put("content", content);
-                return result;
-            } catch (IOException e) {
-                return null;
+            JsonArray mapped = AuthoringTools.routeSources(routes, dir);
+            if (!mapped.isEmpty()) {
+                result.put("routes", mapped);
             }
         }
-        JsonArray files = new JsonArray();
-        try (var stream = Files.list(dir)) {
-            stream.filter(Files::isRegularFile)
-                    .sorted((a, b) -> 
a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
-                    .limit(99)
-                    .forEach(p -> {
-                        JsonObject entry = new JsonObject();
-                        entry.put("name", p.getFileName().toString());
-                        try {
-                            entry.put("size", 
FilesBrowser.formatFileSize(Files.size(p)));
-                        } catch (IOException e) {
-                            entry.put("size", "0 B");
-                        }
-                        entry.put("type", FilesBrowser.fileType(p));
-                        files.add(entry);
-                    });
-        } catch (IOException e) {
-            return null;
-        }
-        if (files.isEmpty()) {
-            return null;
-        }
-        JsonObject result = new JsonObject();
-        describeSourceDirectory(target, dir, result);
-        result.put("files", files);
-        result.put("totalFiles", files.size());
         return result;
     }
 
     /**
      * Writes (creates or replaces) a file in the integration's source 
directory, after the user confirmed it in the TUI
-     * unless {@code confirm} is false. The file must be a plain file name in 
that directory.
+     * unless {@code confirm} is false. The file is a path relative to that 
directory.
      */
     JsonObject writeFile(String name, String file, String content, boolean 
confirm) {
         return writeFile(name, file, content, confirm, true);
@@ -1111,9 +1109,11 @@ class McpFacade {
         if (content == null) {
             return writeError("content is required");
         }
-        Path filePath = dir.resolve(file).normalize();
-        if (!filePath.startsWith(dir) || !dir.equals(filePath.getParent())) {
-            return writeError("file must be a plain file name in the source 
directory " + dir);
+        Path filePath;
+        try {
+            filePath = AuthoringTools.resolveFile(dir, file);
+        } catch (ToolExecutionException e) {
+            return writeError(e.getMessage());
         }
         boolean exists = Files.exists(filePath);
         if (exists && !Files.isRegularFile(filePath)) {
@@ -1183,6 +1183,7 @@ class McpFacade {
             }
         }
         try {
+            Files.createDirectories(filePath.getParent());
             Files.writeString(filePath, content, StandardCharsets.UTF_8);
         } catch (IOException e) {
             return writeError("Failed to write " + filePath + ": " + 
e.getMessage());
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
index f8513a655ca4..20b0490827aa 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteInfo.java
@@ -22,6 +22,8 @@ import java.util.List;
 class RouteInfo {
     String routeId;
     String description;
+    /** Where the runtime loaded the route from (a file, classpath resource or 
jar entry, with the line). */
+    String source;
     String group;
     String from;
     String state;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index 72ac72430175..493d06d1733f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -247,6 +247,7 @@ final class StatusParser {
                 RouteInfo ri = new RouteInfo();
                 ri.routeId = rj.getString("routeId");
                 ri.description = rj.getString("description");
+                ri.source = rj.getString("source");
                 ri.group = rj.getString("group");
                 ri.from = rj.getString("from");
                 ri.state = rj.getString("state");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 4da3058ec206..107be4c2a77b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -91,8 +92,17 @@ class TuiToolRegistry {
      * camel_control knows the TUI's own actions, camel_get_log also reads an 
infra service log.
      */
     private String executeSharedWithTuiExtras(String name, Map<String, Object> 
args) {
+        if (facade != null && args.get("directory") instanceof String d && 
!d.isBlank() && !d.contains("/")
+                && !d.contains("\\") && facade.hasIntegration(d)) {
+            // a model often passes the integration's name where the tool asks 
for its directory
+            Map<String, Object> byName = new HashMap<>(args);
+            byName.remove("directory");
+            byName.put("name", d);
+            args = byName;
+        }
         boolean hasDirectory = args.get("directory") instanceof String d && 
!d.isBlank();
-        boolean facadeSelection = facade != null && 
facade.getSelectedIntegrationName() != null;
+        boolean named = args.get("name") instanceof String n && !n.isBlank();
+        boolean facadeSelection = facade != null && (named || 
facade.getSelectedIntegrationName() != null);
         return switch (name) {
             case CONTROL_TOOL -> facade != null ? callControl(args) : 
executeShared(name, args);
             case LOG_TOOL -> facade != null ? callGetLog(args) : 
executeShared(name, args);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeGetFilesTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeGetFilesTest.java
new file mode 100644
index 000000000000..90c38b4b26dc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeGetFilesTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@code camel_get_files} on the selected integration: an exported Spring 
Boot project is a Maven layout, and the
+ * running routes point at the files under {@code src/main}, so a model finds 
the route file in one call instead of
+ * guessing names in the wrong folder (CAMEL-24798).
+ */
+class McpFacadeGetFilesTest {
+
+    private static final String ROUTE = "- route:\n    id: timer-log\n    
from:\n      uri: timer:tick\n"
+                                        + "      steps:\n        - log:\n      
      message: hi\n";
+
+    private static McpFacade facade(Path projectDir, String routeSource) {
+        return facade(projectDir, "timer-log", routeSource);
+    }
+
+    private static McpFacade facade(Path projectDir, String routeId, String 
routeSource) {
+        IntegrationInfo info = new IntegrationInfo();
+        info.name = "timer-log";
+        info.pid = "1";
+        info.directory = projectDir.toString();
+        RouteInfo route = new RouteInfo();
+        route.routeId = routeId;
+        route.source = routeSource;
+        info.routes.add(route);
+        return new McpFacade(
+                null, new AtomicReference<>(List.of(info)), null, null, null, 
null, null, null, null, null, null,
+                null, null);
+    }
+
+    @Test
+    void 
anExportedProjectListsItsRouteFileAndWhereTheRunningRouteComesFrom(@TempDir 
Path dir) throws IOException {
+        Files.createDirectories(dir.resolve("src/main/resources/camel"));
+        Files.createDirectories(dir.resolve("target/classes/camel"));
+        Files.writeString(dir.resolve("pom.xml"), "<project/>");
+        
Files.writeString(dir.resolve("src/main/resources/camel/timer-log.camel.yaml"), 
ROUTE);
+        
Files.writeString(dir.resolve("src/main/resources/application.properties"), 
"camel.main.name=timer-log\n");
+        
Files.writeString(dir.resolve("target/classes/camel/timer-log.camel.yaml"), 
ROUTE);
+        McpFacade facade = facade(dir, "nested:" + dir
+                                       + 
"/target/timer-log-1.0-SNAPSHOT.jar/!BOOT-INF/classes/!/camel/timer-log.camel.yaml:4");
+
+        JsonObject list = facade.getFiles("timer-log", null);
+
+        assertNotNull(list);
+        assertEquals("maven", list.getString("layout"));
+        assertEquals(List.of("src/main/resources/camel/timer-log.camel.yaml"), 
list.getCollection("routeFiles"));
+        assertEquals(dir.toString(), list.getString("directory"));
+        assertNotNull(list.getString("editing"), "the TUI still says whether 
the directory is editable");
+        JsonArray routes = (JsonArray) list.get("routes");
+        assertEquals(1, routes.size());
+        JsonObject route = (JsonObject) routes.get(0);
+        assertEquals("timer-log", route.getString("routeId"));
+        assertEquals("src/main/resources/camel/timer-log.camel.yaml", 
route.getString("file"));
+        assertEquals(4, route.getInteger("line"));
+        assertNull(route.get("missing"));
+
+        JsonObject file = facade.getFiles("timer-log", 
"src/main/resources/camel/timer-log.camel.yaml");
+        assertEquals(ROUTE, file.getString("content"));
+        assertNull(file.get("routes"), "reading one file does not repeat the 
route list");
+    }
+
+    @Test
+    void theIntegrationNameIsAcceptedWhereTheToolAsksForADirectory(@TempDir 
Path dir) throws Exception {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+        TuiToolRegistry registry = new TuiToolRegistry(facade(dir, "file:" + 
dir.resolve("demo.camel.yaml") + ":1"));
+
+        // the model passed the integration's name as the directory: answered 
for that integration, not an error
+        String answer = registry.execute("camel_get_files", 
Map.of("directory", "timer-log"));
+        assertTrue(answer.contains("\"routeFiles\":[\"demo.camel.yaml\"]"), 
answer);
+        assertTrue(answer.contains("\"editing\""), "answered by the TUI, with 
its directory knowledge: " + answer);
+    }
+
+    @Test
+    void aRouteWithoutAnIdIsListedByItsFileAlone(@TempDir Path dir) throws 
IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+        McpFacade facade = facade(dir, null, "file:" + 
dir.resolve("demo.camel.yaml") + ":1");
+
+        JsonObject list = facade.getFiles("timer-log", null);
+
+        JsonArray routes = (JsonArray) list.get("routes");
+        assertEquals(1, routes.size());
+        JsonObject route = (JsonObject) routes.get(0);
+        assertEquals("demo.camel.yaml", route.getString("file"));
+        assertFalse(route.containsKey("routeId"), "an anonymous route has no 
routeId key, not a \"null\" one: " + route);
+    }
+
+    @Test
+    void aMissingFileIsAnErrorWithTheDirectoryNotAMissingProject(@TempDir Path 
dir) throws IOException {
+        Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
+        McpFacade facade = facade(dir, "file:" + 
dir.resolve("demo.camel.yaml") + ":1");
+
+        JsonObject missing = facade.getFiles("timer-log", "routes/demo.yaml");
+        assertNotNull(missing);
+        assertTrue(missing.getString("error").contains("No such file"), 
missing.getString("error"));
+        assertEquals(dir.toString(), missing.getString("directory"));
+
+        JsonObject escaped = facade.getFiles("timer-log", 
"../demo.camel.yaml");
+        assertTrue(escaped.getString("error").contains("inside the 
directory"), escaped.getString("error"));
+
+        JsonObject list = facade.getFiles("timer-log", null);
+        assertEquals("flat", list.getString("layout"));
+        assertEquals("demo.camel.yaml", ((JsonObject) ((JsonArray) 
list.get("routes")).get(0)).getString("file"));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
index 6e94fc252192..0d59e4edcbf6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacadeWriteFileTest.java
@@ -300,13 +300,20 @@ class McpFacadeWriteFileTest {
         McpFacade facade = facade(dir, true, bridge);
 
         assertEquals("error", facade.writeFile("demo", "../escape.yaml", "x", 
true).getString("status"));
-        assertEquals("error", facade.writeFile("demo", "sub/dir.yaml", "x", 
true).getString("status"));
+        assertEquals("error", facade.writeFile("demo", "sub/../../dir.yaml", 
"x", true).getString("status"));
+        assertEquals("error", facade.writeFile("demo", 
dir.resolve("abs.yaml").toString(), "x", true).getString("status"));
         assertEquals("error", facade.writeFile("demo", "", "x", 
true).getString("status"));
         assertEquals("error", facade.writeFile("demo", "demo.camel.yaml", 
null, true).getString("status"));
         assertEquals("error", facade.writeFile("nope", "demo.camel.yaml", "x", 
true).getString("status"));
         assertEquals(0, bridge.asked);
         assertFalse(Files.exists(dir.getParent().resolve("escape.yaml")));
 
+        // a path into a subdirectory is fine (a Maven project keeps its 
routes under src/main/resources/camel)
+        JsonObject created = facade.writeFile("demo", "sub/dir.camel.yaml", "- 
route: {}", true);
+        assertEquals("created", created.getString("status"));
+        assertEquals("sub/dir.camel.yaml", bridge.request.file());
+        assertTrue(Files.isRegularFile(dir.resolve("sub/dir.camel.yaml")));
+
         // reading reports the same directory knowledge the agent needs before 
writing
         JsonObject files = facade.getFiles("demo", null);
         assertNotNull(files);

Reply via email to