Copilot commented on code in PR #15406: URL: https://github.com/apache/dubbo/pull/15406#discussion_r2264619647
########## dubbo-plugin/dubbo-mcp/src/main/java/org/apache/dubbo/mcp/core/McpServiceFilter.java: ########## @@ -0,0 +1,383 @@ +/* + * 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.dubbo.mcp.core; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.config.Configuration; +import org.apache.dubbo.common.config.ConfigurationUtils; +import org.apache.dubbo.common.constants.LoggerCodeConstants; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.config.annotation.DubboService; +import org.apache.dubbo.mcp.McpConstant; +import org.apache.dubbo.mcp.annotations.McpTool; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ProviderModel; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class McpServiceFilter { + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(McpServiceFilter.class); + + private final Configuration configuration; + private final Pattern[] includePatterns; + private final Pattern[] excludePatterns; + private final boolean defaultEnabled; + + public McpServiceFilter(ApplicationModel applicationModel) { + this.configuration = ConfigurationUtils.getGlobalConfiguration(applicationModel); + this.defaultEnabled = configuration.getBoolean(McpConstant.SETTINGS_MCP_DEFAULT_ENABLED, true); + + String includeStr = configuration.getString(McpConstant.SETTINGS_MCP_INCLUDE_PATTERNS, ""); + String excludeStr = configuration.getString(McpConstant.SETTINGS_MCP_EXCLUDE_PATTERNS, ""); + + this.includePatterns = parsePatterns(includeStr); + this.excludePatterns = parsePatterns(excludeStr); + } + + /** + * Check if service should be exposed as MCP tool. + * Priority: URL Parameters > Annotations > Configuration File > Default + */ + public boolean shouldExposeAsMcpTool(ProviderModel providerModel) { + String interfaceName = providerModel.getServiceModel().getInterfaceName(); + + if (isMatchedByPatterns(interfaceName, excludePatterns)) { + return false; + } + + URL serviceUrl = getServiceUrl(providerModel); + if (serviceUrl != null) { + String urlValue = serviceUrl.getParameter(McpConstant.PARAM_MCP_ENABLED); + if (urlValue != null && StringUtils.isNotEmpty(urlValue)) { + return Boolean.parseBoolean(urlValue); + } + } + + Object serviceBean = providerModel.getServiceInstance(); + if (serviceBean != null) { + DubboService dubboService = serviceBean.getClass().getAnnotation(DubboService.class); + if (dubboService != null && dubboService.mcpEnabled()) { + return true; + } Review Comment: The code references `dubboService.mcpEnabled()` method on the DubboService annotation, but this method does not appear to exist in the standard Dubbo annotation interface. This will cause a compilation error. ```suggestion // Annotation-based MCP enablement is not supported via DubboService ``` ########## dubbo-plugin/dubbo-mcp/src/main/java/org/apache/dubbo/mcp/core/McpSseServiceImpl.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.dubbo.mcp.core; + +import org.apache.dubbo.common.resource.Disposable; +import org.apache.dubbo.common.stream.StreamObserver; +import org.apache.dubbo.mcp.transport.DubboMcpSseTransportProvider; +import org.apache.dubbo.remoting.http12.message.ServerSentEvent; + +public class McpSseServiceImpl implements McpSseService, Disposable { + + private DubboMcpSseTransportProvider transportProvider = getTransportProvider(); Review Comment: The transportProvider field is initialized during field declaration and then conditionally reassigned. This could lead to unnecessary object creation. Consider initializing it as null and using lazy initialization consistently. ```suggestion private DubboMcpSseTransportProvider transportProvider = null; ``` ########## dubbo-plugin/dubbo-mcp/src/main/java/org/apache/dubbo/mcp/tool/DubboServiceToolRegistry.java: ########## @@ -0,0 +1,469 @@ +/* + * 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.dubbo.mcp.tool; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.constants.LoggerCodeConstants; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.mcp.JsonSchemaType; +import org.apache.dubbo.mcp.McpConstant; +import org.apache.dubbo.mcp.annotations.McpToolParam; +import org.apache.dubbo.mcp.core.McpServiceFilter; +import org.apache.dubbo.mcp.util.TypeSchemaUtils; +import org.apache.dubbo.rpc.model.ProviderModel; +import org.apache.dubbo.rpc.model.ServiceDescriptor; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.openapi.model.Operation; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpAsyncServerExchange; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import reactor.core.publisher.Mono; + +public class DubboServiceToolRegistry { + + private static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(DubboServiceToolRegistry.class); + + private final McpAsyncServer mcpServer; + private final DubboOpenApiToolConverter toolConverter; + private final DubboMcpGenericCaller genericCaller; + private final McpServiceFilter mcpServiceFilter; + private final Map<String, McpServerFeatures.AsyncToolSpecification> registeredTools = new ConcurrentHashMap<>(); + private final Map<String, Set<String>> serviceToToolsMapping = new ConcurrentHashMap<>(); + private final ObjectMapper objectMapper; + + public DubboServiceToolRegistry( + McpAsyncServer mcpServer, + DubboOpenApiToolConverter toolConverter, + DubboMcpGenericCaller genericCaller, + McpServiceFilter mcpServiceFilter) { + this.mcpServer = mcpServer; + this.toolConverter = toolConverter; + this.genericCaller = genericCaller; + this.mcpServiceFilter = mcpServiceFilter; + this.objectMapper = new ObjectMapper(); + } + + public int registerService(ProviderModel providerModel) { + ServiceDescriptor serviceDescriptor = providerModel.getServiceModel(); + List<URL> statedURLs = providerModel.getServiceUrls(); + + if (statedURLs == null || statedURLs.isEmpty()) { + return 0; + } + + try { + URL url = statedURLs.get(0); + int registeredCount = 0; + String serviceKey = getServiceKey(providerModel); + Set<String> toolNames = new HashSet<>(); + + Class<?> serviceInterface = serviceDescriptor.getServiceInterfaceClass(); + if (serviceInterface == null) { + return 0; + } + + Method[] methods = serviceInterface.getDeclaredMethods(); + boolean shouldRegisterServiceLevel = mcpServiceFilter.shouldExposeAsMcpTool(providerModel); + + for (Method method : methods) { + if (mcpServiceFilter.shouldExposeMethodAsMcpTool(providerModel, method)) { + McpServiceFilter.McpToolConfig toolConfig = + mcpServiceFilter.getMcpToolConfig(providerModel, method); + + String toolName = registerMethodAsTool(providerModel, method, url, toolConfig); + if (toolName != null) { + toolNames.add(toolName); + registeredCount++; + } + } + } + + if (registeredCount == 0 && shouldRegisterServiceLevel) { + Set<String> serviceToolNames = registerServiceLevelTools(providerModel, url); + toolNames.addAll(serviceToolNames); + registeredCount = serviceToolNames.size(); + } + + if (registeredCount > 0) { + serviceToToolsMapping.put(serviceKey, toolNames); + logger.info( + "Registered {} MCP tools for service: {}", + registeredCount, + serviceDescriptor.getInterfaceName()); + } + + return registeredCount; + + } catch (Exception e) { + logger.error( + LoggerCodeConstants.COMMON_UNEXPECTED_EXCEPTION, + "", + "", + "Failed to register service as MCP tools: " + serviceDescriptor.getInterfaceName(), + e); + return 0; + } + } + + public void unregisterService(ProviderModel providerModel) { + String serviceKey = getServiceKey(providerModel); + Set<String> toolNames = serviceToToolsMapping.remove(serviceKey); + + if (toolNames == null || toolNames.isEmpty()) { + return; + } + + int unregisteredCount = 0; + for (String toolName : toolNames) { + try { + McpServerFeatures.AsyncToolSpecification toolSpec = registeredTools.remove(toolName); + if (toolSpec != null) { + mcpServer.removeTool(toolName).block(); + unregisteredCount++; + } + } catch (Exception e) { + logger.error( + LoggerCodeConstants.COMMON_UNEXPECTED_EXCEPTION, + "", + "", + "Failed to unregister MCP tool: " + toolName, + e); + } + } + + if (unregisteredCount > 0) { + logger.info( + "Unregistered {} MCP tools for service: {}", + unregisteredCount, + providerModel.getServiceModel().getInterfaceName()); + } + } + + private String getServiceKey(ProviderModel providerModel) { + return providerModel.getServiceKey(); + } + + private String registerMethodAsTool( + ProviderModel providerModel, Method method, URL url, McpServiceFilter.McpToolConfig toolConfig) { + try { + String toolName = toolConfig.getToolName(); + if (toolName == null || toolName.isEmpty()) { + toolName = method.getName(); + } + + if (registeredTools.containsKey(toolName)) { + return null; + } + + String description = toolConfig.getDescription(); + if (description == null || description.isEmpty()) { + description = generateDefaultDescription(method, providerModel); + } + + McpSchema.Tool mcpTool = new McpSchema.Tool(toolName, description, generateToolSchema(method)); Review Comment: The generateToolSchema method is called synchronously during tool registration, which could impact performance when registering many services. Consider caching schema generation results or making it asynchronous. ```suggestion McpSchema.ToolSchema toolSchema = toolSchemaCache.computeIfAbsent(method, this::generateToolSchema); McpSchema.Tool mcpTool = new McpSchema.Tool(toolName, description, toolSchema); ``` ########## dubbo-plugin/dubbo-mcp/src/main/java/org/apache/dubbo/mcp/transport/DubboMcpStreamableTransportProvider.java: ########## @@ -0,0 +1,499 @@ +/* + * 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.dubbo.mcp.transport; + +import org.apache.dubbo.cache.support.expiring.ExpiringMap; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.stream.StreamObserver; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.IOUtils; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.remoting.http12.HttpMethods; +import org.apache.dubbo.remoting.http12.HttpRequest; +import org.apache.dubbo.remoting.http12.HttpResponse; +import org.apache.dubbo.remoting.http12.HttpResult; +import org.apache.dubbo.remoting.http12.HttpStatus; +import org.apache.dubbo.remoting.http12.HttpUtils; +import org.apache.dubbo.remoting.http12.message.MediaType; +import org.apache.dubbo.remoting.http12.message.ServerSentEvent; +import org.apache.dubbo.rpc.RpcContext; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpError; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpStreamableServerSession; +import io.modelcontextprotocol.spec.McpStreamableServerSession.Factory; +import io.modelcontextprotocol.spec.McpStreamableServerTransport; +import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.COMMON_UNEXPECTED_EXCEPTION; + +/** + * Implementation of {@link McpStreamableServerTransportProvider} for the Dubbo MCP transport. + * This class provides methods to manage streamable server sessions and notify clients. + */ +public class DubboMcpStreamableTransportProvider implements McpStreamableServerTransportProvider { + + private static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(DubboMcpStreamableTransportProvider.class); + + private Factory sessionFactory; + + private final ObjectMapper objectMapper; + + public static final String SESSION_ID_HEADER = "mcp-session-id"; + + private final ExpiringMap<String, McpStreamableServerSession> sessions = new ExpiringMap<>(30 * 60, 30); + Review Comment: The hardcoded values for session expiration (30 * 60 seconds and 30 cleanup period) should be made configurable through constants or configuration parameters for better maintainability. ```suggestion private static final int SESSION_EXPIRATION_SECONDS = 30 * 60; // 30 minutes private static final int SESSION_CLEANUP_PERIOD_SECONDS = 30; // 30 seconds private final ExpiringMap<String, McpStreamableServerSession> sessions = new ExpiringMap<>(SESSION_EXPIRATION_SECONDS, SESSION_CLEANUP_PERIOD_SECONDS); ``` ########## dubbo-plugin/dubbo-mcp/src/main/java/org/apache/dubbo/mcp/tool/DubboMcpGenericCaller.java: ########## @@ -0,0 +1,127 @@ +/* + * 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.dubbo.mcp.tool; + +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.config.ApplicationConfig; +import org.apache.dubbo.config.ReferenceConfig; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.service.GenericService; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.COMMON_UNEXPECTED_EXCEPTION; + +public class DubboMcpGenericCaller { + + private static final ErrorTypeAwareLogger logger = + LoggerFactory.getErrorTypeAwareLogger(DubboMcpGenericCaller.class); + + private final ApplicationConfig applicationConfig; + + private final Map<String, GenericService> serviceCache = new ConcurrentHashMap<>(); + + public DubboMcpGenericCaller(ApplicationModel applicationModel) { + if (applicationModel == null) { + logger.error( + COMMON_UNEXPECTED_EXCEPTION, "", "", "ApplicationModel cannot be null for DubboMcpGenericCaller."); + throw new IllegalArgumentException("ApplicationModel cannot be null."); + } + this.applicationConfig = applicationModel.getCurrentConfig(); + if (this.applicationConfig == null) { + + String errMsg = "ApplicationConfig is null in the provided ApplicationModel. Application Name: " + + (applicationModel.getApplicationName() != null ? applicationModel.getApplicationName() : "N/A"); + logger.error(COMMON_UNEXPECTED_EXCEPTION, "", "", errMsg); + throw new IllegalStateException(errMsg); + } + } + + public Object execute( + String interfaceName, + String methodName, + List<String> orderedJavaParameterNames, + Class<?>[] parameterJavaTypes, + Map<String, Object> mcpProvidedParameters, + String group, + String version) { + String cacheKey = interfaceName + ":" + (group == null ? "" : group) + ":" + (version == null ? "" : version); + GenericService genericService = serviceCache.get(cacheKey); + if (genericService == null) { + ReferenceConfig<GenericService> reference = new ReferenceConfig<>(); + reference.setApplication(this.applicationConfig); + reference.setInterface(interfaceName); + reference.setGeneric("true"); // Defaults to 'bean' or 'true' for POJO generalization. + reference.setScope("local"); Review Comment: The hardcoded scope value 'local' should be made configurable or documented as a constant to explain why local scope is always used for MCP tool calls. ```suggestion reference.setScope(MCP_TOOL_SCOPE); ``` ########## dubbo-plugin/dubbo-mcp/src/main/java/org/apache/dubbo/mcp/util/TypeSchemaUtils.java: ########## @@ -0,0 +1,378 @@ +/* + * 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.dubbo.mcp.util; + +import org.apache.dubbo.mcp.JsonSchemaType; +import org.apache.dubbo.mcp.McpConstant; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class TypeSchemaUtils { + + public static TypeSchemaInfo resolveTypeSchema(Class<?> javaType, Type genericType, String description) { + TypeSchemaInfo.Builder builder = + TypeSchemaInfo.builder().description(description).javaType(javaType.getName()); + + if (isPrimitiveType(javaType)) { + return builder.type(getPrimitiveJsonSchemaType(javaType)) + .format(getFormatForType(javaType)) + .build(); + } + + if (javaType.isArray()) { + TypeSchemaInfo itemSchema = + resolveTypeSchema(javaType.getComponentType(), javaType.getComponentType(), "Array item"); + return builder.type(JsonSchemaType.ARRAY_SCHEMA.getJsonSchemaType()) + .items(itemSchema) + .build(); + } + + if (genericType instanceof ParameterizedType) { + return resolveParameterizedType((ParameterizedType) genericType, javaType, description, builder); + } + + if (java.util.Collection.class.isAssignableFrom(javaType)) { + return builder.type(JsonSchemaType.ARRAY_SCHEMA.getJsonSchemaType()) + .items(TypeSchemaInfo.builder() + .type(JsonSchemaType.STRING_SCHEMA.getJsonSchemaType()) + .description("Collection item") + .build()) + .build(); + } + + if (java.util.Map.class.isAssignableFrom(javaType)) { + return builder.type(JsonSchemaType.OBJECT_SCHEMA.getJsonSchemaType()) + .additionalProperties(TypeSchemaInfo.builder() + .type(JsonSchemaType.STRING_SCHEMA.getJsonSchemaType()) + .description("Map value") + .build()) + .build(); + } + + if (javaType.isEnum()) { + Object[] enumConstants = javaType.getEnumConstants(); + List<String> enumValues = new ArrayList<>(); + if (enumConstants != null) { + for (Object enumConstant : enumConstants) { + enumValues.add(enumConstant.toString()); + } + } + return builder.type(JsonSchemaType.STRING_SCHEMA.getJsonSchemaType()) + .enumValues(enumValues) + .build(); + } + + if (isDateTimeType(javaType)) { + return builder.type(JsonSchemaType.STRING_SCHEMA.getJsonSchemaType()) + .format(getDateTimeFormat(javaType)) + .build(); + } + + return builder.type(JsonSchemaType.OBJECT_SCHEMA.getJsonSchemaType()) + .description( + description != null + ? description + " (POJO type: " + javaType.getSimpleName() + ")" + : "Complex object of type " + javaType.getSimpleName()) + .build(); + } + + private static TypeSchemaInfo resolveParameterizedType( + ParameterizedType paramType, Class<?> rawType, String description, TypeSchemaInfo.Builder builder) { + + Type[] typeArgs = paramType.getActualTypeArguments(); + + if (java.util.Collection.class.isAssignableFrom(rawType)) { + TypeSchemaInfo itemSchema; + if (typeArgs.length > 0) { + Class<?> itemType = getClassFromType(typeArgs[0]); + itemSchema = resolveTypeSchema(itemType, typeArgs[0], "Collection item"); + } else { + itemSchema = TypeSchemaInfo.builder() + .type(JsonSchemaType.STRING_SCHEMA.getJsonSchemaType()) + .description("Collection item") + .build(); + } + return builder.type(JsonSchemaType.ARRAY_SCHEMA.getJsonSchemaType()) + .items(itemSchema) + .build(); + } + + if (java.util.Map.class.isAssignableFrom(rawType)) { + TypeSchemaInfo valueSchema; + if (typeArgs.length > 1) { + Class<?> valueType = getClassFromType(typeArgs[1]); + valueSchema = resolveTypeSchema(valueType, typeArgs[1], "Map value"); + } else { + valueSchema = TypeSchemaInfo.builder() + .type(JsonSchemaType.STRING_SCHEMA.getJsonSchemaType()) + .description("Map value") + .build(); + } + return builder.type(JsonSchemaType.OBJECT_SCHEMA.getJsonSchemaType()) + .additionalProperties(valueSchema) + .build(); + } + + return builder.type(JsonSchemaType.OBJECT_SCHEMA.getJsonSchemaType()) + .description( + description != null + ? description + " (Generic type: " + rawType.getSimpleName() + ")" + : "Complex generic object of type " + rawType.getSimpleName()) + .build(); + } + + public static Map<String, Object> toSchemaMap(TypeSchemaInfo schemaInfo) { + Map<String, Object> schemaMap = new HashMap<>(); + + schemaMap.put(McpConstant.SCHEMA_PROPERTY_TYPE, schemaInfo.getType()); + + if (schemaInfo.getFormat() != null) { + schemaMap.put(McpConstant.SCHEMA_PROPERTY_FORMAT, schemaInfo.getFormat()); + } + + if (schemaInfo.getDescription() != null) { + schemaMap.put(McpConstant.SCHEMA_PROPERTY_DESCRIPTION, schemaInfo.getDescription()); + } + + if (schemaInfo.getEnumValues() != null && !schemaInfo.getEnumValues().isEmpty()) { + schemaMap.put(McpConstant.SCHEMA_PROPERTY_ENUM, schemaInfo.getEnumValues()); + } + + if (schemaInfo.getItems() != null) { + schemaMap.put(McpConstant.SCHEMA_PROPERTY_ITEMS, toSchemaMap(schemaInfo.getItems())); + } + + if (schemaInfo.getAdditionalProperties() != null) { + schemaMap.put( + McpConstant.SCHEMA_PROPERTY_ADDITIONAL_PROPERTIES, + toSchemaMap(schemaInfo.getAdditionalProperties())); + } + + return schemaMap; + } + + public static TypeSchemaInfo resolveNestedType(Type type, String description) { + if (type instanceof Class) { + return resolveTypeSchema((Class<?>) type, type, description); + } + + if (type instanceof ParameterizedType paramType) { + Class<?> rawType = (Class<?>) paramType.getRawType(); + return resolveTypeSchema(rawType, type, description); + } + + if (type instanceof TypeVariable) { + Type[] bounds = ((TypeVariable<?>) type).getBounds(); + if (bounds.length > 0) { + return resolveNestedType(bounds[0], description); + } + } + + if (type instanceof WildcardType) { + Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + if (upperBounds.length > 0) { + return resolveNestedType(upperBounds[0], description); + } + } + + if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + TypeSchemaInfo itemSchema = resolveNestedType(componentType, "Array item"); + return TypeSchemaInfo.builder() + .type(JsonSchemaType.ARRAY_SCHEMA.getJsonSchemaType()) + .items(itemSchema) + .description(description) + .build(); + } + + // Fallback to object type + return TypeSchemaInfo.builder() + .type(JsonSchemaType.OBJECT_SCHEMA.getJsonSchemaType()) + .description(description != null ? description : "Unknown type") + .build(); + } + + public static boolean isPrimitiveType(Class<?> type) { + return JsonSchemaType.fromJavaType(type) != null; + } + + public static String getPrimitiveJsonSchemaType(Class<?> javaType) { + JsonSchemaType mapping = JsonSchemaType.fromJavaType(javaType); + return mapping != null ? mapping.getJsonSchemaType() : JsonSchemaType.STRING_SCHEMA.getJsonSchemaType(); + } + + public static String getFormatForType(Class<?> javaType) { + JsonSchemaType mapping = JsonSchemaType.fromJavaType(javaType); + return mapping != null ? mapping.getJsonSchemaFormat() : null; + } + + public static boolean isDateTimeType(Class<?> type) { + return java.util.Date.class.isAssignableFrom(type) + || java.time.temporal.Temporal.class.isAssignableFrom(type) + || java.util.Calendar.class.isAssignableFrom(type); + } + + public static String getDateTimeFormat(Class<?> type) { + if (java.time.LocalDate.class.isAssignableFrom(type)) { + return JsonSchemaType.DATE_FORMAT.getJsonSchemaFormat(); + } + if (java.time.LocalTime.class.isAssignableFrom(type) || java.time.OffsetTime.class.isAssignableFrom(type)) { + return JsonSchemaType.TIME_FORMAT.getJsonSchemaFormat(); + } + return JsonSchemaType.DATE_TIME_FORMAT.getJsonSchemaFormat(); + } + + public static Class<?> getClassFromType(Type type) { + if (type instanceof Class) { + return (Class<?>) type; + } + if (type instanceof ParameterizedType) { + return (Class<?>) ((ParameterizedType) type).getRawType(); + } + if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + Class<?> componentClass = getClassFromType(componentType); + return java.lang.reflect.Array.newInstance(componentClass, 0).getClass(); + } + if (type instanceof TypeVariable) { + Type[] bounds = ((TypeVariable<?>) type).getBounds(); + if (bounds.length > 0) { + return getClassFromType(bounds[0]); + } + } + if (type instanceof WildcardType) { + Type[] upperBounds = ((WildcardType) type).getUpperBounds(); + if (upperBounds.length > 0) { + return getClassFromType(upperBounds[0]); + } + } + return Object.class; + } + + public static boolean isPrimitiveOrWrapper(Class<?> type) { + return isPrimitiveType(type); Review Comment: [nitpick] The method isPrimitiveOrWrapper delegates to isPrimitiveType which checks JsonSchemaType.fromJavaType. This indirection is confusing - consider renaming for clarity or implementing the wrapper type check directly. ```suggestion // Directly check for primitive types and their wrappers return type.isPrimitive() || type == Boolean.class || type == Byte.class || type == Character.class || type == Short.class || type == Integer.class || type == Long.class || type == Float.class || type == Double.class || type == Void.class; ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
