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

asf-gitbox-commits pushed a commit to branch CAY-2936
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 5b47664ea2d2417b89e8440a5d2d329daaac656d
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sat May 9 15:05:04 2026 -0400

    CAY-2936 Add a placeholder MCP server to the Modeler distribution
---
 assembly/pom.xml                                   |   6 +
 .../assemblies/generic/assembly-generic.xml        |   7 ++
 .../main/resources/assemblies/mac/assembly-mac.xml |   8 ++
 .../assemblies/windows/assembly-windows.xml        |   7 ++
 cayenne-mcp-server/pom.xml                         | 131 +++++++++++++++++++++
 .../org/apache/cayenne/mcp/CayenneMcpMain.java     |  54 +++++++++
 .../org/apache/cayenne/mcp/CayenneMcpOptions.java  |  60 ++++++++++
 .../org/apache/cayenne/mcp/CayenneMcpServer.java   |  65 ++++++++++
 .../java/org/apache/cayenne/mcp/StderrLogging.java |  58 +++++++++
 .../org/apache/cayenne/mcp/tools/HelloTool.java    |  51 ++++++++
 .../apache/cayenne/mcp/CayenneMcpOptionsTest.java  |  60 ++++++++++
 .../org/apache/cayenne/mcp/CayenneMcpServerIT.java |  72 +++++++++++
 .../java/org/apache/cayenne/mcp/HelloMcpIT.java    | 128 ++++++++++++++++++++
 .../java/org/apache/cayenne/mcp/HelloToolTest.java |  49 ++++++++
 .../org/apache/cayenne/mcp/ServerJarLocator.java   |  27 +++++
 modeler/cayenne-modeler-mac/pom.xml                |   1 +
 pom.xml                                            |  13 ++
 17 files changed, 797 insertions(+)

diff --git a/assembly/pom.xml b/assembly/pom.xml
index 632b226e9..4f5a29e73 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -124,6 +124,12 @@
                        <version>${project.version}</version>
                </dependency>
 
+               <dependency>
+                       <groupId>org.apache.cayenne</groupId>
+                       <artifactId>cayenne-mcp-server</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
        </dependencies>
 
        <build>
diff --git 
a/assembly/src/main/resources/assemblies/generic/assembly-generic.xml 
b/assembly/src/main/resources/assemblies/generic/assembly-generic.xml
index 123820413..96cc74674 100644
--- a/assembly/src/main/resources/assemblies/generic/assembly-generic.xml
+++ b/assembly/src/main/resources/assemblies/generic/assembly-generic.xml
@@ -114,5 +114,12 @@
                                </excludes>
                        </unpackOptions>
                </dependencySet>
+               <dependencySet>
+                       <outputDirectory>bin</outputDirectory>
+                       <includes>
+                               
<include>org.apache.cayenne:cayenne-mcp-server</include>
+                       </includes>
+                       <unpack>false</unpack>
+               </dependencySet>
        </dependencySets>
 </assembly>
diff --git a/assembly/src/main/resources/assemblies/mac/assembly-mac.xml 
b/assembly/src/main/resources/assemblies/mac/assembly-mac.xml
index a2f2b4dc0..fa8efd583 100644
--- a/assembly/src/main/resources/assemblies/mac/assembly-mac.xml
+++ b/assembly/src/main/resources/assemblies/mac/assembly-mac.xml
@@ -112,5 +112,13 @@
                                </excludes>
                        </unpackOptions>
                </dependencySet>
+               <dependencySet>
+                       
<outputDirectory>CayenneModeler.app/Contents/Resources/mcp/</outputDirectory>
+                       <includes>
+                               
<include>org.apache.cayenne:cayenne-mcp-server</include>
+                       </includes>
+                       <unpack>false</unpack>
+                       
<outputFileNameMapping>${artifact.artifactId}${dashClassifier?}.${artifact.extension}</outputFileNameMapping>
+               </dependencySet>
        </dependencySets>
 </assembly>
diff --git 
a/assembly/src/main/resources/assemblies/windows/assembly-windows.xml 
b/assembly/src/main/resources/assemblies/windows/assembly-windows.xml
index 7db437786..432832d4d 100644
--- a/assembly/src/main/resources/assemblies/windows/assembly-windows.xml
+++ b/assembly/src/main/resources/assemblies/windows/assembly-windows.xml
@@ -112,5 +112,12 @@
                                </excludes>
                        </unpackOptions>
                </dependencySet>
