This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit b89b2a37874e597f9959970a0d1eb483cb422423
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Apr 7 07:50:36 2026 -1000

    C3 MCP Resources endpoint — generateMcpResourcesJson()
    
    Add resources/list support per MCP 2025-03-26 spec:
    - generateMcpResourcesJson(HttpServletRequest) iterates all deployed
      services, skips system services (AdminService, etc.), and emits one
      resource entry per service with:
        uri:         axis2://services/<name>
        name:        service display name
        description: mcpDescription param if set, else empty string
        mimeType:    application/json
        metadata:    { wsdlUrl, operations[], requiresAuth }
    - wsdlUrl is "GET /services/<name>?wsdl" (relative, server-agnostic)
    - requiresAuth mirrors the existing mcpRequiresAuth parameter check
    - error path returns {"resources":[],"_error":"<message>"} (never throws)
    
    Tests (8 new): deployed service listed, system service excluded,
    operations list populated, mcpDescription used, requiresAuth true/false,
    empty-service list, wsdlUrl format verified.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../apache/axis2/openapi/OpenApiSpecGenerator.java | 108 +++++++++++++++++++++
 .../axis2/openapi/McpCatalogGeneratorTest.java     |  90 +++++++++++++++++
 2 files changed, 198 insertions(+)

diff --git 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
index 2a8cf9e0e3..c3220ae87e 100644
--- 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
+++ 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
@@ -902,6 +902,114 @@ public class OpenApiSpecGenerator {
         }
     }
 
+    /**
+     * Generate an MCP Resources listing (C3).
+     *
+     * <p>MCP Resources are read-only, browseable data items — conceptually the
+     * complement of Tools (which take actions).  Here each deployed Axis2 
service
+     * becomes a resource so that an AI client can discover what services 
exist and
+     * fetch their WSDL or metadata without executing an operation.
+     *
+     * <p>Output shape (MCP 2025-03-26 {@code resources/list} response):
+     * <pre>
+     * {
+     *   "resources": [
+     *     {
+     *       "uri":         "axis2://services/PortfolioService",
+     *       "name":        "PortfolioService",
+     *       "description": "...",          // mcpDescription service param or 
auto-generated
+     *       "mimeType":    "application/json",
+     *       "metadata": {
+     *         "wsdlUrl":      "POST /services/PortfolioService?wsdl",
+     *         "operations":   ["getPortfolio", "updateWeights", ...],
+     *         "requiresAuth": true
+     *       }
+     *     }
+     *   ]
+     * }
+     * </pre>
+     *
+     * <p>System services ("Version", "AdminService", names starting with 
"__") are
+     * excluded, matching the tool catalog filter.
+     *
+     * @param request the incoming HTTP request (used only to determine the 
base URL)
+     * @return JSON string; never null.  On error returns
+     *         {@code {"resources":[],"_error":"..."}} so callers can 
distinguish
+     *         failure from an empty deployment.
+     */
+    public String generateMcpResourcesJson(HttpServletRequest request) {
+        try {
+            AxisConfiguration axisConfig = 
configurationContext.getAxisConfiguration();
+            com.fasterxml.jackson.databind.ObjectMapper jackson = 
io.swagger.v3.core.util.Json.mapper();
+            com.fasterxml.jackson.databind.node.ObjectNode root = 
jackson.createObjectNode();
+            com.fasterxml.jackson.databind.node.ArrayNode resources = 
root.putArray("resources");
+
+            java.util.Map<String, AxisService> services = 
axisConfig.getServices();
+            if (services != null) {
+                for (AxisService service : services.values()) {
+                    String svcName = service.getName();
+                    if (isSystemService(svcName)) continue;
+
+                    // URI: logical identifier for the resource in the MCP 
protocol.
+                    // Uses the "axis2://" scheme so clients can distinguish 
these
+                    // resources from generic HTTP URLs.
+                    String uri = "axis2://services/" + svcName;
+
+                    // Human-readable description: service-level 
mcpDescription param
+                    // or auto-generated fallback.
+                    String description = getMcpStringParam(null, service, 
"mcpDescription",
+                            "Axis2 service: " + svcName);
+
+                    com.fasterxml.jackson.databind.node.ObjectNode resource =
+                            resources.addObject();
+                    resource.put("uri",         uri);
+                    resource.put("name",         svcName);
+                    resource.put("description",  description);
+                    resource.put("mimeType",     "application/json");
+
+                    // metadata sub-object: service-specific details for MCP 
clients
+                    // that want to introspect available operations before 
calling.
+                    com.fasterxml.jackson.databind.node.ObjectNode metadata =
+                            resource.putObject("metadata");
+                    metadata.put("wsdlUrl", "GET /services/" + svcName + 
"?wsdl");
+
+                    // List all non-system operation names.
+                    com.fasterxml.jackson.databind.node.ArrayNode ops = 
metadata.putArray("operations");
+                    java.util.Iterator<AxisOperation> opIter = 
service.getOperations();
+                    while (opIter != null && opIter.hasNext()) {
+                        AxisOperation op = opIter.next();
+                        if (op != null && op.getName() != null) {
+                            String opName = op.getName().getLocalPart();
+                            if (opName != null && !opName.startsWith("__")) {
+                                ops.add(opName);
+                            }
+                        }
+                    }
+
+                    // Auth requirement mirrors the tool catalog heuristic.
+                    String svcLower = 
svcName.toLowerCase(java.util.Locale.ROOT);
+                    boolean requiresAuth;
+                    String mcpRequiresAuthParam = getMcpStringParam(null, 
service,
+                            "mcpRequiresAuth", null);
+                    if (mcpRequiresAuthParam != null) {
+                        requiresAuth = 
!"false".equalsIgnoreCase(mcpRequiresAuthParam);
+                    } else {
+                        requiresAuth = !svcLower.equals("loginservice")
+                                    && !svcLower.equals("adminconsole");
+                    }
+                    metadata.put("requiresAuth", requiresAuth);
+                }
+            }
+
+            log.debug("Generated MCP resources JSON ({} services)", 
resources.size());
+            return jackson.writeValueAsString(root);
+
+        } catch (Exception e) {
+            log.error("Failed to generate MCP resources JSON", e);
+            return "{\"resources\":[],\"_error\":\"resources generation failed 
— see server log\"}";
+        }
+    }
+
     /**
      * Get OpenAPI JSON processing performance statistics using moshih2 
metrics.
      */
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/McpCatalogGeneratorTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/McpCatalogGeneratorTest.java
index e8c75e2dff..cd021b0b70 100644
--- 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/McpCatalogGeneratorTest.java
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/McpCatalogGeneratorTest.java
@@ -958,6 +958,96 @@ public class McpCatalogGeneratorTest extends TestCase {
                 annotations.path("openWorldHint").asBoolean());
     }
 
