You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/05/09 20:53:13 UTC

(cayenne) branch CAY-2936 updated (a3abf72e4 -> 063a49333)

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

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


 discard a3abf72e4 CAY-2936 Add a placeholder MCP server to the Modeler distribution
     new 063a49333 CAY-2936 Add a placeholder MCP server to the Modeler distribution

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (a3abf72e4)
            \
             N -- N -- N   refs/heads/CAY-2936 (063a49333)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 cayenne-mcp-server/README.md | 96 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 96 insertions(+)
 create mode 100644 cayenne-mcp-server/README.md


(cayenne) 01/01: CAY-2936 Add a placeholder MCP server to the Modeler distribution

Posted by aa...@apache.org.
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 063a493333fd371be52117b9d584de5701e3deea
Author: Andrus Adamchik <an...@objectstyle.com>
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 |   7 ++
 .../assemblies/windows/assembly-windows.xml        |   7 ++
 cayenne-mcp-server/README.md                       |  96 +++++++++++++++
 cayenne-mcp-server/pom.xml                         | 130 +++++++++++++++++++++
 .../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 |  59 ++++++++++
 .../java/org/apache/cayenne/mcp/HelloMcpIT.java    | 121 +++++++++++++++++++
 .../java/org/apache/cayenne/mcp/HelloToolTest.java |  49 ++++++++
 .../java/org/apache/cayenne/mcp/McpStarter.java    |  52 +++++++++
 modeler/cayenne-modeler-mac/pom.xml                |   1 +
 pom.xml                                            |  18 +++
 18 files changed, 901 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..d21127b36 100644
--- a/assembly/src/main/resources/assemblies/mac/assembly-mac.xml
+++ b/assembly/src/main/resources/assemblies/mac/assembly-mac.xml
@@ -112,5 +112,12 @@
 				</excludes>
 			</unpackOptions>
 		</dependencySet>
