diff --git a/mcp-server/README.adoc b/mcp-server/README.adoc
new file mode 100644
index 0000000000..661ecaf1b7
--- /dev/null
+++ b/mcp-server/README.adoc
@@ -0,0 +1,63 @@
+= mcp-server: quickstart for MCP server
+:toc: left
+:icons: font
+:idprefix:
+:idseparator: -
+:keywords: mcp
+:level: Intermediate
+:technologies: Jakarta REST, MCP
+:archiveType: war
+
+[abstract]
+The `mcp-server` quickstart demonstrates how to implement MCP server resources with WildFly.
+
+== Build the Application
+
+[source,shell]
+----
+mvn clean package
+----
+
+== Run WildFly
+
+[source,shell]
+----
+./target/server/bin/standalone.sh --stability=experimental
+----
+
+== Connect with the MCP introspector
+
+You can use the MCP inspector to test your tools
+
+[source,shell]
+----
+npx @modelcontextprotocol/inspector
+----
+
+Once you are on the http://127.0.0.1:6274[MPC inspector UI], you can connect to your WildFly server with the Streamable HTTP transport type using the parameters:
+
+* Transport Type: **Streamable HTTP**
+* URL: **http://localhost:8080/mcp-server/stream**
+
+Alternatively, you can connect with the Server-Sent Events (SSE) transport type using the parameters:
+
+* Transport Type: **SSE**
+* URL: **http://localhost:8080/mcp-server/sse**
+
+You should see all those tools.
+
+== Connect with Claude Code
+
+If you are using Claude Code, you can add this MCP server with:
+
+[source,shell]
+----
+claude mcp add --scope project --transport http wildfly-quickstart http://localhost:8080/mcp-server/stream
+----
+
+You can then ask `claude` questions such as:
+
+* `What is the weather forecast for tomorrow at San Antonio?`
+* `Are there any weather alerts for Denver?`
+* `What is the length of the string "abcdef"?`
+
diff --git a/mcp-server/pom.xml b/mcp-server/pom.xml
new file mode 100644
index 0000000000..6f291b40c0
--- /dev/null
+++ b/mcp-server/pom.xml
@@ -0,0 +1,188 @@
+
+ 4.0.0
+
+ org.wildfly.quickstarts
+ wildfly-quickstart-parent
+
+ 12
+
+
+
+ mcp-server
+ 40.0.1.Final-SNAPSHOT
+ war
+
+ Quickstart: mcp-server
+
+
+
+ 17
+
+ 40.0.0.Final
+
+ ${version.server}
+ ${version.server}
+ 6.0.0.Final
+ 0.11.0-SNAPSHOT
+ true
+
+
+
+
+
+
+ org.wildfly.bom
+ wildfly-expansion
+ ${version.bom.expansion}
+ pom
+ import
+
+
+
+ org.wildfly.bom
+ wildfly-ee-with-tools
+ ${version.bom.ee}
+ pom
+ import
+
+
+ org.wildfly.generative-ai
+ wildfly-ai-bom
+ ${version.ai.feature-pack}
+ pom
+ import
+
+
+
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ provided
+
+
+ jakarta.json
+ jakarta.json-api
+ provided
+
+
+ org.mcp-java
+ mcp-server-api
+ provided
+
+
+
+
+ junit
+ junit
+ test
+ jar
+
+
+
+
+
+ ${project.artifactId}
+
+
+
+ org.wildfly.plugins
+ wildfly-maven-plugin
+ ${version.plugin.wildfly}
+
+
+
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+
+
+
+ ${version.bom.expansion}
+
+
+
+
+
+
+
+
+ provisioned-server
+
+ true
+
+
+
+
+ org.wildfly.plugins
+ wildfly-maven-plugin
+
+
+
+ org.wildfly:wildfly-galleon-pack:${version.server}
+
+
+ org.wildfly.generative-ai:wildfly-ai-feature-pack:${version.ai.feature-pack}
+
+
+
+ jaxrs-server
+ mcp-server
+
+ experimental
+
+
+
+
+ package
+
+
+
+
+
+
+
+
+ integration-testing
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+
+
+
+
+ jboss-public-maven-repository
+ JBoss Public Maven Repository
+ https://repository.jboss.org/nexus/content/groups/public/
+
+
+
+
+ jboss-public-maven-repository
+ JBoss Public Maven Repository
+ https://repository.jboss.org/nexus/content/groups/public/
+
+
+
diff --git a/mcp-server/src/main/java/org/wildfly/quickstart/mcp/SimpleTool.java b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/SimpleTool.java
new file mode 100644
index 0000000000..552abace56
--- /dev/null
+++ b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/SimpleTool.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 JBoss by Red Hat.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.wildfly.quickstart.mcp;
+
+
+import org.mcp_java.server.tools.Tool;
+import org.mcp_java.server.tools.ToolArg;
+
+/*
+ * Example of a closed-world tool that does not rely on any external system
+ */
+public class SimpleTool {
+
+ @Tool(name = "length-of-string",
+ description = "Calculates the length of a string",
+ annotations = @Tool.Annotations(openWorldHint = false, readOnlyHint = true, destructiveHint = false, idempotentHint = true))
+ int stringLength(@ToolArg(description = "the string the length of of which we want to calculate") String string) {
+ return string.length();
+ }
+}
diff --git a/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherApplication.java b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherApplication.java
new file mode 100644
index 0000000000..26834c8195
--- /dev/null
+++ b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherApplication.java
@@ -0,0 +1,33 @@
+package org.wildfly.quickstart.mcp;
+
+import jakarta.json.Json;
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.ext.Provider;
+import jakarta.ws.rs.ext.ReaderInterceptor;
+import jakarta.ws.rs.ext.ReaderInterceptorContext;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+@ApplicationPath("/")
+public class WeatherApplication extends Application {
+
+ /**
+ * Interceptor to handle "application/geo+json" content as a {@code JsonObject}.
+ */
+ @Provider
+ public static class GeoJsonReaderInterceptor implements ReaderInterceptor {
+ public static final MediaType APPLICATION_GEO_JSON_TYPE = new MediaType("application", "geo+json");
+ @Override
+ public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException {
+ if (context.getMediaType().isCompatible(APPLICATION_GEO_JSON_TYPE)) {
+ InputStreamReader reader = new InputStreamReader(context.getInputStream(), StandardCharsets.UTF_8);
+ return Json.createReader(reader).readObject();
+ }
+ return context.proceed();
+ }
+ }
+}
\ No newline at end of file
diff --git a/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherClient.java b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherClient.java
new file mode 100644
index 0000000000..a7136ace36
--- /dev/null
+++ b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherClient.java
@@ -0,0 +1,81 @@
+package org.wildfly.quickstart.mcp;
+
+import jakarta.json.JsonObject;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.List;
+import java.util.Locale;
+
+public class WeatherClient {
+
+ private static final String REST_URI = "https://api.weather.gov";
+
+ private static final Client HTTP_CLIENT = ClientBuilder.newClient()
+ .property("dev.resteasy.client.follow.redirects", "true");
+
+ Alerts getAlerts(String state) {
+ try (Response response = HTTP_CLIENT.target(REST_URI)
+ .path("/alerts/active/area/%s".formatted(state))
+ .request(MediaType.APPLICATION_JSON).get();) {
+ return response.readEntity(Alerts.class);
+ }
+ }
+
+ JsonObject getPoints(double latitude, double longitude) {
+ DecimalFormat decimal = new DecimalFormat("##.####", DecimalFormatSymbols.getInstance(Locale.US));
+ String endpoint = "/points/%s,%s".formatted(
+ decimal.format(latitude),
+ decimal.format(longitude));
+ Response response = HTTP_CLIENT.target(REST_URI)
+ .path(endpoint)
+ .request(MediaType.APPLICATION_JSON)
+ .get();
+ return response.readEntity(JsonObject.class);
+ }
+
+ Forecast getForecast(String url) {
+ return HTTP_CLIENT.target(url)
+ .request(MediaType.APPLICATION_JSON)
+ .get(Forecast.class);
+ }
+
+ public record Properties(
+ String id,
+ String areaDesc,
+ String event,
+ String severity,
+ String description,
+ String instruction) {}
+
+ public record Feature(
+ String id,
+ String type,
+ Object geometry,
+ Properties properties) {}
+
+ public record Alerts(
+ List context,
+ String type,
+ List features,
+ String title,
+ String updated) {}
+
+ public record Period(
+ String name,
+ int temperature,
+ String temperatureUnit,
+ String windSpeed,
+ String windDirection,
+ String detailedForecast) {}
+
+ public record ForecastProperties(
+ List periods) {}
+
+ public record Forecast(
+ ForecastProperties properties) {}
+}
diff --git a/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherTools.java b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherTools.java
new file mode 100644
index 0000000000..4067fd86ff
--- /dev/null
+++ b/mcp-server/src/main/java/org/wildfly/quickstart/mcp/WeatherTools.java
@@ -0,0 +1,68 @@
+package org.wildfly.quickstart.mcp;
+
+
+import org.mcp_java.server.tools.Tool;
+import org.mcp_java.server.tools.ToolArg;
+
+import java.util.stream.Collectors;
+
+/**
+ * Example of "open-world" MCP tools that relies on an external system
+ * (provided by ...) to provide
+ * weather alerts and forecasts in the USA.
+ */
+public class WeatherTools {
+
+ private final static WeatherClient WEATHER = new WeatherClient();
+
+ @Tool(description = "Get weather alerts for a US state.",
+ name = "alerts",
+ annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false))
+ public String getAlerts(
+ @ToolArg(description = "Two-letter US state code (e.g. CA, NY)") String state) {
+ return formatAlerts(WEATHER.getAlerts(state));
+ }
+
+ @Tool(description = "Get weather forecast for a location.",
+ annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false))
+ public String forecast(
+ @ToolArg(description = "Latitude of the location") double latitude,
+ @ToolArg(description = "Longitude of the location") double longitude) {
+ var points = WEATHER.getPoints(latitude, longitude);
+ var url = points.get("properties").asJsonObject().getString("forecast");
+
+ return formatForecast(WEATHER.getForecast(url));
+ }
+
+ private static String formatForecast(WeatherClient.Forecast forecast) {
+ return forecast.properties().periods().stream()
+ .map(WeatherTools::formatPeriod)
+ .collect(Collectors.joining("\n---\n"));
+ }
+
+ private static String formatPeriod(WeatherClient.Period period) {
+ return """
+ Temperature: %d°%s
+ Wind: %s %s
+ Forecast: %s
+ """.formatted(period.temperature(), period.temperatureUnit(),
+ period.windSpeed(), period.windDirection(),
+ period.detailedForecast());
+ }
+
+ private static String formatAlerts(WeatherClient.Alerts alerts) {
+ return alerts.features().stream()
+ .map(feature -> formatProperties(feature.properties()))
+ .collect(Collectors.joining("\n---\n"));
+ }
+
+ private static String formatProperties(WeatherClient.Properties p) {
+ return """
+ Event: %s
+ Area: %s
+ Severity: %s
+ Description: %s
+ Instructions: %s
+ """.formatted(p.event(), p.areaDesc(), p.severity(), p.description(), p.instruction());
+ }
+}
diff --git a/mcp-server/src/main/webapp/WEB-INF/beans.xml b/mcp-server/src/main/webapp/WEB-INF/beans.xml
new file mode 100644
index 0000000000..9dfae34df1
--- /dev/null
+++ b/mcp-server/src/main/webapp/WEB-INF/beans.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/mcp-server/src/main/webapp/WEB-INF/web.xml b/mcp-server/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..4ae23ceae3
--- /dev/null
+++ b/mcp-server/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ 30
+
+
+
diff --git a/pom.xml b/pom.xml
index 0bf26d8354..8a5b52475d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -195,6 +195,7 @@
kitchensink
logging
mail
+ mcp-server
messaging-clustering-singleton
micrometer
microprofile-config