Spring Boot Starter for building Model Context Protocol (MCP) servers with auto-configuration, annotation-based tool/resource/prompt definitions, and support for STDIO, SSE, and Streamable-HTTP transports
GraalVM native image compilation support for Spring AI MCP Server applications, enabling ahead-of-time (AOT) compilation for fast startup and reduced memory footprint.
Native image support is provided automatically through Spring Boot's AOT processing infrastructure. The starter includes runtime hints for all MCP schema classes to ensure proper reflection support in native images.
Runtime hints registrar that registers MCP schema classes for reflection in GraalVM native images.
/**
* Runtime hints registrar for MCP schema types
* Location: org.springframework.ai.mcp.aot.McpHints
* Implements: org.springframework.aot.hint.RuntimeHintsRegistrar
*/
class McpHints implements RuntimeHintsRegistrar {
/**
* Register runtime hints for MCP classes
* @param hints Runtime hints registry
* @param classLoader Class loader
*/
@Override
void registerHints(RuntimeHints hints, ClassLoader classLoader);
}Registered Classes:
The McpHints registrar automatically registers all MCP schema classes from io.modelcontextprotocol.spec.McpSchema for reflection, including:
Usage:
The hints are automatically applied when building a native image with Spring Boot 3.x and GraalVM. No manual configuration is required.
# Install GraalVM
sdk install java 21.0.1-graalce
# Use GraalVM
sdk use java 21.0.1-graalce<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>Build native image:
mvn -Pnative native:compile// build.gradle
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'org.graalvm.buildtools.native' version '0.9.28'
}Build native image:
gradle nativeCompile# application.properties for native builds
# Server configuration
spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.stdio=true
spring.ai.mcp.server.name=native-mcp-server
spring.ai.mcp.server.version=1.0.0
# Reduce startup overhead
spring.main.lazy-initialization=falseIf you have custom types used in MCP tools, add reflection hints:
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
@Configuration
@ImportRuntimeHints(CustomMcpHints.class)
public class NativeConfig {
}
class CustomMcpHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Register custom types used in @McpTool methods
hints.reflection()
.registerType(MyCustomRequest.class)
.registerType(MyCustomResponse.class);
}
}@SpringBootApplication
@ImportRuntimeHints(McpHints.class)
public class CalculatorServerApplication {
public static void main(String[] args) {
SpringApplication.run(CalculatorServerApplication.class, args);
}
}
@Component
class CalculatorTools {
@McpTool(name = "add", description = "Add two numbers")
public double add(
@McpToolParam(description = "First number", required = true) double a,
@McpToolParam(description = "Second number", required = true) double b) {
return a + b;
}
@McpTool(name = "multiply", description = "Multiply two numbers")
public double multiply(
@McpToolParam(description = "First number", required = true) double a,
@McpToolParam(description = "Second number", required = true) double b) {
return a * b;
}
}# Build native image
mvn -Pnative native:compile
# Run native executable
./target/calculator-server
# Performance comparison
echo "Native startup time:"
time ./target/calculator-server --server.port=0 &
NATIVE_PID=$!
sleep 2
kill $NATIVE_PID
echo "JVM startup time:"
time java -jar target/calculator-server.jar --server.port=0 &
JVM_PID=$!
sleep 2
kill $JVM_PIDNative images start in milliseconds instead of seconds:
Native images consume less memory:
No warmup required - native images run at peak performance immediately.
All classes used in MCP operations must be registered for reflection:
McpHints)Avoid dynamic class loading in tool implementations:
// Bad - will fail in native image
@McpTool(name = "dynamic")
public String dynamicTool(String className) throws Exception {
Class<?> clazz = Class.forName(className); // Fails!
return clazz.getName();
}
// Good - use statically known types
@McpTool(name = "process")
public String processTool(String type) {
return switch(type) {
case "text" -> processText();
case "json" -> processJson();
default -> "Unknown type";
};
}Jackson serialization works automatically for registered types. Ensure custom types are properly annotated:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class CustomType {
private final String value;
@JsonCreator
public CustomType(@JsonProperty("value") String value) {
this.value = value;
}
public String getValue() {
return value;
}
}Run tests in native image mode:
# Maven
mvn -PnativeTest test
# Gradle
gradle nativeTestimport org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.junit.jupiter.api.Test;
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
class NativeImageTests {
@Autowired
private ApplicationContext context;
@Test
void contextLoads() {
// Verify MCP server beans are available
assertNotNull(context.getBean("mcpServerAutoConfiguration"));
}
@Test
void mcpToolsRegistered() {
// Verify tools are registered
List<SyncToolSpecification> tools = context.getBeansOfType(
SyncToolSpecification.class
).values().stream().toList();
assertFalse(tools.isEmpty());
}
}If you encounter ClassNotFoundException or reflection errors:
RuntimeHintsRegistrar for your types@RegisterReflectionForBinding for known types@Configuration
@RegisterReflectionForBinding({MyType.class, AnotherType.class})
public class ReflectionConfig {
}For Jackson serialization issues in native images:
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer nativeImageCustomizer() {
return builder -> {
builder.modules(new ParameterNamesModule());
builder.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
};
}
}import org.springframework.ai.mcp.aot.McpHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.nativex.hint.TypeHint;