From 528e0b9a349bebb35131b3eaf009c83dcd447a52 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sun, 26 Oct 2025 23:14:46 +0800 Subject: [PATCH 1/3] feat: introduce McpServerObjectMapperFactory for consistent ObjectMapper configuration Signed-off-by: liugddx --- .../McpServerAutoConfiguration.java | 43 +++++++++++++++++-- .../McpServerSseWebMvcAutoConfiguration.java | 3 +- ...erverStatelessWebMvcAutoConfiguration.java | 3 +- ...StreamableHttpWebMvcAutoConfiguration.java | 3 +- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java index ef10a199ae3..9850c95adda 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java @@ -21,7 +21,11 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpAsyncServerExchange; @@ -50,6 +54,7 @@ import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerChangeNotificationProperties; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; +import org.springframework.ai.util.JacksonUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -91,12 +96,42 @@ public class McpServerAutoConfiguration { private static final LogAccessor logger = new LogAccessor(McpServerAutoConfiguration.class); + /** + * Creates a configured ObjectMapper for MCP server JSON serialization. + *

+ * This ObjectMapper is specifically configured for MCP protocol compliance with: + *

+ *

+ * This bean can be overridden by providing a custom ObjectMapper bean with the name + * "mcpServerObjectMapper". + * @return configured ObjectMapper instance for MCP server operations + */ + @Bean(name = "mcpServerObjectMapper") + @ConditionalOnMissingBean(name = "mcpServerObjectMapper") + public ObjectMapper mcpServerObjectMapper() { + return JsonMapper.builder() + // Deserialization configuration + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) + // Serialization configuration + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .serializationInclusion(JsonInclude.Include.NON_NULL) + // Register standard modules (Jdk8, JavaTime, ParameterNames, Kotlin if + // available) + .addModules(JacksonUtils.instantiateAvailableModules()) + .build(); + } + @Bean @ConditionalOnMissingBean - public McpServerTransportProviderBase stdioServerTransport(ObjectProvider objectMapperProvider) { - ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); - - return new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper)); + public McpServerTransportProviderBase stdioServerTransport(ObjectMapper mcpServerObjectMapper) { + return new StdioServerTransportProvider(new JacksonMcpJsonMapper(mcpServerObjectMapper)); } @Bean diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerSseWebMvcAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerSseWebMvcAutoConfiguration.java index f9c14b140c6..cc0af0a70d6 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerSseWebMvcAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerSseWebMvcAutoConfiguration.java @@ -22,6 +22,7 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperFactory; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties; import org.springframework.beans.factory.ObjectProvider; @@ -78,7 +79,7 @@ public class McpServerSseWebMvcAutoConfiguration { public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider( ObjectProvider objectMapperProvider, McpServerSseProperties serverProperties) { - ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + ObjectMapper objectMapper = McpServerObjectMapperFactory.getOrCreateObjectMapper(objectMapperProvider); return WebMvcSseServerTransportProvider.builder() .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java index c9e00c848c1..7ee08cf7eac 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStatelessWebMvcAutoConfiguration.java @@ -21,6 +21,7 @@ import io.modelcontextprotocol.server.transport.WebMvcStatelessServerTransport; import io.modelcontextprotocol.spec.McpSchema; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperFactory; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStatelessAutoConfiguration; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties; @@ -50,7 +51,7 @@ public class McpServerStatelessWebMvcAutoConfiguration { public WebMvcStatelessServerTransport webMvcStatelessServerTransport( ObjectProvider objectMapperProvider, McpServerStreamableHttpProperties serverProperties) { - ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + ObjectMapper objectMapper = McpServerObjectMapperFactory.getOrCreateObjectMapper(objectMapperProvider); return WebMvcStatelessServerTransport.builder() .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java index 3d7a840a9f5..070281afe2b 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webmvc/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerStreamableHttpWebMvcAutoConfiguration.java @@ -22,6 +22,7 @@ import io.modelcontextprotocol.spec.McpSchema; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperFactory; import org.springframework.ai.mcp.server.common.autoconfigure.McpServerStdioDisabledCondition; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties; @@ -51,7 +52,7 @@ public class McpServerStreamableHttpWebMvcAutoConfiguration { public WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider( ObjectProvider objectMapperProvider, McpServerStreamableHttpProperties serverProperties) { - ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + ObjectMapper objectMapper = McpServerObjectMapperFactory.getOrCreateObjectMapper(objectMapperProvider); return WebMvcStreamableServerTransportProvider.builder() .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) From 327cd93d6f6b66cc73bf2e2aee0c30c0bc02919e Mon Sep 17 00:00:00 2001 From: liugddx Date: Sun, 26 Oct 2025 23:16:16 +0800 Subject: [PATCH 2/3] feat: introduce McpServerObjectMapperFactory for consistent ObjectMapper configuration Signed-off-by: liugddx --- .../McpServerObjectMapperFactory.java | 110 +++++++++ .../autoconfigure/McpToolWithStdioIT.java | 223 ++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java create mode 100644 auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java new file mode 100644 index 00000000000..067fc38eee5 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java @@ -0,0 +1,110 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.ai.mcp.server.common.autoconfigure; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import org.springframework.ai.util.JacksonUtils; + +/** + * Factory class for creating properly configured {@link ObjectMapper} instances for MCP + * server operations. + *

+ * This factory ensures consistent JSON serialization/deserialization configuration across + * all MCP server transport types (STDIO, SSE, Streamable-HTTP, Stateless). The + * configuration is optimized for MCP protocol compliance and handles common edge cases + * that can cause serialization failures. + *

+ * Key configuration features: + *

+ *

+ * This factory was introduced to fix Issue #4451 where @McpTool annotated methods failed + * to load with STDIO protocol due to JSON serialization errors caused by using an + * unconfigured ObjectMapper instance. + * + * @author Spring AI Team + * @see Issue #4451 + */ +public final class McpServerObjectMapperFactory { + + private McpServerObjectMapperFactory() { + // Utility class - prevent instantiation + } + + /** + * Creates a new {@link ObjectMapper} instance configured for MCP server operations. + *

+ * This method creates a fresh ObjectMapper with standard configuration suitable for + * MCP protocol serialization/deserialization. Each call creates a new instance, so + * callers may want to cache the result if creating multiple instances. + * @return a properly configured ObjectMapper instance + */ + public static ObjectMapper createObjectMapper() { + return JsonMapper.builder() + // Deserialization configuration + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) + // Serialization configuration + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .serializationInclusion(JsonInclude.Include.NON_NULL) + // Register standard Jackson modules (Jdk8, JavaTime, ParameterNames, Kotlin) + .addModules(JacksonUtils.instantiateAvailableModules()) + .build(); + } + + /** + * Retrieves an ObjectMapper from the provided provider, or creates a configured + * default if none is available. + *

+ * This method is designed for use in Spring auto-configuration classes where an + * ObjectMapper may optionally be provided by the user. If no ObjectMapper bean is + * available, this method ensures a properly configured instance is used rather than a + * vanilla ObjectMapper. + *

+ * Example usage in auto-configuration: + * + *

{@code
+	 * @Bean
+	 * public TransportProvider transport(ObjectProvider objectMapperProvider) {
+	 *     ObjectMapper mapper = McpServerObjectMapperFactory.getOrCreateObjectMapper(objectMapperProvider);
+	 *     return new TransportProvider(mapper);
+	 * }
+	 * }
+ * @param objectMapperProvider the Spring ObjectProvider for ObjectMapper beans + * @return the provided ObjectMapper, or a newly configured default instance + */ + public static ObjectMapper getOrCreateObjectMapper( + org.springframework.beans.factory.ObjectProvider objectMapperProvider) { + return objectMapperProvider.getIfAvailable(McpServerObjectMapperFactory::createObjectMapper); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java new file mode 100644 index 00000000000..19c3e9aa823 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java @@ -0,0 +1,223 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.ai.mcp.server.common.autoconfigure; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransportProviderBase; +import org.junit.jupiter.api.Test; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; + +import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerAnnotationScannerAutoConfiguration; +import org.springframework.ai.mcp.server.common.autoconfigure.annotations.McpServerSpecificationFactoryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.stereotype.Component; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for @McpTool annotations with STDIO transport. + *

+ * This test verifies the fix for Issue #4451 where @McpTool annotated methods failed to + * load when using STDIO protocol due to JSON serialization issues with unconfigured + * ObjectMapper. + * + * @see Issue #4451 + */ +public class McpToolWithStdioIT { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(McpServerAutoConfiguration.class, McpServerAnnotationScannerAutoConfiguration.class, + McpServerSpecificationFactoryAutoConfiguration.class)); + + /** + * Verifies that a configured ObjectMapper bean is created for MCP server operations. + */ + @Test + void shouldCreateConfiguredObjectMapperForMcpServer() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(ObjectMapper.class); + ObjectMapper objectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); + + assertThat(objectMapper).isNotNull(); + + // Verify that the ObjectMapper is properly configured + String emptyBeanJson = objectMapper.writeValueAsString(new EmptyBean()); + assertThat(emptyBeanJson).isEqualTo("{}"); // Should not fail on empty beans + + String nullValueJson = objectMapper.writeValueAsString(new BeanWithNull()); + assertThat(nullValueJson).doesNotContain("null"); // Should exclude null + // values + }); + } + + /** + * Verifies that STDIO transport uses the configured ObjectMapper. + */ + @Test + void stdioTransportShouldUseConfiguredObjectMapper() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(McpServerTransportProviderBase.class); + assertThat(context.getBean(McpServerTransportProviderBase.class)) + .isInstanceOf(StdioServerTransportProvider.class); + + // Verify that the MCP server was created successfully + assertThat(context).hasSingleBean(McpSyncServer.class); + }); + } + + /** + * Verifies that @McpTool annotated methods are successfully registered with STDIO + * transport. + *

+ * This is the core test for Issue #4451 - it ensures that tool specifications + * generated from @McpTool annotations can be properly serialized to JSON without + * errors. + */ + @Test + @SuppressWarnings("unchecked") + void mcpToolAnnotationsShouldWorkWithStdio() { + this.contextRunner.withBean(TestCalculatorTools.class).run(context -> { + // Verify the server was created + assertThat(context).hasSingleBean(McpSyncServer.class); + McpSyncServer syncServer = context.getBean(McpSyncServer.class); + + // Get the async server from sync server (internal structure) + McpAsyncServer asyncServer = (McpAsyncServer) ReflectionTestUtils.getField(syncServer, "asyncServer"); + assertThat(asyncServer).isNotNull(); + + // Verify that tools were registered + CopyOnWriteArrayList tools = (CopyOnWriteArrayList) ReflectionTestUtils + .getField(asyncServer, "tools"); + + assertThat(tools).isNotEmpty(); + assertThat(tools).hasSize(3); + + // Verify tool names + List toolNames = tools.stream().map(spec -> spec.tool().name()).toList(); + assertThat(toolNames).containsExactlyInAnyOrder("add", "subtract", "multiply"); + + // Verify that each tool has a valid inputSchema that can be serialized + ObjectMapper objectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); + + for (AsyncToolSpecification spec : tools) { + McpSchema.Tool tool = spec.tool(); + + // Verify basic tool properties + assertThat(tool.name()).isNotBlank(); + assertThat(tool.description()).isNotBlank(); + + // Verify inputSchema can be serialized to JSON without errors + if (tool.inputSchema() != null) { + String schemaJson = objectMapper.writeValueAsString(tool.inputSchema()); + assertThat(schemaJson).isNotBlank(); + + // Should be valid JSON + objectMapper.readTree(schemaJson); + } + } + }); + } + + /** + * Verifies that tools with complex parameter types work correctly. + */ + @Test + @SuppressWarnings("unchecked") + void mcpToolWithComplexParametersShouldWorkWithStdio() { + this.contextRunner.withBean(TestComplexTools.class).run(context -> { + assertThat(context).hasSingleBean(McpSyncServer.class); + McpSyncServer syncServer = context.getBean(McpSyncServer.class); + + McpAsyncServer asyncServer = (McpAsyncServer) ReflectionTestUtils.getField(syncServer, "asyncServer"); + + CopyOnWriteArrayList tools = (CopyOnWriteArrayList) ReflectionTestUtils + .getField(asyncServer, "tools"); + + assertThat(tools).hasSize(1); + + AsyncToolSpecification spec = tools.get(0); + assertThat(spec.tool().name()).isEqualTo("processData"); + + // Verify the tool can be serialized + ObjectMapper objectMapper = context.getBean("mcpServerObjectMapper", ObjectMapper.class); + String toolJson = objectMapper.writeValueAsString(spec.tool()); + assertThat(toolJson).isNotBlank(); + }); + } + + // Test components + + @Component + static class TestCalculatorTools { + + @McpTool(name = "add", description = "Add two numbers") + public int add(@McpToolParam(description = "First number", required = true) int a, + @McpToolParam(description = "Second number", required = true) int b) { + return a + b; + } + + @McpTool(name = "subtract", description = "Subtract two numbers") + public int subtract(@McpToolParam(description = "First number", required = true) int a, + @McpToolParam(description = "Second number", required = true) int b) { + return a - b; + } + + @McpTool(name = "multiply", description = "Multiply two numbers") + public int multiply(@McpToolParam(description = "First number", required = true) int a, + @McpToolParam(description = "Second number", required = true) int b) { + return a * b; + } + + } + + @Component + static class TestComplexTools { + + @McpTool(name = "processData", description = "Process complex data") + public String processData(@McpToolParam(description = "Input data", required = true) String input, + @McpToolParam(description = "Options", required = false) String options) { + return "Processed: " + input + " with options: " + options; + } + + } + + // Test beans for ObjectMapper configuration verification + + static class EmptyBean { + + } + + static class BeanWithNull { + + public String value = null; + + public String anotherValue = "test"; + + } + +} From 333ca178bc0000b1aebab5561e8ef74838510595 Mon Sep 17 00:00:00 2001 From: liugddx Date: Wed, 29 Oct 2025 23:06:23 +0800 Subject: [PATCH 3/3] feat: refactor ObjectMapper configuration to use McpServerObjectMapperFactory Signed-off-by: liugddx --- .../McpServerAutoConfiguration.java | 18 +----------------- .../McpServerObjectMapperFactory.java | 5 ----- .../autoconfigure/McpToolWithStdioIT.java | 11 +---------- 3 files changed, 2 insertions(+), 32 deletions(-) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java index 9850c95adda..c82409f3097 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerAutoConfiguration.java @@ -21,11 +21,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpAsyncServer; import io.modelcontextprotocol.server.McpAsyncServerExchange; @@ -54,7 +50,6 @@ import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerChangeNotificationProperties; import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties; -import org.springframework.ai.util.JacksonUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -114,18 +109,7 @@ public class McpServerAutoConfiguration { @Bean(name = "mcpServerObjectMapper") @ConditionalOnMissingBean(name = "mcpServerObjectMapper") public ObjectMapper mcpServerObjectMapper() { - return JsonMapper.builder() - // Deserialization configuration - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) - // Serialization configuration - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .serializationInclusion(JsonInclude.Include.NON_NULL) - // Register standard modules (Jdk8, JavaTime, ParameterNames, Kotlin if - // available) - .addModules(JacksonUtils.instantiateAvailableModules()) - .build(); + return McpServerObjectMapperFactory.createObjectMapper(); } @Bean diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java index 067fc38eee5..65f2989c721 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/main/java/org/springframework/ai/mcp/server/common/autoconfigure/McpServerObjectMapperFactory.java @@ -45,13 +45,8 @@ *

  • Jackson Modules: Registers standard modules for Java 8, JSR-310, parameter * names, and Kotlin (if available)
  • * - *

    - * This factory was introduced to fix Issue #4451 where @McpTool annotated methods failed - * to load with STDIO protocol due to JSON serialization errors caused by using an - * unconfigured ObjectMapper instance. * * @author Spring AI Team - * @see Issue #4451 */ public final class McpServerObjectMapperFactory { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java index 19c3e9aa823..034aded7a35 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/McpToolWithStdioIT.java @@ -41,12 +41,6 @@ /** * Integration tests for @McpTool annotations with STDIO transport. - *

    - * This test verifies the fix for Issue #4451 where @McpTool annotated methods failed to - * load when using STDIO protocol due to JSON serialization issues with unconfigured - * ObjectMapper. - * - * @see Issue #4451 */ public class McpToolWithStdioIT { @@ -92,10 +86,7 @@ void stdioTransportShouldUseConfiguredObjectMapper() { /** * Verifies that @McpTool annotated methods are successfully registered with STDIO - * transport. - *

    - * This is the core test for Issue #4451 - it ensures that tool specifications - * generated from @McpTool annotations can be properly serialized to JSON without + * transport and that tool specifications can be properly serialized to JSON without * errors. */ @Test