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 49acb495 feat(mcp-server): split contributions into seperate files
49acb495 is described below
commit 49acb495dc3f3c73105c7526b0870d5d15f7bec9
Author: Robert Munteanu <[email protected]>
AuthorDate: Tue Dec 9 16:39:25 2025 +0100
feat(mcp-server): split contributions into seperate files
The API is bundle-internal, incomplete and sub-optimal, but it makes it
possible to use separate
files right now.
---
.../mcp/server/impl/McpJsonMapperProvider.java | 47 ++++++
.../mcp/server/impl/McpServerContribution.java | 50 +++++++
.../apache/sling/mcp/server/impl/McpServlet.java | 161 +++++----------------
.../impl/contribs/BundleResourceContribution.java | 108 ++++++++++++++
.../impl/contribs/RefreshPackagesContribution.java | 72 +++++++++
.../impl/contribs/ServletPromptContribution.java | 71 +++++++++
6 files changed, 385 insertions(+), 124 deletions(-)
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
new file mode 100644
index 00000000..74bece17
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
@@ -0,0 +1,47 @@
+/*
+ * 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 com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+
+@Component
+public class McpJsonMapperProvider {
+
+ private ServiceRegistration<McpJsonMapper> serviceRegistration;
+
+ @Activate
+ public void activate(BundleContext bundleContext) {
+ McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new
ObjectMapper());
+ serviceRegistration =
bundleContext.registerService(McpJsonMapper.class, jsonMapper, null);
+ }
+
+ @Deactivate
+ public void deactivate() {
+ if (serviceRegistration != null) {
+ serviceRegistration.unregister();
+ }
+ }
+}
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
new file mode 100644
index 00000000..4e3168d5
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
@@ -0,0 +1,50 @@
+/*
+ * 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.util.Optional;
+
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+
+public interface McpServerContribution {
+
+ default Optional<SyncToolSpecification> getSyncToolSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncResourceSpecification> getSyncResourceSpecification()
{
+ return Optional.empty();
+ }
+
+ default Optional<SyncResourceTemplateSpecification>
getSyncResourceTemplateSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncPromptSpecification> getSyncPromptSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncCompletionSpecification>
getSyncCompletionSpecification() {
+ return Optional.empty();
+ }
+}
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
index b12faa6b..daf9c573 100644
--- 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
@@ -21,36 +21,15 @@ 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 java.util.Optional;
-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.SyncCompletionSpecification;
-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.CompleteResult.CompleteCompletion;
-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.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;
@@ -60,16 +39,18 @@ 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;
+import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import static
org.osgi.service.component.annotations.ReferenceCardinality.MULTIPLE;
+import static
org.osgi.service.component.annotations.ReferencePolicyOption.GREEDY;
+
@Component(service = Servlet.class)
@SlingServletPaths(value = {McpServlet.ENDPOINT})
@Designate(ocd = McpServlet.Config.class)
@@ -99,22 +80,13 @@ public class McpServlet extends
SlingJakartaAllMethodsServlet {
private MethodHandle doGetMethod;
private MethodHandle doPostMethod;
- private static String getStateString(int state) {
- return switch (state) {
- case Bundle.UNINSTALLED -> "UNINSTALLED";
- case Bundle.INSTALLED -> "INSTALLED";
- case Bundle.RESOLVED -> "RESOLVED";
- case Bundle.STARTING -> "STARTING";
- case Bundle.STOPPING -> "STOPPING";
- case Bundle.ACTIVE -> "ACTIVE";
- default -> "UNKNOWN";
- };
- }
-
@Activate
- public McpServlet(BundleContext ctx, Config config) throws
IllegalAccessException, NoSuchMethodException {
-
- McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new
ObjectMapper());
+ public McpServlet(
+ BundleContext ctx,
+ Config config,
+ @Reference McpJsonMapper jsonMapper,
+ @Reference(cardinality = MULTIPLE, policyOption = GREEDY)
List<McpServerContribution> contributions)
+ throws IllegalAccessException, NoSuchMethodException {
transportProvider = HttpServletStatelessServerTransport.builder()
.messageEndpoint(ENDPOINT)
@@ -135,21 +107,22 @@ public class McpServlet extends
SlingJakartaAllMethodsServlet {
java.lang.invoke.MethodType.methodType(
void.class, HttpServletRequest.class,
HttpServletResponse.class));
- SyncCompletionSpecification servletCompletionSpec = new
McpStatelessServerFeatures.SyncCompletionSpecification(
- new McpSchema.PromptReference("ref/prompt",
"new-sling-servlet"), (context, request) -> {
- return new McpSchema.CompleteResult(new
CompleteCompletion(List.of(), 0, false));
- });
-
String serverVersion = config.serverVersion();
if (serverVersion == null || serverVersion.isEmpty()) {
serverVersion = ctx.getBundle().getVersion().toString();
}
+
+ var completions = contributions.stream()
+ .map(McpServerContribution::getSyncCompletionSpecification)
+ .flatMap(Optional::stream)
+ .toList();
+
syncServer = McpServer.sync(transportProvider)
.serverInfo(config.serverTitle(), serverVersion)
.jsonMapper(jsonMapper)
.jsonSchemaValidator(new DefaultJsonSchemaValidator())
.instructions(config.instructions())
- .completions(List.of(servletCompletionSpec))
+ .completions(completions)
.capabilities(ServerCapabilities.builder()
.tools(false)
.prompts(false)
@@ -158,85 +131,25 @@ public class McpServlet extends
SlingJakartaAllMethodsServlet {
.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) -> {
- String bundleInfo = Stream.of(ctx.getBundles())
- .map(b -> "Bundle " + b.getSymbolicName() + " is
in state " + getStateString(b.getState())
- + " (" + b.getState() + ")")
- .collect(Collectors.joining("\n"));
-
- TextResourceContents contents = new
TextResourceContents("bundle://", "text/plain", bundleInfo);
-
- return new McpSchema.ReadResourceResult(List.of(contents));
- }));
-
- syncServer.addResourceTemplate(new
McpStatelessServerFeatures.SyncResourceTemplateSpecification(
- new ResourceTemplate.Builder()
- .uriTemplate("bundles://state/{state}")
- .name("bundles")
- .build(),
- (context, request) -> {
- String bundleInfo = "";
- if
("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
- bundleInfo = Arrays.stream(ctx.getBundles())
- .filter(b -> b.getState() == Bundle.RESOLVED)
- .map(b -> "Bundle " + b.getSymbolicName() + "
is in state "
- + getStateString(b.getState()) + " ("
+ b.getState() + ")")
- .collect(Collectors.joining("\n"));
- }
-
- TextResourceContents contents = new
TextResourceContents(request.uri(), "text/plain", bundleInfo);
-
- return new ReadResourceResult(List.of(contents));
- }));
+ contributions.stream()
+ .map(McpServerContribution::getSyncToolSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncTool -> syncServer.addTool(syncTool));
+
+ contributions.stream()
+ .map(McpServerContribution::getSyncResourceSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncResource -> syncServer.addResource(syncResource));
+
+ contributions.stream()
+
.map(McpServerContribution::getSyncResourceTemplateSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncResource ->
syncServer.addResourceTemplate(syncResource));
+
+ contributions.stream()
+ .map(McpServerContribution::getSyncPromptSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncPrompt -> syncServer.addPrompt(syncPrompt));
}
@Override
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java
new file mode 100644
index 00000000..605eeb3f
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java
@@ -0,0 +1,108 @@
+/*
+ * 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.contribs;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.Resource;
+import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
+import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
+import org.apache.sling.mcp.server.impl.McpServerContribution;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+@Component
+public class BundleResourceContribution implements McpServerContribution {
+
+ private static String getStateString(int state) {
+ return switch (state) {
+ case Bundle.UNINSTALLED -> "UNINSTALLED";
+ case Bundle.INSTALLED -> "INSTALLED";
+ case Bundle.RESOLVED -> "RESOLVED";
+ case Bundle.STARTING -> "STARTING";
+ case Bundle.STOPPING -> "STOPPING";
+ case Bundle.ACTIVE -> "ACTIVE";
+ default -> "UNKNOWN";
+ };
+ }
+
+ private BundleContext ctx;
+
+ @Activate
+ public BundleResourceContribution(BundleContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public Optional<SyncResourceSpecification> getSyncResourceSpecification() {
+
+ return Optional.of(new
McpStatelessServerFeatures.SyncResourceSpecification(
+ new Resource.Builder()
+ .name("bundle")
+ .uri("bundle://")
+ .description("OSGi bundle status")
+ .mimeType("text/plain")
+ .build(),
+ (context, request) -> {
+ String bundleInfo = Stream.of(ctx.getBundles())
+ .map(b -> "Bundle " + b.getSymbolicName() + " is
in state " + getStateString(b.getState())
+ + " (" + b.getState() + ")")
+ .collect(Collectors.joining("\n"));
+
+ TextResourceContents contents = new
TextResourceContents("bundle://", "text/plain", bundleInfo);
+
+ return new McpSchema.ReadResourceResult(List.of(contents));
+ }));
+ }
+
+ @Override
+ public Optional<SyncResourceTemplateSpecification>
getSyncResourceTemplateSpecification() {
+ return Optional.of(new
McpStatelessServerFeatures.SyncResourceTemplateSpecification(
+ new ResourceTemplate.Builder()
+ .uriTemplate("bundles://state/{state}")
+ .name("bundles")
+ .build(),
+ (context, request) -> {
+ String bundleInfo = "";
+ if
("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
+ bundleInfo = Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getState() == Bundle.RESOLVED)
+ .map(b -> "Bundle " + b.getSymbolicName() + "
is in state "
+ + getStateString(b.getState()) + " ("
+ b.getState() + ")")
+ .collect(Collectors.joining("\n"));
+ }
+
+ TextResourceContents contents = new
TextResourceContents(request.uri(), "text/plain", bundleInfo);
+
+ return new ReadResourceResult(List.of(contents));
+ }));
+ }
+}
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java
new file mode 100644
index 00000000..0b02dcd8
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java
@@ -0,0 +1,72 @@
+/*
+ * 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.contribs;
+
+import java.util.Optional;
+
+import io.modelcontextprotocol.json.McpJsonMapper;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+import org.apache.sling.mcp.server.impl.McpServerContribution;
+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.Reference;
+
+@Component
+public class RefreshPackagesContribution implements McpServerContribution {
+
+ @Reference
+ private McpJsonMapper jsonMapper;
+
+ private final BundleContext ctx;
+
+ @Activate
+ public RefreshPackagesContribution(BundleContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public Optional<SyncToolSpecification> getSyncToolSpecification() {
+
+ var schema = """
+ {
+ "type" : "object",
+ "id" : "urn:jsonschema:Operation",
+ "properties" : { }
+ }
+ """;
+
+ return Optional.of(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);
+ }));
+ }
+}
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/ServletPromptContribution.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/ServletPromptContribution.java
new file mode 100644
index 00000000..3e676669
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/ServletPromptContribution.java
@@ -0,0 +1,71 @@
+/*
+ * 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.contribs;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
+import io.modelcontextprotocol.spec.McpSchema;
+import
io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
+import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import org.apache.sling.mcp.server.impl.McpServerContribution;
+import org.osgi.service.component.annotations.Component;
+
+@Component
+public class ServletPromptContribution implements McpServerContribution {
+
+ @Override
+ public Optional<SyncPromptSpecification> getSyncPromptSpecification() {
+ return Optional.of(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));
+ }));
+ }
+
+ @Override
+ public Optional<SyncCompletionSpecification>
getSyncCompletionSpecification() {
+ // supply no completions for various resource types because it's
supposed to be specified by the user
+ return Optional.of(new
McpStatelessServerFeatures.SyncCompletionSpecification(
+ new McpSchema.PromptReference("ref/prompt",
"new-sling-servlet"), (context, request) -> {
+ return new McpSchema.CompleteResult(new
CompleteCompletion(List.of(), 0, false));
+ }));
+ }
+}