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 441a329e chore(mcp): wip on discovering prompts from the repository
441a329e is described below
commit 441a329ec5b6b833516f579a6c6725919a676f07
Author: Robert Munteanu <[email protected]>
AuthorDate: Thu Dec 11 17:45:54 2025 +0100
chore(mcp): wip on discovering prompts from the repository
---
mcp-server/bnd.bnd | 4 +-
mcp-server/src/main/features/main.json | 10 +-
.../sling/mcp/server/impl/DiscoveredPrompt.java | 31 ++++
.../apache/sling/mcp/server/impl/McpServlet.java | 24 +++
.../impl/contribs/RepositoryPromptsRegistrar.java | 176 +++++++++++++++++++++
.../libs/sling/mcp/prompts/troubleshoot.md | 41 +++++
6 files changed, 284 insertions(+), 2 deletions(-)
diff --git a/mcp-server/bnd.bnd b/mcp-server/bnd.bnd
index 27af7f12..f1ef54d5 100644
--- a/mcp-server/bnd.bnd
+++ b/mcp-server/bnd.bnd
@@ -1,3 +1,5 @@
# 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
+ io.modelcontextprotocol.json.schema.jackson
+
+Sling-Initial-Content:
SLING-INF/libs/sling/mcp/prompts;path:=/libs/sling/mcp/prompts;overwrite:=true
\ No newline at end of file
diff --git a/mcp-server/src/main/features/main.json
b/mcp-server/src/main/features/main.json
index fc49b6fd..656a96ba 100644
--- a/mcp-server/src/main/features/main.json
+++ b/mcp-server/src/main/features/main.json
@@ -32,5 +32,13 @@
"id": "org.yaml:snakeyaml:2.3",
"start-order": 25
}
- ]
+ ],
+ "configurations": {
+
"org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~mcp-server":{
+ "whitelist.bundles":[
+ "org.apache.sling.mcp-server"
+ ],
+ "whitelist.name":"mcp-server"
+ }
+ }
}
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
new file mode 100644
index 00000000..6a52ce51
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
@@ -0,0 +1,31 @@
+/*
+ * 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.List;
+
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import org.apache.sling.api.resource.ResourceResolver;
+
+public interface DiscoveredPrompt {
+
+ public static final String SERVICE_PROP_NAME = "mcp.prompt.name";
+
+ List<PromptMessage> getPromptMessages(ResourceResolver resolver);
+}
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 daf9c573..ac708c6d 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
@@ -22,13 +22,18 @@ import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
import io.modelcontextprotocol.server.McpServer;
+import
io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import
io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
@@ -36,6 +41,7 @@ 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.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.jetbrains.annotations.NotNull;
@@ -44,6 +50,7 @@ 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.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -91,6 +98,8 @@ public class McpServlet extends SlingJakartaAllMethodsServlet
{
transportProvider = HttpServletStatelessServerTransport.builder()
.messageEndpoint(ENDPOINT)
.jsonMapper(jsonMapper)
+ .contextExtractor(request -> McpTransportContext.create(
+ Map.of("resourceResolver",
((SlingJakartaHttpServletRequest) request).getResourceResolver())))
.build();
MethodHandles.Lookup privateLookup =
@@ -152,6 +161,21 @@ public class McpServlet extends
SlingJakartaAllMethodsServlet {
.forEach(syncPrompt -> syncServer.addPrompt(syncPrompt));
}
+ @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = GREEDY,
cardinality = MULTIPLE)
+ protected void bindPrompt(DiscoveredPrompt prompt, Map<String, Object>
properties) {
+ String promptName = (String)
properties.get(DiscoveredPrompt.SERVICE_PROP_NAME);
+ syncServer.addPrompt(new SyncPromptSpecification(new
Prompt(promptName, null, List.of()), (c, r) -> {
+ ResourceResolver resourceResolver = (ResourceResolver)
c.get("resourceResolver");
+ var messages = prompt.getPromptMessages(resourceResolver);
+ return new McpSchema.GetPromptResult(null, messages);
+ }));
+ }
+
+ protected void unbindPrompt(Map<String, Object> properties) {
+ String promptName = (String)
properties.get(DiscoveredPrompt.SERVICE_PROP_NAME);
+ syncServer.removePrompt(promptName);
+ }
+
@Override
protected void doGet(
@NotNull SlingJakartaHttpServletRequest request, @NotNull
SlingJakartaHttpServletResponse response)
diff --git
a/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
new file mode 100644
index 00000000..5d1b47cf
--- /dev/null
+++
b/mcp-server/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
@@ -0,0 +1,176 @@
+/*
+ * 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.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.mcp.server.impl.DiscoveredPrompt;
+import org.jetbrains.annotations.NotNull;
+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.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+public class RepositoryPromptsRegistrar {
+
+ private static final String PROMPT_LIBS_DIR = "/libs/sling/mcp/prompts";
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private ConcurrentMap<String, ServiceRegistration<DiscoveredPrompt>>
registrations = new ConcurrentHashMap<>();
+
+ @Activate
+ public RepositoryPromptsRegistrar(@Reference ResourceResolverFactory rrf,
BundleContext ctx) throws LoginException {
+
+ ctx.registerService(
+ ResourceChangeListener.class,
+ new ResourceChangeListener() {
+
+ @Override
+ public void onChange(@NotNull List<ResourceChange>
changes) {
+ changes.forEach(a -> logger.info("Received change {}
at {}", a.getType(), a.getPath()));
+
+ try (ResourceResolver resolver =
rrf.getAdministrativeResourceResolver(null)) {
+ for (ResourceChange change : changes) {
+ if (change.getType() == ChangeType.REMOVED) {
+ String promptName =
getPromptName(change.getPath());
+ ServiceRegistration<DiscoveredPrompt> sr =
registrations.remove(promptName);
+ if (sr != null) {
+ sr.unregister();
+ } else {
+ logger.warn(
+ "No registered prompt found
for removed prompt {} at path {}, unable to unregister prompt.",
+ promptName,
+ change.getPath());
+ }
+ } else {
+ String promptName =
getPromptName(change.getPath());
+ ServiceRegistration<DiscoveredPrompt> sr =
registrations.remove(promptName);
+ if (sr != null) {
+ sr.unregister();
+ }
+ Resource prompt =
resolver.getResource(change.getPath());
+ if (prompt != null) {
+ registerPrompt(ctx, promptName,
prompt);
+ } else {
+ logger.warn(
+ "Prompt resource at {} not
found for change type {}, unable to register prompt.",
+ change.getPath(),
+ change.getType());
+ }
+ }
+ }
+ } catch (LoginException e) {
+ logger.error("Error processing resource change",
e);
+ }
+ }
+ },
+ new Hashtable<>(Map.of(ResourceChangeListener.PATHS,
PROMPT_LIBS_DIR, ResourceChangeListener.CHANGES, new String[] {
ResourceChangeListener.CHANGE_ADDED, ResourceChangeListener.CHANGE_CHANGED,
ResourceChangeListener.CHANGE_REMOVED } )));
+
+ // TODO - use service user
+ try (ResourceResolver resolver =
rrf.getAdministrativeResourceResolver(null)) {
+
+ Iterator<Resource> prompts = resolver.findResources(
+ "/jcr:root" + PROMPT_LIBS_DIR +
"//element(*,nt:file)[jcr:like(fn:name(), '%.md')]", "xpath");
+ while (prompts.hasNext()) {
+ Resource prompt = prompts.next();
+ String promptName = getPromptName(prompt.getPath());
+
+ registerPrompt(ctx, promptName, prompt);
+ }
+ }
+ }
+
+ private void registerPrompt(BundleContext ctx, String promptName, Resource
prompt) {
+
+ Map<String, String> serviceProps =
Map.of(DiscoveredPrompt.SERVICE_PROP_NAME, promptName);
+
+ // TODO - discover additional properties from the markdown file (front
matter)
+
+ var sr = ctx.registerService(
+ DiscoveredPrompt.class,
+ new RepositoryPrompt(prompt.getPath()),
+ new Hashtable<String, String>(serviceProps));
+
+ registrations.put(promptName, sr);
+ }
+
+ private String getPromptName(String path) {
+
+ // remove prefix
+ String promptName = path.substring(PROMPT_LIBS_DIR.length() + 1); //
account for trailing slash
+
+ // remove optional /jcr:content node name
+ if (promptName.endsWith("/jcr:content")) {
+ promptName = promptName.substring(0, promptName.length() -
"/jcr:content".length());
+ }
+
+ // remove .md extension
+ return promptName.substring(0, promptName.length() - ".md".length());
+ }
+
+ class RepositoryPrompt implements DiscoveredPrompt {
+ private final String promptPath;
+
+ RepositoryPrompt(String promptPath) {
+ this.promptPath = promptPath;
+ }
+
+ @Override
+ public List<PromptMessage> getPromptMessages(ResourceResolver
resolver) {
+
+ try {
+ Resource promptResource = resolver.getResource(promptPath);
+ String encoding =
promptResource.getResourceMetadata().getCharacterEncoding();
+ if ( encoding == null ) {
+ encoding = StandardCharsets.UTF_8.name();
+ }
+ try (InputStream stream =
promptResource.adaptTo(InputStream.class)) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ stream.transferTo(baos);
+ String contents = baos.toString(encoding);
+ return List.of(new PromptMessage(Role.ASSISTANT, new
TextContent(contents)));
+ }
+ } catch (IOException e) {
+ return List.of();
+ }
+ }
+ }
+}
diff --git
a/mcp-server/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
b/mcp-server/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
new file mode 100644
index 00000000..e60aed89
--- /dev/null
+++
b/mcp-server/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
@@ -0,0 +1,41 @@
+# Troubleshooting Guide
+
+## Overview
+This guide helps you diagnose and resolve common issues with Apache Sling MCP
Server.
+
+## Common Issues
+
+### MCP Server Not Responding
+- Verify the MCP servlet is registered and active
+- Check OSGi component status
+- Review log files for errors
+
+### Bundle Issues
+- Check bundle state (ACTIVE, RESOLVED, INSTALLED)
+- Verify all dependencies are satisfied
+- Use the OSGi diagnostic tools
+
+### Performance Problems
+- Review recent requests and response times
+- Check system resources
+- Analyze thread dumps if available
+
+### Component Registration Issues
+- Verify component configurations
+- Check service dependencies
+- Review component lifecycle logs
+
+## Diagnostic Tools
+The MCP Server provides several diagnostic tools:
+- Bundle state inspection
+- Component resource analysis
+- Recent request tracking
+- Log file access
+- OSGi diagnostic reports
+
+## Getting Help
+If you continue to experience issues:
+1. Gather diagnostic information using the MCP tools
+2. Review the Apache Sling documentation
+3. Check the Apache Sling mailing lists
+4. File an issue in the Apache Sling JIRA