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
commit 9fb2a6649eed9f38cceb15ebbb4605f681b9728d Author: Robert Munteanu <[email protected]> AuthorDate: Wed Nov 12 14:30:20 2025 +0100 Initial commit of a basic MCP server implementation for Sling --- README.md | 13 ++ bnd.bnd | 3 + pom.xml | 206 ++++++++++++++++++ src/main/features/main.json | 36 ++++ .../apache/sling/mcp/server/impl/McpServlet.java | 230 +++++++++++++++++++++ 5 files changed, 488 insertions(+) diff --git a/README.md b/README.md new file mode 100644 index 0000000..599c6a0 --- /dev/null +++ b/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/bnd.bnd b/bnd.bnd new file mode 100644 index 0000000..27af7f1 --- /dev/null +++ b/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/pom.xml b/pom.xml new file mode 100644 index 0000000..4a2323c --- /dev/null +++ b/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/src/main/features/main.json b/src/main/features/main.json new file mode 100644 index 0000000..c34fb1f --- /dev/null +++ b/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/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java new file mode 100644 index 0000000..80078df --- /dev/null +++ b/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(); + } + } +}
