Skip to content

Commit 3138029

Browse files
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) <noreply@anthropic.com>
1 parent b03e96b commit 3138029

1 file changed

Lines changed: 129 additions & 4 deletions

File tree

modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,117 @@ private String getServiceClassName(AxisService service) {
587587
return null;
588588
}
589589

590+
/**
591+
* Auto-generate a JSON Schema from the Java service method's parameter type.
592+
*
593+
* <p>Looks up the service class, finds the method matching the operation name,
594+
* and introspects the parameter POJO's fields to produce a schema. This is the
595+
* "Option 2" fallback when no explicit {@code mcpInputSchema} is set in
596+
* services.xml.
597+
*
598+
* <p>Supports: primitives (int/long/double/boolean/String), arrays, and
599+
* nested POJOs (one level). Returns null if introspection fails for any reason.
600+
*
601+
* @param service the Axis2 service descriptor
602+
* @param operationName the operation (method) name
603+
* @return an ObjectNode containing the JSON Schema, or null
604+
*/
605+
private com.fasterxml.jackson.databind.node.ObjectNode generateSchemaFromServiceClass(
606+
AxisService service, String operationName) {
607+
try {
608+
String className = getServiceClassName(service);
609+
if (className == null) return null;
610+
611+
Class<?> serviceClass = Thread.currentThread().getContextClassLoader().loadClass(className);
612+
java.lang.reflect.Method targetMethod = null;
613+
for (java.lang.reflect.Method m : serviceClass.getMethods()) {
614+
if (m.getName().equals(operationName) && m.getParameterCount() == 1) {
615+
targetMethod = m;
616+
break;
617+
}
618+
}
619+
if (targetMethod == null) return null;
620+
621+
Class<?> paramType = targetMethod.getParameterTypes()[0];
622+
// Skip primitives and common JDK types — only introspect POJOs
623+
if (paramType.isPrimitive() || paramType == String.class
624+
|| paramType.getName().startsWith("java.")) {
625+
return null;
626+
}
627+
628+
com.fasterxml.jackson.databind.ObjectMapper mapper = io.swagger.v3.core.util.Json.mapper();
629+
com.fasterxml.jackson.databind.node.ObjectNode schema = mapper.createObjectNode();
630+
schema.put("type", "object");
631+
com.fasterxml.jackson.databind.node.ObjectNode properties = schema.putObject("properties");
632+
com.fasterxml.jackson.databind.node.ArrayNode required = schema.putArray("required");
633+
634+
for (java.lang.reflect.Method getter : paramType.getMethods()) {
635+
String name = getter.getName();
636+
if (!name.startsWith("get") || name.equals("getClass") || getter.getParameterCount() != 0) {
637+
if (name.startsWith("is") && getter.getParameterCount() == 0
638+
&& (getter.getReturnType() == boolean.class || getter.getReturnType() == Boolean.class)) {
639+
// boolean getter: isNormalizeWeights -> normalizeWeights
640+
String fieldName = Character.toLowerCase(name.charAt(2)) + name.substring(3);
641+
com.fasterxml.jackson.databind.node.ObjectNode prop = properties.putObject(fieldName);
642+
prop.put("type", "boolean");
643+
}
644+
continue;
645+
}
646+
// getWeights -> weights
647+
String fieldName = Character.toLowerCase(name.charAt(3)) + name.substring(4);
648+
Class<?> returnType = getter.getReturnType();
649+
650+
com.fasterxml.jackson.databind.node.ObjectNode prop = properties.putObject(fieldName);
651+
mapJavaTypeToJsonSchema(returnType, getter.getGenericReturnType(), prop);
652+
}
653+
654+
return schema;
655+
} catch (Exception e) {
656+
log.debug("[MCP] Could not auto-generate schema for " + service.getName()
657+
+ "/" + operationName + ": " + e.getMessage());
658+
return null;
659+
}
660+
}
661+
662+
/**
663+
* Maps a Java type to a JSON Schema type/format in the given ObjectNode.
664+
*/
665+
private void mapJavaTypeToJsonSchema(Class<?> type, java.lang.reflect.Type genericType,
666+
com.fasterxml.jackson.databind.node.ObjectNode prop) {
667+
if (type == int.class || type == Integer.class) {
668+
prop.put("type", "integer");
669+
} else if (type == long.class || type == Long.class) {
670+
prop.put("type", "integer");
671+
} else if (type == double.class || type == Double.class || type == float.class || type == Float.class) {
672+
prop.put("type", "number");
673+
} else if (type == boolean.class || type == Boolean.class) {
674+
prop.put("type", "boolean");
675+
} else if (type == String.class) {
676+
prop.put("type", "string");
677+
} else if (type.isArray()) {
678+
prop.put("type", "array");
679+
com.fasterxml.jackson.databind.node.ObjectNode items = prop.putObject("items");
680+
Class<?> componentType = type.getComponentType();
681+
if (componentType.isArray()) {
682+
// double[][] -> array of arrays of numbers
683+
items.put("type", "array");
684+
com.fasterxml.jackson.databind.node.ObjectNode innerItems = items.putObject("items");
685+
mapJavaTypeToJsonSchema(componentType.getComponentType(), null, innerItems);
686+
} else {
687+
mapJavaTypeToJsonSchema(componentType, null, items);
688+
}
689+
} else if (java.util.List.class.isAssignableFrom(type) && genericType instanceof java.lang.reflect.ParameterizedType) {
690+
prop.put("type", "array");
691+
java.lang.reflect.Type[] typeArgs = ((java.lang.reflect.ParameterizedType) genericType).getActualTypeArguments();
692+
if (typeArgs.length > 0 && typeArgs[0] instanceof Class) {
693+
com.fasterxml.jackson.databind.node.ObjectNode items = prop.putObject("items");
694+
mapJavaTypeToJsonSchema((Class<?>) typeArgs[0], null, items);
695+
}
696+
} else {
697+
prop.put("type", "object");
698+
}
699+
}
700+
590701
/**
591702
* Check if a package is included in the configured resource packages.
592703
*/
@@ -820,11 +931,25 @@ public String generateMcpCatalogJson(HttpServletRequest request) {
820931
schema.putArray("required");
821932
}
822933
} else {
934+
// Option 2: auto-generate schema from Java method parameter type.
935+
// Introspects the service class to find the method matching
936+
// this operation name, then reflects on the request POJO's
937+
// fields to build a JSON Schema. Falls back to empty schema
938+
// if introspection fails (e.g., no ServiceClass parameter,
939+
// method not found, or primitive parameters).
823940
com.fasterxml.jackson.databind.node.ObjectNode schema =
824-
toolNode.putObject("inputSchema");
825-
schema.put("type", "object");
826-
schema.putObject("properties");
827-
schema.putArray("required");
941+
generateSchemaFromServiceClass(service, opName);
942+
if (schema != null) {
943+
toolNode.set("inputSchema", schema);
944+
log.debug("[MCP] Auto-generated inputSchema for "
945+
+ service.getName() + "/" + opName
946+
+ " from Java type introspection");
947+
} else {
948+
schema = toolNode.putObject("inputSchema");
949+
schema.put("type", "object");
950+
schema.putObject("properties");
951+
schema.putArray("required");
952+
}
828953
}
829954

830955
toolNode.put("endpoint", "POST " + path);

0 commit comments

Comments
 (0)