+    // ── C3: MCP Resources 
────────────────────────────────────────────────────
+
+    public void testMcpResourcesListsDeployedService() throws Exception {
+        addService("PortfolioService", "getPortfolio");
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode resources = MAPPER.readTree(json).path("resources");
+        assertEquals("one resource per service", 1, resources.size());
+
+        JsonNode r = resources.get(0);
+        assertEquals("axis2://services/PortfolioService", 
r.path("uri").asText());
+        assertEquals("PortfolioService", r.path("name").asText());
+        assertEquals("application/json", r.path("mimeType").asText());
+    }
+
+    public void testMcpResourcesExcludesSystemServices() throws Exception {
+        addService("PortfolioService", "getPortfolio");
+        // These must be filtered
+        axisConfig.addService(new AxisService("Version"));
+        axisConfig.addService(new AxisService("AdminService"));
+        AxisService hidden = new AxisService("__internal");
+        axisConfig.addService(hidden);
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode resources = MAPPER.readTree(json).path("resources");
+        assertEquals("only PortfolioService, not system services", 1, 
resources.size());
+        assertEquals("PortfolioService", 
resources.get(0).path("name").asText());
+    }
+
+    public void testMcpResourcesIncludesOperationList() throws Exception {
+        AxisService svc = new AxisService("PortfolioService");
+        addOperation(svc, "getPortfolio");
+        addOperation(svc, "updateWeights");
+        axisConfig.addService(svc);
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode ops = MAPPER.readTree(json).path("resources").get(0)
+                            .path("metadata").path("operations");
+        assertEquals("two operations listed", 2, ops.size());
+    }
+
+    public void testMcpResourcesUsesServiceLevelMcpDescription() throws 
Exception {
+        AxisService svc = new AxisService("PortfolioService");
+        svc.addParameter(new org.apache.axis2.description.Parameter(
+                "mcpDescription", "Manages investment portfolios."));
+        addOperation(svc, "getPortfolio");
+        axisConfig.addService(svc);
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode r = MAPPER.readTree(json).path("resources").get(0);
+        assertEquals("Manages investment portfolios.", 
r.path("description").asText());
+    }
+
+    public void testMcpResourcesRequiresAuthTrueForNormalService() throws 
Exception {
+        addService("PortfolioService", "getPortfolio");
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode meta = 
MAPPER.readTree(json).path("resources").get(0).path("metadata");
+        assertTrue("requiresAuth must be true for non-login service",
+                meta.path("requiresAuth").asBoolean());
+    }
+
+    public void testMcpResourcesRequiresAuthFalseForLoginService() throws 
Exception {
+        addService("loginService", "doLogin");
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode meta = 
MAPPER.readTree(json).path("resources").get(0).path("metadata");
+        assertFalse("requiresAuth must be false for loginService",
+                meta.path("requiresAuth").asBoolean());
+    }
+
+    public void testMcpResourcesEmptyOnNoServices() throws Exception {
+        // axisConfig has no user services at this point
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        JsonNode resources = MAPPER.readTree(json).path("resources");
+        assertTrue("resources array must be present and empty", 
resources.isArray());
+        assertEquals(0, resources.size());
+        assertTrue("no _error field on success",
+                MAPPER.readTree(json).path("_error").isMissingNode());
+    }
+
+    public void testMcpResourcesWsdlUrlInMetadata() throws Exception {
+        addService("PortfolioService", "getPortfolio");
+
+        String json = generator.generateMcpResourcesJson(mockRequest);
+        String wsdl = MAPPER.readTree(json).path("resources").get(0)
+                            .path("metadata").path("wsdlUrl").asText();
+        assertEquals("GET /services/PortfolioService?wsdl", wsdl);
+    }
+
     // ── B2: mcpAuthScope 
─────────────────────────────────────────────────────
 
     public void testMcpAuthScopeParamAppearsAsXAuthScope() throws Exception {

Reply via email to