+		<dependencySet>
+			<outputDirectory>CayenneModeler.app/Contents/Resources/mcp/</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/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/README.md b/cayenne-mcp-server/README.md
new file mode 100644
index 000000000..7a4b5f5d9
--- /dev/null
+++ b/cayenne-mcp-server/README.md
@@ -0,0 +1,96 @@
+# Cayenne MCP Server
+
+A self-contained MCP (Model Context Protocol) server that exposes CayenneModeler
+operations to AI coding agents over stdio.
+
+## Requirements
+
+Java 21 or later must be available on the system PATH.
+
+## Finding the JAR
+
+The JAR location depends on how CayenneModeler was installed.
+Downloads are available at https://cayenne.apache.org/download/.
+
+### macOS (DMG)
+
+```
+<install-dir>/CayenneModeler.app/Contents/Resources/mcp/cayenne-mcp-server-<VERSION>.jar
+
+# But typically, in:
+/Applications/CayenneModeler.app/Contents/Resources/mcp/cayenne-mcp-server-<VERSION>.jar
+
+```
+
+### Windows (ZIP)
+
+```
+<install-dir>\bin\cayenne-mcp-server-<VERSION>.jar
+```
+
+### Linux / cross-platform (tar.gz)
+
+```
+<install-dir>/bin/cayenne-mcp-server-<VERSION>.jar
+```
+
+### Development build (own Cayenne source)
+
+Build the module first:
+
+```bash
+mvn clean package -pl cayenne-mcp-server -am -DskipTests
+```
+
+The JAR is then at:
+
+```
+cayenne-mcp-server/target/cayenne-mcp-server-<VERSION>.jar
+```
+
+## Configuring AI clients
+
+The server communicates over **stdio** and is launched on demand by the client.
+Replace `/path/to/cayenne-mcp-server.jar` with the actual path from the section above.
+
+### Claude Code
+
+```bash
+claude mcp add cayenne -- java -jar /path/to/cayenne-mcp-server-<VERSION>.jar
+```
+
+### Cursor
+
+Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project:
+
+```json
+{
+  "mcpServers": {
+    "cayenne": {
+      "command": "java",
+      "args": ["-jar", "/path/to/cayenne-mcp-server-<VERSION>.jar"]
+    }
+  }
+}
+```
+
+### VS Code (GitHub Copilot)
+
+Add to `.vscode/mcp.json` in your project:
+
+```json
+{
+  "servers": {
+    "cayenne": {
+      "type": "stdio",
+      "command": "java",
+      "args": ["-jar", "/path/to/cayenne-mcp-server-<VERSION>.jar"]
+    }
+  }
+}
+```
+
+## Verifying the setup
+
+Once configured, ask your AI agent to call the `hello` tool. A response of
+`"hello world"` confirms the server is running and reachable.
diff --git a/cayenne-mcp-server/pom.xml b/cayenne-mcp-server/pom.xml
new file mode 100644
index 000000000..e6f989f7d
--- /dev/null
+++ b/cayenne-mcp-server/pom.xml
@@ -0,0 +1,130 @@
+<?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>
+                <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..efcde70dd
--- /dev/null
+++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/CayenneMcpServerIT.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.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 {
+
+    @Test
+    public void unknownArgExitsWithCode2() throws Exception {
+        Process process = McpStarter.start("--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 = McpStarter.start("--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 = McpStarter.start("--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);
+    }
+
+
+}
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..cab9bd4b7
--- /dev/null
+++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/HelloMcpIT.java
@@ -0,0 +1,121 @@
+/*****************************************************************
+ *   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.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 Process process;
+    private BufferedWriter writer;
+    private BufferedReader reader;
+
+    @BeforeEach
+    void startServer() throws Exception {
+        process = McpStarter.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/McpStarter.java b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/McpStarter.java
new file mode 100644
index 000000000..690110317
--- /dev/null
+++ b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/McpStarter.java
@@ -0,0 +1,52 @@
+package org.apache.cayenne.mcp;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+class McpStarter {
+
+    private static final String JAVA_EXE = javaExecutable();
+    private static final String MCP_JAR = mcpJar();
+
+    public static Process start(String... flags) {
+        List<String> cmd = new ArrayList<>();
+        cmd.add(JAVA_EXE);
+        cmd.add("-jar");
+        cmd.add(MCP_JAR);
+        cmd.addAll(List.of(flags));
+        try {
+            return new ProcessBuilder(cmd).start();
+        } catch (IOException e) {
+            throw new IllegalStateException("Error starting MCP server", e);
+        }
+    }
+
+    private static String javaExecutable() {
+        String name = System.getProperty("os.name", "").toLowerCase().contains("win") ? "java.exe" : "java";
+        return Path.of(System.getProperty("java.home"), "bin", name).toString();
+    }
+
+    private static String mcpJar() {
+        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"))
+                    .filter(p -> !p.getFileName().toString().contains("sources"))
+                    .map(Path::toString)
+                    .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..8a872f09a 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>
@@ -83,6 +85,7 @@
         <maven-release-plugin.version>3.3.1</maven-release-plugin.version>
         <maven-remote-resources-plugin.version>3.3.0</maven-remote-resources-plugin.version>
         <maven-resources-plugin.version>3.5.0</maven-resources-plugin.version>
+        <maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
         <maven-source-plugin.version>3.4.0</maven-source-plugin.version>
         <maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
     </properties>
@@ -101,6 +104,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 +475,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>
@@ -647,6 +661,10 @@
                     <artifactId>maven-surefire-report-plugin</artifactId>
                     <version>${maven-surefire-plugin.version}</version>
                 </plugin>
+                <plugin>
+                    <artifactId>maven-shade-plugin</artifactId>
+                    <version>${maven-shade-plugin.version}</version>
+                </plugin>
                 <plugin>
                     <artifactId>maven-remote-resources-plugin</artifactId>
                     <version>${maven-remote-resources-plugin.version}</version>