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);
+    }
+}

Reply via email to