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..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
@@ -91,12 +91,31 @@ 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:
+ *
+ * - Lenient deserialization that doesn't fail on unknown properties
+ * - Proper handling of empty beans during serialization
+ * - Exclusion of null values from JSON output
+ * - Standard Jackson modules for Java 8, JSR-310, and Kotlin support
+ *
+ *
+ * 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 McpServerObjectMapperFactory.createObjectMapper();
+ }
+
@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-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..65f2989c721
--- /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,105 @@
+/*
+ * 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:
+ *
+ * - Lenient Deserialization: Does not fail on unknown JSON properties, allowing
+ * forward compatibility
+ * - Empty Bean Handling: Does not fail when serializing beans without
+ * properties
+ * - Null Value Exclusion: Excludes null values from JSON output for cleaner
+ * messages
+ * - Date/Time Formatting: Uses ISO-8601 format instead of timestamps
+ * - Jackson Modules: Registers standard modules for Java 8, JSR-310, parameter
+ * names, and Kotlin (if available)
+ *
+ *
+ * @author Spring AI Team
+ */
+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..034aded7a35
--- /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,214 @@
+/*
+ * 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.
+ */
+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 and that tool specifications 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";
+
+ }
+
+}
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))