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 {
