This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new e3572e8 chore: add an integration test (#11)
e3572e8 is described below
commit e3572e82e7ee7d62ab7c9fe35c87c9c2b312001f
Author: Robert Munteanu <[email protected]>
AuthorDate: Fri Mar 13 17:07:00 2026 +0100
chore: add an integration test (#11)
---
.sling-module.json | 3 +
pom.xml | 100 ++++++++++++++++-
.../org/apache/sling/mcp/server/McpServerIT.java | 124 +++++++++++++++++++++
.../mcp/server/itbundle/ItToolContribution.java | 52 +++++++++
.../mcp/server/itbundle/McpItSupportBundle.java | 81 ++++++++++++++
5 files changed, 355 insertions(+), 5 deletions(-)
diff --git a/.sling-module.json b/.sling-module.json
index c16686a..3cd35e0 100644
--- a/.sling-module.json
+++ b/.sling-module.json
@@ -3,6 +3,9 @@
"jdks": [
17,
21
+ ],
+ "operatingSystems": [
+ "linux"
]
}
}
diff --git a/pom.xml b/pom.xml
index c76aa7b..b48f693 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,8 @@
<slingfeature.app.vmOption />
<!-- Directory for legacy variant classes -->
<legacy.classes.directory>${project.build.directory}/classes-legacy</legacy.classes.directory>
+ <!-- Integration test start timeout -->
+ <it.startTimeoutSeconds>120</it.startTimeoutSeconds>
</properties>
<dependencies>
@@ -174,6 +176,45 @@
<version>3.27.6</version>
<scope>test</scope>
</dependency>
+
+ <!-- integration test dependencies -->
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.clients</artifactId>
+ <version>3.1.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ops4j.pax.tinybundles</groupId>
+ <artifactId>tinybundles</artifactId>
+ <version>4.0.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>biz.aQute.bndlib</artifactId>
+ <version>${bnd.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <version>4.2.2</version>
+ <scope>test</scope>
+ </dependency>
+ <!-- slf4j simple for IT logging -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <!-- McpJsonMapperSupplier SPI implementation (Jackson2) needed by MCP
client at test runtime -->
+ <dependency>
+ <groupId>io.modelcontextprotocol.sdk</groupId>
+ <artifactId>mcp-json-jackson2</artifactId>
+ <version>1.0.0</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
@@ -272,23 +313,72 @@
<failOnMissing>false</failOnMissing>
</configuration>
</plugin>
- <!-- Added: Feature Launcher plugin for manually starting the
aggregated 'app' feature -->
+ <!-- Reserve a random port for integration tests -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>reserve-network-port</id>
+ <goals>
+ <goal>reserve-network-port</goal>
+ </goals>
+ <phase>pre-integration-test</phase>
+ <configuration>
+ <portNames>
+ <portName>http.port</portName>
+ </portNames>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- Feature Launcher plugin: manual start via 'mvn
feature-launcher:start', and lifecycle-bound IT start/stop -->
<plugin>
<groupId>org.apache.sling</groupId>
<artifactId>feature-launcher-maven-plugin</artifactId>
+ <version>1.0.4</version>
<configuration>
<launches>
<launch>
- <id>app</id>
+ <id>sling-starter-oak-tar</id>
+ <skip>${skipITs}</skip>
<featureFile>${project.slingfeature.outputDirectory}/feature-app.json</featureFile>
+ <repositoryUrls>
+
<repositoryUrl>file://${project.build.directory}/artifacts</repositoryUrl>
+ </repositoryUrls>
<launcherArguments>
- <vmOptions>
-
<vmOption>${slingfeature.app.vmOption}</vmOption>
- </vmOptions>
+ <frameworkProperties>
+
<org.osgi.service.http.port>${http.port}</org.osgi.service.http.port>
+ </frameworkProperties>
</launcherArguments>
+
<startTimeoutSeconds>${it.startTimeoutSeconds}</startTimeoutSeconds>
</launch>
</launches>
</configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>start</goal>
+ <goal>stop</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <sling.http.port>${http.port}</sling.http.port>
+ </systemPropertyVariables>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
</plugin>
<plugin>
<groupId>org.apache.rat</groupId>
diff --git a/src/test/java/org/apache/sling/mcp/server/McpServerIT.java
b/src/test/java/org/apache/sling/mcp/server/McpServerIT.java
new file mode 100644
index 0000000..b3b7b10
--- /dev/null
+++ b/src/test/java/org/apache/sling/mcp/server/McpServerIT.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import
io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.sling.mcp.server.itbundle.ItToolContribution;
+import org.apache.sling.mcp.server.itbundle.McpItSupportBundle;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.osgi.OsgiConsoleClient;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class McpServerIT {
+
+ private static final Duration AWAIT_TIMEOUT = Duration.ofSeconds(30);
+ private static final Duration AWAIT_POLL_INTERVAL = Duration.ofMillis(500);
+
+ private static int slingPort;
+ private static McpItSupportBundle supportBundle;
+
+ private SlingClient sling;
+ private McpSyncClient mcpClient;
+
+ @BeforeAll
+ static void buildSupportBundle(@TempDir Path tempDir) throws Exception {
+ slingPort = Integer.getInteger("sling.http.port", 8080);
+ supportBundle = new McpItSupportBundle(tempDir);
+ supportBundle.generate();
+ }
+
+ @BeforeEach
+ void setup() throws ClientException, InterruptedException,
TimeoutException {
+ sling = SlingClient.Builder.create(URI.create("http://localhost:" +
slingPort), "admin", "admin")
+ .build();
+
+ // deploy the IT tool contribution bundle.
+ supportBundle.install(sling.adaptTo(OsgiConsoleClient.class));
+
+ // build the MCP sync client with HTTP Basic Auth for admin access.
+ String basicAuthHeader = "Basic " +
Base64.getEncoder().encodeToString("admin:admin".getBytes());
+ HttpClientStreamableHttpTransport transport =
HttpClientStreamableHttpTransport.builder(
+ "http://localhost:" + slingPort)
+ .endpoint("/bin/mcp")
+ .customizeRequest(rb -> rb.header("Authorization",
basicAuthHeader))
+ .build();
+
+ mcpClient = McpClient.sync(transport)
+ .clientInfo(new McpSchema.Implementation("mcp-server-it",
"1.0"))
+ .requestTimeout(Duration.ofSeconds(30))
+ .build();
+ mcpClient.initialize();
+ }
+
+ @AfterEach
+ void teardown() {
+ if (mcpClient != null) {
+ mcpClient.close();
+ }
+ if (sling != null) {
+ try {
+
supportBundle.uninstall(sling.adaptTo(OsgiConsoleClient.class));
+ } catch (ClientException e) {
+ // ignore
+ }
+ }
+ }
+
+ @Test
+ void toolIsRegisteredAndCanBeInvoked() {
+ // wait for the tool to be registered
+ await("it-hello tool appears in tool listing")
+ .atMost(AWAIT_TIMEOUT)
+ .pollInterval(AWAIT_POLL_INTERVAL)
+ .until(() -> mcpClient.listTools().tools().stream()
+ .anyMatch(t ->
ItToolContribution.TOOL_NAME.equals(t.name())));
+
+ // call the tool and verify the response.
+ McpSchema.CallToolResult result =
+ mcpClient.callTool(new
McpSchema.CallToolRequest(ItToolContribution.TOOL_NAME, Map.of()));
+
+ assertThat(result.isError())
+ .as("tool result should not indicate an error")
+ .isNotEqualTo(Boolean.TRUE);
+ assertThat(result.content())
+ .as("tool result content")
+ .hasSize(1)
+ .first()
+ .isInstanceOfSatisfying(McpSchema.TextContent.class, tc ->
assertThat(tc.text())
+ .isEqualTo(ItToolContribution.TOOL_RESPONSE));
+ }
+}
diff --git
a/src/test/java/org/apache/sling/mcp/server/itbundle/ItToolContribution.java
b/src/test/java/org/apache/sling/mcp/server/itbundle/ItToolContribution.java
new file mode 100644
index 0000000..c71878c
--- /dev/null
+++ b/src/test/java/org/apache/sling/mcp/server/itbundle/ItToolContribution.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.itbundle;
+
+import java.util.List;
+import java.util.Map;
+
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * A minimal {@link McpServerContribution} deployed via TinyBundles during
integration tests.
+ * Registers a single tool named {@value #TOOL_NAME} that returns a fixed text
response.
+ */
+@Component(service = McpServerContribution.class)
+public class ItToolContribution implements McpServerContribution {
+
+ public static final String TOOL_NAME = "it-hello";
+ public static final String TOOL_RESPONSE = "Hello from IT";
+
+ @Override
+ public List<SyncToolSpecification> getSyncToolSpecification() {
+ McpSchema.Tool tool = McpSchema.Tool.builder()
+ .name(TOOL_NAME)
+ .description("Integration test tool that returns a fixed
greeting")
+ .inputSchema(new McpSchema.JsonSchema("object", Map.of(),
null, null, null, null))
+ .build();
+
+ return List.of(new SyncToolSpecification(
+ tool,
+ (ctx, req) -> new McpSchema.CallToolResult(
+ List.of(new McpSchema.TextContent(TOOL_RESPONSE)),
Boolean.FALSE, null, null)));
+ }
+}
diff --git
a/src/test/java/org/apache/sling/mcp/server/itbundle/McpItSupportBundle.java
b/src/test/java/org/apache/sling/mcp/server/itbundle/McpItSupportBundle.java
new file mode 100644
index 0000000..40bb782
--- /dev/null
+++ b/src/test/java/org/apache/sling/mcp/server/itbundle/McpItSupportBundle.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.itbundle;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.osgi.OsgiConsoleClient;
+import org.ops4j.pax.tinybundles.TinyBundles;
+import org.osgi.framework.Constants;
+
+/**
+ * Builds and manages the IT support bundle containing {@link
ItToolContribution}.
+ *
+ * <p>The bundle is assembled at test time using TinyBundles + bnd so it gets
correct
+ * OSGi {@code Import-Package} headers for all MCP SDK and Sling SPI
packages.</p>
+ */
+public class McpItSupportBundle {
+
+ public static final String BUNDLE_SYMBOLIC_NAME =
"org.apache.sling.mcp.server.itbundle";
+
+ private final Path parentDirectory;
+ private Path bundlePath;
+
+ public McpItSupportBundle(Path parentDirectory) {
+ this.parentDirectory = parentDirectory;
+ }
+
+ /**
+ * Builds the support bundle JAR and returns its path.
+ */
+ public Path generate() throws IOException {
+ InputStream bundleStream = TinyBundles.bundle()
+ .setHeader(Constants.BUNDLE_SYMBOLICNAME, BUNDLE_SYMBOLIC_NAME)
+ .setHeader(Constants.BUNDLE_VERSION, "1.0.0.SNAPSHOT")
+ .addClass(ItToolContribution.class)
+ .build(TinyBundles.bndBuilder());
+
+ bundlePath = parentDirectory.resolve("mcp-it-support-bundle.jar");
+ Files.copy(bundleStream, bundlePath);
+ return bundlePath;
+ }
+
+ /**
+ * Installs the support bundle into the running Sling instance and waits
until it is active.
+ */
+ public void install(OsgiConsoleClient client) throws ClientException,
InterruptedException, TimeoutException {
+ if (bundlePath == null) {
+ throw new IllegalStateException("Bundle not generated; call
generate() first");
+ }
+ client.waitInstallBundle(bundlePath.toFile(), true, 10,
TimeUnit.SECONDS.toMillis(10), 500);
+ }
+
+ /**
+ * Uninstalls the support bundle from the running Sling instance.
+ */
+ public void uninstall(OsgiConsoleClient client) throws ClientException {
+ client.uninstallBundle(BUNDLE_SYMBOLIC_NAME);
+ }
+}