You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2026/05/04 11:16:09 UTC
(sling-org-apache-sling-mcp-server-contributions) 01/17: chore(mcp-server): move contributions to a separate bundle
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-contributions.git
commit 6a2fb41256f052346c1f2b59e94203190aa9b68e
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Jan 13 15:08:06 2026 +0100
chore(mcp-server): move contributions to a separate bundle
---
README.md | 3 +
pom.xml | 137 ++++++
.../impl/contribs/BundleResourceContribution.java | 142 ++++++
.../mcp/server/impl/contribs/BundleState.java | 57 +++
.../contribs/ComponentResourceContribution.java | 126 +++++
.../server/impl/contribs/LogToolContribution.java | 359 ++++++++++++++
.../contribs/OsgiBundleDiagnosticContribution.java | 525 +++++++++++++++++++++
.../contribs/OsgiDiagnosticPromptContribution.java | 155 ++++++
.../impl/contribs/RecentRequestsContribution.java | 73 +++
.../impl/contribs/RefreshPackagesContribution.java | 74 +++
.../impl/contribs/ServletPromptContribution.java | 71 +++
.../libs/sling/mcp/prompts/new-sling-servlet.md | 17 +
.../libs/sling/mcp/prompts/troubleshoot.md | 45 ++
13 files changed, 1784 insertions(+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..24a2950
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Apache Sling MCP Server contributions
+
+This project contains contributions ( tools, resources, prompts) for the Apache Sling MCP Server. See [../mcp-server/README.md](../mcp-server/README.md) for details on how to set up and run the MCP Server itself.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..3f10631
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,137 @@
+<?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>65</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.mcp-server-contributions</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+
+ <name>Apache Sling MCP Server Contributions</name>
+
+ <properties>
+ <sling.java.version>17</sling.java.version>
+ <!-- Run with -Dslingfeature.app.vmOption=-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=30303 for debug mode-->
+ <slingfeature.app.vmOption />
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.framework</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.resource</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.component</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>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.engine</artifactId>
+ <version>2.10.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.17.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.log</artifactId>
+ <version>1.3.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.mcp-server</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <!-- 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>
+ </plugins>
+ </build>
+
+</project>
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java
new file mode 100644
index 0000000..d54e598
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java
@@ -0,0 +1,142 @@
+/*
+ * 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.SyncCompletionSpecification;
+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.spi.McpServerContribution;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+@Component
+public class BundleResourceContribution implements McpServerContribution {
+
+ private static final String URI_BUNDLES_ALL = "bundles://all";
+ private static final String RESOURCE_TEMPLATE_BUNDLES_STATE_PREFIX = "bundles://state/";
+ private static final String RESOURCE_TEMPLATE_BUNDLES_STATE_PATTERN =
+ RESOURCE_TEMPLATE_BUNDLES_STATE_PREFIX + "{state}";
+
+ 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("bundles")
+ .uri(URI_BUNDLES_ALL)
+ .description(
+ "List all OSGi bundles with symbolic name, version, and state. Fragment bundles are marked with [Fragment]")
+ .mimeType("text/plain")
+ .build(),
+ (context, request) -> {
+ String bundleInfo =
+ Stream.of(ctx.getBundles()).map(this::describe).collect(Collectors.joining("\n"));
+
+ TextResourceContents contents = new TextResourceContents(URI_BUNDLES_ALL, "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(RESOURCE_TEMPLATE_BUNDLES_STATE_PATTERN)
+ .name("bundles")
+ .build(),
+ (context, request) -> {
+ String requestedState = request.uri().substring(RESOURCE_TEMPLATE_BUNDLES_STATE_PREFIX.length());
+ try {
+ BundleState bundleState = BundleState.valueOf(requestedState.toUpperCase(Locale.ENGLISH));
+ if (!bundleState.isValid()) {
+ throw new IllegalArgumentException("Invalid bundle state: " + requestedState);
+ }
+ String bundleInfo = Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getState() == bundleState.getState())
+ .map(this::describe)
+ .collect(Collectors.joining("\n"));
+
+ TextResourceContents contents =
+ new TextResourceContents(request.uri(), "text/plain", bundleInfo);
+
+ return new ReadResourceResult(List.of(contents));
+ } catch (IllegalArgumentException e) {
+ return new ReadResourceResult(List.of(new TextResourceContents(
+ request.uri(), "text/plain", "Invalid bundle state requested: " + requestedState)));
+ }
+ }));
+ }
+
+ @Override
+ public Optional<SyncCompletionSpecification> getSyncCompletionSpecification() {
+
+ return Optional.of(new McpStatelessServerFeatures.SyncCompletionSpecification(
+ new McpSchema.ResourceReference("ref/resource", RESOURCE_TEMPLATE_BUNDLES_STATE_PATTERN),
+ (context, request) -> {
+
+ // expect argument name to always be "state"
+ String requestedState = request.argument().value();
+ List<String> states = Stream.of(BundleState.values())
+ .filter(BundleState::isValid)
+ .map(s -> s.name().toLowerCase(Locale.ENGLISH))
+ .toList();
+ if (requestedState != null && !requestedState.isEmpty()) {
+ states = states.stream()
+ .filter(s -> s.startsWith(requestedState.toLowerCase(Locale.ENGLISH)))
+ .toList();
+ }
+ return new McpSchema.CompleteResult(
+ new McpSchema.CompleteResult.CompleteCompletion(states, states.size(), false));
+ }));
+ }
+
+ private String describe(Bundle b) {
+ boolean isFragment = Optional.ofNullable(b.adapt(BundleRevision.class)).stream()
+ .map(br -> (br.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0)
+ .findAny()
+ .orElse(false);
+ String additionalInfo = isFragment ? " [Fragment]" : "";
+ return "Bundle " + b.getSymbolicName() + additionalInfo + " (version " + b.getVersion() + ") is in state "
+ + BundleState.fromState(b.getState()) + " (" + b.getState() + ")";
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleState.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleState.java
new file mode 100644
index 0000000..29e5bba
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleState.java
@@ -0,0 +1,57 @@
+/*
+ * 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 org.osgi.framework.Bundle;
+
+/**
+ * Enum representing OSGi bundle states.
+ */
+public enum BundleState {
+ UNINSTALLED(Bundle.UNINSTALLED),
+ INSTALLED(Bundle.INSTALLED),
+ RESOLVED(Bundle.RESOLVED),
+ STARTING(Bundle.STARTING),
+ STOPPING(Bundle.STOPPING),
+ ACTIVE(Bundle.ACTIVE),
+ UNKNOWN(-1);
+
+ private final int state;
+
+ BundleState(int state) {
+ this.state = state;
+ }
+
+ public static BundleState fromState(int state) {
+ for (BundleState bs : values()) {
+ if (bs.state == state) {
+ return bs;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ public boolean isValid() {
+ return this != UNKNOWN;
+ }
+
+ public int getState() {
+ return state;
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/ComponentResourceContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/ComponentResourceContribution.java
new file mode 100644
index 0000000..937ad9d
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/ComponentResourceContribution.java
@@ -0,0 +1,126 @@
+/*
+ * 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.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+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.spi.McpServerContribution;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.runtime.ServiceComponentRuntime;
+import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
+
+@Component
+public class ComponentResourceContribution implements McpServerContribution {
+
+ private static String getStateString(int state) {
+ return switch (state) {
+ case 1 -> "UNSATISFIED_CONFIGURATION";
+ case 2 -> "UNSATISFIED_REFERENCE";
+ case 4 -> "SATISFIED";
+ case 8 -> "ACTIVE";
+ case 16 -> "REGISTERED";
+ case 32 -> "FACTORY";
+ case 64 -> "DISABLED";
+ case 128 -> "ENABLING";
+ case 256 -> "ENABLED";
+ case 512 -> "DISABLING";
+ default -> "UNKNOWN";
+ };
+ }
+
+ @Reference
+ private ServiceComponentRuntime scr;
+
+ @Activate
+ public ComponentResourceContribution() {}
+
+ @Override
+ public Optional<SyncResourceSpecification> getSyncResourceSpecification() {
+
+ return Optional.of(new McpStatelessServerFeatures.SyncResourceSpecification(
+ new Resource.Builder()
+ .name("component")
+ .uri("component://")
+ .description("OSGi component status")
+ .mimeType("text/plain")
+ .build(),
+ (context, request) -> {
+ Collection<ComponentDescriptionDTO> components = scr.getComponentDescriptionDTOs();
+ String componentInfo = components.stream()
+ .map(c -> {
+ String state = scr.getComponentConfigurationDTOs(c).stream()
+ .map(config -> getStateString(config.state))
+ .collect(Collectors.joining(", "));
+ return "Component " + c.name + " is in state(s): " + state;
+ })
+ .collect(Collectors.joining("\n"));
+
+ TextResourceContents contents =
+ new TextResourceContents("component://", "text/plain", componentInfo);
+
+ return new McpSchema.ReadResourceResult(List.of(contents));
+ }));
+ }
+
+ @Override
+ public Optional<SyncResourceTemplateSpecification> getSyncResourceTemplateSpecification() {
+ return Optional.of(new McpStatelessServerFeatures.SyncResourceTemplateSpecification(
+ new ResourceTemplate.Builder()
+ .uriTemplate("components://state/{state}")
+ .name("components")
+ .build(),
+ (context, request) -> {
+ String componentInfo = "";
+ String uri = request.uri().toLowerCase(Locale.ENGLISH);
+
+ if (uri.startsWith("components://state/")) {
+ String requestedState = uri.substring("components://state/".length());
+ Collection<ComponentDescriptionDTO> components = scr.getComponentDescriptionDTOs();
+
+ componentInfo = components.stream()
+ .flatMap(c -> scr.getComponentConfigurationDTOs(c).stream()
+ .filter(config -> getStateString(config.state)
+ .toLowerCase(Locale.ENGLISH)
+ .equals(requestedState))
+ .map(config -> "Component " + c.name + " is in state: "
+ + getStateString(config.state)))
+ .collect(Collectors.joining("\n"));
+ }
+
+ TextResourceContents contents =
+ new TextResourceContents(request.uri(), "text/plain", componentInfo);
+
+ return new ReadResourceResult(List.of(contents));
+ }));
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java
new file mode 100644
index 0000000..8e61147
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java
@@ -0,0 +1,359 @@
+/*
+ * 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.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+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.spi.McpServerContribution;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogReaderService;
+import org.osgi.service.log.LogService;
+
+/**
+ * MCP Tool that provides access to AEM/OSGi logs with filtering capabilities.
+ * Allows filtering by regex pattern, log level, and maximum number of entries.
+ */
+@Component
+public class LogToolContribution implements McpServerContribution {
+
+ @Reference
+ private LogReaderService logReaderService;
+
+ @Reference
+ private McpJsonMapper jsonMapper;
+
+ private static final int DEFAULT_MAX_LOGS = 200;
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ @Override
+ public Optional<SyncToolSpecification> getSyncToolSpecification() {
+
+ var schema = """
+ {
+ "type" : "object",
+ "id" : "urn:jsonschema:LogFilterInput",
+ "properties" : {
+ "regex" : {
+ "type" : "string",
+ "description" : "Optional regex pattern to filter log messages. If not provided, all logs are returned."
+ },
+ "logLevel" : {
+ "type" : "string",
+ "description" : "Minimum log level to return. Options: ERROR, WARN, INFO, DEBUG, TRACE. Defaults to ERROR.",
+ "enum" : ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"]
+ },
+ "maxEntries" : {
+ "type" : "integer",
+ "description" : "Maximum number of log entries to return. Defaults to 200.",
+ "minimum" : 1,
+ "maximum" : 1000
+ }
+ }
+ }
+ """;
+
+ return Optional.of(new SyncToolSpecification(
+ Tool.builder()
+ .name("aem-logs")
+ .description("Retrieve AEM/OSGi logs with optional filtering. "
+ + "Supports filtering by regex pattern, log level (ERROR, WARN, INFO, DEBUG, TRACE), "
+ + "and maximum number of entries. Returns most recent logs first.")
+ .inputSchema(jsonMapper, schema)
+ .build(),
+ (exchange, request) -> {
+ String regexPattern = (String) request.arguments().get("regex");
+ String logLevelStr = (String) request.arguments().get("logLevel");
+ Object maxEntriesObj = request.arguments().get("maxEntries");
+
+ // Parse parameters
+ int maxEntries = DEFAULT_MAX_LOGS;
+ if (maxEntriesObj instanceof Number) {
+ maxEntries = ((Number) maxEntriesObj).intValue();
+ maxEntries = Math.min(maxEntries, 1000); // Cap at 1000
+ }
+
+ int minLogLevel = LogService.LOG_ERROR;
+ if (logLevelStr != null && !logLevelStr.isEmpty()) {
+ minLogLevel = parseLogLevel(logLevelStr);
+ if (minLogLevel == -1) {
+ return CallToolResult.builder()
+ .addTextContent("Invalid log level: " + logLevelStr
+ + ". Valid options are: ERROR, WARN, INFO, DEBUG, TRACE")
+ .isError(true)
+ .build();
+ }
+ }
+
+ // Compile regex pattern if provided
+ Pattern pattern = null;
+ if (regexPattern != null && !regexPattern.isEmpty()) {
+ try {
+ pattern = Pattern.compile(regexPattern, Pattern.CASE_INSENSITIVE);
+ } catch (PatternSyntaxException e) {
+ return CallToolResult.builder()
+ .addTextContent("Invalid regex pattern: " + e.getMessage())
+ .isError(true)
+ .build();
+ }
+ }
+
+ // Collect and filter logs
+ List<LogEntry> filteredLogs = collectLogs(pattern, minLogLevel, maxEntries);
+
+ // Format output
+ String result = formatLogs(filteredLogs, regexPattern, minLogLevel, maxEntries);
+
+ return CallToolResult.builder().addTextContent(result).build();
+ }));
+ }
+
+ private List<LogEntry> collectLogs(Pattern pattern, int minLogLevel, int maxEntries) {
+ List<LogEntry> logs = new ArrayList<>();
+
+ @SuppressWarnings("unchecked")
+ Enumeration<LogEntry> logEntries = logReaderService.getLog();
+ while (logEntries.hasMoreElements() && logs.size() < maxEntries) {
+ LogEntry entry = logEntries.nextElement();
+
+ // Filter by log level (lower values = higher severity)
+ if (entry.getLevel() > minLogLevel) {
+ continue;
+ }
+
+ // Filter by regex pattern if provided - search entire log entry
+ if (pattern != null) {
+ String fullLogEntry = buildFullLogEntryText(entry);
+ if (!pattern.matcher(fullLogEntry).find()) {
+ continue;
+ }
+ }
+
+ logs.add(entry);
+ }
+
+ return logs;
+ }
+
+ private String buildFullLogEntryText(LogEntry entry) {
+ StringBuilder text = new StringBuilder();
+
+ // Add log level
+ text.append(getLogLevelName(entry.getLevel())).append(" ");
+
+ // Add bundle name
+ Bundle bundle = entry.getBundle();
+ if (bundle != null) {
+ text.append(getBundleName(bundle)).append(" ");
+ }
+
+ // Add message
+ String message = entry.getMessage();
+ if (message != null) {
+ text.append(message).append(" ");
+ }
+
+ // Add service reference info
+ ServiceReference<?> serviceRef = entry.getServiceReference();
+ if (serviceRef != null) {
+ String serviceDesc = getServiceDescription(serviceRef);
+ if (serviceDesc != null && !serviceDesc.isEmpty()) {
+ text.append(serviceDesc).append(" ");
+ }
+ }
+
+ // Add exception info
+ Throwable exception = entry.getException();
+ if (exception != null) {
+ text.append(exception.getClass().getName()).append(" ");
+ if (exception.getMessage() != null) {
+ text.append(exception.getMessage()).append(" ");
+ }
+
+ // Add stack trace
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exception.printStackTrace(pw);
+ text.append(sw.toString());
+ }
+
+ return text.toString();
+ }
+
+ private int parseLogLevel(String levelStr) {
+ return switch (levelStr.toUpperCase()) {
+ case "ERROR" -> LogService.LOG_ERROR;
+ case "WARN", "WARNING" -> LogService.LOG_WARNING;
+ case "INFO" -> LogService.LOG_INFO;
+ case "DEBUG" -> LogService.LOG_DEBUG;
+ default -> -1;
+ };
+ }
+
+ private String formatLogs(List<LogEntry> logs, String regexPattern, int minLogLevel, int maxEntries) {
+ StringBuilder result = new StringBuilder();
+
+ result.append("=== AEM Log Entries ===\n\n");
+ result.append("Filter Settings:\n");
+ result.append(" - Log Level: ").append(getLogLevelName(minLogLevel)).append(" and higher severity\n");
+ result.append(" - Regex Pattern: ")
+ .append(regexPattern != null ? regexPattern : "(none)")
+ .append("\n");
+ result.append(" - Max Entries: ").append(maxEntries).append("\n");
+ result.append(" - Entries Found: ").append(logs.size()).append("\n\n");
+
+ if (logs.isEmpty()) {
+ result.append("No log entries found matching the criteria.\n");
+ return result.toString();
+ }
+
+ result.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
+
+ for (int i = 0; i < logs.size(); i++) {
+ LogEntry entry = logs.get(i);
+ formatLogEntry(entry, i + 1, result);
+
+ if (i < logs.size() - 1) {
+ result.append("\n");
+ }
+ }
+
+ return result.toString();
+ }
+
+ private void formatLogEntry(LogEntry entry, int index, StringBuilder result) {
+ result.append("[").append(index).append("] ");
+ result.append(DATE_FORMAT.format(new Date(entry.getTime())));
+ result.append(" [").append(getLogLevelName(entry.getLevel())).append("] ");
+
+ // Add bundle information
+ Bundle bundle = entry.getBundle();
+ if (bundle != null) {
+ String bundleName = getBundleName(bundle);
+ result.append("[").append(bundleName).append("] ");
+ }
+
+ // Add message
+ String message = entry.getMessage();
+ result.append(message != null ? message : "(no message)");
+ result.append("\n");
+
+ // Add service reference info if available
+ ServiceReference<?> serviceRef = entry.getServiceReference();
+ if (serviceRef != null) {
+ String serviceDesc = getServiceDescription(serviceRef);
+ if (serviceDesc != null && !serviceDesc.isEmpty()) {
+ result.append(" Service: ").append(serviceDesc).append("\n");
+ }
+ }
+
+ // Add exception info if available
+ Throwable exception = entry.getException();
+ if (exception != null) {
+ result.append(" Exception: ").append(exception.getClass().getName());
+ if (exception.getMessage() != null) {
+ result.append(": ").append(exception.getMessage());
+ }
+ result.append("\n");
+
+ // Add stack trace (first few lines)
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ exception.printStackTrace(pw);
+ String stackTrace = sw.toString();
+
+ // Limit stack trace to first 10 lines
+ String[] lines = stackTrace.split("\n");
+ int maxLines = Math.min(lines.length, 10);
+ for (int i = 0; i < maxLines; i++) {
+ result.append(" ").append(lines[i]).append("\n");
+ }
+ if (lines.length > maxLines) {
+ result.append(" ... (").append(lines.length - maxLines).append(" more lines)\n");
+ }
+ }
+ }
+
+ private String getBundleName(Bundle bundle) {
+ String name = bundle.getHeaders().get(Constants.BUNDLE_NAME);
+ if (name == null || name.isEmpty()) {
+ name = bundle.getSymbolicName();
+ }
+ if (name == null || name.isEmpty()) {
+ name = "Bundle#" + bundle.getBundleId();
+ }
+ return name;
+ }
+
+ private String getServiceDescription(ServiceReference<?> ref) {
+ if (ref == null) {
+ return null;
+ }
+
+ Object serviceId = ref.getProperty(Constants.SERVICE_ID);
+ Object objectClass = ref.getProperty(Constants.OBJECTCLASS);
+
+ StringBuilder desc = new StringBuilder();
+ if (objectClass instanceof String[]) {
+ String[] classes = (String[]) objectClass;
+ if (classes.length > 0) {
+ desc.append(classes[0]);
+ if (classes.length > 1) {
+ desc.append(" (").append(classes.length - 1).append(" more interfaces)");
+ }
+ }
+ }
+
+ if (serviceId != null) {
+ if (desc.length() > 0) {
+ desc.append(" ");
+ }
+ desc.append("[id=").append(serviceId).append("]");
+ }
+
+ return desc.toString();
+ }
+
+ private String getLogLevelName(int level) {
+ return switch (level) {
+ case LogService.LOG_ERROR -> "ERROR";
+ case LogService.LOG_WARNING -> "WARN";
+ case LogService.LOG_INFO -> "INFO";
+ case LogService.LOG_DEBUG -> "DEBUG";
+ default -> "LEVEL_" + level;
+ };
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiBundleDiagnosticContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiBundleDiagnosticContribution.java
new file mode 100644
index 0000000..fdc3be7
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiBundleDiagnosticContribution.java
@@ -0,0 +1,525 @@
+/*
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+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.spi.McpServerContribution;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.runtime.ServiceComponentRuntime;
+import org.osgi.service.component.runtime.dto.ComponentConfigurationDTO;
+import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
+import org.osgi.service.component.runtime.dto.UnsatisfiedReferenceDTO;
+
+/**
+ * MCP Tool that diagnoses why OSGi bundles and components aren't starting.
+ * This tool provides detailed diagnostic information about:
+ * - Bundle state and resolution issues
+ * - Unsatisfied package imports
+ * - Missing service dependencies for components
+ * - Configuration problems
+ */
+@Component
+public class OsgiBundleDiagnosticContribution implements McpServerContribution {
+
+ @Reference
+ private McpJsonMapper jsonMapper;
+
+ @Reference
+ private ServiceComponentRuntime scr;
+
+ private final BundleContext ctx;
+
+ @Activate
+ public OsgiBundleDiagnosticContribution(BundleContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public Optional<SyncToolSpecification> getSyncToolSpecification() {
+
+ var schema = """
+ {
+ "type" : "object",
+ "id" : "urn:jsonschema:DiagnoseBundleInput",
+ "properties" : {
+ "bundleSymbolicName" : {
+ "type" : "string",
+ "description" : "The symbolic name of the bundle to diagnose. If not provided, will diagnose all problematic bundles."
+ }
+ }
+ }
+ """;
+
+ return Optional.of(new SyncToolSpecification(
+ Tool.builder()
+ .name("diagnose-osgi-bundle")
+ .description(
+ "Diagnose why an OSGi bundle or component isn't starting. Provides detailed information about unsatisfied dependencies, missing packages, and component configuration issues.")
+ .inputSchema(jsonMapper, schema)
+ .build(),
+ (exchange, request) -> {
+ String bundleSymbolicName = (String) request.arguments().get("bundleSymbolicName");
+
+ if (bundleSymbolicName != null && !bundleSymbolicName.isEmpty()) {
+ return diagnoseSpecificBundle(bundleSymbolicName);
+ } else {
+ return diagnoseAllProblematicBundles();
+ }
+ }));
+ }
+
+ private CallToolResult diagnoseSpecificBundle(String symbolicName) {
+ Bundle bundle = findBundle(symbolicName);
+ if (bundle == null) {
+ return CallToolResult.builder()
+ .addTextContent("Bundle '" + symbolicName + "' not found.")
+ .isError(true)
+ .build();
+ }
+
+ StringBuilder result = new StringBuilder();
+ result.append("=== Bundle Diagnostic Report ===\n\n");
+ result.append("Bundle: ").append(bundle.getSymbolicName()).append("\n");
+ result.append("Version: ").append(bundle.getVersion()).append("\n");
+ result.append("State: ").append(getStateName(bundle.getState())).append("\n\n");
+
+ if (bundle.getState() == Bundle.ACTIVE) {
+ result.append("✓ Bundle is ACTIVE and running normally.\n\n");
+ // Check components
+ appendComponentDiagnostics(bundle, result);
+ } else {
+ result.append("✗ Bundle is NOT active. Analyzing issues...\n\n");
+ analyzeBundleIssues(bundle, result);
+ appendComponentDiagnostics(bundle, result);
+ }
+
+ return CallToolResult.builder().addTextContent(result.toString()).build();
+ }
+
+ private CallToolResult diagnoseAllProblematicBundles() {
+ StringBuilder result = new StringBuilder();
+ result.append("=== OSGi System Diagnostic Report ===\n\n");
+
+ List<Bundle> problematicBundles = Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getState() != Bundle.ACTIVE && b.getState() != Bundle.UNINSTALLED)
+ .collect(Collectors.toList());
+
+ if (problematicBundles.isEmpty()) {
+ result.append("✓ All bundles are active!\n\n");
+ } else {
+ result.append("Found ").append(problematicBundles.size()).append(" problematic bundle(s):\n\n");
+
+ for (Bundle bundle : problematicBundles) {
+ result.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
+ result.append("Bundle: ").append(bundle.getSymbolicName()).append("\n");
+ result.append("State: ").append(getStateName(bundle.getState())).append("\n");
+ analyzeBundleIssues(bundle, result);
+ result.append("\n");
+ }
+ }
+
+ // Check for components with issues
+ appendAllComponentIssues(result);
+
+ return CallToolResult.builder().addTextContent(result.toString()).build();
+ }
+
+ private void analyzeBundleIssues(Bundle bundle, StringBuilder result) {
+ if (bundle.getState() == Bundle.INSTALLED) {
+ result.append("\n⚠ Bundle is INSTALLED but not RESOLVED\n");
+ result.append("This typically means there are unsatisfied dependencies.\n\n");
+
+ boolean foundIssues = false;
+
+ // First try to get info from BundleWiring (for resolved requirements)
+ BundleWiring wiring = bundle.adapt(BundleWiring.class);
+ if (wiring != null) {
+ List<BundleRequirement> requirements = wiring.getRequirements(null);
+
+ for (BundleRequirement req : requirements) {
+ List<BundleWire> wires = wiring.getRequiredWires(req.getNamespace());
+ if (wires == null || wires.isEmpty()) {
+ if (!foundIssues) {
+ result.append("Unsatisfied Requirements:\n");
+ foundIssues = true;
+ }
+ result.append(" ✗ ")
+ .append(req.getNamespace())
+ .append(": ")
+ .append(req.getDirectives())
+ .append("\n");
+
+ // For package imports, show which package is missing
+ if ("osgi.wiring.package".equals(req.getNamespace())) {
+ String filter = req.getDirectives().get("filter");
+ result.append(" Missing package: ")
+ .append(filter)
+ .append("\n");
+ }
+ }
+ }
+ }
+
+ // If no issues found via wiring, check manifest directly and compare with available exports
+ if (!foundIssues) {
+ String importPackage = bundle.getHeaders().get(Constants.IMPORT_PACKAGE);
+
+ if (importPackage != null && !importPackage.isEmpty()) {
+ result.append("Analyzing Package Dependencies:\n\n");
+ analyzePackageImports(importPackage, result);
+ foundIssues = true;
+ }
+
+ String requireBundle = bundle.getHeaders().get(Constants.REQUIRE_BUNDLE);
+ if (requireBundle != null && !requireBundle.isEmpty()) {
+ result.append("\nRequired Bundles (from manifest):\n");
+ result.append(" ")
+ .append(requireBundle.replace(",", ",\n "))
+ .append("\n\n");
+ result.append("⚠ One or more of these bundles are not available or not in the correct state.\n");
+ foundIssues = true;
+ }
+ }
+
+ if (!foundIssues) {
+ result.append("No dependency information found. Bundle may have internal errors.\n");
+ }
+ } else if (bundle.getState() == Bundle.RESOLVED) {
+ result.append("\n✓ Bundle is RESOLVED (all dependencies satisfied)\n");
+ result.append("Bundle can be started manually if it's not a fragment.\n");
+ } else if (bundle.getState() == Bundle.STARTING) {
+ result.append("\n⚠ Bundle is STARTING (stuck during activation)\n");
+ result.append("Check for errors in bundle activator or circular dependencies.\n");
+ }
+
+ // Check for fragment information
+ if ((bundle.getHeaders().get(Constants.FRAGMENT_HOST)) != null) {
+ result.append("\nNote: This is a fragment bundle (attached to: ")
+ .append(bundle.getHeaders().get(Constants.FRAGMENT_HOST))
+ .append(")\n");
+ }
+ }
+
+ /**
+ * Analyzes imported packages by checking if they are available in the OSGi environment.
+ * Scans all bundles to find which packages are exported and matches them against imports.
+ */
+ private void analyzePackageImports(String importPackageHeader, StringBuilder result) {
+ // Build a map of all exported packages in the system
+ java.util.Map<String, List<PackageExport>> exportedPackages = new java.util.HashMap<>();
+
+ for (Bundle b : ctx.getBundles()) {
+ if (b.getState() == Bundle.UNINSTALLED) {
+ continue;
+ }
+
+ String exportPackage = b.getHeaders().get(Constants.EXPORT_PACKAGE);
+ if (exportPackage != null && !exportPackage.isEmpty()) {
+ List<PackageInfo> exports = parsePackages(exportPackage);
+ for (PackageInfo pkg : exports) {
+ if (pkg.name != null && !pkg.name.isEmpty() && pkg.name.contains(".")) {
+ exportedPackages
+ .computeIfAbsent(pkg.name, k -> new ArrayList<>())
+ .add(new PackageExport(b, pkg.name, pkg.version));
+ }
+ }
+ }
+ }
+
+ // Parse and check each imported package
+ List<PackageInfo> imports = parsePackages(importPackageHeader);
+ int missingCount = 0;
+ int availableCount = 0;
+ List<String> missingPackages = new ArrayList<>();
+
+ for (PackageInfo importPkg : imports) {
+ // Skip invalid package names
+ if (importPkg.name == null || importPkg.name.isEmpty() || !importPkg.name.contains(".")) {
+ continue;
+ }
+
+ List<PackageExport> availableExports = exportedPackages.get(importPkg.name);
+
+ if (availableExports == null || availableExports.isEmpty()) {
+ missingCount++;
+ missingPackages.add(" ✗ " + importPkg.name
+ + (importPkg.version.isEmpty() ? "" : " " + importPkg.version)
+ + (importPkg.optional ? " (optional)" : "") + "\n");
+ } else {
+ availableCount++;
+ }
+ }
+
+ // Only show missing packages
+ if (missingCount > 0) {
+ result.append("Missing Packages (")
+ .append(missingCount)
+ .append(" of ")
+ .append(imports.size())
+ .append(" imports):\n\n");
+ for (String missing : missingPackages) {
+ result.append(missing);
+ }
+ result.append(
+ "\n⚠ Action Required: Install bundles that provide the missing packages, or downgrade / change the dependencies.\n");
+ } else {
+ result.append("✓ All ").append(imports.size()).append(" imported packages are available.\n");
+ result.append("Bundle should be resolvable. Check for other issues.\n");
+ }
+ }
+
+ /**
+ * Parse OSGi package header (Import-Package or Export-Package).
+ * Handles complex cases with version ranges, attributes, and directives.
+ */
+ private List<PackageInfo> parsePackages(String header) {
+ List<PackageInfo> packages = new ArrayList<>();
+ if (header == null || header.isEmpty()) {
+ return packages;
+ }
+
+ // State machine for parsing
+ StringBuilder current = new StringBuilder();
+ int depth = 0; // Track depth of quotes and brackets
+ boolean inQuotes = false;
+
+ for (int i = 0; i < header.length(); i++) {
+ char c = header.charAt(i);
+
+ if (c == '"') {
+ inQuotes = !inQuotes;
+ current.append(c);
+ } else if (!inQuotes && (c == '[' || c == '(')) {
+ depth++;
+ current.append(c);
+ } else if (!inQuotes && (c == ']' || c == ')')) {
+ depth--;
+ current.append(c);
+ } else if (c == ',' && depth == 0 && !inQuotes) {
+ // This is a package separator
+ String pkg = current.toString().trim();
+ if (!pkg.isEmpty()) {
+ packages.add(parsePackageEntry(pkg));
+ }
+ current = new StringBuilder();
+ } else {
+ current.append(c);
+ }
+ }
+
+ // Don't forget the last package
+ String pkg = current.toString().trim();
+ if (!pkg.isEmpty()) {
+ packages.add(parsePackageEntry(pkg));
+ }
+
+ return packages;
+ }
+
+ /**
+ * Parse a single package entry like "com.example.pkg;version="[1.0,2.0)";resolution:=optional"
+ */
+ private PackageInfo parsePackageEntry(String entry) {
+ // Split on first semicolon to separate package name from attributes
+ int semicolonPos = entry.indexOf(';');
+ String packageName;
+ String attributes;
+
+ if (semicolonPos > 0) {
+ packageName = entry.substring(0, semicolonPos).trim();
+ attributes = entry.substring(semicolonPos + 1);
+ } else {
+ packageName = entry.trim();
+ attributes = "";
+ }
+
+ // Extract version
+ String version = "";
+ if (attributes.contains("version=")) {
+ int vStart = attributes.indexOf("version=") + 8;
+ int vEnd = attributes.length();
+ // Find the end of the version value (next semicolon outside quotes)
+ boolean inQuote = false;
+ for (int i = vStart; i < attributes.length(); i++) {
+ char c = attributes.charAt(i);
+ if (c == '"') {
+ inQuote = !inQuote;
+ } else if (c == ';' && !inQuote) {
+ vEnd = i;
+ break;
+ }
+ }
+ version = attributes.substring(vStart, vEnd).trim().replaceAll("\"", "");
+ }
+
+ boolean optional = attributes.contains("resolution:=optional");
+
+ return new PackageInfo(packageName, version, optional);
+ }
+
+ private String extractVersion(String exportEntry) {
+ if (exportEntry.contains("version=")) {
+ int start = exportEntry.indexOf("version=") + 8;
+ int end = exportEntry.indexOf(";", start);
+ if (end == -1) {
+ end = exportEntry.length();
+ }
+ return exportEntry.substring(start, end).replaceAll("\"", "").trim();
+ }
+ return "0.0.0";
+ }
+
+ private static class PackageInfo {
+ final String name;
+ final String version;
+ final boolean optional;
+
+ PackageInfo(String name, String version, boolean optional) {
+ this.name = name;
+ this.version = version;
+ this.optional = optional;
+ }
+ }
+
+ private static class PackageExport {
+ final Bundle bundle;
+ final String packageName;
+ final String version;
+
+ PackageExport(Bundle bundle, String packageName, String version) {
+ this.bundle = bundle;
+ this.packageName = packageName;
+ this.version = version;
+ }
+ }
+
+ private void appendComponentDiagnostics(Bundle bundle, StringBuilder result) {
+ Collection<ComponentDescriptionDTO> components = scr.getComponentDescriptionDTOs(bundle);
+
+ if (components.isEmpty()) {
+ return;
+ }
+
+ result.append("\n--- Declarative Services Components ---\n\n");
+
+ for (ComponentDescriptionDTO desc : components) {
+ Collection<ComponentConfigurationDTO> configs = scr.getComponentConfigurationDTOs(desc);
+
+ result.append("Component: ").append(desc.name).append("\n");
+
+ if (configs.isEmpty()) {
+ result.append(" Status: Not configured/instantiated\n");
+ }
+
+ for (ComponentConfigurationDTO config : configs) {
+ result.append(" State: ")
+ .append(getComponentStateName(config.state))
+ .append("\n");
+
+ if (config.state == ComponentConfigurationDTO.UNSATISFIED_REFERENCE) {
+ result.append(" ✗ Unsatisfied Service References:\n");
+ for (UnsatisfiedReferenceDTO ref : config.unsatisfiedReferences) {
+ result.append(" - ")
+ .append(ref.name)
+ .append(" (")
+ .append(ref.target)
+ .append(")\n");
+ }
+ } else if (config.state == ComponentConfigurationDTO.UNSATISFIED_CONFIGURATION) {
+ result.append(" ✗ Missing required configuration\n");
+ } else if (config.state == ComponentConfigurationDTO.SATISFIED
+ || config.state == ComponentConfigurationDTO.ACTIVE) {
+ result.append(" ✓ Component is working correctly\n");
+ }
+ }
+ result.append("\n");
+ }
+ }
+
+ private void appendAllComponentIssues(StringBuilder result) {
+ List<ComponentDescriptionDTO> allComponents = new ArrayList<>(scr.getComponentDescriptionDTOs());
+ List<String> problematicComponents = new ArrayList<>();
+
+ for (ComponentDescriptionDTO desc : allComponents) {
+ Collection<ComponentConfigurationDTO> configs = scr.getComponentConfigurationDTOs(desc);
+
+ for (ComponentConfigurationDTO config : configs) {
+ if (config.state == ComponentConfigurationDTO.UNSATISFIED_REFERENCE
+ || config.state == ComponentConfigurationDTO.UNSATISFIED_CONFIGURATION) {
+ problematicComponents.add(desc.name + " (" + getComponentStateName(config.state) + ")");
+ }
+ }
+ }
+
+ if (!problematicComponents.isEmpty()) {
+ result.append("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
+ result.append("Problematic Components:\n\n");
+ for (String comp : problematicComponents) {
+ result.append(" ✗ ").append(comp).append("\n");
+ }
+ }
+ }
+
+ private Bundle findBundle(String symbolicName) {
+ return Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getSymbolicName().equals(symbolicName))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private String getStateName(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 (" + state + ")";
+ };
+ }
+
+ private String getComponentStateName(int state) {
+ return switch (state) {
+ case ComponentConfigurationDTO.UNSATISFIED_CONFIGURATION -> "UNSATISFIED_CONFIGURATION";
+ case ComponentConfigurationDTO.UNSATISFIED_REFERENCE -> "UNSATISFIED_REFERENCE";
+ case ComponentConfigurationDTO.SATISFIED -> "SATISFIED";
+ case ComponentConfigurationDTO.ACTIVE -> "ACTIVE";
+ default -> "UNKNOWN (" + state + ")";
+ };
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiDiagnosticPromptContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiDiagnosticPromptContribution.java
new file mode 100644
index 0000000..10e6e38
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiDiagnosticPromptContribution.java
@@ -0,0 +1,155 @@
+/*
+ * 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.SyncPromptSpecification;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
+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.spi.McpServerContribution;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * MCP Prompt that helps developers diagnose and fix OSGi bundle issues.
+ * This prompt teaches Cursor how to:
+ * - Identify why bundles aren't starting
+ * - Understand common OSGi issues
+ * - Provide actionable solutions
+ */
+@Component
+public class OsgiDiagnosticPromptContribution implements McpServerContribution {
+
+ @Override
+ public Optional<SyncPromptSpecification> getSyncPromptSpecification() {
+ return Optional.of(new SyncPromptSpecification(
+ new Prompt(
+ "diagnose-osgi-issue",
+ "Diagnose OSGi Bundle Issues",
+ "Helps diagnose why an OSGi bundle or component isn't starting in AEM/Sling. Provides step-by-step troubleshooting guidance.",
+ List.of(new PromptArgument(
+ "bundle-name",
+ "Bundle Symbolic Name",
+ "The symbolic name of the bundle that isn't starting (optional - if not provided, will check all bundles)",
+ false))),
+ (context, request) -> {
+ String bundleName = (String) request.arguments().get("bundle-name");
+
+ String instructions = buildDiagnosticInstructions(bundleName);
+
+ PromptMessage msg = new PromptMessage(Role.ASSISTANT, new McpSchema.TextContent(instructions));
+
+ return new GetPromptResult("OSGi Diagnostic Guide", List.of(msg));
+ }));
+ }
+
+ private String buildDiagnosticInstructions(String bundleName) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("# OSGi Bundle Diagnostic Assistant\n\n");
+
+ if (bundleName != null && !bundleName.isEmpty()) {
+ sb.append("I'll help you diagnose why the bundle '")
+ .append(bundleName)
+ .append("' isn't starting.\n\n");
+ sb.append("## Step 1: Run Diagnostic Tool\n\n");
+ sb.append("First, use the `diagnose-osgi-bundle` tool with bundleSymbolicName='")
+ .append(bundleName)
+ .append("'\n\n");
+ } else {
+ sb.append("I'll help you diagnose OSGi bundle issues in your AEM/Sling environment.\n\n");
+ sb.append("## Step 1: Identify Problematic Bundles\n\n");
+ sb.append("Use the `diagnose-osgi-bundle` tool without parameters to scan all bundles.\n\n");
+ }
+
+ sb.append("## Step 2: Interpret Common Issues\n\n");
+
+ sb.append("### Bundle State: INSTALLED (Not Resolved)\n");
+ sb.append("**Problem**: Bundle dependencies aren't satisfied.\n\n");
+ sb.append("**Common Causes**:\n");
+ sb.append(
+ "- Missing package imports: Another bundle that exports the required package isn't installed or active\n");
+ sb.append("- Version conflicts: Required package version doesn't match available versions\n");
+ sb.append("- Missing bundle: A required bundle hasn't been deployed\n\n");
+ sb.append("**Solutions**:\n");
+ sb.append("1. Check the diagnostic output for 'Unsatisfied Requirements'\n");
+ sb.append("2. Look for the missing packages in the Import-Package errors\n");
+ sb.append("3. Use `bundle://` resource to find bundles that export the needed packages\n");
+ sb.append("4. Install missing bundles or update bundle manifests to match available versions\n\n");
+
+ sb.append("### Bundle State: RESOLVED (Not Active)\n");
+ sb.append("**Problem**: Bundle has all dependencies but hasn't been started.\n\n");
+ sb.append("**Common Causes**:\n");
+ sb.append("- Bundle has lazy activation policy\n");
+ sb.append("- Bundle is a fragment (fragments never become ACTIVE)\n");
+ sb.append("- Manual start required\n\n");
+ sb.append("**Solutions**:\n");
+ sb.append("1. If it's not a fragment, the bundle might just need to be started\n");
+ sb.append("2. Check if the bundle has Bundle-ActivationPolicy: lazy\n");
+ sb.append("3. Fragments are normal - they attach to their host bundle\n\n");
+
+ sb.append("### Component State: UNSATISFIED_REFERENCE\n");
+ sb.append("**Problem**: Component can't find required OSGi services.\n\n");
+ sb.append("**Common Causes**:\n");
+ sb.append("- Required service isn't registered (bundle providing it isn't active)\n");
+ sb.append("- Service filter doesn't match any available services\n");
+ sb.append("- Circular dependency between components\n\n");
+ sb.append("**Solutions**:\n");
+ sb.append("1. Check the 'Unsatisfied Service References' in the diagnostic output\n");
+ sb.append("2. Use `component://` resource to verify the service provider is active\n");
+ sb.append("3. Check if the target filter is too restrictive\n");
+ sb.append("4. Make the reference optional if possible (cardinality=OPTIONAL)\n\n");
+
+ sb.append("### Component State: UNSATISFIED_CONFIGURATION\n");
+ sb.append("**Problem**: Component requires configuration that hasn't been provided.\n\n");
+ sb.append("**Common Causes**:\n");
+ sb.append("- Missing OSGi configuration in /apps or /libs\n");
+ sb.append("- Configuration not deployed to the environment\n");
+ sb.append("- Wrong configuration PID\n\n");
+ sb.append("**Solutions**:\n");
+ sb.append("1. Check if component has @Designate annotation requiring config\n");
+ sb.append("2. Verify configuration exists in repository or as .config file\n");
+ sb.append("3. Check configuration PID matches component name\n");
+ sb.append("4. Make configuration optional by removing 'required' policy\n\n");
+
+ sb.append("## Step 3: Apply Fixes\n\n");
+ sb.append("Based on the diagnostic results, I'll help you:\n");
+ sb.append("1. Identify which dependencies need to be added to your pom.xml\n");
+ sb.append("2. Fix package import/export statements in bnd.bnd or MANIFEST.MF\n");
+ sb.append("3. Create missing OSGi configurations\n");
+ sb.append("4. Update component annotations to fix reference issues\n");
+ sb.append("5. Suggest architectural changes if circular dependencies are detected\n\n");
+
+ sb.append("## Step 4: Verify Fix\n\n");
+ sb.append("After applying fixes:\n");
+ sb.append("1. Rebuild and redeploy the bundle\n");
+ sb.append("2. Run the diagnostic tool again to verify all issues are resolved\n");
+ sb.append("3. Check that the bundle state is ACTIVE\n");
+ sb.append("4. Verify components are in ACTIVE or SATISFIED state\n\n");
+
+ sb.append("Let me know what the diagnostic tool returns, and I'll provide specific solutions for your issue.");
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RecentRequestsContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RecentRequestsContribution.java
new file mode 100644
index 0000000..d4149b2
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RecentRequestsContribution.java
@@ -0,0 +1,73 @@
+/*
+ * 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 java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.Resource;
+import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
+import org.apache.sling.engine.RequestInfo;
+import org.apache.sling.engine.RequestInfoProvider;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+@Component
+public class RecentRequestsContribution implements McpServerContribution {
+
+ private RequestInfoProvider requestInfoProvider;
+
+ @Activate
+ public RecentRequestsContribution(@Reference RequestInfoProvider requestInfoProvider) {
+ this.requestInfoProvider = requestInfoProvider;
+ }
+
+ private String describe(RequestInfo ri) {
+ return "Id: " + ri.getId() + "\n" + "Method: "
+ + ri.getMethod() + "\n" + "Path: " + ri.getPath() + "\n" + "User id: " + ri.getUserId() + "\n"
+ + ":\n" + ri.getLog();
+ }
+
+ @Override
+ public Optional<SyncResourceSpecification> getSyncResourceSpecification() {
+ return Optional.of(new SyncResourceSpecification(
+ new Resource.Builder()
+ .uri("recent-requests://all")
+ .description(
+ "Prints all recent requests ( excluding /bin/mcp ). Contains information about method, path, user id and a verbose log of internal operations, including authentication, resource resolution, script resolution, nested scripts/servlets and filters.")
+ .name("recent-requests-all")
+ .build(),
+ (context, request) -> {
+ String allRequests = StreamSupport.stream(
+ requestInfoProvider.getRequestInfos().spliterator(), false)
+ .filter((ri) -> !ri.getPath().equals("/bin/mcp"))
+ .map(this::describe)
+ .collect(Collectors.joining("\n\n" + "-".repeat(20) + "\n\n"));
+
+ return new ReadResourceResult(
+ List.of(new TextResourceContents("recent-requests://all", "text/plain", allRequests)));
+ }));
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java
new file mode 100644
index 0000000..1e0b9ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java
@@ -0,0 +1,74 @@
+/*
+ * 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.spi.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 CallToolResult.builder()
+ .addTextContent("Bundles refreshed successfully")
+ .build();
+ }));
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/ServletPromptContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/ServletPromptContribution.java
new file mode 100644
index 0000000..cb2f181
--- /dev/null
+++ b/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.spi.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));
+ }));
+ }
+}
diff --git a/src/main/resources/SLING-INF/libs/sling/mcp/prompts/new-sling-servlet.md b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/new-sling-servlet.md
new file mode 100644
index 0000000..c92cb83
--- /dev/null
+++ b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/new-sling-servlet.md
@@ -0,0 +1,17 @@
+---
+title: "Create new Sling Servlet"
+description: "Creates a new Sling Servlet in the current project using annotations"
+argument.resource-type:
+ - title: Resource Type
+ - required: true
+ - description: The Sling resource type to bind this servlet to
+---
+
+# Create a new Sling Servlet
+
+ Create a new Sling Servlet for resource type: `{resource-type}`
+
+ 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}'.
+
\ No newline at end of file
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..d5e9587
--- /dev/null
+++ b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
@@ -0,0 +1,45 @@
+---
+title: "Troubleshooting guide"
+description: "Guide to troubleshoot common issues with Apache Sling applications"
+---
+# 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