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
