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 3138029cce60e5c55f6530e237116898d6e437c1 Author: Robert Lazarski <[email protected]> AuthorDate: Sat Apr 11 11:43:03 2026 -1000 Auto-generate MCP inputSchema from Java method parameter types When mcpInputSchema is not set in services.xml, the MCP catalog generator now introspects the service class to find the method matching the operation name and generates a JSON Schema from the request POJO's getter methods. Supported types: int/long -> integer, double/float -> number, boolean -> boolean, String -> string, arrays -> array (including nested arrays like double[][]), List<T> -> array with typed items, POJOs -> object. Precedence: explicit mcpInputSchema in services.xml always wins. Auto-generation is the fallback when no schema is declared. Limitation: requires ServiceClass parameter in services.xml. Spring-bean-only services (SpringBeanName without ServiceClass) cannot be introspected at catalog generation time because the bean class is resolved by Spring at invocation time, not at deployment time. These services still need mcpInputSchema. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- .../apache/axis2/openapi/OpenApiSpecGenerator.java | 133 ++++++++++++++++++++- 1 file changed, 129 insertions(+), 4 deletions(-) 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 33996e5c1a..5c6fbc25ab 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 @@ -587,6 +587,117 @@ public class OpenApiSpecGenerator { return null; } + /** + * Auto-generate a JSON Schema from the Java service method's parameter type. + * + * <p>Looks up the service class, finds the method matching the operation name, + * and introspects the parameter POJO's fields to produce a schema. This is the + * "Option 2" fallback when no explicit {@code mcpInputSchema} is set in + * services.xml. + * + * <p>Supports: primitives (int/long/double/boolean/String), arrays, and + * nested POJOs (one level). Returns null if introspection fails for any reason. + * + * @param service the Axis2 service descriptor + * @param operationName the operation (method) name + * @return an ObjectNode containing the JSON Schema, or null + */ + private com.fasterxml.jackson.databind.node.ObjectNode generateSchemaFromServiceClass( + AxisService service, String operationName) { + try { + String className = getServiceClassName(service); + if (className == null) return null; + + Class<?> serviceClass = Thread.currentThread().getContextClassLoader().loadClass(className); + java.lang.reflect.Method targetMethod = null; + for (java.lang.reflect.Method m : serviceClass.getMethods()) { + if (m.getName().equals(operationName) && m.getParameterCount() == 1) { + targetMethod = m; + break; + } + } + if (targetMethod == null) return null; + + Class<?> paramType = targetMethod.getParameterTypes()[0]; + // Skip primitives and common JDK types — only introspect POJOs + if (paramType.isPrimitive() || paramType == String.class + || paramType.getName().startsWith("java.")) { + return null; + } + + com.fasterxml.jackson.databind.ObjectMapper mapper = io.swagger.v3.core.util.Json.mapper(); + com.fasterxml.jackson.databind.node.ObjectNode schema = mapper.createObjectNode(); + schema.put("type", "object"); + com.fasterxml.jackson.databind.node.ObjectNode properties = schema.putObject("properties"); + com.fasterxml.jackson.databind.node.ArrayNode required = schema.putArray("required"); + + for (java.lang.reflect.Method getter : paramType.getMethods()) { + String name = getter.getName(); + if (!name.startsWith("get") || name.equals("getClass") || getter.getParameterCount() != 0) { + if (name.startsWith("is") && getter.getParameterCount() == 0 + && (getter.getReturnType() == boolean.class || getter.getReturnType() == Boolean.class)) { + // boolean getter: isNormalizeWeights -> normalizeWeights + String fieldName = Character.toLowerCase(name.charAt(2)) + name.substring(3); + com.fasterxml.jackson.databind.node.ObjectNode prop = properties.putObject(fieldName); + prop.put("type", "boolean"); + } + continue; + } + // getWeights -> weights + String fieldName = Character.toLowerCase(name.charAt(3)) + name.substring(4); + Class<?> returnType = getter.getReturnType(); + + com.fasterxml.jackson.databind.node.ObjectNode prop = properties.putObject(fieldName); + mapJavaTypeToJsonSchema(returnType, getter.getGenericReturnType(), prop); + } + + return schema; + } catch (Exception e) { + log.debug("[MCP] Could not auto-generate schema for " + service.getName() + + "/" + operationName + ": " + e.getMessage()); + return null; + } + } + + /** + * Maps a Java type to a JSON Schema type/format in the given ObjectNode. + */ + private void mapJavaTypeToJsonSchema(Class<?> type, java.lang.reflect.Type genericType, + com.fasterxml.jackson.databind.node.ObjectNode prop) { + if (type == int.class || type == Integer.class) { + prop.put("type", "integer"); + } else if (type == long.class || type == Long.class) { + prop.put("type", "integer"); + } else if (type == double.class || type == Double.class || type == float.class || type == Float.class) { + prop.put("type", "number"); + } else if (type == boolean.class || type == Boolean.class) { + prop.put("type", "boolean"); + } else if (type == String.class) { + prop.put("type", "string"); + } else if (type.isArray()) { + prop.put("type", "array"); + com.fasterxml.jackson.databind.node.ObjectNode items = prop.putObject("items"); + Class<?> componentType = type.getComponentType(); + if (componentType.isArray()) { + // double[][] -> array of arrays of numbers + items.put("type", "array"); + com.fasterxml.jackson.databind.node.ObjectNode innerItems = items.putObject("items"); + mapJavaTypeToJsonSchema(componentType.getComponentType(), null, innerItems); + } else { + mapJavaTypeToJsonSchema(componentType, null, items); + } + } else if (java.util.List.class.isAssignableFrom(type) && genericType instanceof java.lang.reflect.ParameterizedType) { + prop.put("type", "array"); + java.lang.reflect.Type[] typeArgs = ((java.lang.reflect.ParameterizedType) genericType).getActualTypeArguments(); + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + com.fasterxml.jackson.databind.node.ObjectNode items = prop.putObject("items"); + mapJavaTypeToJsonSchema((Class<?>) typeArgs[0], null, items); + } + } else { + prop.put("type", "object"); + } + } + /** * Check if a package is included in the configured resource packages. */ @@ -820,11 +931,25 @@ public class OpenApiSpecGenerator { schema.putArray("required"); } } else { + // Option 2: auto-generate schema from Java method parameter type. + // Introspects the service class to find the method matching + // this operation name, then reflects on the request POJO's + // fields to build a JSON Schema. Falls back to empty schema + // if introspection fails (e.g., no ServiceClass parameter, + // method not found, or primitive parameters). com.fasterxml.jackson.databind.node.ObjectNode schema = - toolNode.putObject("inputSchema"); - schema.put("type", "object"); - schema.putObject("properties"); - schema.putArray("required"); + generateSchemaFromServiceClass(service, opName); + if (schema != null) { + toolNode.set("inputSchema", schema); + log.debug("[MCP] Auto-generated inputSchema for " + + service.getName() + "/" + opName + + " from Java type introspection"); + } else { + schema = toolNode.putObject("inputSchema"); + schema.put("type", "object"); + schema.putObject("properties"); + schema.putArray("required"); + } } toolNode.put("endpoint", "POST " + path);
