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/01/15 14:40:06 UTC
(sling-org-apache-sling-mcp-server) branch master created (now 5b5f1be)
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-mcp-server.git
at 5b5f1be chore: set scm information in the pom
This branch includes the following new commits:
new 9fb2a66 Initial commit of a basic MCP server implementation for Sling
new 8cbe5db chore(mcp-server): serve from /bin/mcp, follow existing conventions
new a0fe0a3 chore(mcp-server): formatting in JSON file
new 859e082 fix(mcp-server): make bundles output useful
new b199ac0 feat(mcp-server): various improvements
new ef902bb feat(mcp-server): split contributions into seperate files
new 5feb421 chore(mcp): minor code cleanups in the BundleResourceContribution
new a361f5a feat(mcp): expose the recent requests data as a resource
new 4c0cc21 chore(mcp): document limitations of the internal API
new 200082a feat(mcp-server): clearly mark fragment bundles
new 2ea3c52 chore(mcp-server): formatting, typos
new de1960d chore(mcp-server): update to mcp SDK 0.17.0
new f22c8cb chore(mcp-server): stop using deprecated apis
new feb8a9a mcp bundle diagnoser, component status, logs (#118)
new c5719fe chore(mcp): stop using deprecated apis
new 6c0f804 chore(mcp): wip on discovering prompts from the repository
new 9184907 chore(mcp): fix formatting
new 2340996 feat(mcp): register templates from the repository
new 128443d feat(mcp): repository prompts support parameters defined in the front matter
new 713be81 chore(mcp-server): allow starting feature launcher in debug mode
new 02e911a feat(mcp-server): do not output front matter when rendering prompt, only replace args with non-empty values
new c3b0e6b fix(deps): update dependency org.apache.sling:sling to v65
new 8e6ffef fix(mcp-server): ignore unknown properties during deserialisation
new daa8d7a chore(mcp-server): extract SPI so contributions can be moved to other bundles
new f381d10 chore(mcp-server): move contributions to a separate bundle
new ccefd17 chore(mcp): update McpServerContribution methods to return List instead of Optional
new 5b5f1be chore: set scm information in the pom
The 27 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(sling-org-apache-sling-mcp-server) 21/27: feat(mcp-server): do not output front matter when rendering prompt, only replace args with non-empty values
Posted by ro...@apache.org.
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 02e911a1b3b778e260d42c3c3c95fdd6441b018a
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Dec 12 17:23:19 2025 +0100
feat(mcp-server): do not output front matter when rendering prompt, only replace args with non-empty values
---
.../impl/contribs/DiscoveredPromptBuilder.java | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java
index ca2f9a7..99f2335 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java
@@ -18,7 +18,6 @@
*/
package org.apache.sling.mcp.server.impl.contribs;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -40,6 +39,7 @@ import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
import org.commonmark.ext.front.matter.YamlFrontMatterVisitor;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -133,16 +133,27 @@ class DiscoveredPromptBuilder {
if (encoding == null) {
encoding = StandardCharsets.UTF_8.name();
}
+
+ Parser parser = Parser.builder()
+ .extensions(List.of(YamlFrontMatterExtension.create()))
+ .build();
+
try (InputStream stream = promptResource.adaptTo(InputStream.class)) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- stream.transferTo(baos);
- String contents = baos.toString(encoding);
+
+ Node node = parser.parseReader(new InputStreamReader(stream, encoding));
+
+ // Render markdown without the front matter extension for clarity and for keeping
+ // the context usage low
+ MarkdownRenderer renderer = MarkdownRenderer.builder().build();
+ String contents = renderer.render(node);
for (PromptArgument arg : arguments) {
String placeholder = "{" + arg.name() + "}";
Object requestedArg = req.arguments().get(arg.name());
String value = requestedArg != null ? requestedArg.toString() : "";
- contents = contents.replace(placeholder, value);
+ if (!value.isEmpty()) {
+ contents = contents.replace(placeholder, value);
+ }
}
return List.of(new PromptMessage(Role.ASSISTANT, new TextContent(contents)));
(sling-org-apache-sling-mcp-server) 25/27: chore(mcp-server): move contributions to a separate bundle
Posted by ro...@apache.org.
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 <ro...@apache.org>
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;
(sling-org-apache-sling-mcp-server) 16/27: chore(mcp): wip on discovering prompts from the repository
Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-mcp-server.git
commit 6c0f8049d1b3739d639f12eb73ab82b5276031bb
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Thu Dec 11 17:45:54 2025 +0100
chore(mcp): wip on discovering prompts from the repository
---
bnd.bnd | 4 +-
src/main/features/main.json | 10 +-
.../sling/mcp/server/impl/DiscoveredPrompt.java | 31 ++++
.../apache/sling/mcp/server/impl/McpServlet.java | 24 +++
.../impl/contribs/RepositoryPromptsRegistrar.java | 176 +++++++++++++++++++++
.../libs/sling/mcp/prompts/troubleshoot.md | 41 +++++
6 files changed, 284 insertions(+), 2 deletions(-)
diff --git a/bnd.bnd b/bnd.bnd
index 27af7f1..f1ef54d 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,3 +1,5 @@
# workaround for https://github.com/modelcontextprotocol/java-sdk/issues/562
Private-Package: io.modelcontextprotocol.json.jackson, \
- io.modelcontextprotocol.json.schema.jackson
\ No newline at end of file
+ io.modelcontextprotocol.json.schema.jackson
+
+Sling-Initial-Content: SLING-INF/libs/sling/mcp/prompts;path:=/libs/sling/mcp/prompts;overwrite:=true
\ No newline at end of file
diff --git a/src/main/features/main.json b/src/main/features/main.json
index fc49b6f..656a96b 100644
--- a/src/main/features/main.json
+++ b/src/main/features/main.json
@@ -32,5 +32,13 @@
"id": "org.yaml:snakeyaml:2.3",
"start-order": 25
}
- ]
+ ],
+ "configurations": {
+ "org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~mcp-server":{
+ "whitelist.bundles":[
+ "org.apache.sling.mcp-server"
+ ],
+ "whitelist.name":"mcp-server"
+ }
+ }
}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
new file mode 100644
index 0000000..6a52ce5
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.impl;
+
+import java.util.List;
+
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import org.apache.sling.api.resource.ResourceResolver;
+
+public interface DiscoveredPrompt {
+
+ public static final String SERVICE_PROP_NAME = "mcp.prompt.name";
+
+ List<PromptMessage> getPromptMessages(ResourceResolver resolver);
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index daf9c57..ac708c6 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -22,13 +22,18 @@ import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
@@ -36,6 +41,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingJakartaHttpServletRequest;
import org.apache.sling.api.SlingJakartaHttpServletResponse;
+import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.jetbrains.annotations.NotNull;
@@ -44,6 +50,7 @@ import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -91,6 +98,8 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
transportProvider = HttpServletStatelessServerTransport.builder()
.messageEndpoint(ENDPOINT)
.jsonMapper(jsonMapper)
+ .contextExtractor(request -> McpTransportContext.create(
+ Map.of("resourceResolver", ((SlingJakartaHttpServletRequest) request).getResourceResolver())))
.build();
MethodHandles.Lookup privateLookup =
@@ -152,6 +161,21 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
.forEach(syncPrompt -> syncServer.addPrompt(syncPrompt));
}
+ @Reference(policy = ReferencePolicy.DYNAMIC, policyOption = GREEDY, cardinality = MULTIPLE)
+ protected void bindPrompt(DiscoveredPrompt prompt, Map<String, Object> properties) {
+ String promptName = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_NAME);
+ syncServer.addPrompt(new SyncPromptSpecification(new Prompt(promptName, null, List.of()), (c, r) -> {
+ ResourceResolver resourceResolver = (ResourceResolver) c.get("resourceResolver");
+ var messages = prompt.getPromptMessages(resourceResolver);
+ return new McpSchema.GetPromptResult(null, messages);
+ }));
+ }
+
+ protected void unbindPrompt(Map<String, Object> properties) {
+ String promptName = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_NAME);
+ syncServer.removePrompt(promptName);
+ }
+
@Override
protected void doGet(
@NotNull SlingJakartaHttpServletRequest request, @NotNull SlingJakartaHttpServletResponse response)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
new file mode 100644
index 0000000..5d1b47c
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.impl.contribs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import io.modelcontextprotocol.spec.McpSchema.TextContent;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.observation.ResourceChange;
+import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
+import org.apache.sling.api.resource.observation.ResourceChangeListener;
+import org.apache.sling.mcp.server.impl.DiscoveredPrompt;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component
+public class RepositoryPromptsRegistrar {
+
+ private static final String PROMPT_LIBS_DIR = "/libs/sling/mcp/prompts";
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private ConcurrentMap<String, ServiceRegistration<DiscoveredPrompt>> registrations = new ConcurrentHashMap<>();
+
+ @Activate
+ public RepositoryPromptsRegistrar(@Reference ResourceResolverFactory rrf, BundleContext ctx) throws LoginException {
+
+ ctx.registerService(
+ ResourceChangeListener.class,
+ new ResourceChangeListener() {
+
+ @Override
+ public void onChange(@NotNull List<ResourceChange> changes) {
+ changes.forEach(a -> logger.info("Received change {} at {}", a.getType(), a.getPath()));
+
+ try (ResourceResolver resolver = rrf.getAdministrativeResourceResolver(null)) {
+ for (ResourceChange change : changes) {
+ if (change.getType() == ChangeType.REMOVED) {
+ String promptName = getPromptName(change.getPath());
+ ServiceRegistration<DiscoveredPrompt> sr = registrations.remove(promptName);
+ if (sr != null) {
+ sr.unregister();
+ } else {
+ logger.warn(
+ "No registered prompt found for removed prompt {} at path {}, unable to unregister prompt.",
+ promptName,
+ change.getPath());
+ }
+ } else {
+ String promptName = getPromptName(change.getPath());
+ ServiceRegistration<DiscoveredPrompt> sr = registrations.remove(promptName);
+ if (sr != null) {
+ sr.unregister();
+ }
+ Resource prompt = resolver.getResource(change.getPath());
+ if (prompt != null) {
+ registerPrompt(ctx, promptName, prompt);
+ } else {
+ logger.warn(
+ "Prompt resource at {} not found for change type {}, unable to register prompt.",
+ change.getPath(),
+ change.getType());
+ }
+ }
+ }
+ } catch (LoginException e) {
+ logger.error("Error processing resource change", e);
+ }
+ }
+ },
+ new Hashtable<>(Map.of(ResourceChangeListener.PATHS, PROMPT_LIBS_DIR, ResourceChangeListener.CHANGES, new String[] { ResourceChangeListener.CHANGE_ADDED, ResourceChangeListener.CHANGE_CHANGED, ResourceChangeListener.CHANGE_REMOVED } )));
+
+ // TODO - use service user
+ try (ResourceResolver resolver = rrf.getAdministrativeResourceResolver(null)) {
+
+ Iterator<Resource> prompts = resolver.findResources(
+ "/jcr:root" + PROMPT_LIBS_DIR + "//element(*,nt:file)[jcr:like(fn:name(), '%.md')]", "xpath");
+ while (prompts.hasNext()) {
+ Resource prompt = prompts.next();
+ String promptName = getPromptName(prompt.getPath());
+
+ registerPrompt(ctx, promptName, prompt);
+ }
+ }
+ }
+
+ private void registerPrompt(BundleContext ctx, String promptName, Resource prompt) {
+
+ Map<String, String> serviceProps = Map.of(DiscoveredPrompt.SERVICE_PROP_NAME, promptName);
+
+ // TODO - discover additional properties from the markdown file (front matter)
+
+ var sr = ctx.registerService(
+ DiscoveredPrompt.class,
+ new RepositoryPrompt(prompt.getPath()),
+ new Hashtable<String, String>(serviceProps));
+
+ registrations.put(promptName, sr);
+ }
+
+ private String getPromptName(String path) {
+
+ // remove prefix
+ String promptName = path.substring(PROMPT_LIBS_DIR.length() + 1); // account for trailing slash
+
+ // remove optional /jcr:content node name
+ if (promptName.endsWith("/jcr:content")) {
+ promptName = promptName.substring(0, promptName.length() - "/jcr:content".length());
+ }
+
+ // remove .md extension
+ return promptName.substring(0, promptName.length() - ".md".length());
+ }
+
+ class RepositoryPrompt implements DiscoveredPrompt {
+ private final String promptPath;
+
+ RepositoryPrompt(String promptPath) {
+ this.promptPath = promptPath;
+ }
+
+ @Override
+ public List<PromptMessage> getPromptMessages(ResourceResolver resolver) {
+
+ try {
+ Resource promptResource = resolver.getResource(promptPath);
+ String encoding = promptResource.getResourceMetadata().getCharacterEncoding();
+ if ( encoding == null ) {
+ encoding = StandardCharsets.UTF_8.name();
+ }
+ try (InputStream stream = promptResource.adaptTo(InputStream.class)) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ stream.transferTo(baos);
+ String contents = baos.toString(encoding);
+ return List.of(new PromptMessage(Role.ASSISTANT, new TextContent(contents)));
+ }
+ } catch (IOException e) {
+ return List.of();
+ }
+ }
+ }
+}
diff --git a/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
new file mode 100644
index 0000000..e60aed8
--- /dev/null
+++ b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
@@ -0,0 +1,41 @@
+# Troubleshooting Guide
+
+## Overview
+This guide helps you diagnose and resolve common issues with Apache Sling MCP Server.
+
+## Common Issues
+
+### MCP Server Not Responding
+- Verify the MCP servlet is registered and active
+- Check OSGi component status
+- Review log files for errors
+
+### Bundle Issues
+- Check bundle state (ACTIVE, RESOLVED, INSTALLED)
+- Verify all dependencies are satisfied
+- Use the OSGi diagnostic tools
+
+### Performance Problems
+- Review recent requests and response times
+- Check system resources
+- Analyze thread dumps if available
+
+### Component Registration Issues
+- Verify component configurations
+- Check service dependencies
+- Review component lifecycle logs
+
+## Diagnostic Tools
+The MCP Server provides several diagnostic tools:
+- Bundle state inspection
+- Component resource analysis
+- Recent request tracking
+- Log file access
+- OSGi diagnostic reports
+
+## Getting Help
+If you continue to experience issues:
+1. Gather diagnostic information using the MCP tools
+2. Review the Apache Sling documentation
+3. Check the Apache Sling mailing lists
+4. File an issue in the Apache Sling JIRA
(sling-org-apache-sling-mcp-server) 27/27: chore: set scm information in the pom
Posted by ro...@apache.org.
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 5b5f1befc3f42eeb2bd758612847dbafac5f9583
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Thu Jan 15 15:36:28 2026 +0100
chore: set scm information in the pom
---
pom.xml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/pom.xml b/pom.xml
index 68b2502..ab1074a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,6 +32,12 @@
<name>Apache Sling MCP Server</name>
+ <scm>
+ <connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-mcp-server.git</connection>
+ <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-mcp-server.git</developerConnection>
+ <url>https://github.com/apache/sling-org-apache-sling-mcp-server.git</url>
+ </scm>
+
<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-->
(sling-org-apache-sling-mcp-server) 01/27: Initial commit of a basic MCP server implementation for Sling
Posted by ro...@apache.org.
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 9fb2a6649eed9f38cceb15ebbb4605f681b9728d
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Nov 12 14:30:20 2025 +0100
Initial commit of a basic MCP server implementation for Sling
---
README.md | 13 ++
bnd.bnd | 3 +
pom.xml | 206 ++++++++++++++++++
src/main/features/main.json | 36 ++++
.../apache/sling/mcp/server/impl/McpServlet.java | 230 +++++++++++++++++++++
5 files changed, 488 insertions(+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..599c6a0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# Apache Sling MCP Server
+
+Experimental MCP Server implementation for Apache Sling.
+
+## Usage
+
+Start up the MCP server, based on the Apache Sling Starter
+
+```
+$ mvn package feature-launcher:start feature-launcher:stop -Dfeature-launcher.waitForInput
+```
+
+Then open up your coding assistant tool and add an remote MCP server with location http://localhost:8080/mcp .
diff --git a/bnd.bnd b/bnd.bnd
new file mode 100644
index 0000000..27af7f1
--- /dev/null
+++ b/bnd.bnd
@@ -0,0 +1,3 @@
+# workaround for https://github.com/modelcontextprotocol/java-sdk/issues/562
+Private-Package: io.modelcontextprotocol.json.jackson, \
+ io.modelcontextprotocol.json.schema.jackson
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4a2323c
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,206 @@
+<?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>63</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.mcp-server</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+
+ <name>Apache Sling MCP Server</name>
+
+ <properties>
+ <sling.java.version>17</sling.java.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.framework</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.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>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.modelcontextprotocol.sdk</groupId>
+ <artifactId>mcp</artifactId>
+ <version>0.15.0</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>slingfeature-maven-plugin</artifactId>
+ <version>1.9.2</version>
+ <extensions>true</extensions>
+ <configuration>
+ <skipAddFeatureDependencies>true</skipAddFeatureDependencies>
+ <framework>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ <version>7.0.5</version>
+ </framework>
+
+ <!--
+ Prepares a feature model aggregate that takes the following features from
+ the Sling Starter:
+ - nosample_base - the base Sling Starter
+ - oak_persistence_sns - SegmentNodeStore persistence for Oak
+ - composum - The Composum Nodes administration tool
+ Notably missing are the slingshot and starter samples
+ -->
+ <aggregates>
+ <aggregate>
+ <classifier>app</classifier>
+ <filesInclude>main.json</filesInclude>
+ <attach>false</attach>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.starter</artifactId>
+ <classifier>nosample_base</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.starter</artifactId>
+ <classifier>oak_persistence_sns</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.starter</artifactId>
+ <classifier>starter</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ <includeArtifact>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.starter</artifactId>
+ <classifier>composum</classifier>
+ <version>14-SNAPSHOT</version>
+ <type>slingosgifeature</type>
+ </includeArtifact>
+ </aggregate>
+ </aggregates>
+ <repositories>
+ <repository>
+ <includeClassifier>app</includeClassifier>
+ </repository>
+ </repositories>
+ <scans>
+ <scan>
+ <includeClassifier>app</includeClassifier>
+ </scan>
+ </scans>
+ </configuration>
+ <executions>
+ <execution>
+ <id>prepare-features</id>
+ <goals>
+ <goal>aggregate-features</goal>
+ <goal>analyse-features</goal>
+ <goal>attach-features</goal>
+ </goals>
+ <phase>prepare-package</phase>
+ </execution>
+ <execution>
+ <id>create-app-repository</id>
+ <goals>
+ <goal>repository</goal>
+ </goals>
+ <phase>package</phase>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- 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>
+ <!-- Added: Feature Launcher plugin for manually starting the aggregated 'app' feature -->
+ <plugin>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>feature-launcher-maven-plugin</artifactId>
+ <configuration>
+ <launches>
+ <launch>
+ <id>app</id>
+ <featureFile>${project.slingfeature.outputDirectory}/feature-app.json</featureFile>
+ </launch>
+ </launches>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/src/main/features/main.json b/src/main/features/main.json
new file mode 100644
index 0000000..c34fb1f
--- /dev/null
+++ b/src/main/features/main.json
@@ -0,0 +1,36 @@
+{
+ "bundles": [
+ {
+ "id": "${project.groupId}:${project.artifactId}:${project.version}",
+ "start-order": 25
+ },
+ {
+ "id": "io.modelcontextprotocol.sdk:mcp-core:0.15.0",
+ "start-order": 25
+ },
+ {
+ "id": "io.projectreactor:reactor-core:3.7.0",
+ "start-order": 25
+ },
+ {
+ "id": "org.reactivestreams:reactive-streams:1.0.4",
+ "start-order": 25
+ },
+ {
+ "id": "com.networknt:json-schema-validator:1.5.7",
+ "start-order": 25
+ },
+ {
+ "id": "com.ethlo.time:itu:1.10.3",
+ "start-order": 25
+ },
+ {
+ "id": "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.3",
+ "start-order": 25
+ },
+ {
+ "id": "org.yaml:snakeyaml:2.3",
+ "start-order": 25
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
new file mode 100644
index 0000000..80078df
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.impl;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+import io.modelcontextprotocol.server.McpStatelessSyncServer;
+import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
+import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.Resource;
+import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
+import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
+import io.modelcontextprotocol.spec.McpSchema.Role;
+import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
+import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.sling.api.SlingJakartaHttpServletRequest;
+import org.apache.sling.api.SlingJakartaHttpServletResponse;
+import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
+import org.apache.sling.servlets.annotations.SlingServletPaths;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.Bundle;
+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.Deactivate;
+
+@Component(service = Servlet.class)
+@SlingServletPaths(value = {McpServlet.ENDPOINT})
+public class McpServlet extends SlingJakartaAllMethodsServlet {
+
+ static final String ENDPOINT = "/mcp";
+ private static final long serialVersionUID = 1L;
+ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
+
+ private McpStatelessSyncServer syncServer;
+ private HttpServletStatelessServerTransport transportProvider;
+ private MethodHandle doGetMethod;
+ private MethodHandle doPostMethod;
+
+ @Activate
+ public McpServlet(BundleContext ctx) throws IllegalAccessException, NoSuchMethodException {
+
+ McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
+
+ transportProvider = HttpServletStatelessServerTransport.builder()
+ .messageEndpoint(ENDPOINT)
+ .jsonMapper(jsonMapper)
+ .build();
+
+ MethodHandles.Lookup privateLookup =
+ MethodHandles.privateLookupIn(HttpServletStatelessServerTransport.class, LOOKUP);
+
+ doGetMethod = privateLookup.findVirtual(
+ HttpServletStatelessServerTransport.class,
+ "doGet",
+ java.lang.invoke.MethodType.methodType(
+ void.class, HttpServletRequest.class, HttpServletResponse.class));
+ doPostMethod = privateLookup.findVirtual(
+ HttpServletStatelessServerTransport.class,
+ "doPost",
+ java.lang.invoke.MethodType.methodType(
+ void.class, HttpServletRequest.class, HttpServletResponse.class));
+
+ syncServer = McpServer.sync(transportProvider)
+ .serverInfo("apache-sling", "0.1.0")
+ .jsonMapper(jsonMapper)
+ .jsonSchemaValidator(new DefaultJsonSchemaValidator())
+ .capabilities(ServerCapabilities.builder()
+ .tools(true)
+ .prompts(true)
+ .resources(true, true)
+ .build())
+ .build();
+
+ var schema = """
+ {
+ "type" : "object",
+ "id" : "urn:jsonschema:Operation",
+ "properties" : { }
+ }
+ """;
+ var syncToolSpecification = 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 new CallToolResult("Bundles refreshed successfully", Boolean.FALSE);
+ });
+
+ // Register tools, resources, and prompts
+ syncServer.addTool(syncToolSpecification);
+ syncServer.addPrompt(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));
+ }));
+ syncServer.addResource(new McpStatelessServerFeatures.SyncResourceSpecification(
+ new Resource.Builder()
+ .name("bundle")
+ .uri("bundle://")
+ .description("OSGi bundle status")
+ .mimeType("text/plain")
+ .build(),
+ (context, request) -> {
+ List<McpSchema.ResourceContents> res = Stream.of(ctx.getBundles())
+ .map(b -> new TextResourceContents(
+ "bundle://" + b.getSymbolicName(),
+ "text-plain",
+ "Bundle " + b.getSymbolicName() + " is in state " + b.getState()))
+ .collect(Collectors.toList());
+
+ return new McpSchema.ReadResourceResult(res);
+ }));
+
+ syncServer.addResourceTemplate(new McpStatelessServerFeatures.SyncResourceTemplateSpecification(
+ new ResourceTemplate.Builder()
+ .uriTemplate("bundles://state/{state}")
+ .name("bundles")
+ .build(),
+ (context, request) -> {
+ List<ResourceContents> bundles = List.of();
+ if ("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
+ bundles = Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getState() == Bundle.RESOLVED)
+ .<ResourceContents>map(b -> new TextResourceContents(
+ "bundle://" + b.getSymbolicName(),
+ "text-plain",
+ "Bundle " + b.getSymbolicName() + " is in state " + b.getState()))
+ .toList();
+ }
+
+ return new ReadResourceResult(bundles);
+ }));
+ }
+
+ @Override
+ protected void doGet(
+ @NotNull SlingJakartaHttpServletRequest request, @NotNull SlingJakartaHttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ doGetMethod.invoke(transportProvider, request, response);
+ } catch (ServletException | IOException | RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new ServletException(t);
+ }
+ }
+
+ @Override
+ protected void doPost(
+ @NotNull SlingJakartaHttpServletRequest request, @NotNull SlingJakartaHttpServletResponse response)
+ throws ServletException, IOException {
+ try {
+ doPostMethod.invoke(transportProvider, request, response);
+ } catch (ServletException | IOException | RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable t) {
+ throw new ServletException(t);
+ }
+ }
+
+ @Deactivate
+ public void close() {
+ if (syncServer != null) {
+ syncServer.close();
+ }
+ }
+}
(sling-org-apache-sling-mcp-server) 17/27: chore(mcp): fix formatting
Posted by ro...@apache.org.
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 918490755076ec9edee06d9d6be69942ef47af8b
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Thu Dec 11 17:48:39 2025 +0100
chore(mcp): fix formatting
---
.../mcp/server/impl/contribs/RepositoryPromptsRegistrar.java | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
index 5d1b47c..990de0d 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
@@ -102,7 +102,12 @@ public class RepositoryPromptsRegistrar {
}
}
},
- new Hashtable<>(Map.of(ResourceChangeListener.PATHS, PROMPT_LIBS_DIR, ResourceChangeListener.CHANGES, new String[] { ResourceChangeListener.CHANGE_ADDED, ResourceChangeListener.CHANGE_CHANGED, ResourceChangeListener.CHANGE_REMOVED } )));
+ new Hashtable<>(Map.of(
+ ResourceChangeListener.PATHS, PROMPT_LIBS_DIR, ResourceChangeListener.CHANGES, new String[] {
+ ResourceChangeListener.CHANGE_ADDED,
+ ResourceChangeListener.CHANGE_CHANGED,
+ ResourceChangeListener.CHANGE_REMOVED
+ })));
// TODO - use service user
try (ResourceResolver resolver = rrf.getAdministrativeResourceResolver(null)) {
@@ -136,7 +141,7 @@ public class RepositoryPromptsRegistrar {
// remove prefix
String promptName = path.substring(PROMPT_LIBS_DIR.length() + 1); // account for trailing slash
-
+
// remove optional /jcr:content node name
if (promptName.endsWith("/jcr:content")) {
promptName = promptName.substring(0, promptName.length() - "/jcr:content".length());
@@ -159,7 +164,7 @@ public class RepositoryPromptsRegistrar {
try {
Resource promptResource = resolver.getResource(promptPath);
String encoding = promptResource.getResourceMetadata().getCharacterEncoding();
- if ( encoding == null ) {
+ if (encoding == null) {
encoding = StandardCharsets.UTF_8.name();
}
try (InputStream stream = promptResource.adaptTo(InputStream.class)) {
(sling-org-apache-sling-mcp-server) 10/27: feat(mcp-server): clearly mark fragment bundles
Posted by ro...@apache.org.
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 200082a05a9723bfcaa579066c25623f8b6efd78
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 17:51:53 2025 +0100
feat(mcp-server): clearly mark fragment bundles
---
pom.xml | 5 +++++
.../impl/contribs/BundleResourceContribution.java | 22 ++++++++++++++--------
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/pom.xml b/pom.xml
index e41f259..722343a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,11 @@
<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>
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
index fc7ac2a..833264c 100644
--- 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
@@ -37,12 +37,14 @@ import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
import org.apache.sling.mcp.server.impl.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}";
@@ -59,16 +61,17 @@ public class BundleResourceContribution implements McpServerContribution {
return Optional.of(new McpStatelessServerFeatures.SyncResourceSpecification(
new Resource.Builder()
- .name("bundle")
- .uri("bundle://")
- .description("OSGi bundle status")
+ .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("bundle://", "text/plain", bundleInfo);
+ TextResourceContents contents = new TextResourceContents(URI_BUNDLES_ALL, "text/plain", bundleInfo);
return new McpSchema.ReadResourceResult(List.of(contents));
}));
@@ -88,9 +91,7 @@ public class BundleResourceContribution implements McpServerContribution {
if (!bundleState.isValid()) {
throw new IllegalArgumentException("Invalid bundle state: " + requestedState);
}
- String bundleInfo = "";
- // extract desired state from URI
- bundleInfo = Arrays.stream(ctx.getBundles())
+ String bundleInfo = Arrays.stream(ctx.getBundles())
.filter(b -> b.getState() == bundleState.getState())
.map(this::describe)
.collect(Collectors.joining("\n"));
@@ -130,7 +131,12 @@ public class BundleResourceContribution implements McpServerContribution {
}
private String describe(Bundle b) {
- return "Bundle " + b.getSymbolicName() + " (version " + b.getVersion() + ") is in state "
+ 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() + ")";
}
}
(sling-org-apache-sling-mcp-server) 19/27: feat(mcp): repository prompts support parameters defined in the front matter
Posted by ro...@apache.org.
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 128443db4e1fcd2f6203e28180b13a9512c9a79a
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Dec 12 17:03:33 2025 +0100
feat(mcp): repository prompts support parameters defined in the front matter
---
pom.xml | 19 +++
.../sling/mcp/server/impl/DiscoveredPrompt.java | 10 +-
.../apache/sling/mcp/server/impl/McpServlet.java | 15 +-
.../impl/contribs/DiscoveredPromptBuilder.java | 160 +++++++++++++++++++++
.../impl/contribs/RepositoryPromptsRegistrar.java | 81 ++---------
.../libs/sling/mcp/prompts/new-sling-servlet.md | 17 +++
.../impl/contribs/DiscoveredPromptBuilderTest.java | 129 +++++++++++++++++
7 files changed, 346 insertions(+), 85 deletions(-)
diff --git a/pom.xml b/pom.xml
index d90ecce..919caad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -123,6 +123,25 @@
<version>0.27.0</version>
<scope>provided</scope>
</dependency>
+
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>5.20.0</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>3.27.6</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
index ad64197..4e6e54f 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
@@ -20,14 +20,16 @@ package org.apache.sling.mcp.server.impl;
import java.util.List;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
-import org.apache.sling.api.resource.ResourceResolver;
public interface DiscoveredPrompt {
public static final String SERVICE_PROP_NAME = "mcp.prompt.name";
- public static final String SERVICE_PROP_TITLE = "mcp.prompt.title";
- public static final String SERVICE_PROP_DESCRIPTION = "mcp.prompt.description";
- List<PromptMessage> getPromptMessages(ResourceResolver resolver);
+ List<PromptMessage> getPromptMessages(McpTransportContext c, GetPromptRequest r);
+
+ Prompt asPrompt();
}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index 8deb6dc..06bb9c4 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -33,7 +33,6 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpeci
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
@@ -41,7 +40,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingJakartaHttpServletRequest;
import org.apache.sling.api.SlingJakartaHttpServletResponse;
-import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.jetbrains.annotations.NotNull;
@@ -163,15 +161,10 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
@Reference(policy = ReferencePolicy.DYNAMIC, policyOption = GREEDY, cardinality = MULTIPLE)
protected void bindPrompt(DiscoveredPrompt prompt, Map<String, Object> properties) {
- String promptName = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_NAME);
- String promptTitle = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_TITLE);
- String promptDescription = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_DESCRIPTION);
- syncServer.addPrompt(new SyncPromptSpecification(
- new Prompt(promptName, promptTitle, promptDescription, List.of()), (c, r) -> {
- ResourceResolver resourceResolver = (ResourceResolver) c.get("resourceResolver");
- var messages = prompt.getPromptMessages(resourceResolver);
- return new McpSchema.GetPromptResult(null, messages);
- }));
+ syncServer.addPrompt(new SyncPromptSpecification(prompt.asPrompt(), (c, r) -> {
+ var messages = prompt.getPromptMessages(c, r);
+ return new McpSchema.GetPromptResult(null, messages);
+ }));
}
protected void unbindPrompt(Map<String, Object> properties) {
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java
new file mode 100644
index 0000000..ca2f9a7
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilder.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.impl.contribs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
+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 io.modelcontextprotocol.spec.McpSchema.TextContent;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.mcp.server.impl.DiscoveredPrompt;
+import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
+import org.commonmark.ext.front.matter.YamlFrontMatterVisitor;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class DiscoveredPromptBuilder {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public DiscoveredPrompt buildPrompt(Resource prompt, String promptName) throws IOException {
+
+ Parser parser = Parser.builder()
+ .extensions(List.of(YamlFrontMatterExtension.create()))
+ .build();
+ String charset = prompt.getResourceMetadata().getCharacterEncoding();
+ if (charset == null) {
+ charset = StandardCharsets.UTF_8.name();
+ }
+ try (InputStream is = prompt.adaptTo(InputStream.class)) {
+ Node node = parser.parseReader(new InputStreamReader(is, charset));
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ node.accept(visitor);
+
+ String title = visitor.getData().getOrDefault("title", List.of()).stream()
+ .findFirst()
+ .orElse(null);
+ String description = visitor.getData().getOrDefault("description", List.of()).stream()
+ .findFirst()
+ .orElse(null);
+
+ List<PromptArgument> arguments = new ArrayList<>();
+
+ visitor.getData().entrySet().stream()
+ .filter(e -> e.getKey().startsWith("argument."))
+ .forEach(e -> {
+ String argName = e.getKey().substring("argument.".length());
+ List<String> values = e.getValue();
+ String argTitle = null;
+ String argDesc = null;
+ boolean argRequired = false;
+ for (String value : values) {
+
+ if (value.startsWith("title:")) {
+ argTitle = value.substring("title:".length()).trim();
+ } else if (value.startsWith("description:")) {
+ argDesc =
+ value.substring("description:".length()).trim();
+ } else if (value.startsWith("required:")) {
+ String argRequiredRaw =
+ value.substring("required:".length()).trim();
+ argRequired = Boolean.valueOf(argRequiredRaw);
+ } else {
+ logger.warn("Unknown argument property in prompt {}: {}", promptName, value);
+ }
+ }
+
+ arguments.add(new PromptArgument(argName, argTitle, argDesc, argRequired));
+ });
+
+ return new RepositoryPrompt(prompt.getPath(), promptName, title, description, arguments);
+ }
+ }
+
+ static class RepositoryPrompt implements DiscoveredPrompt {
+
+ private final String promptPath;
+ private final String promptName;
+ private final String promptTitle;
+ private final String promptDescription;
+ private final List<PromptArgument> arguments;
+
+ RepositoryPrompt(
+ String promptPath,
+ String promptName,
+ String promptTitle,
+ String promptDescription,
+ List<PromptArgument> arguments) {
+ this.promptPath = promptPath;
+ this.promptName = promptName;
+ this.promptTitle = promptTitle;
+ this.promptDescription = promptDescription;
+ this.arguments = arguments;
+ }
+
+ @Override
+ public List<PromptMessage> getPromptMessages(McpTransportContext c, GetPromptRequest req) {
+
+ ResourceResolver rr = (ResourceResolver) c.get("resourceResolver");
+
+ try {
+ Resource promptResource = rr.getResource(promptPath);
+ String encoding = promptResource.getResourceMetadata().getCharacterEncoding();
+ if (encoding == null) {
+ encoding = StandardCharsets.UTF_8.name();
+ }
+ try (InputStream stream = promptResource.adaptTo(InputStream.class)) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ stream.transferTo(baos);
+ String contents = baos.toString(encoding);
+
+ for (PromptArgument arg : arguments) {
+ String placeholder = "{" + arg.name() + "}";
+ Object requestedArg = req.arguments().get(arg.name());
+ String value = requestedArg != null ? requestedArg.toString() : "";
+ contents = contents.replace(placeholder, value);
+ }
+
+ return List.of(new PromptMessage(Role.ASSISTANT, new TextContent(contents)));
+ }
+ } catch (IOException e) {
+ return List.of();
+ }
+ }
+
+ @Override
+ public Prompt asPrompt() {
+ return new Prompt(promptName, promptTitle, promptDescription, arguments);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
index fe0e97e..81b07a4 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
@@ -18,12 +18,7 @@
*/
package org.apache.sling.mcp.server.impl.contribs;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
@@ -31,9 +26,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
-import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
-import io.modelcontextprotocol.spec.McpSchema.Role;
-import io.modelcontextprotocol.spec.McpSchema.TextContent;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
@@ -42,10 +34,6 @@ import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.mcp.server.impl.DiscoveredPrompt;
-import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
-import org.commonmark.ext.front.matter.YamlFrontMatterVisitor;
-import org.commonmark.node.Node;
-import org.commonmark.parser.Parser;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
@@ -61,6 +49,7 @@ public class RepositoryPromptsRegistrar {
private static final String PROMPT_LIBS_DIR = "/libs/sling/mcp/prompts";
private final Logger logger = LoggerFactory.getLogger(getClass());
private ConcurrentMap<String, ServiceRegistration<DiscoveredPrompt>> registrations = new ConcurrentHashMap<>();
+ private final DiscoveredPromptBuilder promptBuilder = new DiscoveredPromptBuilder();
@Activate
public RepositoryPromptsRegistrar(@Reference ResourceResolverFactory rrf, BundleContext ctx) throws LoginException {
@@ -128,40 +117,20 @@ public class RepositoryPromptsRegistrar {
}
}
- private void registerPrompt(BundleContext ctx, String promptName, Resource prompt) {
+ private void registerPrompt(BundleContext ctx, String promptName, Resource promptResource) {
- Map<String, String> serviceProps = new HashMap<>(Map.of(DiscoveredPrompt.SERVICE_PROP_NAME, promptName));
+ try {
+ DiscoveredPrompt prompt = promptBuilder.buildPrompt(promptResource, promptName);
- Parser parser = Parser.builder()
- .extensions(List.of(YamlFrontMatterExtension.create()))
- .build();
- String charset = prompt.getResourceMetadata().getCharacterEncoding();
- if (charset == null) {
- charset = StandardCharsets.UTF_8.name();
- }
- try (InputStream is = prompt.adaptTo(InputStream.class)) {
- Node node = parser.parseReader(new InputStreamReader(is, charset));
- YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
- node.accept(visitor);
-
- visitor.getData().getOrDefault("title", List.of()).stream()
- .findFirst()
- .ifPresent(title -> serviceProps.put(DiscoveredPrompt.SERVICE_PROP_TITLE, title));
- visitor.getData().getOrDefault("description", List.of()).stream()
- .findFirst()
- .ifPresent(title -> serviceProps.put(DiscoveredPrompt.SERVICE_PROP_DESCRIPTION, title));
+ var sr = ctx.registerService(
+ DiscoveredPrompt.class,
+ prompt,
+ new Hashtable<>(Map.of(DiscoveredPrompt.SERVICE_PROP_NAME, promptName)));
+
+ registrations.put(promptName, sr);
} catch (IOException e) {
- logger.error("Error reading prompt markdown file at {}", prompt.getPath(), e);
+ logger.warn("Error registering prompt {} at path {}", promptName, promptResource.getPath(), e);
}
-
- // TODO - discover additional properties from the markdown file (front matter)
-
- var sr = ctx.registerService(
- DiscoveredPrompt.class,
- new RepositoryPrompt(prompt.getPath()),
- new Hashtable<String, String>(serviceProps));
-
- registrations.put(promptName, sr);
}
private String getPromptName(String path) {
@@ -177,32 +146,4 @@ public class RepositoryPromptsRegistrar {
// remove .md extension
return promptName.substring(0, promptName.length() - ".md".length());
}
-
- class RepositoryPrompt implements DiscoveredPrompt {
- private final String promptPath;
-
- RepositoryPrompt(String promptPath) {
- this.promptPath = promptPath;
- }
-
- @Override
- public List<PromptMessage> getPromptMessages(ResourceResolver resolver) {
-
- try {
- Resource promptResource = resolver.getResource(promptPath);
- String encoding = promptResource.getResourceMetadata().getCharacterEncoding();
- if (encoding == null) {
- encoding = StandardCharsets.UTF_8.name();
- }
- try (InputStream stream = promptResource.adaptTo(InputStream.class)) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- stream.transferTo(baos);
- String contents = baos.toString(encoding);
- return List.of(new PromptMessage(Role.ASSISTANT, new TextContent(contents)));
- }
- } catch (IOException e) {
- return List.of();
- }
- }
- }
}
diff --git a/src/main/resources/SLING-INF/libs/sling/mcp/prompts/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/test/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilderTest.java b/src/test/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilderTest.java
new file mode 100644
index 0000000..cd8d58e
--- /dev/null
+++ b/src/test/java/org/apache/sling/mcp/server/impl/contribs/DiscoveredPromptBuilderTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import io.modelcontextprotocol.spec.McpSchema.Prompt;
+import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.mcp.server.impl.DiscoveredPrompt;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class DiscoveredPromptBuilderTest {
+
+ @Test
+ void basicMarkdownFile() throws IOException {
+
+ String markdownFile = """
+ # Basic Prompt
+ """;
+
+ Resource promptResource = mock(Resource.class);
+ when(promptResource.getResourceMetadata()).thenReturn(new ResourceMetadata());
+ when(promptResource.adaptTo(InputStream.class))
+ .thenReturn(new ByteArrayInputStream(markdownFile.getBytes(StandardCharsets.UTF_8)));
+
+ DiscoveredPromptBuilder builder = new DiscoveredPromptBuilder();
+ DiscoveredPrompt prompt = builder.buildPrompt(promptResource, "basic-prompt");
+
+ assertThat(prompt)
+ .as("prompt")
+ .extracting(DiscoveredPrompt::asPrompt)
+ .extracting(Prompt::name, Prompt::title, Prompt::description, Prompt::arguments)
+ .containsExactly("basic-prompt", null, null, List.of());
+ }
+
+ @Test
+ void basicFrontMatter() throws IOException {
+
+ String markdownFile = """
+ ---
+ title: Basic Prompt
+ description: |
+ A basic prompt for testing.
+ Spans multiple lines.
+ ---
+ # Basic Prompt
+ """;
+
+ Resource promptResource = mock(Resource.class);
+ when(promptResource.getResourceMetadata()).thenReturn(new ResourceMetadata());
+ when(promptResource.adaptTo(InputStream.class))
+ .thenReturn(new ByteArrayInputStream(markdownFile.getBytes(StandardCharsets.UTF_8)));
+
+ DiscoveredPromptBuilder builder = new DiscoveredPromptBuilder();
+ DiscoveredPrompt prompt = builder.buildPrompt(promptResource, "prompt-with-front-matter");
+
+ assertThat(prompt)
+ .as("prompt")
+ .extracting(DiscoveredPrompt::asPrompt)
+ .extracting(Prompt::name, Prompt::title, Prompt::description, Prompt::arguments)
+ .containsExactly(
+ "prompt-with-front-matter",
+ "Basic Prompt",
+ "A basic prompt for testing.\nSpans multiple lines.",
+ List.of());
+ }
+
+ @Test
+ void arguments() throws IOException {
+
+ String markdownFile = """
+ ---
+ title: Basic Prompt
+ argument.first:
+ - title: First Argument
+ - description: This is the first argument
+ - required: true
+ argument.second:
+ - title: Second Argument
+ ---
+ # Basic Prompt
+ """;
+
+ Resource promptResource = mock(Resource.class);
+ when(promptResource.getResourceMetadata()).thenReturn(new ResourceMetadata());
+ when(promptResource.adaptTo(InputStream.class))
+ .thenReturn(new ByteArrayInputStream(markdownFile.getBytes(StandardCharsets.UTF_8)));
+
+ DiscoveredPromptBuilder builder = new DiscoveredPromptBuilder();
+ DiscoveredPrompt prompt = builder.buildPrompt(promptResource, "prompt-with-arguments");
+ assertThat(prompt)
+ .as("prompt")
+ .extracting(DiscoveredPrompt::asPrompt)
+ .extracting(Prompt::name, Prompt::title, Prompt::description, Prompt::arguments)
+ .containsExactly(
+ "prompt-with-arguments",
+ "Basic Prompt",
+ null,
+ List.of(
+ new PromptArgument("first", "First Argument", "This is the first argument", true),
+ new PromptArgument("second", "Second Argument", null, false)));
+ }
+}
(sling-org-apache-sling-mcp-server) 13/27: chore(mcp-server): stop using deprecated apis
Posted by ro...@apache.org.
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 f22c8cb10d41658ce19b96d874325db640ec527a
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 21:31:58 2025 +0100
chore(mcp-server): stop using deprecated apis
---
.../sling/mcp/server/impl/contribs/RefreshPackagesContribution.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
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
index 0b02dcd..c02d808 100644
--- 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
@@ -66,7 +66,9 @@ public class RefreshPackagesContribution implements McpServerContribution {
fw.refreshBundles(null);
- return new CallToolResult("Bundles refreshed successfully", Boolean.FALSE);
+ return CallToolResult.builder()
+ .addTextContent("Bundles refreshed successfully")
+ .build();
}));
}
}
(sling-org-apache-sling-mcp-server) 23/27: fix(mcp-server): ignore unknown properties during deserialisation
Posted by ro...@apache.org.
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 8e6ffef6b6ab363ece13b125ca347349bcbe60a1
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Jan 13 13:30:01 2026 +0100
fix(mcp-server): ignore unknown properties during deserialisation
Workaround for upstream issue https://github.com/modelcontextprotocol/java-sdk/issues/724 . This
appears with MCP clients implementing the 2025-11-25 version of the spec which supports more
properties for the 'elicitation' object.
For now, we ignore unknown properties since there is no release of the MCP Java SDK that contains
the fix.
---
.../org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java b/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
index 74bece1..5df93df 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
@@ -27,6 +27,8 @@ import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
+import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+
@Component
public class McpJsonMapperProvider {
@@ -34,7 +36,10 @@ public class McpJsonMapperProvider {
@Activate
public void activate(BundleContext bundleContext) {
- McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
+ ObjectMapper objectMapper = new ObjectMapper();
+ // work around https://github.com/modelcontextprotocol/java-sdk/issues/724
+ objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
+ McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(objectMapper);
serviceRegistration = bundleContext.registerService(McpJsonMapper.class, jsonMapper, null);
}
(sling-org-apache-sling-mcp-server) 18/27: feat(mcp): register templates from the repository
Posted by ro...@apache.org.
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 234099621ea2124b9f435196c71f053915c0b29c
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Dec 12 14:51:36 2025 +0100
feat(mcp): register templates from the repository
Still need to add support for parameters
---
pom.xml | 12 +++++++++
src/main/features/main.json | 10 ++++++-
.../sling/mcp/server/impl/DiscoveredPrompt.java | 2 ++
.../apache/sling/mcp/server/impl/McpServlet.java | 13 +++++----
.../impl/contribs/RepositoryPromptsRegistrar.java | 31 ++++++++++++++++++++--
.../libs/sling/mcp/prompts/troubleshoot.md | 4 +++
6 files changed, 64 insertions(+), 8 deletions(-)
diff --git a/pom.xml b/pom.xml
index edca093..d90ecce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,6 +111,18 @@
<version>1.3.0</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.commonmark</groupId>
+ <artifactId>commonmark</artifactId>
+ <version>0.27.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.commonmark</groupId>
+ <artifactId>commonmark-ext-yaml-front-matter</artifactId>
+ <version>0.27.0</version>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/src/main/features/main.json b/src/main/features/main.json
index 656a96b..ffae06a 100644
--- a/src/main/features/main.json
+++ b/src/main/features/main.json
@@ -31,7 +31,15 @@
{
"id": "org.yaml:snakeyaml:2.3",
"start-order": 25
- }
+ },
+ {
+ "id": "org.commonmark:commonmark:0.27.0",
+ "start-order": 25
+ },
+ {
+ "id": "org.commonmark:commonmark-ext-yaml-front-matter:0.27.0",
+ "start-order": 25
+ }
],
"configurations": {
"org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~mcp-server":{
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
index 6a52ce5..ad64197 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/DiscoveredPrompt.java
@@ -26,6 +26,8 @@ import org.apache.sling.api.resource.ResourceResolver;
public interface DiscoveredPrompt {
public static final String SERVICE_PROP_NAME = "mcp.prompt.name";
+ public static final String SERVICE_PROP_TITLE = "mcp.prompt.title";
+ public static final String SERVICE_PROP_DESCRIPTION = "mcp.prompt.description";
List<PromptMessage> getPromptMessages(ResourceResolver resolver);
}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index ac708c6..8deb6dc 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -164,11 +164,14 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
@Reference(policy = ReferencePolicy.DYNAMIC, policyOption = GREEDY, cardinality = MULTIPLE)
protected void bindPrompt(DiscoveredPrompt prompt, Map<String, Object> properties) {
String promptName = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_NAME);
- syncServer.addPrompt(new SyncPromptSpecification(new Prompt(promptName, null, List.of()), (c, r) -> {
- ResourceResolver resourceResolver = (ResourceResolver) c.get("resourceResolver");
- var messages = prompt.getPromptMessages(resourceResolver);
- return new McpSchema.GetPromptResult(null, messages);
- }));
+ String promptTitle = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_TITLE);
+ String promptDescription = (String) properties.get(DiscoveredPrompt.SERVICE_PROP_DESCRIPTION);
+ syncServer.addPrompt(new SyncPromptSpecification(
+ new Prompt(promptName, promptTitle, promptDescription, List.of()), (c, r) -> {
+ ResourceResolver resourceResolver = (ResourceResolver) c.get("resourceResolver");
+ var messages = prompt.getPromptMessages(resourceResolver);
+ return new McpSchema.GetPromptResult(null, messages);
+ }));
}
protected void unbindPrompt(Map<String, Object> properties) {
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
index 990de0d..fe0e97e 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RepositoryPromptsRegistrar.java
@@ -21,7 +21,9 @@ package org.apache.sling.mcp.server.impl.contribs;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
@@ -40,6 +42,10 @@ import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.mcp.server.impl.DiscoveredPrompt;
+import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
+import org.commonmark.ext.front.matter.YamlFrontMatterVisitor;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
@@ -65,7 +71,6 @@ public class RepositoryPromptsRegistrar {
@Override
public void onChange(@NotNull List<ResourceChange> changes) {
- changes.forEach(a -> logger.info("Received change {} at {}", a.getType(), a.getPath()));
try (ResourceResolver resolver = rrf.getAdministrativeResourceResolver(null)) {
for (ResourceChange change : changes) {
@@ -125,7 +130,29 @@ public class RepositoryPromptsRegistrar {
private void registerPrompt(BundleContext ctx, String promptName, Resource prompt) {
- Map<String, String> serviceProps = Map.of(DiscoveredPrompt.SERVICE_PROP_NAME, promptName);
+ Map<String, String> serviceProps = new HashMap<>(Map.of(DiscoveredPrompt.SERVICE_PROP_NAME, promptName));
+
+ Parser parser = Parser.builder()
+ .extensions(List.of(YamlFrontMatterExtension.create()))
+ .build();
+ String charset = prompt.getResourceMetadata().getCharacterEncoding();
+ if (charset == null) {
+ charset = StandardCharsets.UTF_8.name();
+ }
+ try (InputStream is = prompt.adaptTo(InputStream.class)) {
+ Node node = parser.parseReader(new InputStreamReader(is, charset));
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ node.accept(visitor);
+
+ visitor.getData().getOrDefault("title", List.of()).stream()
+ .findFirst()
+ .ifPresent(title -> serviceProps.put(DiscoveredPrompt.SERVICE_PROP_TITLE, title));
+ visitor.getData().getOrDefault("description", List.of()).stream()
+ .findFirst()
+ .ifPresent(title -> serviceProps.put(DiscoveredPrompt.SERVICE_PROP_DESCRIPTION, title));
+ } catch (IOException e) {
+ logger.error("Error reading prompt markdown file at {}", prompt.getPath(), e);
+ }
// TODO - discover additional properties from the markdown file (front matter)
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
index e60aed8..d5e9587 100644
--- a/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
+++ b/src/main/resources/SLING-INF/libs/sling/mcp/prompts/troubleshoot.md
@@ -1,3 +1,7 @@
+---
+title: "Troubleshooting guide"
+description: "Guide to troubleshoot common issues with Apache Sling applications"
+---
# Troubleshooting Guide
## Overview
(sling-org-apache-sling-mcp-server) 04/27: fix(mcp-server): make bundles output useful
Posted by ro...@apache.org.
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 859e0823412391f34df331b8b0251c2621c1b8ea
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Mon Dec 8 17:20:33 2025 +0100
fix(mcp-server): make bundles output useful
- include the friendly name of the bundle state
- fix MCP clients only reading the first entry
---
.../apache/sling/mcp/server/impl/McpServlet.java | 44 ++++++++++++++--------
1 file changed, 28 insertions(+), 16 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index a135157..3f62a10 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -79,6 +79,18 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
private MethodHandle doGetMethod;
private MethodHandle doPostMethod;
+ private static String getStateString(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";
+ };
+ }
+
@Activate
public McpServlet(BundleContext ctx) throws IllegalAccessException, NoSuchMethodException {
@@ -164,14 +176,14 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
.mimeType("text/plain")
.build(),
(context, request) -> {
- List<McpSchema.ResourceContents> res = Stream.of(ctx.getBundles())
- .map(b -> new TextResourceContents(
- "bundle://" + b.getSymbolicName(),
- "text-plain",
- "Bundle " + b.getSymbolicName() + " is in state " + b.getState()))
- .collect(Collectors.toList());
-
- return new McpSchema.ReadResourceResult(res);
+ String bundleInfo = Stream.of(ctx.getBundles())
+ .map(b -> "Bundle " + b.getSymbolicName() + " is in state " + getStateString(b.getState())
+ + " (" + b.getState() + ")")
+ .collect(Collectors.joining("\n"));
+
+ TextResourceContents contents = new TextResourceContents("bundle://", "text/plain", bundleInfo);
+
+ return new McpSchema.ReadResourceResult(List.of(contents));
}));
syncServer.addResourceTemplate(new McpStatelessServerFeatures.SyncResourceTemplateSpecification(
@@ -180,18 +192,18 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
.name("bundles")
.build(),
(context, request) -> {
- List<ResourceContents> bundles = List.of();
+ String bundleInfo = "";
if ("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
- bundles = Arrays.stream(ctx.getBundles())
+ bundleInfo = Arrays.stream(ctx.getBundles())
.filter(b -> b.getState() == Bundle.RESOLVED)
- .<ResourceContents>map(b -> new TextResourceContents(
- "bundle://" + b.getSymbolicName(),
- "text-plain",
- "Bundle " + b.getSymbolicName() + " is in state " + b.getState()))
- .toList();
+ .map(b -> "Bundle " + b.getSymbolicName() + " is in state "
+ + getStateString(b.getState()) + " (" + b.getState() + ")")
+ .collect(Collectors.joining("\n"));
}
- return new ReadResourceResult(bundles);
+ TextResourceContents contents = new TextResourceContents(request.uri(), "text/plain", bundleInfo);
+
+ return new ReadResourceResult(List.of(contents));
}));
}
(sling-org-apache-sling-mcp-server) 06/27: feat(mcp-server): split contributions into seperate files
Posted by ro...@apache.org.
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 ef902bbb5ee86096b6ea394e599e3d23ef643e2a
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Dec 9 16:39:25 2025 +0100
feat(mcp-server): split contributions into seperate files
The API is bundle-internal, incomplete and sub-optimal, but it makes it possible to use separate
files right now.
---
.../mcp/server/impl/McpJsonMapperProvider.java | 47 ++++++
.../mcp/server/impl/McpServerContribution.java | 50 +++++++
.../apache/sling/mcp/server/impl/McpServlet.java | 161 +++++----------------
.../impl/contribs/BundleResourceContribution.java | 108 ++++++++++++++
.../impl/contribs/RefreshPackagesContribution.java | 72 +++++++++
.../impl/contribs/ServletPromptContribution.java | 71 +++++++++
6 files changed, 385 insertions(+), 124 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java b/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
new file mode 100644
index 0000000..74bece1
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpJsonMapperProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+
+@Component
+public class McpJsonMapperProvider {
+
+ private ServiceRegistration<McpJsonMapper> serviceRegistration;
+
+ @Activate
+ public void activate(BundleContext bundleContext) {
+ McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
+ serviceRegistration = bundleContext.registerService(McpJsonMapper.class, jsonMapper, null);
+ }
+
+ @Deactivate
+ public void deactivate() {
+ if (serviceRegistration != null) {
+ serviceRegistration.unregister();
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
new file mode 100644
index 0000000..4e3168d
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.mcp.server.impl;
+
+import java.util.Optional;
+
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+
+public interface McpServerContribution {
+
+ default Optional<SyncToolSpecification> getSyncToolSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncResourceSpecification> getSyncResourceSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncResourceTemplateSpecification> getSyncResourceTemplateSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncPromptSpecification> getSyncPromptSpecification() {
+ return Optional.empty();
+ }
+
+ default Optional<SyncCompletionSpecification> getSyncCompletionSpecification() {
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index b12faa6..daf9c57 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -21,36 +21,15 @@ package org.apache.sling.mcp.server.impl;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
-import java.util.Arrays;
import java.util.List;
-import java.util.Locale;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.Optional;
-import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.McpJsonMapper;
-import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
import io.modelcontextprotocol.server.McpServer;
-import io.modelcontextprotocol.server.McpStatelessServerFeatures;
-import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification;
-import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
-import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-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.ReadResourceResult;
-import io.modelcontextprotocol.spec.McpSchema.Resource;
-import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
-import io.modelcontextprotocol.spec.McpSchema.Role;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
-import io.modelcontextprotocol.spec.McpSchema.Tool;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -60,16 +39,18 @@ import org.apache.sling.api.SlingJakartaHttpServletResponse;
import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.jetbrains.annotations.NotNull;
-import org.osgi.framework.Bundle;
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.Deactivate;
+import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import static org.osgi.service.component.annotations.ReferenceCardinality.MULTIPLE;
+import static org.osgi.service.component.annotations.ReferencePolicyOption.GREEDY;
+
@Component(service = Servlet.class)
@SlingServletPaths(value = {McpServlet.ENDPOINT})
@Designate(ocd = McpServlet.Config.class)
@@ -99,22 +80,13 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
private MethodHandle doGetMethod;
private MethodHandle doPostMethod;
- private static String getStateString(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";
- };
- }
-
@Activate
- public McpServlet(BundleContext ctx, Config config) throws IllegalAccessException, NoSuchMethodException {
-
- McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
+ public McpServlet(
+ BundleContext ctx,
+ Config config,
+ @Reference McpJsonMapper jsonMapper,
+ @Reference(cardinality = MULTIPLE, policyOption = GREEDY) List<McpServerContribution> contributions)
+ throws IllegalAccessException, NoSuchMethodException {
transportProvider = HttpServletStatelessServerTransport.builder()
.messageEndpoint(ENDPOINT)
@@ -135,21 +107,22 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
java.lang.invoke.MethodType.methodType(
void.class, HttpServletRequest.class, HttpServletResponse.class));
- SyncCompletionSpecification servletCompletionSpec = new McpStatelessServerFeatures.SyncCompletionSpecification(
- new McpSchema.PromptReference("ref/prompt", "new-sling-servlet"), (context, request) -> {
- return new McpSchema.CompleteResult(new CompleteCompletion(List.of(), 0, false));
- });
-
String serverVersion = config.serverVersion();
if (serverVersion == null || serverVersion.isEmpty()) {
serverVersion = ctx.getBundle().getVersion().toString();
}
+
+ var completions = contributions.stream()
+ .map(McpServerContribution::getSyncCompletionSpecification)
+ .flatMap(Optional::stream)
+ .toList();
+
syncServer = McpServer.sync(transportProvider)
.serverInfo(config.serverTitle(), serverVersion)
.jsonMapper(jsonMapper)
.jsonSchemaValidator(new DefaultJsonSchemaValidator())
.instructions(config.instructions())
- .completions(List.of(servletCompletionSpec))
+ .completions(completions)
.capabilities(ServerCapabilities.builder()
.tools(false)
.prompts(false)
@@ -158,85 +131,25 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
.build())
.build();
- var schema = """
- {
- "type" : "object",
- "id" : "urn:jsonschema:Operation",
- "properties" : { }
- }
- """;
- var syncToolSpecification = 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 new CallToolResult("Bundles refreshed successfully", Boolean.FALSE);
- });
-
- // Register tools, resources, and prompts
- syncServer.addTool(syncToolSpecification);
- syncServer.addPrompt(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));
- }));
- syncServer.addResource(new McpStatelessServerFeatures.SyncResourceSpecification(
- new Resource.Builder()
- .name("bundle")
- .uri("bundle://")
- .description("OSGi bundle status")
- .mimeType("text/plain")
- .build(),
- (context, request) -> {
- String bundleInfo = Stream.of(ctx.getBundles())
- .map(b -> "Bundle " + b.getSymbolicName() + " is in state " + getStateString(b.getState())
- + " (" + b.getState() + ")")
- .collect(Collectors.joining("\n"));
-
- TextResourceContents contents = new TextResourceContents("bundle://", "text/plain", bundleInfo);
-
- return new McpSchema.ReadResourceResult(List.of(contents));
- }));
-
- syncServer.addResourceTemplate(new McpStatelessServerFeatures.SyncResourceTemplateSpecification(
- new ResourceTemplate.Builder()
- .uriTemplate("bundles://state/{state}")
- .name("bundles")
- .build(),
- (context, request) -> {
- String bundleInfo = "";
- if ("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
- bundleInfo = Arrays.stream(ctx.getBundles())
- .filter(b -> b.getState() == Bundle.RESOLVED)
- .map(b -> "Bundle " + b.getSymbolicName() + " is in state "
- + getStateString(b.getState()) + " (" + b.getState() + ")")
- .collect(Collectors.joining("\n"));
- }
-
- TextResourceContents contents = new TextResourceContents(request.uri(), "text/plain", bundleInfo);
-
- return new ReadResourceResult(List.of(contents));
- }));
+ contributions.stream()
+ .map(McpServerContribution::getSyncToolSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncTool -> syncServer.addTool(syncTool));
+
+ contributions.stream()
+ .map(McpServerContribution::getSyncResourceSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncResource -> syncServer.addResource(syncResource));
+
+ contributions.stream()
+ .map(McpServerContribution::getSyncResourceTemplateSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncResource -> syncServer.addResourceTemplate(syncResource));
+
+ contributions.stream()
+ .map(McpServerContribution::getSyncPromptSpecification)
+ .flatMap(Optional::stream)
+ .forEach(syncPrompt -> syncServer.addPrompt(syncPrompt));
}
@Override
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..605eeb3
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/BundleResourceContribution.java
@@ -0,0 +1,108 @@
+/*
+ * 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.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.impl.McpServerContribution;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+@Component
+public class BundleResourceContribution implements McpServerContribution {
+
+ private static String getStateString(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";
+ };
+ }
+
+ 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("bundle")
+ .uri("bundle://")
+ .description("OSGi bundle status")
+ .mimeType("text/plain")
+ .build(),
+ (context, request) -> {
+ String bundleInfo = Stream.of(ctx.getBundles())
+ .map(b -> "Bundle " + b.getSymbolicName() + " is in state " + getStateString(b.getState())
+ + " (" + b.getState() + ")")
+ .collect(Collectors.joining("\n"));
+
+ TextResourceContents contents = new TextResourceContents("bundle://", "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("bundles://state/{state}")
+ .name("bundles")
+ .build(),
+ (context, request) -> {
+ String bundleInfo = "";
+ if ("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
+ bundleInfo = Arrays.stream(ctx.getBundles())
+ .filter(b -> b.getState() == Bundle.RESOLVED)
+ .map(b -> "Bundle " + b.getSymbolicName() + " is in state "
+ + getStateString(b.getState()) + " (" + b.getState() + ")")
+ .collect(Collectors.joining("\n"));
+ }
+
+ TextResourceContents contents = new TextResourceContents(request.uri(), "text/plain", bundleInfo);
+
+ return new ReadResourceResult(List.of(contents));
+ }));
+ }
+}
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..0b02dcd
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/RefreshPackagesContribution.java
@@ -0,0 +1,72 @@
+/*
+ * 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.impl.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 new CallToolResult("Bundles refreshed successfully", Boolean.FALSE);
+ }));
+ }
+}
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..3e67666
--- /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.impl.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));
+ }));
+ }
+}
(sling-org-apache-sling-mcp-server) 02/27: chore(mcp-server): serve from /bin/mcp, follow existing conventions
Posted by ro...@apache.org.
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 8cbe5db37198b869a2e6e5f1d34feeb7197af5db
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 3 14:14:54 2025 +0100
chore(mcp-server): serve from /bin/mcp, follow existing conventions
---
src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index 80078df..a135157 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -70,7 +70,7 @@ import org.osgi.service.component.annotations.Deactivate;
@SlingServletPaths(value = {McpServlet.ENDPOINT})
public class McpServlet extends SlingJakartaAllMethodsServlet {
- static final String ENDPOINT = "/mcp";
+ static final String ENDPOINT = "/bin/mcp";
private static final long serialVersionUID = 1L;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
(sling-org-apache-sling-mcp-server) 14/27: mcp bundle diagnoser, component status, logs (#118)
Posted by ro...@apache.org.
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 feb8a9a60634287d8fb6f2983c517b1871a035a8
Author: Niek Raaijmakers <ni...@gmail.com>
AuthorDate: Thu Dec 11 14:24:55 2025 +0100
mcp bundle diagnoser, component status, logs (#118)
Co-authored-by: Niek Raaijmakers <ra...@adobe.com>
---
pom.xml | 11 +
.../contribs/ComponentResourceContribution.java | 126 +++++
.../server/impl/contribs/LogToolContribution.java | 355 ++++++++++++++
.../contribs/OsgiBundleDiagnosticContribution.java | 522 +++++++++++++++++++++
.../contribs/OsgiDiagnosticPromptContribution.java | 155 ++++++
5 files changed, 1169 insertions(+)
diff --git a/pom.xml b/pom.xml
index d2c09fb..edca093 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,11 @@
<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>
@@ -100,6 +105,12 @@
<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>
</dependencies>
<build>
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..7a307fd
--- /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.impl.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..9e68661
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java
@@ -0,0 +1,355 @@
+/*
+ * 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.impl.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 new CallToolResult(
+ "Invalid log level: " + logLevelStr
+ + ". Valid options are: ERROR, WARN, INFO, DEBUG, TRACE",
+ Boolean.TRUE);
+ }
+ }
+
+ // 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 new CallToolResult("Invalid regex pattern: " + e.getMessage(), Boolean.TRUE);
+ }
+ }
+
+ // Collect and filter logs
+ List<LogEntry> filteredLogs = collectLogs(pattern, minLogLevel, maxEntries);
+
+ // Format output
+ String result = formatLogs(filteredLogs, regexPattern, minLogLevel, maxEntries);
+
+ return new CallToolResult(result, Boolean.FALSE);
+ }));
+ }
+
+ 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..a4affef
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/OsgiBundleDiagnosticContribution.java
@@ -0,0 +1,522 @@
+/*
+ * 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.impl.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 new CallToolResult("Bundle '" + symbolicName + "' not found.", Boolean.TRUE);
+ }
+
+ 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 new CallToolResult(result.toString(), Boolean.FALSE);
+ }
+
+ 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 new CallToolResult(result.toString(), Boolean.FALSE);
+ }
+
+ 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..25ea95b
--- /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.impl.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();
+ }
+}
(sling-org-apache-sling-mcp-server) 26/27: chore(mcp): update McpServerContribution methods to return List instead of Optional
Posted by ro...@apache.org.
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 ccefd17931ce40f909841b88d540fca7e2bacd1f
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Jan 13 15:58:35 2026 +0100
chore(mcp): update McpServerContribution methods to return List instead of Optional
This makes the implementations much more flexible
---
.../apache/sling/mcp/server/impl/McpServlet.java | 11 +++++-----
.../mcp/server/spi/McpServerContribution.java | 25 ++++++++++++----------
2 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index 316b8a0..a7ae71b 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -23,7 +23,6 @@ import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
@@ -122,7 +121,7 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
var completions = contributions.stream()
.map(McpServerContribution::getSyncCompletionSpecification)
- .flatMap(Optional::stream)
+ .flatMap(List::stream)
.toList();
syncServer = McpServer.sync(transportProvider)
@@ -141,22 +140,22 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
contributions.stream()
.map(McpServerContribution::getSyncToolSpecification)
- .flatMap(Optional::stream)
+ .flatMap(List::stream)
.forEach(syncTool -> syncServer.addTool(syncTool));
contributions.stream()
.map(McpServerContribution::getSyncResourceSpecification)
- .flatMap(Optional::stream)
+ .flatMap(List::stream)
.forEach(syncResource -> syncServer.addResource(syncResource));
contributions.stream()
.map(McpServerContribution::getSyncResourceTemplateSpecification)
- .flatMap(Optional::stream)
+ .flatMap(List::stream)
.forEach(syncResource -> syncServer.addResourceTemplate(syncResource));
contributions.stream()
.map(McpServerContribution::getSyncPromptSpecification)
- .flatMap(Optional::stream)
+ .flatMap(List::stream)
.forEach(syncPrompt -> syncServer.addPrompt(syncPrompt));
}
diff --git a/src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java b/src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java
index 58a5d42..70ada37 100644
--- a/src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java
+++ b/src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java
@@ -18,7 +18,7 @@
*/
package org.apache.sling.mcp.server.spi;
-import java.util.Optional;
+import java.util.List;
import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification;
import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
@@ -37,26 +37,29 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecifi
*
* <p>This abstraction is needed because the contributions themselves are Java records and cannot
* be registered as OSGi services directly.</p>
+ *
+ * <p><strong>Note:</strong> This API can change in incompatible ways while the bundle version is < 1.0 . The exported package
+ * version will remain 1.0.0 and will be changed to 1.1.0 once the bundle version becomes 1.0 .</p>
*/
public interface McpServerContribution {
- default Optional<SyncToolSpecification> getSyncToolSpecification() {
- return Optional.empty();
+ default List<SyncToolSpecification> getSyncToolSpecification() {
+ return List.of();
}
- default Optional<SyncResourceSpecification> getSyncResourceSpecification() {
- return Optional.empty();
+ default List<SyncResourceSpecification> getSyncResourceSpecification() {
+ return List.of();
}
- default Optional<SyncResourceTemplateSpecification> getSyncResourceTemplateSpecification() {
- return Optional.empty();
+ default List<SyncResourceTemplateSpecification> getSyncResourceTemplateSpecification() {
+ return List.of();
}
- default Optional<SyncPromptSpecification> getSyncPromptSpecification() {
- return Optional.empty();
+ default List<SyncPromptSpecification> getSyncPromptSpecification() {
+ return List.of();
}
- default Optional<SyncCompletionSpecification> getSyncCompletionSpecification() {
- return Optional.empty();
+ default List<SyncCompletionSpecification> getSyncCompletionSpecification() {
+ return List.of();
}
}
(sling-org-apache-sling-mcp-server) 15/27: chore(mcp): stop using deprecated apis
Posted by ro...@apache.org.
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 c5719fea956977f8864bff59c37f87b7d1143064
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Thu Dec 11 14:32:07 2025 +0100
chore(mcp): stop using deprecated apis
---
.../mcp/server/impl/contribs/LogToolContribution.java | 16 ++++++++++------
.../impl/contribs/OsgiBundleDiagnosticContribution.java | 9 ++++++---
2 files changed, 16 insertions(+), 9 deletions(-)
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
index 9e68661..37fe051 100644
--- 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
@@ -110,10 +110,11 @@ public class LogToolContribution implements McpServerContribution {
if (logLevelStr != null && !logLevelStr.isEmpty()) {
minLogLevel = parseLogLevel(logLevelStr);
if (minLogLevel == -1) {
- return new CallToolResult(
- "Invalid log level: " + logLevelStr
- + ". Valid options are: ERROR, WARN, INFO, DEBUG, TRACE",
- Boolean.TRUE);
+ return CallToolResult.builder()
+ .addTextContent("Invalid log level: " + logLevelStr
+ + ". Valid options are: ERROR, WARN, INFO, DEBUG, TRACE")
+ .isError(true)
+ .build();
}
}
@@ -123,7 +124,10 @@ public class LogToolContribution implements McpServerContribution {
try {
pattern = Pattern.compile(regexPattern, Pattern.CASE_INSENSITIVE);
} catch (PatternSyntaxException e) {
- return new CallToolResult("Invalid regex pattern: " + e.getMessage(), Boolean.TRUE);
+ return CallToolResult.builder()
+ .addTextContent("Invalid regex pattern: " + e.getMessage())
+ .isError(true)
+ .build();
}
}
@@ -133,7 +137,7 @@ public class LogToolContribution implements McpServerContribution {
// Format output
String result = formatLogs(filteredLogs, regexPattern, minLogLevel, maxEntries);
- return new CallToolResult(result, Boolean.FALSE);
+ return CallToolResult.builder().addTextContent(result).build();
}));
}
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
index a4affef..4efaf3d 100644
--- 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
@@ -105,7 +105,10 @@ public class OsgiBundleDiagnosticContribution implements McpServerContribution {
private CallToolResult diagnoseSpecificBundle(String symbolicName) {
Bundle bundle = findBundle(symbolicName);
if (bundle == null) {
- return new CallToolResult("Bundle '" + symbolicName + "' not found.", Boolean.TRUE);
+ return CallToolResult.builder()
+ .addTextContent("Bundle '" + symbolicName + "' not found.")
+ .isError(true)
+ .build();
}
StringBuilder result = new StringBuilder();
@@ -124,7 +127,7 @@ public class OsgiBundleDiagnosticContribution implements McpServerContribution {
appendComponentDiagnostics(bundle, result);
}
- return new CallToolResult(result.toString(), Boolean.FALSE);
+ return CallToolResult.builder().addTextContent(result.toString()).build();
}
private CallToolResult diagnoseAllProblematicBundles() {
@@ -152,7 +155,7 @@ public class OsgiBundleDiagnosticContribution implements McpServerContribution {
// Check for components with issues
appendAllComponentIssues(result);
- return new CallToolResult(result.toString(), Boolean.FALSE);
+ return CallToolResult.builder().addTextContent(result.toString()).build();
}
private void analyzeBundleIssues(Bundle bundle, StringBuilder result) {
(sling-org-apache-sling-mcp-server) 09/27: chore(mcp): document limitations of the internal API
Posted by ro...@apache.org.
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 4c0cc21cf2f79bff95fab36887b4df1265d2b5ac
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 16:30:15 2025 +0100
chore(mcp): document limitations of the internal API
---
.../org/apache/sling/mcp/server/impl/McpServerContribution.java | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
index 4e3168d..ea15572 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
@@ -26,8 +26,14 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpe
import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification;
import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
+// Temporary abstraction to make it easier to contribute various MCP server features
+//
+// The major problem with this approach is that all contributions are loaded once the MCPServlet
+// is activated and therefore it does not take into account added/changed/removed contributions
+//
+// Expect this abstraction to change
public interface McpServerContribution {
-
+
default Optional<SyncToolSpecification> getSyncToolSpecification() {
return Optional.empty();
}
(sling-org-apache-sling-mcp-server) 08/27: feat(mcp): expose the recent requests data as a resource
Posted by ro...@apache.org.
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 a361f5a1ac7665999b4aef966183211108419384
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 15:52:01 2025 +0100
feat(mcp): expose the recent requests data as a resource
---
pom.xml | 6 ++
.../impl/contribs/RecentRequestsContribution.java | 73 ++++++++++++++++++++++
2 files changed, 79 insertions(+)
diff --git a/pom.xml b/pom.xml
index 4a2323c..e41f259 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,12 @@
<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>
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..53a127e
--- /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 org.apache.sling.engine.RequestInfo;
+import org.apache.sling.engine.RequestInfoProvider;
+import org.apache.sling.mcp.server.impl.McpServerContribution;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.Resource;
+import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
+
+@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 authenticaiton, 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)));
+ }));
+ }
+}
(sling-org-apache-sling-mcp-server) 20/27: chore(mcp-server): allow starting feature launcher in debug mode
Posted by ro...@apache.org.
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 713be81acb41f7a8db03dd35cf3c220b5d199803
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Fri Dec 12 17:22:46 2025 +0100
chore(mcp-server): allow starting feature launcher in debug mode
---
pom.xml | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/pom.xml b/pom.xml
index 919caad..d286092 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,8 @@
<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>
@@ -249,6 +251,11 @@
<launch>
<id>app</id>
<featureFile>${project.slingfeature.outputDirectory}/feature-app.json</featureFile>
+ <launcherArguments>
+ <vmOptions>
+ <vmOption>${slingfeature.app.vmOption}</vmOption>
+ </vmOptions>
+ </launcherArguments>
</launch>
</launches>
</configuration>
(sling-org-apache-sling-mcp-server) 03/27: chore(mcp-server): formatting in JSON file
Posted by ro...@apache.org.
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 a0fe0a3dcb2427d0b558e9fe8fe3444f8f56adc3
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Mon Dec 8 17:19:39 2025 +0100
chore(mcp-server): formatting in JSON file
---
src/main/features/main.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/features/main.json b/src/main/features/main.json
index c34fb1f..052bae5 100644
--- a/src/main/features/main.json
+++ b/src/main/features/main.json
@@ -1,6 +1,6 @@
{
"bundles": [
- {
+ {
"id": "${project.groupId}:${project.artifactId}:${project.version}",
"start-order": 25
},
@@ -33,4 +33,4 @@
"start-order": 25
}
]
-}
\ No newline at end of file
+}
(sling-org-apache-sling-mcp-server) 22/27: fix(deps): update dependency org.apache.sling:sling to v65
Posted by ro...@apache.org.
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 c3b0e6bcfa191b58962929705fa4be382159be3e
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Dec 23 12:42:33 2025 +0100
fix(deps): update dependency org.apache.sling:sling to v65
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index d286092..68b2502 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
<parent>
<groupId>org.apache.sling</groupId>
<artifactId>sling-bundle-parent</artifactId>
- <version>63</version>
+ <version>65</version>
<relativePath />
</parent>
(sling-org-apache-sling-mcp-server) 12/27: chore(mcp-server): update to mcp SDK 0.17.0
Posted by ro...@apache.org.
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 de1960df4002a857a053bf14a838013395b61b55
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 17:59:06 2025 +0100
chore(mcp-server): update to mcp SDK 0.17.0
We also bump the compile-time dependency in the pom.xml since it transitively brings in a new
major version of the json-schema-validator bundle.
---
pom.xml | 2 +-
src/main/features/main.json | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/pom.xml b/pom.xml
index 722343a..d2c09fb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,7 +97,7 @@
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
- <version>0.15.0</version>
+ <version>0.17.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
diff --git a/src/main/features/main.json b/src/main/features/main.json
index 052bae5..fc49b6f 100644
--- a/src/main/features/main.json
+++ b/src/main/features/main.json
@@ -5,7 +5,7 @@
"start-order": 25
},
{
- "id": "io.modelcontextprotocol.sdk:mcp-core:0.15.0",
+ "id": "io.modelcontextprotocol.sdk:mcp-core:0.17.0",
"start-order": 25
},
{
@@ -17,11 +17,11 @@
"start-order": 25
},
{
- "id": "com.networknt:json-schema-validator:1.5.7",
+ "id": "com.networknt:json-schema-validator:2.0.0",
"start-order": 25
},
{
- "id": "com.ethlo.time:itu:1.10.3",
+ "id": "com.ethlo.time:itu:1.14.0",
"start-order": 25
},
{
(sling-org-apache-sling-mcp-server) 05/27: feat(mcp-server): various improvements
Posted by ro...@apache.org.
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 b199ac0ae1ea75edbd091e5a08c4a5191d3d045e
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Dec 9 11:43:36 2025 +0100
feat(mcp-server): various improvements
- add empty completions for the new-sling-servlet prompt to remove errors in the MCP inspector
about missing completions
- make the name/version/instructions for the MCP configurable and provide sensible defaults
---
.../apache/sling/mcp/server/impl/McpServlet.java | 44 +++++++++++++++++++---
1 file changed, 38 insertions(+), 6 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index 3f62a10..b12faa6 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -33,18 +33,19 @@ import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.json.schema.jackson.DefaultJsonSchemaValidator;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
+import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification;
import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification;
import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpStatelessSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+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.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.Resource;
-import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.Role;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
@@ -65,11 +66,30 @@ 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.Deactivate;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@Component(service = Servlet.class)
@SlingServletPaths(value = {McpServlet.ENDPOINT})
+@Designate(ocd = McpServlet.Config.class)
public class McpServlet extends SlingJakartaAllMethodsServlet {
+ @ObjectClassDefinition(name = "Apache Sling MCP Server Configuration")
+ public @interface Config {
+ @AttributeDefinition(name = "Server Title", description = "The title of the MCP server")
+ String serverTitle() default "Apache Sling";
+
+ @AttributeDefinition(
+ name = "Server Version",
+ description = "The version of the MCP server. Defaults to the bundle version if not set")
+ String serverVersion();
+
+ @AttributeDefinition(name = "Instructions", description = "Initial instructions for the MCP server")
+ String instructions() default
+ "This MCP server provides access to an Apache Sling development instance. Exposed tools and information always reference the Sling deployment and not local projects or files";
+ }
+
static final String ENDPOINT = "/bin/mcp";
private static final long serialVersionUID = 1L;
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
@@ -92,7 +112,7 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
}
@Activate
- public McpServlet(BundleContext ctx) throws IllegalAccessException, NoSuchMethodException {
+ public McpServlet(BundleContext ctx, Config config) throws IllegalAccessException, NoSuchMethodException {
McpJsonMapper jsonMapper = new JacksonMcpJsonMapper(new ObjectMapper());
@@ -115,14 +135,26 @@ public class McpServlet extends SlingJakartaAllMethodsServlet {
java.lang.invoke.MethodType.methodType(
void.class, HttpServletRequest.class, HttpServletResponse.class));
+ SyncCompletionSpecification servletCompletionSpec = new McpStatelessServerFeatures.SyncCompletionSpecification(
+ new McpSchema.PromptReference("ref/prompt", "new-sling-servlet"), (context, request) -> {
+ return new McpSchema.CompleteResult(new CompleteCompletion(List.of(), 0, false));
+ });
+
+ String serverVersion = config.serverVersion();
+ if (serverVersion == null || serverVersion.isEmpty()) {
+ serverVersion = ctx.getBundle().getVersion().toString();
+ }
syncServer = McpServer.sync(transportProvider)
- .serverInfo("apache-sling", "0.1.0")
+ .serverInfo(config.serverTitle(), serverVersion)
.jsonMapper(jsonMapper)
.jsonSchemaValidator(new DefaultJsonSchemaValidator())
+ .instructions(config.instructions())
+ .completions(List.of(servletCompletionSpec))
.capabilities(ServerCapabilities.builder()
- .tools(true)
- .prompts(true)
- .resources(true, true)
+ .tools(false)
+ .prompts(false)
+ .resources(false, false)
+ .completions()
.build())
.build();
(sling-org-apache-sling-mcp-server) 07/27: chore(mcp): minor code cleanups in the BundleResourceContribution
Posted by ro...@apache.org.
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 5feb4210127cc09028eee0da3f3d630a67fb0db4
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 11:21:44 2025 +0100
chore(mcp): minor code cleanups in the BundleResourceContribution
---
.../impl/contribs/BundleResourceContribution.java | 74 +++++++++++++++-------
.../mcp/server/impl/contribs/BundleState.java | 57 +++++++++++++++++
2 files changed, 108 insertions(+), 23 deletions(-)
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
index 605eeb3..fc7ac2a 100644
--- 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
@@ -26,6 +26,7 @@ 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;
@@ -42,17 +43,9 @@ import org.osgi.service.component.annotations.Component;
@Component
public class BundleResourceContribution implements McpServerContribution {
- private static String getStateString(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";
- };
- }
+ 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;
@@ -72,10 +65,8 @@ public class BundleResourceContribution implements McpServerContribution {
.mimeType("text/plain")
.build(),
(context, request) -> {
- String bundleInfo = Stream.of(ctx.getBundles())
- .map(b -> "Bundle " + b.getSymbolicName() + " is in state " + getStateString(b.getState())
- + " (" + b.getState() + ")")
- .collect(Collectors.joining("\n"));
+ String bundleInfo =
+ Stream.of(ctx.getBundles()).map(this::describe).collect(Collectors.joining("\n"));
TextResourceContents contents = new TextResourceContents("bundle://", "text/plain", bundleInfo);
@@ -87,22 +78,59 @@ public class BundleResourceContribution implements McpServerContribution {
public Optional<SyncResourceTemplateSpecification> getSyncResourceTemplateSpecification() {
return Optional.of(new McpStatelessServerFeatures.SyncResourceTemplateSpecification(
new ResourceTemplate.Builder()
- .uriTemplate("bundles://state/{state}")
+ .uriTemplate(RESOURCE_TEMPLATE_BUNDLES_STATE_PATTERN)
.name("bundles")
.build(),
(context, request) -> {
- String bundleInfo = "";
- if ("bundles://state/resolved".equals(request.uri().toLowerCase(Locale.ENGLISH))) {
+ 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 = "";
+ // extract desired state from URI
bundleInfo = Arrays.stream(ctx.getBundles())
- .filter(b -> b.getState() == Bundle.RESOLVED)
- .map(b -> "Bundle " + b.getSymbolicName() + " is in state "
- + getStateString(b.getState()) + " (" + b.getState() + ")")
+ .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)));
}
+ }));
+ }
- TextResourceContents contents = new TextResourceContents(request.uri(), "text/plain", bundleInfo);
+ @Override
+ public Optional<SyncCompletionSpecification> getSyncCompletionSpecification() {
- return new ReadResourceResult(List.of(contents));
+ 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) {
+ return "Bundle " + b.getSymbolicName() + " (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;
+ }
+}
(sling-org-apache-sling-mcp-server) 11/27: chore(mcp-server): formatting, typos
Posted by ro...@apache.org.
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 2ea3c529919091bbcbf1ea0827107196f692449a
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Wed Dec 10 17:53:15 2025 +0100
chore(mcp-server): formatting, typos
---
.../apache/sling/mcp/server/impl/McpServerContribution.java | 2 +-
.../mcp/server/impl/contribs/RecentRequestsContribution.java | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
index ea15572..c32a526 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
@@ -33,7 +33,7 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecifi
//
// Expect this abstraction to change
public interface McpServerContribution {
-
+
default Optional<SyncToolSpecification> getSyncToolSpecification() {
return Optional.empty();
}
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
index 53a127e..5d4f077 100644
--- 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
@@ -23,6 +23,10 @@ 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.impl.McpServerContribution;
@@ -30,11 +34,6 @@ import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
-import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification;
-import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
-import io.modelcontextprotocol.spec.McpSchema.Resource;
-import io.modelcontextprotocol.spec.McpSchema.TextResourceContents;
-
@Component
public class RecentRequestsContribution implements McpServerContribution {
@@ -56,7 +55,8 @@ public class RecentRequestsContribution implements McpServerContribution {
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 authenticaiton, resource resolution, script resolution, nested scripts/servlets and filters.")
+ .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) -> {
(sling-org-apache-sling-mcp-server) 24/27: chore(mcp-server): extract SPI so contributions can be moved to other bundles
Posted by ro...@apache.org.
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 daa8d7ad5f82acaa89b192435773f1d172fd99f7
Author: Robert Munteanu <ro...@apache.org>
AuthorDate: Tue Jan 13 14:57:02 2026 +0100
chore(mcp-server): extract SPI so contributions can be moved to other bundles
---
src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java | 1 +
.../mcp/server/impl/contribs/BundleResourceContribution.java | 2 +-
.../mcp/server/impl/contribs/ComponentResourceContribution.java | 2 +-
.../sling/mcp/server/impl/contribs/LogToolContribution.java | 2 +-
.../server/impl/contribs/OsgiBundleDiagnosticContribution.java | 2 +-
.../server/impl/contribs/OsgiDiagnosticPromptContribution.java | 2 +-
.../mcp/server/impl/contribs/RecentRequestsContribution.java | 2 +-
.../mcp/server/impl/contribs/RefreshPackagesContribution.java | 2 +-
.../sling/mcp/server/impl/contribs/ServletPromptContribution.java | 2 +-
.../sling/mcp/server/{impl => spi}/McpServerContribution.java | 8 +++++++-
src/main/java/org/apache/sling/mcp/server/spi/package-info.java | 2 ++
11 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
index 06bb9c4..316b8a0 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
+++ b/src/main/java/org/apache/sling/mcp/server/impl/McpServlet.java
@@ -41,6 +41,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingJakartaHttpServletRequest;
import org.apache.sling.api.SlingJakartaHttpServletResponse;
import org.apache.sling.api.servlets.SlingJakartaAllMethodsServlet;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.BundleContext;
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
index 833264c..d54e598 100644
--- 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
@@ -34,7 +34,7 @@ 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.impl.McpServerContribution;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.wiring.BundleRevision;
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
index 7a307fd..937ad9d 100644
--- 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
@@ -32,7 +32,7 @@ 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.impl.McpServerContribution;
+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;
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
index 37fe051..8e61147 100644
--- 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
@@ -33,7 +33,7 @@ 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.impl.McpServerContribution;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
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
index 4efaf3d..fdc3be7 100644
--- 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
@@ -29,7 +29,7 @@ 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.impl.McpServerContribution;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
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
index 25ea95b..10e6e38 100644
--- 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
@@ -28,7 +28,7 @@ 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.impl.McpServerContribution;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
import org.osgi.service.component.annotations.Component;
/**
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
index 5d4f077..d4149b2 100644
--- 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
@@ -29,7 +29,7 @@ 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.impl.McpServerContribution;
+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;
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
index c02d808..1e0b9ae 100644
--- 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
@@ -24,7 +24,7 @@ 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.impl.McpServerContribution;
+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;
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
index 3e67666..cb2f181 100644
--- 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
@@ -30,7 +30,7 @@ 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.impl.McpServerContribution;
+import org.apache.sling.mcp.server.spi.McpServerContribution;
import org.osgi.service.component.annotations.Component;
@Component
diff --git a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java b/src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java
similarity index 89%
rename from src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
rename to src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java
index c32a526..58a5d42 100644
--- a/src/main/java/org/apache/sling/mcp/server/impl/McpServerContribution.java
+++ b/src/main/java/org/apache/sling/mcp/server/spi/McpServerContribution.java
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.sling.mcp.server.impl;
+package org.apache.sling.mcp.server.spi;
import java.util.Optional;
@@ -32,6 +32,12 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecifi
// is activated and therefore it does not take into account added/changed/removed contributions
//
// Expect this abstraction to change
+/**
+ * Provisional API for registering MCP server contributions
+ *
+ * <p>This abstraction is needed because the contributions themselves are Java records and cannot
+ * be registered as OSGi services directly.</p>
+ */
public interface McpServerContribution {
default Optional<SyncToolSpecification> getSyncToolSpecification() {
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
new file mode 100644
index 0000000..13b7525
--- /dev/null
+++ b/src/main/java/org/apache/sling/mcp/server/spi/package-info.java
@@ -0,0 +1,2 @@
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.mcp.server.spi;