+               <dependencySet>
+                       <outputDirectory>bin</outputDirectory>
+                       <includes>
+                               
<include>org.apache.cayenne:cayenne-mcp-server</include>
+                       </includes>
+                       <unpack>false</unpack>
+               </dependencySet>
        </dependencySets>
 </assembly>
diff --git a/cayenne-mcp-server/pom.xml b/cayenne-mcp-server/pom.xml
new file mode 100644
index 000000000..46b5a85f3
--- /dev/null
+++ b/cayenne-mcp-server/pom.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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
+
+    https://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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.cayenne</groupId>
+        <artifactId>cayenne-parent</artifactId>
+        <version>5.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>cayenne-mcp-server</artifactId>
+    <name>cayenne-mcp-server: CayenneModeler MCP Server</name>
+    <packaging>jar</packaging>
+
+    <description>
+        Self-contained MCP server that exposes CayenneModeler operations
+        to AI coding agents via the Model Context Protocol over stdio.
+    </description>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>io.modelcontextprotocol.sdk</groupId>
+            <artifactId>mcp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+
+            <plugin>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.6.0</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            
<shadedArtifactAttached>false</shadedArtifactAttached>
+                            
<createDependencyReducedPom>false</createDependencyReducedPom>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                            <transformers>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    
<mainClass>org.apache.cayenne.mcp.CayenneMcpMain</mainClass>
+                                    <manifestEntries>
+                                        
<Implementation-Version>${project.version}</Implementation-Version>
+                                    </manifestEntries>
+                                </transformer>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
+                                    <addHeader>false</addHeader>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <version>${maven-surefire-plugin.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+
+</project>
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpMain.java 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpMain.java
new file mode 100644
index 000000000..4270e30be
--- /dev/null
+++ 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpMain.java
@@ -0,0 +1,54 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+/**
+ * Entry point for the Cayenne MCP server.
+ */
+public class CayenneMcpMain {
+
+    public static void main(String[] args) {
+        StderrLogging.install();
+
+        CayenneMcpOptions opts = CayenneMcpOptions.parse(args);
+
+        String version = version();
+
+        if (opts.isHelp()) {
+            System.err.println("Usage: java -jar cayenne-mcp-server.jar 
[options]");
+            System.err.println();
+            System.err.println("Options:");
+            System.err.println("  -h, --help     Print this help and exit.");
+            System.err.println("  -V, --version  Print the server version and 
exit.");
+            System.exit(0);
+        }
+
+        if (opts.isVersion()) {
+            System.err.println("cayenne-mcp-server " + version);
+            System.exit(0);
+        }
+
+        new CayenneMcpServer().run(version);
+    }
+
+    static String version() {
+        String v = 
CayenneMcpMain.class.getPackage().getImplementationVersion();
+        return v != null ? v : "dev";
+    }
+}
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpOptions.java
 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpOptions.java
new file mode 100644
index 000000000..36426c1de
--- /dev/null
+++ 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpOptions.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+/**
+ * Parsed command-line options for the MCP server.
+ */
+public class CayenneMcpOptions {
+
+    private final boolean help;
+    private final boolean version;
+
+    private CayenneMcpOptions(boolean help, boolean version) {
+        this.help = help;
+        this.version = version;
+    }
+
+    public static CayenneMcpOptions parse(String[] args) {
+        boolean help = false;
+        boolean version = false;
+
+        for (String arg : args) {
+            switch (arg) {
+                case "-h", "--help" -> help = true;
+                case "-V", "--version" -> version = true;
+                default -> {
+                    System.err.println("Unknown option: " + arg);
+                    System.err.println("Run with -h for usage.");
+                    System.exit(2);
+                }
+            }
+        }
+
+        return new CayenneMcpOptions(help, version);
+    }
+
+    public boolean isHelp() {
+        return help;
+    }
+
+    public boolean isVersion() {
+        return version;
+    }
+}
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpServer.java 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpServer.java
new file mode 100644
index 000000000..459d26d24
--- /dev/null
+++ 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/CayenneMcpServer.java
@@ -0,0 +1,65 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.McpJsonMapperSupplier;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.cayenne.mcp.tools.HelloTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ServiceLoader;
+
+/**
+ * Assembles and runs the MCP server on stdio.
+ */
+public class CayenneMcpServer {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(CayenneMcpServer.class);
+
+    public void run(String version) {
+        LOGGER.info("Starting Cayenne MCP server {}", version);
+
+        McpJsonMapper jsonMapper = 
ServiceLoader.load(McpJsonMapperSupplier.class)
+                .findFirst()
+                .orElseThrow(() -> new IllegalStateException("No 
McpJsonMapperSupplier found on classpath"))
+                .get();
+
+        StdioServerTransportProvider transport = new 
StdioServerTransportProvider(jsonMapper);
+
+        McpSyncServer server = McpServer.sync(transport)
+                .serverInfo("cayenne-mcp-server", version)
+                
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
+                .tools(HelloTool.spec())
+                .build();
+
+        // Close server cleanly on SIGTERM / ctrl-C
+        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+            LOGGER.info("Cayenne MCP server stopping");
+            server.closeGracefully();
+        }));
+
+        LOGGER.info("Cayenne MCP server started, listening on stdio");
+        // The transport's non-daemon threads keep the JVM alive until stdin 
closes.
+    }
+}
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/StderrLogging.java 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/StderrLogging.java
new file mode 100644
index 000000000..f2a06b4e1
--- /dev/null
+++ b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/StderrLogging.java
@@ -0,0 +1,58 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.ConsoleAppender;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Forces all logging to stderr so that stdout remains clean for the MCP stdio 
transport.
+ * Must be called before any other code that may log.
+ */
+class StderrLogging {
+
+    private static final String PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level 
%logger{36} - %msg%n";
+
+    public static void install() {
+        LoggerContext context = (LoggerContext) 
LoggerFactory.getILoggerFactory();
+        context.reset();
+
+        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
+        encoder.setContext(context);
+        encoder.setPattern(PATTERN);
+        encoder.start();
+
+        ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
+        appender.setContext(context);
+        appender.setName("STDERR");
+        // "System.err" is the target name Logback recognises for stderr
+        appender.setTarget("System.err");
+        appender.setEncoder(encoder);
+        appender.start();
+
+        Logger root = context.getLogger(Logger.ROOT_LOGGER_NAME);
+        root.setLevel(Level.INFO);
+        root.addAppender(appender);
+    }
+}
diff --git 
a/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/HelloTool.java 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/HelloTool.java
new file mode 100644
index 000000000..fedc309b9
--- /dev/null
+++ 
b/cayenne-mcp-server/src/main/java/org/apache/cayenne/mcp/tools/HelloTool.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp.tools;
+
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.spec.McpSchema;
+
+import java.util.List;
+
+/**
+ * Placeholder tool that returns "hello world". Verifies the MCP wiring is 
functional.
+ */
+public class HelloTool {
+
+    public static final String NAME = "hello";
+
+    public static McpServerFeatures.SyncToolSpecification spec() {
+        McpSchema.Tool tool = new McpSchema.Tool(
+                NAME,
+                null,
+                "Returns a greeting. Use this to verify the Cayenne MCP server 
is running.",
+                new McpSchema.JsonSchema("object", null, null, null, null, 
null),
+                null,
+                null,
+                null
+        );
+
+        return new McpServerFeatures.SyncToolSpecification(tool, (exchange, 
request) ->
+                McpSchema.CallToolResult.builder()
+                        .content(List.of(new McpSchema.TextContent("hello 
world")))
+                        .isError(false)
+                        .build()
+        );
+    }
+}
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpOptionsTest.java
 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpOptionsTest.java
new file mode 100644
index 000000000..0b55b1f54
--- /dev/null
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpOptionsTest.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CayenneMcpOptionsTest {
+
+    @Test
+    public void emptyArgs() {
+        CayenneMcpOptions opts = CayenneMcpOptions.parse(new String[0]);
+        assertFalse(opts.isHelp());
+        assertFalse(opts.isVersion());
+    }
+
+    @Test
+    public void shortHelp() {
+        CayenneMcpOptions opts = CayenneMcpOptions.parse(new String[]{"-h"});
+        assertTrue(opts.isHelp());
+        assertFalse(opts.isVersion());
+    }
+
+    @Test
+    public void longHelp() {
+        CayenneMcpOptions opts = CayenneMcpOptions.parse(new 
String[]{"--help"});
+        assertTrue(opts.isHelp());
+    }
+
+    @Test
+    public void shortVersion() {
+        CayenneMcpOptions opts = CayenneMcpOptions.parse(new String[]{"-V"});
+        assertTrue(opts.isVersion());
+        assertFalse(opts.isHelp());
+    }
+
+    @Test
+    public void longVersion() {
+        CayenneMcpOptions opts = CayenneMcpOptions.parse(new 
String[]{"--version"});
+        assertTrue(opts.isVersion());
+    }
+}
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpServerIT.java
 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpServerIT.java
new file mode 100644
index 000000000..cb4b84d63
--- /dev/null
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpServerIT.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration tests for server startup flags.
+ * Requires the shaded jar to be built before this test runs (failsafe 
executes after package).
+ */
+public class CayenneMcpServerIT {
+
+    private static final Path MCP_JAR = ServerJarLocator.findShadedJar();
+
+    @Test
+    public void unknownArgExitsWithCode2() throws Exception {
+        Process process = startProcess("--unknown-flag");
+        assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Process did not 
exit");
+        assertEquals(2, process.exitValue(), "Expected exit code 2 for unknown 
arg");
+    }
+
+    @Test
+    public void helpFlagExitsWithCode0() throws Exception {
+        Process process = startProcess("--help");
+        assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Process did not 
exit");
+        assertEquals(0, process.exitValue(), "Expected exit code 0 for 
--help");
+    }
+
+    @Test
+    public void versionFlagExitsWithCode0() throws Exception {
+        Process process = startProcess("--version");
+        assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Process did not 
exit");
+        assertEquals(0, process.exitValue(), "Expected exit code 0 for 
--version");
+
+        String err = new String(process.getErrorStream().readAllBytes());
+        assertTrue(err.contains("cayenne-mcp-server"), "Version output missing 
server name: " + err);
+    }
+
+    private static Process startProcess(String... flags) throws IOException {
+        List<String> cmd = new ArrayList<>();
+        cmd.add(ProcessHandle.current().info().command().orElse("java"));
+        cmd.add("-jar");
+        cmd.add(MCP_JAR.toString());
+        cmd.addAll(List.of(flags));
+        return new ProcessBuilder(cmd).start();
+    }
+}
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloMcpIT.java 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloMcpIT.java
new file mode 100644
index 000000000..9c454f159
--- /dev/null
+++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloMcpIT.java
@@ -0,0 +1,128 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration tests for the hello tool over the MCP stdio protocol.
+ * Requires the shaded jar to be built before this test runs (failsafe 
executes after package).
+ */
+public class HelloMcpIT {
+
+    private static final Path MCP_JAR = ServerJarLocator.findShadedJar();
+
+    private Process process;
+    private BufferedWriter writer;
+    private BufferedReader reader;
+
+    @BeforeEach
+    void startServer() throws Exception {
+        process = new ProcessBuilder(
+                ProcessHandle.current().info().command().orElse("java"),
+                "-jar",
+                MCP_JAR.toString()
+        ).start();
+
+        Thread stderrDrain = new Thread(() -> {
+            try (InputStream in = process.getErrorStream()) {
+                in.transferTo(OutputStream.nullOutputStream());
+            } catch (IOException ignored) {
+            }
+        });
+        stderrDrain.setDaemon(true);
+        stderrDrain.start();
+
+        writer = new BufferedWriter(new 
OutputStreamWriter(process.getOutputStream()));
+        reader = new BufferedReader(new 
InputStreamReader(process.getInputStream()));
+
+        sendMessage("""
+                {"jsonrpc":"2.0","id":1,"method":"initialize","params":{\
+                "protocolVersion":"2024-11-05",\
+                "capabilities":{},\
+                "clientInfo":{"name":"test","version":"1.0"}}}""");
+        String initResponse = readLine();
+        assertTrue(initResponse.contains("\"id\":1"), "initialize response 
missing: " + initResponse);
+
+        sendMessage("""
+                
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}""");
+    }
+
+    @AfterEach
+    void stopServer() throws Exception {
+        writer.close();
+        assertTrue(process.waitFor(10, TimeUnit.SECONDS), "Server process did 
not exit after stdin was closed");
+    }
+
+    @Test
+    public void toolsListIncludesHello() throws Exception {
+        sendMessage("""
+                {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}""");
+
+        String listResponse = readLine();
+        assertTrue(listResponse.contains("\"hello\""), "tools/list missing 
'hello' tool: " + listResponse);
+    }
+
+    @Test
+    public void helloToolReturnsGreeting() throws Exception {
+        sendMessage("""
+                {"jsonrpc":"2.0","id":3,"method":"tools/call",\
+                "params":{"name":"hello","arguments":{}}}""");
+
+        String callResponse = readLine();
+        assertTrue(callResponse.contains("hello world"), "tools/call missing 
'hello world': " + callResponse);
+        assertTrue(callResponse.contains("\"id\":3"), "tools/call response id 
mismatch: " + callResponse);
+    }
+
+    private void sendMessage(String json) throws IOException {
+        writer.write(json);
+        writer.newLine();
+        writer.flush();
+    }
+
+    private String readLine() throws Exception {
+        long deadline = System.currentTimeMillis() + 5000L;
+        while (System.currentTimeMillis() < deadline) {
+            if (reader.ready()) {
+                String line = reader.readLine();
+                if (line != null && !line.isBlank()) {
+                    return line;
+                }
+            }
+            Thread.sleep(20);
+        }
+        throw new AssertionError("No response within " + 5000L + "ms");
+    }
+
+
+}
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloToolTest.java 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloToolTest.java
new file mode 100644
index 000000000..0b57eceb3
--- /dev/null
+++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloToolTest.java
@@ -0,0 +1,49 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.cayenne.mcp.tools.HelloTool;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class HelloToolTest {
+
+    @Test
+    public void toolNameIsHello() {
+        McpServerFeatures.SyncToolSpecification spec = HelloTool.spec();
+        assertEquals("hello", spec.tool().name());
+    }
+
+    @Test
+    public void callReturnsHelloWorld() {
+        McpServerFeatures.SyncToolSpecification spec = HelloTool.spec();
+        McpSchema.CallToolResult result = spec.callHandler().apply(null, null);
+
+        assertFalse(result.isError());
+        assertEquals(1, result.content().size());
+
+        McpSchema.TextContent first = 
assertInstanceOf(McpSchema.TextContent.class, result.content().get(0));
+        assertEquals("hello world", first.text());
+    }
+}
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/ServerJarLocator.java 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/ServerJarLocator.java
new file mode 100644
index 000000000..32177efb6
--- /dev/null
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/ServerJarLocator.java
@@ -0,0 +1,27 @@
+package org.apache.cayenne.mcp;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+class ServerJarLocator {
+
+    public static Path findShadedJar() {
+        Path targetDir = Path.of(System.getProperty("user.dir"), "target");
+
+        if (!Files.isDirectory(targetDir)) {
+            throw new IllegalStateException("Shaded jar not found — run mvn 
package first");
+        }
+
+        try (Stream<Path> stream = Files.list(targetDir)) {
+            return stream
+                    .filter(p -> 
p.getFileName().toString().matches("cayenne-mcp-server-.*\\.jar"))
+                    .filter(p -> 
!p.getFileName().toString().contains("original"))
+                    .findFirst()
+                    .orElseThrow(() -> new IllegalStateException("Shaded jar 
not found — run mvn package first"));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/modeler/cayenne-modeler-mac/pom.xml 
b/modeler/cayenne-modeler-mac/pom.xml
index 78c6521cc..a1bc63b2f 100644
--- a/modeler/cayenne-modeler-mac/pom.xml
+++ b/modeler/cayenne-modeler-mac/pom.xml
@@ -64,6 +64,7 @@
                        <artifactId>cayenne-wocompat</artifactId>
                        <version>${project.version}</version>
                </dependency>
+
        </dependencies>
 
     <profiles>
diff --git a/pom.xml b/pom.xml
index c050869b8..505099e54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,8 @@
         <!-- Dependency versions -->
         <ant.version>1.10.15</ant.version>
         <junit.version>5.14.4</junit.version>
+        <logback.version>1.5.32</logback.version>
+        <mcp-sdk.version>1.1.2</mcp-sdk.version>
         <mockito.version>5.23.0</mockito.version>
         <mysql.connector.version>9.5.0</mysql.connector.version>
         <slf4j.version>2.0.17</slf4j.version>
@@ -101,6 +103,7 @@
         <module>cayenne-gradle-plugin</module>
         <module>cayenne-jcache</module>
         <module>cayenne-lifecycle</module>
+        <module>cayenne-mcp-server</module>
         <module>cayenne-project</module>
         <module>cayenne-project-compatibility</module>
         <module>cayenne-velocity</module>
@@ -471,6 +474,16 @@
                 <version>${slf4j.version}</version>
                 <scope>test</scope>
             </dependency>
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-classic</artifactId>
+                <version>${logback.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.modelcontextprotocol.sdk</groupId>
+                <artifactId>mcp</artifactId>
+                <version>${mcp-sdk.version}</version>
+            </dependency>
             <!-- This dependency update the one provided by the testcontainers,
                  should be removed once testcontainers uses a version without 
active CVEs -->
             <dependency>


Reply via email to