This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit f5c97f33bc5cae35e22ca48fb9869111f8e36644 Author: Andrus Adamchik <[email protected]> AuthorDate: Thu Jun 25 18:44:08 2026 -0400 CAY-2965 MCP Cgen should not fail on an absent "<cgen>" tag --- RELEASE-NOTES.txt | 1 + ai-plugin/references/cgen-config.md | 6 ++-- ai-plugin/references/mcp-tools.md | 8 +++-- ai-plugin/skills/cayenne-cgen/SKILL.md | 11 +++--- cayenne-mcp-server/PROTOCOL.md | 4 +-- .../apache/cayenne/mcp/tools/cgen/CgenRunTool.java | 36 ++++++++++++++----- .../mcp/tools/cgen/protocol/CgenErrorCode.java | 1 - .../apache/cayenne/mcp/tools/cgen/CgenRunIT.java | 41 ++++++++++++++++++++++ .../mcp/tools/cgen/CgenRunValidationTest.java | 16 --------- 9 files changed, 86 insertions(+), 38 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 67f12f917..c04c01616 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -26,6 +26,7 @@ CAY-2959 Modeler: DbRelationship dialog "Cancel" doesn't cancel CAY-2960 Undoing renamed relationship change throws CAY-2961 PostgreSQL "text" column is reverse-engineered as CLOB CAY-2964 ClassCastException for non-generated meaningful PKs +CAY-2965 MCP Cgen should not fail on an absent "<cgen>" tag ---------------------------------- Release: 5.0-M2 diff --git a/ai-plugin/references/cgen-config.md b/ai-plugin/references/cgen-config.md index 08ab76be0..0a755c8ce 100644 --- a/ai-plugin/references/cgen-config.md +++ b/ai-plugin/references/cgen-config.md @@ -106,11 +106,13 @@ The MCP tool: 1. Loads the project at `projectPath`. 2. Finds the named DataMap (`dataMap` argument). -3. Reads its embedded `<cgen>` block. +3. Reads its embedded `<cgen>` block — or, if there is none, synthesizes a default config (see below). 4. Resolves `destDir` relative to the `*.map.xml` file. 5. Runs the generator. Returns a JSON object listing files `written`, files `skipped` (already up-to-date), and any `errors`. -If the DataMap has no `<cgen>` block, the tool returns an error and the user must add one — typically via the `cayenne-modeling` skill or the Modeler GUI. +If the DataMap has no `<cgen>` block, the tool does **not** fail. It synthesizes a default config — all entities and embeddables, `makePairs=true`, with `destDir` derived from the Maven layout (`src/main/resources` → `src/main/java`, likewise for `test`; non-Maven layouts fall back to the DataMap's own directory). This mirrors what the Maven plugin and CayenneModeler do. + +The synthesized config is **not** written back to the DataMap — it is used for that one run only. If the user wants to persist or customize it (different destination, templates, entity filtering), add a `<cgen>` block via the `cayenne-modeling` skill or the Modeler GUI. The starting config below is a good template for that. ## Determining `destDir` diff --git a/ai-plugin/references/mcp-tools.md b/ai-plugin/references/mcp-tools.md index 2be65650d..cad3c0c8a 100644 --- a/ai-plugin/references/mcp-tools.md +++ b/ai-plugin/references/mcp-tools.md @@ -32,7 +32,7 @@ Verify the server is registered with `claude mcp list` — you should see an ent ## Tool: `cgen_run` -Runs Cayenne's class generator for one DataMap, using the `<cgen>` config block embedded in that DataMap. +Runs Cayenne's class generator for one DataMap. Uses the `<cgen>` config block embedded in that DataMap if present; if the DataMap has no `<cgen>` block, the tool synthesizes a default config (all entities and embeddables, `makePairs=true`, destination derived from the standard Maven layout) and generates anyway — a missing block is **not** an error. **Arguments:** @@ -47,7 +47,7 @@ Runs Cayenne's class generator for one DataMap, using the `<cgen>` config block { "status": "ok" | "error", "summary": { "writtenCount": 3, "skippedCount": 12, "errorCount": 0 }, - "resolvedConfig": { "destDir": "...", "mode": "entity", ... }, + "resolvedConfig": { "destDir": "..." }, "writtenFiles": [{ "path": "...", "size": 1234 }], "skippedFiles": [{ "path": "...", "reason": "up-to-date" }], "errors": [] @@ -56,11 +56,13 @@ Runs Cayenne's class generator for one DataMap, using the `<cgen>` config block Surface the `summary` to the user verbatim. List the first few `writtenFiles` paths; full list is informational. If `errors` is non-empty, those are blocking — show them. +When the DataMap had no `<cgen>` block, the tool generated from a synthesized default config. The default destination is derived from the Maven layout (`src/main/resources` → `src/main/java`); for non-Maven layouts it falls back to the DataMap's own directory, so check `resolvedConfig.destDir` and confirm with the user if it looks wrong. Offer to persist a `<cgen>` block (via `cayenne-modeling` or the Modeler) if they want to customize destination, templates, or filtering. + **Failure modes:** -- DataMap has no `<cgen>` block → error; user must add one (see `cgen-config.md` for the XML to insert). - `projectPath` not readable or not a valid Cayenne project → validation error. - `dataMap` doesn't match any `<map name="...">` → validation error. +- `<cgen>` block present but specifies no resolvable destination directory → `destdir_not_specified`. **Source:** `cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java`. diff --git a/ai-plugin/skills/cayenne-cgen/SKILL.md b/ai-plugin/skills/cayenne-cgen/SKILL.md index 5a7ed1912..73b833ee9 100644 --- a/ai-plugin/skills/cayenne-cgen/SKILL.md +++ b/ai-plugin/skills/cayenne-cgen/SKILL.md @@ -23,7 +23,7 @@ description: "Use this skill whenever the user wants to (re)generate Cayenne ent --> # cayenne-cgen -Run Cayenne's class generator on a DataMap via the `mcp__cayenne__cgen_run` MCP tool. The tool reads the embedded `<cgen>` block in the DataMap to determine destination, mode, templates, etc.; if no block is present the tool reports an error and the skill adds a default one then retries. +Run Cayenne's class generator on a DataMap via the `mcp__cayenne__cgen_run` MCP tool. The tool reads the embedded `<cgen>` block in the DataMap to determine destination, mode, templates, etc.; **if no block is present the tool generates anyway using a sensible default config** (all entities, `makePairs=true`, destination derived from the Maven layout). A missing `<cgen>` block is **not** an error and must never stop you from running the tool. ## Required reading @@ -51,11 +51,9 @@ mcp__cayenne__cgen_run({ }) ``` -If the tool is not available (MCP server not registered), surface `cayenne-mcp-server/README.md` and stop. **Do not** suggest `mvn cayenne:cgen` or the Gradle cgen task. - -If the tool errors because no `<cgen>` block is present in the DataMap, add a minimal one using the starting config approach from `cgen-config.md` +Call the tool directly for each DataMap — never inspect the DataMap for a `<cgen>` block first, and never treat its absence as a blocker. The tool generates with a default config when no block exists. -If the `destDir` can't be determined reliably (non-standard project layout, empty project, etc.), conform it with the user +If the tool is not available (MCP server not registered), surface `cayenne-mcp-server/README.md` and stop. **Do not** suggest `mvn cayenne:cgen` or the Gradle cgen task. ## Step 3 — Surface the result @@ -64,6 +62,7 @@ The tool returns structured JSON. Report: - `summary.writtenCount`, `summary.skippedCount`, `summary.errorCount` verbatim — these are the headline. - The first few entries in `writtenFiles` (relative paths). Full list is informational; offer to dump it if the user asks. - Any entries in `errors` — these are blocking. Read the messages and explain in user terms (a missing entity class name, a bad template path, an invalid `<destDir>`, etc.). +- `resolvedConfig.destDir` — the absolute output directory. If the DataMap had no `<cgen>` block, the tool generated from a synthesized default; report this destination so the user can confirm it's right, and offer to persist a `<cgen>` block (via `cayenne-modeling`) if they want to customize destination, templates, or entity filtering. If `writtenCount` is 0 and `skippedCount` covers everything, say so — it means everything is already up-to-date and no work was needed. @@ -74,7 +73,7 @@ If `writtenCount` is 0 and `skippedCount` covers everything, say so — it means ## Anti-patterns -- **Do not** pre-check the DataMap for a `<cgen>` block before calling `cgen_run`. Call the tool first; add the default config only if the tool reports it is missing. +- **Do not** pre-check the DataMap for a `<cgen>` block before calling `cgen_run`, and **do not** add a block before running. A missing block is not an error — the tool generates with defaults. Only add a `<cgen>` block afterward if the user wants to persist or customize the config. - **Do not** suggest Maven (`mvn cayenne:cgen`) or Gradle (`cayenneCgen`) goals when MCP is unavailable. Those build plugins are out of scope. Point at MCP setup instead. - **Do not** edit `_<Entity>.java` files. Generated superclasses. Edit `<Entity>.java` subclasses. - **Do not** confuse `projectPath` and `dataMap` arguments. `projectPath` is a file system path to `cayenne-*.xml`. `dataMap` is a logical name (e.g. `mydb`), not a path to `mydb.map.xml`. diff --git a/cayenne-mcp-server/PROTOCOL.md b/cayenne-mcp-server/PROTOCOL.md index b5d78d764..3d3e7b4e9 100644 --- a/cayenne-mcp-server/PROTOCOL.md +++ b/cayenne-mcp-server/PROTOCOL.md @@ -9,7 +9,7 @@ A result always carries: ## `cgen_run` -Runs Cayenne class generation for a named DataMap. +Runs Cayenne class generation for a named DataMap. If the DataMap has an embedded `<cgen>` block it is used; otherwise the tool synthesizes a default config (all entities and embeddables, `makePairs=true`, destination derived from the Maven layout) and generates anyway. A missing `<cgen>` block is not an error. ### Input @@ -50,7 +50,7 @@ Both fields are required. "destDirWritable": true }, "error": { - "code": "project_not_found | project_parse_failed | datamap_not_found | cgen_config_missing | destdir_not_specified | destdir_not_writable | cgen_runtime_error", + "code": "project_not_found | project_parse_failed | datamap_not_found | destdir_not_specified | destdir_not_writable | cgen_runtime_error", "message": "string" } } diff --git a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java index 4b027f81b..bbf45f51f 100644 --- a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java +++ b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/CgenRunTool.java @@ -28,6 +28,7 @@ import org.apache.cayenne.gen.CgenConfigList; import org.apache.cayenne.gen.CgenConfiguration; import org.apache.cayenne.gen.MetadataUtils; import org.apache.cayenne.gen.ToolsUtilsFactory; +import org.apache.cayenne.gen.internal.Utils; import org.apache.cayenne.map.DataMap; import org.apache.cayenne.mcp.project.McpProjectLoaderModule; import org.apache.cayenne.mcp.log.McpLoggingHandler; @@ -157,16 +158,15 @@ public class CgenRunTool { new CgenValidation(true, false, null, null, null)); } - // Step 4 — cgen configuration present? + // Step 4 — cgen configuration: use the <cgen> block stored in the DataMap, or, if there is none, + // synthesize a default. This mirrors CayenneModeler and the Maven/Gradle plugins, all of which + // fall back to a default config rather than failing — a missing <cgen> block is not an error. DataChannelMetaData metaData = injector.getInstance(DataChannelMetaData.class); CgenConfigList configList = metaData.get(dataMap, CgenConfigList.class); - if (configList == null || configList.getAll().isEmpty()) { - return validationFailed(CgenErrorCode.cgen_config_missing, - "DataMap '" + dataMapName + "' has no <cgen> configuration. " - + "Configure code generation for this DataMap in CayenneModeler before invoking the tool.", - new CgenValidation(true, true, false, null, null)); - } - CgenConfiguration cgenConfig = configList.getAll().getFirst(); + boolean usedDefaultConfig = configList == null || configList.getAll().isEmpty(); + CgenConfiguration cgenConfig = usedDefaultConfig + ? defaultConfig(dataMap) + : configList.getAll().getFirst(); // Set the DataMap file's mtime as the timestamp so fileNeedUpdate() can detect DataMap changes correctly. if (dataMap.getConfigurationSource() != null) { @@ -246,6 +246,26 @@ public class CgenRunTool { ); } + /** + * Builds a default cgen configuration for a DataMap that has no embedded {@code <cgen>} block. + * Mirrors {@code CgenPanel.createDefaultCgenConfiguration} in CayenneModeler: generate every + * entity and embeddable, with the destination derived from the standard Maven source layout + * ({@code src/main/resources} → {@code src/main/java}, likewise for {@code test}). For projects + * that don't follow the Maven layout the destination falls back to the DataMap's own directory. + */ + private static CgenConfiguration defaultConfig(DataMap dataMap) { + Path mapDir = Utils.getRootPathForDataMap(dataMap); + Path outputPath = Utils.getMavenSrcPathForPath(mapDir).map(Path::of).orElse(mapDir); + + CgenConfiguration config = new CgenConfiguration(); + config.setDataMap(dataMap); + dataMap.getObjEntities().forEach(config::loadEntity); + dataMap.getEmbeddables().forEach(config::loadEmbeddable); + config.setRootPath(mapDir); + config.updateOutputPath(outputPath); + return config; + } + private static CgenRunResult validationFailed(CgenErrorCode code, String message, CgenValidation validation) { return new CgenRunResult( "validation_failed", diff --git a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/protocol/CgenErrorCode.java b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/protocol/CgenErrorCode.java index e2fde135d..2beface80 100644 --- a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/protocol/CgenErrorCode.java +++ b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/cgen/protocol/CgenErrorCode.java @@ -28,7 +28,6 @@ public enum CgenErrorCode { project_not_found, project_parse_failed, datamap_not_found, - cgen_config_missing, destdir_not_specified, destdir_not_writable, cgen_runtime_error diff --git a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java index f02c14e15..3401918c2 100644 --- a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java +++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunIT.java @@ -142,6 +142,47 @@ public class CgenRunIT { assertTrue(second.summary().filesWritten() > 0); } + @Test + public void generatesWithDefaultConfigWhenNoCgenBlock() throws IOException { + // A DataMap with no <cgen> block, laid out in a standard Maven structure. The tool must + // synthesize a default config rather than failing, deriving destDir from src/main/resources + // → src/main/java. + Path resources = tempDir.resolve("src/main/resources"); + Path mapFile = resources.resolve("DefaultMap.map.xml"); + Files.createDirectories(resources); + Files.writeString(resources.resolve("cayenne-project.xml"), """ + <?xml version="1.0" encoding="utf-8"?> + <domain xmlns="http://cayenne.apache.org/schema/12/domain" project-version="12"> + <map name="DefaultMap"/> + </domain> + """); + Files.writeString(mapFile, """ + <?xml version="1.0" encoding="utf-8"?> + <data-map xmlns="http://cayenne.apache.org/schema/12/modelMap" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + project-version="12"> + <property name="defaultPackage" value="com.example"/> + <obj-entity name="Person" className="com.example.Person"/> + </data-map> + """); + + CgenRunResult result = tool.run(resources.resolve("cayenne-project.xml").toString(), "DefaultMap"); + + assertEquals("generated", result.status(), "Missing <cgen> block must not block generation"); + assertNull(result.error()); + assertNotNull(result.resolved()); + + // src/main/resources → src/main/java + Path expectedDest = tempDir.resolve("src/main/java"); + assertEquals(expectedDest.toAbsolutePath().toString(), result.resolved().destDir()); + + // makePairs defaults to true → superclass under the ".auto" package + user subclass + assertTrue(Files.exists(expectedDest.resolve("com/example/auto/_Person.java")), + "Expected generated superclass"); + assertTrue(Files.exists(expectedDest.resolve("com/example/Person.java")), + "Expected generated subclass"); + } + private Path writeFixture(String mapName, String pkg, Path destDir, boolean makePairs) throws IOException { // Project descriptor diff --git a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunValidationTest.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunValidationTest.java index 716495dda..32318b67a 100644 --- a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunValidationTest.java +++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/cgen/CgenRunValidationTest.java @@ -96,22 +96,6 @@ public class CgenRunValidationTest { assertNull(result.validation().destDirWritable()); } - @Test - public void cgenConfigMissing() throws URISyntaxException { - String projectPath = fixtureProject("no-cgen"); - - CgenRunResult result = tool.run(projectPath, "TestMap"); - - assertEquals("validation_failed", result.status()); - assertEquals(CgenErrorCode.cgen_config_missing, result.error().code()); - - assertTrue(result.validation().projectFound()); - assertTrue(result.validation().dataMapFound()); - assertFalse(result.validation().cgenConfigPresent()); - assertNull(result.validation().destDirSpecified()); - assertNull(result.validation().destDirWritable()); - } - @Test public void destDirNotSpecified() throws URISyntaxException { String projectPath = fixtureProject("no-destdir");
