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 f381d1066e0fa668e6c888560f61c882478d1220 Author: Robert Munteanu <[email protected]> AuthorDate: Tue Jan 13 15:08:06 2026 +0100 chore(mcp-server): move contributions to a separate bundle --- README.md | 10 +- .../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 --- .../apache/sling/mcp/server/spi/package-info.java | 18 + 11 files changed, 26 insertions(+), 1584 deletions(-) diff --git a/README.md b/README.md index 599c6a0..d2988d4 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,16 @@ Experimental MCP Server implementation for Apache Sling. ## Usage -Start up the MCP server, based on the Apache Sling Starter +Build the project with Maven and start up the MCP server, based on the Apache Sling Starter: ``` -$ mvn package feature-launcher:start feature-launcher:stop -Dfeature-launcher.waitForInput +$ mvn install feature-launcher:start feature-launcher:stop -Dfeature-launcher.waitForInput +``` + +Then build and deploy the sibling contributions package: + +``` +$ mvn -f ../mcp-server-contributions/ install sling:install ``` Then open up your coding assistant tool and add an remote MCP server with location http://localhost:8080/mcp . 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 deleted file mode 100644 index d54e598..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 deleted file mode 100644 index 29e5bba..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleState.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 deleted file mode 100644 index 937ad9d..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/ComponentResourceContribution.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 deleted file mode 100644 index 8e61147..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * 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 deleted file mode 100644 index fdc3be7..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiBundleDiagnosticContribution.java +++ /dev/null @@ -1,525 +0,0 @@ -/* - * 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 deleted file mode 100644 index 10e6e38..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiDiagnosticPromptContribution.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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 deleted file mode 100644 index d4149b2..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RecentRequestsContribution.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 deleted file mode 100644 index 1e0b9ae..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 deleted file mode 100644 index cb2f181..0000000 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/ServletPromptContribution.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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/java/org/apache/sling/mcp/server/spi/package-info.java b/src/main/java/org/apache/sling/mcp/server/spi/package-info.java index 13b7525..aec19a4 100644 --- a/src/main/java/org/apache/sling/mcp/server/spi/package-info.java +++ b/src/main/java/org/apache/sling/mcp/server/spi/package-info.java @@ -1,2 +1,20 @@ +/* + * 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. + */ @org.osgi.annotation.versioning.Version("1.0.0") package org.apache.sling.mcp.server.spi;
