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-whiteboard.git
The following commit(s) were added to refs/heads/master by this push:
new 4996a8f9 Initial commit of a basic MCP server implementation for Sling
4996a8f9 is described below
commit 4996a8f9efc8c85c5fa45d20027af5386d5c0764
Author: Robert Munteanu <[email protected]>
AuthorDate: Wed Nov 12 14:30:20 2025 +0100
Initial commit of a basic MCP server implementation for Sling
---
mcp-server/README.md | 13 ++
mcp-server/bnd.bnd | 3 +
mcp-server/pom.xml | 206 ++++++++++++++++++
mcp-server/src/main/features/main.json | 36 ++++
.../apache/sling/mcp/server/impl/McpServlet.java | 230 +++++++++++++++++++++
5 files changed, 488 insertions(+)
diff --git a/mcp-server/README.md b/mcp-server/README.md
new file mode 100644
index 00000000..599c6a0d
--- /dev/null
+++ b/mcp-server/README.md
@@ -0,0 +1,13 @@
+# Apache Sling MCP Server
+
+Experimental MCP Server implementation for Apache Sling.
+
+## Usage
+
+Start up the MCP server, based on the Apache Sling Starter
+
+```
+$ mvn package feature-launcher:start feature-launcher:stop
-Dfeature-launcher.waitForInput
+```
+
+Then open up your coding assistant tool and add an remote MCP server with
location http://localhost:8080/mcp .
diff --git a/mcp-server/bnd.bnd b/mcp-server/bnd.bnd
new file mode 100644
index 00000000..27af7f12
--- /dev/null
+++ b/mcp-server/bnd.bnd
@@ -0,0 +1,3 @@
+# workaround for https://github.com/modelcontextprotocol/java-sdk/issues/562
+Private-Package: io.modelcontextprotocol.json.jackson, \
+ io.modelcontextprotocol.json.schema.jackson
\ No newline at end of file
diff --git a/mcp-server/pom.xml b/mcp-server/pom.xml
new file mode 100644
index 00000000..4a2323c4
--- /dev/null
+++ b/mcp-server/pom.xml
@@ -0,0 +1,206 @@
+<?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
+
+ 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.
+-->
+<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.sling</groupId>
+ <artifactId>sling-bundle-parent</artifactId>
+ <version>63</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.mcp-server</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+
+ <name>Apache Sling MCP Server</name>
+
+ <properties>
+ <sling.java.version>17</sling.java.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.framework</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.annotation.bundle</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.annotation.versioning</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.metatype.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.servlets.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.api</artifactId>
+ <version>3.0.2</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.modelcontextprotocol.sdk</groupId>
+ <artifactId>mcp</artifactId>
+ <version>0.15.0</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>slingfeature-maven-plugin</artifactId>
+ <version>1.9.2</version>
+ <extensions>true</extensions>
+ <configuration>
+
<skipAddFeatureDependencies>true</skipAddFeatureDependencies>
+ <framework>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ <version>7.0.5</version>
+ </framework>
+
+ <!--
+ Prepares a feature model aggregate that takes the
following features from
+ the Sling Starter:
+ - nosample_base - the base Sling Starter
+ - oak_persistence_sns - SegmentNodeStore persistence
for Oak
+ - composum - The Composum Nodes administration tool
+ Notably missing are the slingshot and starter samples
+ -->
+ <aggregates>
+ <aggregate>
+ <classifier>app</classifier>
+ <filesInclude>main.json</filesInclude>
+ <attach>false</attach>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+
<artifactId>org.apache.sling.starter</artifactId>
+ <classifier>nosample_base</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+
<artifactId>org.apache.sling.starter</artifactId>
+ <classifier>oak_persistence_sns</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+
<artifactId>org.apache.sling.starter</artifactId>
+ <classifier>starter</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+
<artifactId>org.apache.sling.starter</artifactId>
+ <classifier>composum</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ </aggregate>
+ </aggregates>
+ <repositories>
+ <repository>
+ <includeClassifier>app</includeClassifier>
+ </repository>
+ </repositories>
+ <scans>
+ <scan>
+ <includeClassifier>app</includeClassifier>
+ </scan>
+ </scans>
+ </configuration>
+ <executions>
+ <execution>
+ <id>prepare-features</id>
+ <goals>
+ <goal>aggregate-features</goal>
+ <goal>analyse-features</goal>
+ <goal>attach-features</goal>
+ </goals>
+ <phase>prepare-package</phase>
+ </execution>
+ <execution>
+ <id>create-app-repository</id>
+ <goals>
+ <goal>repository</goal>
+ </goals>
+ <phase>package</phase>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- Configure bnd-baseline to skip when no previous version
exists. Remove after first release -->
+ <plugin>
+ <groupId>biz.aQute.bnd</groupId>
+ <artifactId>bnd-baseline-maven-plugin</artifactId>
+ <configuration>
+ <failOnMissing>false</failOnMissing>
+ </configuration>
+ </plugin>
+ <!-- Added: Feature Launcher plugin for manually starting the
aggregated 'app' feature -->
+ <plugin>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>feature-launcher-maven-plugin</artifactId>
+ <configuration>
+ <launches>
+ <launch>
+ <id>app</id>
+
<featureFile>${project.slingfeature.outputDirectory}/feature-app.json</featureFile>
+ </launch>
+ </launches>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/mcp-server/src/main/features/main.json
b/mcp-server/src/main/features/main.json
new file mode 100644
index 00000000..c34fb1f5
--- /dev/null
+++ b/mcp-server/src/main/features/main.json
@@ -0,0 +1,36 @@
+{
+ "bundles": [
+ {
+ "id": "${project.groupId}:${project.artifactId}:${project.version}",
+ "start-order": 25
+ },
+ {
+ "id": "io.modelcontextprotocol.sdk:mcp-core:0.15.0",
+ "start-order": 25
+ },
+ {
+ "id": "io.projectreactor:reactor-core:3.7.0",
+ "start-order": 25
+ },
+ {
+ "id": "org.reactivestreams:reactive-streams:1.0.4",
+ "start-order": 25
+ },
+ {
+ "id": "com.networknt:json-schema-validator:1.5.7",
+ "start-order": 25
+ },
+ {
+ "id": "com.ethlo.time:itu:1.10.3",
+ "start-order": 25
+ },
+ {
+ "id":
"com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.3",
+ "start-order": 25
+ },
+ {
+ "id": "org.yaml:snakeyaml:2.3",
+ "start-order": 25
+ }
+ ]
+}
\ No newline at end of file
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
new file mode 100644
index 00000000..80078dfe
--- /dev/null
+++ b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -0,0 +1,230 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+import io.modelcontextprotocol.server.McpStatelessSyncServer;
+import
io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
+import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.Resource;
+import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
+import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
+import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.sling.api.SlingJakartaHttpServletRequest;
+import org.apache.sling.api.SlingJakartaHttpServletResponse;
+import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
+import org.apache.sling.servlets.annotations.SlingServletPaths;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.FrameworkWiring;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+
+@Component(service = Servlet.class)
+@SlingServletPaths(value = {McpServlet.ENDPOINT})
+public class McpServlet extends SlingJakartaAllMethodsServlet {
+
+ static final String ENDPOINT = "/mcp";
+ private static final long serialVersionUID = 1L;
+ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
+
+ private McpStatelessSyncServer syncServer;
+ private HttpServletStatelessServerTransport transportProvider;
+ private MethodHandle doGetMethod;
+ private MethodHandle doPostMethod;
+
+ @Activate
+ public McpServlet(BundleContext ctx) throws IllegalAccessException,
NoSuchMethodException {
+
+ McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new
ObjectMapper());
+
+ transportProvider = HttpServletStatelessServerTransport.builder()
+ .messageEndpoint(ENDPOINT)
+ .jsonMapper(jsonMapper)
+ .build();
+
+ MethodHandles.Lookup privateLookup =
+
MethodHandles.privateLookupIn(HttpServletStatelessServerTransport.class,
LOOKUP);
+
+ doGetMethod = privateLookup.findVirtual(
+ HttpServletStatelessServerTransport.class,
+ "doGet",
+ java.lang.invoke.MethodType.methodType(
+ void.class, HttpServletRequest.class,
HttpServletResponse.class));
+ doPostMethod = privateLookup.findVirtual(
+ HttpServletStatelessServerTransport.class,
+ "doPost",
+ java.lang.invoke.MethodType.methodType(
+ void.class, HttpServletRequest.class,
HttpServletResponse.class));
+
+ syncServer = McpServer.sync(transportProvider)
+ .serverInfo("apache-sling", "0.1.0")
+ .jsonMapper(jsonMapper)
+ .jsonSchemaValidator(new DefaultJsonSchemaValidator())
+ .capabilities(ServerCapabilities.builder()
+ .tools(true)
+ .prompts(true)
+ .resources(true, true)
+ .build())
+ .build();
+
+ var schema = """
+ {
+ "type" : "object",
+ "id" : "urn:jsonschema:Operation",
+ "properties" : { }
+ }
+ """;
+ var syncToolSpecification = new SyncToolSpecification(
+ Tool.builder()
+ .name("refresh-packages")
+ .description("Refresh Packages")
+ .inputSchema(jsonMapper, schema)
+ .build(),
+ (exchange, arguments) -> {
+ FrameworkWiring fw =
ctx.getBundle(0).adapt(FrameworkWiring.class);
+
+ fw.refreshBundles(null);
+
+ return new CallToolResult("Bundles refreshed
successfully", Boolean.FALSE);
+ });
+
+ // Register tools, resources, and prompts
+ syncServer.addTool(syncToolSpecification);
+ syncServer.addPrompt(new SyncPromptSpecification(
+ new Prompt(
+ "new-sling-servlet",
+ "Create new Sling Servlet",
+ "Creates a new Sling Servlet in the current project
using annotations",
+ List.of(new PromptArgument(
+ "resource-type",
+ "Resource type",
+ "The Sling resource type to bind this servlet
to.",
+ true))),
+ (context, request) -> {
+ String resourceType = (String)
request.arguments().get("resource-type");
+ PromptMessage msg = new PromptMessage(
+ Role.ASSISTANT,
+ new McpSchema.TextContent(
+ "Create a new Sling Servlet for resource
type: " + resourceType
+ + " . Use the Sling-specific OSGi
declarative services annotations - @SlingServletResourceTypes and @Component .
Configure by default with the GET method and the json extension. Provide a
basic implementation of the doGet method that returns a JSON response with a
message 'Hello from Sling Servlet at resource type <resource-type>'."));
+ return new McpSchema.GetPromptResult("Result of creation",
List.of(msg));
+ }));
+ syncServer.addResource(new
McpStatelessServerFeatures.SyncResourceSpecification(
+ new Resource.Builder()
+ .name("bundle")
+ .uri("bundle://")
+ .description("OSGi bundle status")
+ .mimeType("text/plain")
+ .build(),
+ (context, request) -> {
+ List<McpSchema.ResourceContents> res =
Stream.of(ctx.getBundles())
+ .map(b -> new TextResourceContents(
+ "bundle://" + b.getSymbolicName(),
+ "text-plain",
+ "Bundle " + b.getSymbolicName() + " is in
state " + b.getState()))
+ .collect(Collectors.toList());
+
+ return new McpSchema.ReadResourceResult(res);
+ }));
+
+ syncServer.addResourceTemplate(new
McpStatelessServerFeatures.SyncResourceTemplateSpecification(
+ new ResourceTemplate.Builder()
+ .uriTemplate("bundles://state/{state}")
+ .name("bundles")
+ .build(),
+ (context, request) -> {
+ List<ResourceContents> bundles = List.of();
+ if
("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
+ bundles = Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getState() == Bundle.RESOLVED)
+ .<ResourceContents>map(b -> new
TextResourceContents(
+ "bundle://" + b.getSymbolicName(),
+ "text-plain",
+ "Bundle " + b.getSymbolicName() + " is
in state " + b.getState()))
+ .toList();
+ }
+
+ return new ReadResourceResult(bundles);
+ }));
+ }
+
+ @Override
+ protected void doGet(
+ @NotNull SlingJakartaHttpServletRequest request, @NotNull
SlingJakartaHttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ doGetMethod.invoke(transportProvider, request, response);
+ } catch (ServletException | IOException | RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new ServletException(t);
+ }
+ }
+
+ @Override
+ protected void doPost(
+ @NotNull SlingJakartaHttpServletRequest request, @NotNull
SlingJakartaHttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ doPostMethod.invoke(transportProvider, request, response);
+ } catch (ServletException | IOException | RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new ServletException(t);
+ }
+ }
+
+ @Deactivate
+ public void close() {
+ if (syncServer != null) {
+ syncServer.close();
+ }
+ }
+}