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 6c0f8049d1b3739d639f12eb73ab82b5276031bb
Author: Robert Munteanu <[email protected]>
AuthorDate: Thu Dec 11 17:45:54 2025 +0100

    chore(mcp): wip on discovering prompts from the repository
---
 bnd.bnd                                            |   4 +-
 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/bnd.bnd b/bnd.bnd
index 27af7f1..f1ef54d 100644
--- a/bnd.bnd
+++ b/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/src/main/features/main.json b/src/main/features/main.json
index fc49b6f..656a96b 100644
--- a/src/main/features/main.json
+++ b/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/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java 
b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
new file mode 100644
index 0000000..6a52ce5
--- /dev/null
+++ b/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/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java 
b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index daf9c57..ac708c6 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/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/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
 
b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
new file mode 100644
index 0000000..5d1b47c
--- /dev/null
+++ 
b/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/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md 
b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
new file mode 100644
index 0000000..e60aed8
--- /dev/null
+++ b/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

Reply via email to