
Spring Ai Mcp Server Patterns
- 1.7k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
Provides Spring Boot MCP server patterns that create Model Context Protocol servers with Spring AI by defining tool handlers, exposing resources, configuring prompt templates, and setting up transport
About
The spring ai mcp server patterns skill Provides Spring Boot MCP server patterns that create Model Context Protocol servers with Spring AI by defining tool handlers, exposing resources, configuring prompt templates, and setting up transports for AI function calling and tool calling. Use when building MCP servers to extend AI capabilities with Spring's official AI framework, implementing AI tools, custom function calling, or MCP client integration. Documentation covers workflows, commands, and guardrails agents should follow when users invoke this capability. Key documented areas include Keep tools focused - one operation per tool; Use clear, action-oriented names (`getWeather`, `executeQuery`); Always annotate parameters with `@ToolParam` and descriptive text; Return structured records/DTOs, not raw strings or maps. Use when developers or agents need structured guidance for spring ai mcp server patterns tasks with evidence grounded in the bundled SKILL.md rather than generic advice.
- Keep tools focused - one operation per tool
- Use clear, action-oriented names (`getWeather`, `executeQuery`)
- Always annotate parameters with `@ToolParam` and descriptive text
- Return structured records/DTOs, not raw strings or maps
- Design tools to be idempotent when possible
Spring Ai Mcp Server Patterns by the numbers
- 1,694 all-time installs (skills.sh)
- +55 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #706 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
spring-ai-mcp-server-patterns capabilities & compatibility
- Capabilities
- keep tools focused one operation per tool · use clear, action oriented names (`getweather`, · always annotate parameters with `@toolparam` and · return structured records/dtos, not raw strings · design tools to be idempotent when possible
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-ai-mcp-server-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.7k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I handle spring ai mcp server patterns tasks with agent guidance?
Provides Spring Boot MCP server patterns that create Model Context Protocol servers with Spring AI by defining tool handlers, exposing resources, configuring prompt templates, and setting up transport
Who is it for?
Teams needing documented spring ai mcp server patterns workflows.
Skip if: Node.js or Python MCP servers, frontend-only projects, or teams not using Spring Boot and Spring AI for backend services.
When should I use this skill?
Provides Spring Boot MCP server patterns that create Model Context Protocol servers with Spring AI by defining tool handlers, exposing resources, configuring prompt templates, and setting up transport
What you get
Structured workflow from spring ai mcp server patterns documentation applied to the user request.
- mcp tool handler classes
- transport configuration
- prompt template definitions
Files
Spring AI MCP Server Implementation Patterns
Implements MCP servers with Spring AI for AI function calling, tool handlers, and MCP transport configuration.
Overview
Production-ready MCP server patterns: @Tool functions, @PromptTemplate resources, and stdio/HTTP/SSE transports with Spring AI security.
When to Use
MCP servers, Spring AI function calling, AI tools, tool calling, custom tool handlers, Spring Boot MCP, resource endpoints, or MCP transport configuration.
Quick Reference
Core Annotations
| Annotation | Target | Purpose |
|---|---|---|
@EnableMcpServer | Class | Enable MCP server auto-configuration |
@Tool(description) | Method | Declare AI-callable tool |
@ToolParam(value) | Parameter | Document tool parameter for AI |
@PromptTemplate(name) | Method | Declare reusable prompt template |
@PromptParam(value) | Parameter | Document prompt parameter |
Transport Types
| Transport | Use Case | Config |
|---|---|---|
stdio | Local process / Claude Desktop | Default |
http | Remote HTTP clients | port, path |
sse | Real-time streaming clients | port, path |
Key Dependencies
<!-- Maven -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>// Gradle
implementation 'org.springframework.ai:spring-ai-mcp-server:1.0.0'
implementation 'org.springframework.ai:spring-ai-starter-model-openai:1.0.0'Instructions
1. Project Setup
Add Spring AI MCP dependencies (see Quick Reference above), configure the AI model in application.properties, and enable MCP with @EnableMcpServer:
@SpringBootApplication
@EnableMcpServer
public class MyMcpApplication {
public static void main(String[] args) {
SpringApplication.run(MyMcpApplication.class, args);
}
}spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.mcp.enabled=true
spring.ai.mcp.transport.type=stdio2. Define Tools
Annotate methods with @Tool inside @Component beans. Use @ToolParam to document parameters:
@Component
public class WeatherTools {
@Tool(description = "Get current weather for a city")
public WeatherData getWeather(@ToolParam("City name") String city) {
return weatherService.getCurrentWeather(city);
}
@Tool(description = "Get 5-day forecast for a city")
public ForecastData getForecast(
@ToolParam("City name") String city,
@ToolParam(value = "Unit: celsius or fahrenheit", required = false) String unit) {
return weatherService.getForecast(city, unit != null ? unit : "celsius");
}
}See references/implementation-patterns.md for database tools, API integration tools, and the FunctionCallback low-level pattern.
3. Create Prompt Templates
@Component
public class CodeReviewPrompts {
@PromptTemplate(
name = "java-code-review",
description = "Review Java code for best practices and issues"
)
public Prompt createCodeReviewPrompt(
@PromptParam("code") String code,
@PromptParam(value = "focusAreas", required = false) List<String> focusAreas) {
String focus = focusAreas != null ? String.join(", ", focusAreas) : "general best practices";
return Prompt.builder()
.system("You are an expert Java code reviewer with 20 years of experience.")
.user("Review the following Java code for " + focus + ":\n```java\n" + code + "\n```")
.build();
}
}See references/implementation-patterns.md for additional prompt template patterns.
4. Configure Transport
spring:
ai:
mcp:
enabled: true
transport:
type: stdio # stdio | http | sse
http:
port: 8080
path: /mcp
server:
name: my-mcp-server
version: 1.0.05. Add Security
@Configuration
public class McpSecurityConfig {
@Bean
public ToolFilter toolFilter(SecurityService securityService) {
return (tool, context) -> {
User user = securityService.getCurrentUser();
if (tool.name().startsWith("admin_")) {
return user.hasRole("ADMIN");
}
return securityService.isToolAllowed(user, tool.name());
};
}
}Use @PreAuthorize("hasRole('ADMIN')") on tool methods for method-level security. See references/implementation-patterns.md for full security patterns.
6. Testing
@SpringBootTest
class WeatherToolsTest {
@Autowired
private WeatherTools weatherTools;
@MockBean
private WeatherService weatherService;
@Test
void testGetWeather_Success() {
when(weatherService.getCurrentWeather("London"))
.thenReturn(new WeatherData("London", "Cloudy", 15.0));
WeatherData result = weatherTools.getWeather("London");
assertThat(result.city()).isEqualTo("London");
verify(weatherService).getCurrentWeather("London");
}
}See references/testing-guide.md for integration tests, Testcontainers, security tests, and slice tests.
Best Practices
Tool Design
- Keep tools focused — one operation per tool
- Use clear, action-oriented names (
getWeather,executeQuery) - Always annotate parameters with
@ToolParamand descriptive text - Return structured records/DTOs, not raw strings or maps
- Design tools to be idempotent when possible
Security
- Validate and sanitize all inputs — AI-generated parameters are untrusted
- Use parameterized queries for SQL; validate and normalize paths for file tools
- Apply
@PreAuthorizefor role-based access on sensitive tools - Audit log all data-modifying tool executions
- Never expose credentials or sensitive data in tool descriptions or error messages
Performance
- Use
@Cacheablefor expensive operations with appropriate TTL - Set timeouts for all external calls
- Use
@Asyncfor long-running operations - Monitor with Micrometer metrics
Error Handling
- Return structured error responses with user-friendly messages
- Log context (user, tool name, parameters) for debugging
- Implement retry logic for transient failures
- Implement
@ControllerAdvicefor consistent error responses
Examples
Example 1: Minimal Weather MCP Server
@SpringBootApplication
@EnableMcpServer
public class WeatherMcpApplication {
public static void main(String[] args) {
SpringApplication.run(WeatherMcpApplication.class, args);
}
}
@Component
public class WeatherTools {
@Tool(description = "Get current weather for a city")
public WeatherData getWeather(@ToolParam("City name") String city) {
return new WeatherData(city, "Sunny", 22.5);
}
}
record WeatherData(String city, String condition, double temperatureCelsius) {}Example 2: Secure Database Tool
@Component
@PreAuthorize("hasRole('USER')")
public class DatabaseTools {
private final JdbcTemplate jdbcTemplate;
@Tool(description = "Execute a read-only SQL query and return results")
public QueryResult executeQuery(
@ToolParam("SQL SELECT query") String sql,
@ToolParam(value = "Parameters as JSON map", required = false) String paramsJson) {
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
throw new IllegalArgumentException("Only SELECT queries are allowed");
}
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
return new QueryResult(rows, rows.size());
}
}See references/examples.md for complete examples including file system tools, REST API integration, and prompt template servers.
Constraints and Warnings
Security
- Never expose sensitive data in tool descriptions, parameters, or error messages
- Input validation is mandatory — always validate before executing
- External content is untrusted — tools fetching URLs may receive prompt injection payloads; validate all fetched content
- SQL injection: use parameterized queries exclusively
- Path traversal: normalize and validate all file paths against a base path
Operational
- Responses should be concise — large responses can exceed AI context window limits
- All tools must implement timeouts; default should be configurable
- Rate limit expensive operations
- Tools may be called concurrently — ensure thread safety
Spring AI Specific
- Spring AI is actively developed — pin specific versions in production
- Error messages thrown by tools are exposed to AI models; sanitize them
- Choose transport type carefully:
stdiofor local processes,http/ssefor remote clients
References
Consult these files for detailed patterns and examples:
- [references/implementation-patterns.md](references/implementation-patterns.md) - Tool creation, prompt templates, FunctionCallback, Spring Boot auto-configuration, application properties
- [references/advanced-patterns.md](references/advanced-patterns.md) - Dynamic tool registration, multi-model support, caching, error handling
- [references/testing-guide.md](references/testing-guide.md) - Unit tests, integration tests, Testcontainers, security tests, slice tests
- [references/examples.md](references/examples.md) - Complete server examples: weather, database, file system, REST API, prompt templates
- [references/api-reference.md](references/api-reference.md) - Full API: annotations, interfaces, configuration classes, transport implementations, event system
- [references/migration-guide.md](references/migration-guide.md) - Migrating from LangChain4j MCP to Spring AI MCP
Spring AI MCP Server — Advanced Patterns
Advanced implementation patterns for dynamic tools, multi-model support, caching, error handling, and security.
Dynamic Tool Registration
Register tools at runtime based on external configuration or user requests:
@Service
public class DynamicToolRegistry {
private final McpServer mcpServer;
private final Map<String, ToolRegistration> registeredTools = new ConcurrentHashMap<>();
public void registerTool(ToolRegistration registration) {
registeredTools.put(registration.getId(), registration);
Tool tool = Tool.builder()
.name(registration.getName())
.description(registration.getDescription())
.inputSchema(registration.getInputSchema())
.function(args -> executeDynamicTool(registration.getId(), args))
.build();
mcpServer.addTool(tool);
}
public void unregisterTool(String toolId) {
ToolRegistration registration = registeredTools.remove(toolId);
if (registration != null) {
mcpServer.removeTool(registration.getName());
}
}
private Object executeDynamicTool(String toolId, Map<String, Object> args) {
ToolRegistration registration = registeredTools.get(toolId);
if (registration == null) throw new IllegalStateException("Tool not found: " + toolId);
return switch (registration.getType()) {
case GROOVY_SCRIPT -> executeGroovyScript(registration, args);
case SPRING_BEAN -> executeSpringBeanMethod(registration, args);
case HTTP_ENDPOINT -> callHttpEndpoint(registration, args);
};
}
}
@Data
@Builder
class ToolRegistration {
private String id;
private String name;
private String description;
private Map<String, Object> inputSchema;
private ToolType type;
private String target;
private Map<String, String> metadata;
}
enum ToolType { GROOVY_SCRIPT, SPRING_BEAN, HTTP_ENDPOINT }Multi-Model Support
Configure and select between multiple AI models:
@Configuration
public class MultiModelConfig {
@Bean
@Primary
public ChatModel primaryChatModel(@Value("${spring.ai.primary.model}") String modelName) {
return switch (modelName) {
case "gpt-4" -> new OpenAiChatModel(OpenAiApi.builder()
.apiKey(System.getenv("OPENAI_API_KEY")).build());
case "claude" -> new AnthropicChatModel(AnthropicApi.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY")).build());
default -> throw new IllegalArgumentException("Unsupported model: " + modelName);
};
}
@Bean
public ModelSelector modelSelector(Map<String, ChatModel> models) {
return new SpringAiModelSelector(models);
}
}
@Component
public class SpringAiModelSelector implements ModelSelector {
private final Map<String, ChatModel> models;
@Override
public ChatModel selectModel(Prompt prompt, Map<String, Object> context) {
// Select based on complexity, cost, or latency constraints
String modelName = determineBestModel(prompt, context);
return models.get(modelName);
}
private String determineBestModel(Prompt prompt, Map<String, Object> context) {
// Implement selection logic (prompt length, cost, latency)
return "gpt-4";
}
}Caching and Performance
@Configuration
@EnableCaching
public class McpCacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("tool-results", "prompt-templates");
}
}
@Component
public class CachedToolExecutor {
private final McpServer mcpServer;
@Cacheable(
value = "tool-results",
key = "#toolName + '_' + #args.hashCode()",
unless = "#result.isCacheable() == false"
)
public ToolResult executeTool(String toolName, Map<String, Object> args) {
return mcpServer.executeTool(toolName, args);
}
@CacheEvict(value = "tool-results", allEntries = true)
public void clearToolCache() { }
@Cacheable(value = "prompt-templates", key = "#templateName")
public PromptTemplate getPromptTemplate(String templateName) {
return mcpServer.getPromptTemplate(templateName);
}
}Secure Tool Execution
Full secure tool executor with Spring Security:
@Component
public class SecureToolExecutor {
private final McpServer mcpServer;
public ToolResult executeTool(String toolName, Map<String, Object> arguments) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!(auth instanceof UserAuthentication userAuth)) {
throw new AccessDeniedException("User not authenticated");
}
if (!hasToolPermission(userAuth.getUser(), toolName)) {
throw new AccessDeniedException("Tool not allowed: " + toolName);
}
validateArguments(arguments);
logToolExecution(userAuth.getUser(), toolName, arguments);
try {
ToolResult result = mcpServer.executeTool(toolName, arguments);
logToolSuccess(userAuth.getUser(), toolName);
return result;
} catch (Exception e) {
logToolFailure(userAuth.getUser(), toolName, e);
throw new ToolExecutionException("Tool execution failed", e);
}
}
private boolean hasToolPermission(User user, String toolName) {
return user.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("TOOL_" + toolName) ||
a.getAuthority().equals("ROLE_ADMIN"));
}
private void validateArguments(Map<String, Object> arguments) {
arguments.forEach((key, value) -> {
if (value instanceof String str && (str.contains(";") || str.contains("--"))) {
throw new IllegalArgumentException("Invalid characters in argument: " + key);
}
});
}
}Input Validation with Bean Validation
@Component
public class ValidatedTools {
@Tool(description = "Process user data with validation")
@Validated
public ProcessingResult processUserData(
@ToolParam("User data to process") @Valid UserData data) {
return new ProcessingResult("success", data);
}
}
record UserData(
@NotBlank(message = "Name is required")
@Size(max = 100)
String name,
@NotNull
@Min(18) @Max(120)
Integer age,
@NotBlank @Email
String email
) {}Error Handling
Consistent error handling via @ControllerAdvice:
@ControllerAdvice
public class McpExceptionHandler {
@ExceptionHandler(ToolExecutionException.class)
public ResponseEntity<ErrorResponse> handleToolExecutionException(
ToolExecutionException ex, WebRequest request) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(500)
.error("Tool Execution Failed")
.message(ex.getMessage())
.build());
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(403)
.error("Access Denied")
.message("You do not have permission to execute this tool")
.build());
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleValidation(IllegalArgumentException ex) {
return ResponseEntity.badRequest()
.body(ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(400)
.error("Validation Error")
.message(ex.getMessage())
.build());
}
@Data
@Builder
static class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
}
}Async Tool Execution
For long-running operations that should return immediately:
@Tool(description = "Execute long-running task asynchronously")
public AsyncResult executeAsyncTask(
@ToolParam("Task name") String taskName,
@ToolParam(value = "Task parameters", required = false) String paramsJson) {
String taskId = UUID.randomUUID().toString();
CompletableFuture.supplyAsync(() -> performLongRunningTask(taskName, paramsJson), asyncExecutor)
.thenAccept(result -> taskResults.put(taskId, result));
return new AsyncResult(taskId, "pending", null);
}
@Tool(description = "Check status of an async task")
public AsyncResult getTaskStatus(@ToolParam("Task ID") String taskId) {
Object result = taskResults.get(taskId);
if (result == null) return new AsyncResult(taskId, "pending", null);
return new AsyncResult(taskId, "completed", result);
}
record AsyncResult(String taskId, String status, Object result) {}Health Check
@Component
public class McpHealthIndicator implements HealthIndicator {
private final McpServer mcpServer;
private final ToolRegistry toolRegistry;
@Override
public Health health() {
try {
Transport transport = mcpServer.getTransport();
List<Tool> tools = toolRegistry.listTools();
return Health.up()
.withDetail("transport", transport.getClass().getSimpleName())
.withDetail("connected", transport.isConnected())
.withDetail("tools.count", tools.size())
.build();
} catch (Exception e) {
return Health.down().withDetail("error", e.getMessage()).build();
}
}
}Micrometer Metrics
@Component
public class McpMetrics {
private final MeterRegistry meterRegistry;
public void recordToolExecution(String toolName, long durationMs, boolean success) {
meterRegistry.counter("mcp.tool.executions",
"tool", toolName, "success", String.valueOf(success)).increment();
meterRegistry.timer("mcp.tool.execution.time", "tool", toolName)
.record(durationMs, TimeUnit.MILLISECONDS);
}
public void recordPromptRender(String templateName) {
meterRegistry.counter("mcp.prompt.renders", "template", templateName).increment();
}
}Spring AI MCP Server API Reference
Complete API documentation for Spring AI MCP server implementations.
Table of Contents
1. Core Annotations 2. Functional Interfaces 3. Configuration Classes 4. Transport Implementations 5. Security Interfaces 6. Utility Classes 7. Property Bindings 8. Event System
Core Annotations
@Tool
Marks a method as an MCP tool that can be invoked by AI models.
Target: Method Retention: Runtime
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tool {
/**
* Description of what this tool does.
* Used by AI models to understand when to invoke the tool.
*/
String description() default "";
/**
* Whether this tool requires confirmation before execution.
*/
boolean requiresConfirmation() default false;
/**
* Maximum execution time in milliseconds.
*/
long maxExecutionTime() default 30000;
/**
* Whether execution time should be monitored.
*/
boolean monitorExecution() default true;
}Example:
@Tool(
description = "Get current weather for a city",
requiresConfirmation = false,
maxExecutionTime = 5000
)
public WeatherData getWeather(@ToolParam("City name") String city) {
// Implementation
}@ToolParam
Documents a parameter for tool methods.
Target: Parameter Retention: Runtime
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolParam {
/**
* Description of the parameter purpose.
*/
String value() default "";
/**
* Whether this parameter is required.
*/
boolean required() default true;
/**
* Example value for documentation.
*/
String example() default "";
/**
* Default value if not provided.
*/
String defaultValue() default "";
}@PromptTemplate
Marks a method as a prompt template provider.
Target: Method Retention: Runtime
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PromptTemplate {
/**
* Unique name of the prompt template.
*/
String name() default "";
/**
* Description of when to use this template.
*/
String description() default "";
/**
* The template string with placeholders.
* Use {placeholder} syntax for parameters.
*/
String template() default "";
/**
* Model to use for this prompt.
*/
String model() default "";
/**
* Temperature for model generation.
*/
double temperature() default 0.7;
}Example:
@PromptTemplate(
name = "code-review-java",
description = "Review Java code for best practices",
template = """
Review the following Java code:{code}
Focus on: {focusAreas}
""",
temperature = 0.3
)
public Prompt createCodeReviewPrompt(@PromptParam("code") String code) {
// Return populated prompt
}@PromptParam
Documents a parameter for prompt template methods.
Target: Parameter Retention: Runtime
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface PromptParam {
/**
* Name of the parameter in the template.
*/
String value();
/**
* Description of the parameter.
*/
String description() default "";
/**
* Whether this parameter is required.
*/
boolean required() default true;
/**
* Example value.
*/
String example() default "";
}@EnableMcpServer
Enables MCP server auto-configuration.
Target: Type Retention: Runtime
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(McpServerAutoConfiguration.class)
public @interface EnableMcpServer {
/**
* Base packages to scan for tools and prompts.
*/
String[] basePackages() default {};
/**
* Whether to enable automatic tool discovery.
*/
boolean autoDiscovery() default true;
/**
* Configuration class to use.
*/
Class<?>[] configuration() default {};
}Functional Interfaces
ToolExecutor
Functional interface for tool execution.
@FunctionalInterface
public interface ToolExecutor {
/**
* Execute a tool with the given arguments.
*
* @param toolName Name of the tool to execute
* @param arguments Arguments as a map
* @return Execution result
* @throws ToolExecutionException if execution fails
*/
ToolResult execute(String toolName, Map<String, Object> arguments)
throws ToolExecutionException;
}ToolFilter
Filter for tool execution.
@FunctionalInterface
public interface ToolFilter {
/**
* Determine if a tool should be allowed to execute.
*
* @param tool The tool being requested
* @param context Execution context
* @return true if tool should be allowed
*/
boolean isAllowed(Tool tool, ToolExecutionContext context);
}Default Implementation:
public class DefaultToolFilter implements ToolFilter {
@Override
public boolean isAllowed(Tool tool, ToolExecutionContext context) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// Admin tools require admin role
if (tool.getName().startsWith("admin_")) {
return auth != null && auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
return true;
}
}PromptRenderer
Renders prompt templates with parameters.
@FunctionalInterface
public interface PromptRenderer {
/**
* Render a prompt template with parameters.
*
* @param template The prompt template
* @param parameters Parameters to substitute
* @return Rendered prompt
*/
Prompt render(PromptTemplate template, Map<String, Object> parameters);
}Configuration Classes
McpServerAutoConfiguration
Auto-configuration for MCP servers.
@Configuration
@AutoConfigureAfter({WebMvcAutoConfiguration.class})
@ConditionalOnClass({McpServer.class})
@ConditionalOnProperty(name = "spring.ai.mcp.enabled", havingValue = "true", matchIfMissing = true)
public class McpServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public McpServerProperties mcpProperties() {
return new McpServerProperties();
}
@Bean
@ConditionalOnMissingBean
public McpServer mcpServer(
McpServerProperties properties,
ObjectProvider<List<Tool>> tools,
ObjectProvider<List<PromptTemplate>> prompts
) {
McpServer.Builder builder = McpServer.builder()
.serverInfo(properties.getServer().getName(), properties.getServer().getVersion())
.transport(createTransport(properties.getTransport()));
tools.ifAvailable(toolList -> toolList.forEach(builder::tool));
prompts.ifAvailable(promptList -> promptList.forEach(builder::prompt));
return builder.build();
}
private Transport createTransport(TransportConfig config) {
// Create transport based on configuration
return switch (config.getType()) {
case STDIO -> new StdioTransport();
case HTTP -> new HttpTransport(config.getHttp().getPort());
case SSE -> new SseTransport(config.getHttp().getPort(), config.getHttp().getPath());
};
}
@Bean
@ConditionalOnMissingBean
public ToolRegistry toolRegistry(ApplicationContext context) {
ToolRegistry registry = new ToolRegistry();
Map<String, Object> toolBeans = context.getBeansWithAnnotation(Component.class);
toolBeans.values().forEach(bean -> {
Method[] methods = bean.getClass().getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Tool.class)) {
registry.register(Tool.fromMethod(method, bean));
}
}
});
return registry;
}
}McpServerProperties
Configuration properties for MCP server.
@ConfigurationProperties(prefix = "spring.ai.mcp")
public class McpServerProperties {
private ServerProperties server = new ServerProperties();
private TransportProperties transport = new TransportProperties();
private SecurityProperties security = new SecurityProperties();
private ToolsProperties tools = new ToolsProperties();
private PromptsProperties prompts = new PromptsProperties();
private LoggingProperties logging = new LoggingProperties();
private MetricsProperties metrics = new MetricsProperties();
@Data
public static class ServerProperties {
private String name = "spring-ai-mcp-server";
private String version = "1.0.0";
private String description = "Spring AI MCP Server";
}
@Data
public static class TransportProperties {
private TransportType type = TransportType.STDIO;
private HttpProperties http = new HttpProperties();
@Data
public static class HttpProperties {
private int port = 8080;
private String path = "/mcp";
private CorsProperties cors = new CorsProperties();
@Data
public static class CorsProperties {
private boolean enabled = true;
private List<String> allowedOrigins = List.of("*");
private List<String> allowedMethods = List.of("GET", "POST");
private List<String> allowedHeaders = List.of("*");
}
}
}
@Data
public static class SecurityProperties {
private boolean enabled = false;
private AuthorizationProperties authorization = new AuthorizationProperties();
private AuditProperties audit = new AuditProperties();
@Data
public static class AuthorizationProperties {
private AuthorizationMode mode = AuthorizationMode.ROLE_BASED;
private boolean defaultDeny = true;
private List<String> allowedTools = List.of();
private List<String> adminTools = List.of("admin_*");
}
@Data
public static class AuditProperties {
private boolean enabled = true;
private List<String> auditedOperations = List.of("*");
}
public enum AuthorizationMode {
NONE, ROLE_BASED, PERMISSION_BASED, ATTRIBUTE_BASED
}
}
@Data
public static class ToolsProperties {
private String packageScan = "com.example.mcp.tools";
private ValidationProperties validation = new ValidationProperties();
private CachingProperties caching = new CachingProperties();
@Data
public static class ValidationProperties {
private boolean enabled = true;
private Duration maxExecutionTime = Duration.ofSeconds(30);
private int maxArgumentsSize = 1000000; // 1MB
}
@Data
public static class CachingProperties {
private boolean enabled = true;
private Duration ttl = Duration.ofMinutes(5);
private int maxSize = 100;
}
}
@Data
public static class PromptsProperties {
private String packageScan = "com.example.mcp.prompts";
private CachingProperties caching = new CachingProperties();
@Data
public static class CachingProperties {
private boolean enabled = true;
private Duration ttl = Duration.ofHours(1);
private int maxSize = 1000;
}
}
// Additional nested properties...
}Transport Implementations
Transport Interface
public interface Transport {
/**
* Start the transport.
*/
void start() throws IOException;
/**
* Stop the transport.
*/
void stop() throws IOException;
/**
* Send a message.
*
* @param message The message to send
*/
void send(Message message) throws IOException;
/**
* Receive a message.
*
* @return The received message
*/
Message receive() throws IOException;
/**
* Check if transport is connected.
*/
boolean isConnected();
}StdioTransport
Standard input/output transport for local process communication.
public class StdioTransport implements Transport {
private final ObjectMapper objectMapper = new ObjectMapper();
private final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private final PrintWriter writer = new PrintWriter(System.out, true);
private volatile boolean running = false;
@Override
public void start() throws IOException {
running = true;
log.info("STDIO transport started");
}
@Override
public void stop() throws IOException {
running = false;
reader.close();
writer.close();
log.info("STDIO transport stopped");
}
@Override
public void send(Message message) throws IOException {
String json = objectMapper.writeValueAsString(message);
writer.println(json);
writer.flush();
}
@Override
public Message receive() throws IOException {
String line = reader.readLine();
if (line == null) {
throw new EOFException("End of stream");
}
return objectMapper.readValue(line, Message.class);
}
@Override
public boolean isConnected() {
return running;
}
}HttpTransport
HTTP transport for remote communication.
public class HttpTransport implements Transport {
private final int port;
private final String path;
private final HttpServer server;
private final List<Consumer<Message>> messageHandlers = new CopyOnWriteArrayList<>();
private volatile boolean running = false;
public HttpTransport(int port, String path) throws IOException {
this.port = port;
this.path = path;
this.server = HttpServer.create(new InetSocketAddress(port), 0);
}
@Override
public void start() throws IOException {
server.createContext(path, exchange -> {
if ("POST".equals(exchange.getRequestMethod())) {
String requestBody = new String(exchange.getRequestBody().readAllBytes());
Message message = objectMapper.readValue(requestBody, Message.class);
messageHandlers.forEach(handler -> handler.accept(message));
String response = "{\"status\":\"acknowledged\"}";
exchange.sendResponseHeaders(200, response.getBytes().length);
exchange.getResponseBody().write(response.getBytes());
}
exchange.close();
});
server.start();
running = true;
log.info("HTTP transport started on port {} path {}", port, path);
}
@Override
public void stop() throws IOException {
server.stop(0);
running = false;
log.info("HTTP transport stopped");
}
@Override
public void send(Message message) throws IOException {
// HTTP transport is request-response based
throw new UnsupportedOperationException("Use HTTP client for sending");
}
@Override
public Message receive() throws IOException {
// HTTP transport receives via POST requests
throw new UnsupportedOperationException("HTTP transport is async");
}
public void addMessageHandler(Consumer<Message> handler) {
messageHandlers.add(handler);
}
@Override
public boolean isConnected() {
return running;
}
}SseTransport
Server-Sent Events transport for real-time communication.
public class SseTransport implements Transport {
private final int port;
private final String path;
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
private final HttpServer server;
private volatile boolean running = false;
public SseTransport(int port, String path) throws IOException {
this.port = port;
this.path = path;
this.server = HttpServer.create(new InetSocketAddress(port), 0);
}
@Override
public void start() throws IOException {
// SSE endpoint for receiving messages
server.createContext(path + "/sse", exchange -> {
if ("GET".equals(exchange.getRequestMethod())) {
handleSseConnection(exchange);
}
});
// POST endpoint for sending messages
server.createContext(path, exchange -> {
if ("POST".equals(exchange.getRequestMethod())) {
handleMessage(exchange);
}
});
server.start();
running = true;
log.info("SSE transport started on port {} path {}", port, path);
}
private void handleSseConnection(HttpExchange exchange) throws IOException {
Headers headers = exchange.getResponseHeaders();
headers.add("Content-Type", "text/event-stream");
headers.add("Cache-Control", "no-cache");
headers.add("Connection", "keep-alive");
exchange.sendResponseHeaders(200, 0);
// Keep connection open
OutputStream os = exchange.getResponseBody();
emitters.add(new SseEmitter(os, exchange));
}
private void handleMessage(HttpExchange exchange) throws IOException {
String requestBody = new String(exchange.getRequestBody().readAllBytes());
Message message = objectMapper.readValue(requestBody, Message.class);
// Send to all SSE clients
broadcast(message);
String response = "{\"status\":\"broadcasted\"}";
exchange.sendResponseHeaders(200, response.getBytes().length);
exchange.getResponseBody().write(response.getBytes());
exchange.close();
}
private void broadcast(Message message) {
String data = "data: " + toJson(message) + "\n\n";
emitters.removeIf(emitter -> !emitter.send(data));
}
@Override
public void send(Message message) throws IOException {
broadcast(message);
}
@Override
public Message receive() throws IOException {
// SSE transport is async, use event-driven approach
throw new UnsupportedOperationException("SSE transport is async");
}
// Additional methods...
}Security Interfaces
ToolValidator
Validates tool arguments and execution context.
public interface ToolValidator {
/**
* Validate tool arguments before execution.
*
* @param tool The tool being executed
* @param arguments The provided arguments
* @throws ValidationException if validation fails
*/
void validateArguments(Tool tool, Map<String, Object> arguments)
throws ValidationException;
/**
* Validate execution context.
*
* @param tool The tool being executed
* @param context The execution context
* @throws ValidationException if validation fails
*/
void validateContext(Tool tool, ToolExecutionContext context)
throws ValidationException;
}Implementation Example:
@Component
public class DefaultToolValidator implements ToolValidator {
private final McpServerProperties properties;
@Override
public void validateArguments(Tool tool, Map<String, Object> arguments)
throws ValidationException {
// Check argument size
int size = arguments.toString().getBytes().length;
if (size > properties.getTools().getValidation().getMaxArgumentsSize()) {
throw new ValidationException("Arguments too large: " + size + " bytes");
}
// Validate based on tool parameter annotations
Arrays.stream(tool.getMethod().getParameters())
.filter(param -> param.isAnnotationPresent(ToolParam.class))
.forEach(param -> validateParameter(param, arguments));
}
private void validateParameter(Parameter param, Map<String, Object> arguments) {
ToolParam annotation = param.getAnnotation(ToolParam.class);
String paramName = param.getName();
if (annotation.required() && !arguments.containsKey(paramName)) {
throw new ValidationException("Required parameter missing: " + paramName);
}
Object value = arguments.get(paramName);
if (value != null) {
validateParameterType(param.getType(), value, paramName);
validateParameterContent(value, paramName);
}
}
private void validateParameterType(Class<?> expectedType, Object value, String paramName) {
if (!expectedType.isAssignableFrom(value.getClass())) {
throw new ValidationException(
String.format("Parameter %s: expected %s, got %s",
paramName, expectedType.getSimpleName(), value.getClass().getSimpleName()));
}
}
private void validateParameterContent(Object value, String paramName) {
if (value instanceof String str) {
// Check for injection patterns
if (str.contains(";") || str.contains("&") || str.contains("|")) {
throw new ValidationException("Invalid characters in parameter: " + paramName);
}
}
}
@Override
public void validateContext(Tool tool, ToolExecutionContext context)
throws ValidationException {
// Check authentication if required
if (tool.requiresAuthentication() && !context.isAuthenticated()) {
throw new ValidationException("Authentication required for tool: " + tool.getName());
}
// Check rate limits
if (exceedsRateLimit(context.getUser(), tool)) {
throw new ValidationException("Rate limit exceeded for tool: " + tool.getName());
}
}
private boolean exceedsRateLimit(User user, Tool tool) {
// Implement rate limiting logic
return false;
}
}SecurityContext
Provides security context for tool execution.
public interface SecurityContext {
/**
* Get the current authentication.
*/
Optional<Authentication> getAuthentication();
/**
* Check if current user has permission.
*/
boolean hasPermission(String permission);
/**
* Check if current user has any of the given roles.
*/
boolean hasAnyRole(String... roles);
/**
* Get user details if authenticated.
*/
Optional<UserDetails> getUserDetails();
/**
* Validate MFA token if required.
*/
boolean validateMfaToken(String token);
}Utility Classes
ToolRegistry
Manages tool registration and lookup.
@Component
public class ToolRegistry {
private final Map<String, Tool> tools = new ConcurrentHashMap<>();
private final List<ToolRegistrationListener> listeners = new CopyOnWriteArrayList<>();
/**
* Register a tool.
*/
public void register(Tool tool) {
tools.put(tool.getName(), tool);
notifyListeners(tool, ToolEvent.Type.REGISTERED);
}
/**
* Unregister a tool.
*/
public void unregister(String toolName) {
Tool removed = tools.remove(toolName);
if (removed != null) {
notifyListeners(removed, ToolEvent.Type.UNREGISTERED);
}
}
/**
* Get a tool by name.
*/
public Optional<Tool> getTool(String name) {
return Optional.ofNullable(tools.get(name));
}
/**
* List all tools.
*/
public List<Tool> listTools() {
return List.copyOf(tools.values());
}
/**
* Add registration listener.
*/
public void addListener(ToolRegistrationListener listener) {
listeners.add(listener);
}
private void notifyListeners(Tool tool, ToolEvent.Type type) {
ToolEvent event = new ToolEvent(tool, type);
listeners.forEach(listener -> listener.onToolEvent(event));
}
}McpMessage
Represents MCP protocol messages.
public final class McpMessage {
private final String jsonrpc = "2.0";
private final String id;
private final String method;
private final Map<String, Object> params;
private final Object result;
private final McpError error;
private McpMessage(Builder builder) {
this.id = builder.id;
this.method = builder.method;
this.params = builder.params;
this.result = builder.result;
this.error = builder.error;
}
public static class Builder {
private String id;
private String method;
private Map<String, Object> params;
private Object result;
private McpError error;
public Builder id(String id) {
this.id = id;
return this;
}
public Builder method(String method) {
this.method = method;
return this;
}
public Builder params(Map<String, Object> params) {
this.params = params;
return this;
}
public Builder result(Object result) {
this.result = result;
return this;
}
public Builder error(McpError error) {
this.error = error;
return this;
}
public McpMessage build() {
return new McpMessage(this);
}
}
// Getters and utility methods...
}McpError
Represents errors in MCP communication.
public class McpError {
private final int code;
private final String message;
private final Map<String, Object> data;
// Error codes
public static final int PARSE_ERROR = -32700;
public static final int INVALID_REQUEST = -32600;
public static final int METHOD_NOT_FOUND = -32601;
public static final int INVALID_PARAMS = -32602;
public static final int INTERNAL_ERROR = -32603;
public McpError(int code, String message) {
this.code = code;
this.message = message;
this.data = null;
}
public McpError(int code, String message, Map<String, Object> data) {
this.code = code;
this.message = message;
this.data = data;
}
// Static factory methods...
}Property Bindings
spring.ai.mcp.*
Main configuration properties.
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.mcp.enabled | boolean | true | Enable MCP server |
spring.ai.mcp.server.name | string | spring-ai-mcp-server | Server name |
spring.ai.mcp.server.version | string | 1.0.0 | Server version |
spring.ai.mcp.transport.type | enum | stdio | Transport type (stdio, http, sse) |
spring.ai.mcp.transport.http.port | int | 8080 | HTTP port |
spring.ai.mcp.transport.http.path | string | /mcp | HTTP path |
spring.ai.mcp.security.enabled | boolean | false | Enable security |
spring.ai.mcp.security.authorization.mode | enum | role-based | Authorization mode |
spring.ai.mcp.security.audit.enabled | boolean | true | Enable auditing |
spring.ai.mcp.tools.package-scan | string | com.example.mcp.tools | Package to scan for tools |
spring.ai.mcp.prompts.package-scan | string | com.example.mcp.prompts | Package to scan for prompts |
Rate Limiting Properties
spring:
ai:
mcp:
rate-limiting:
enabled: true
requests-per-minute: 100
burst-capacity: 150
limit-by: user # user, ip, global
redis:
enabled: true
host: localhost
port: 6379Threading Properties
spring:
ai:
mcp:
threading:
executor:
core-pool-size: 10
max-pool-size: 50
queue-capacity: 100
keep-alive-time: 60s
thread-name-prefix: mcp-
timeout:
default: 30s
per-tool:
long-running-tool: 5m
admin-tool: 1mEvent System
McpEvent
Base class for MCP events.
public abstract class McpEvent extends ApplicationEvent {
private final Instant timestamp;
private final String source;
protected McpEvent(Object source, String eventSource) {
super(source);
this.timestamp = Instant.now();
this.source = eventSource;
}
public Instant getTimestamp() {
return timestamp;
}
public String getSource() {
return source;
}
}ToolEvent
Events related to tool lifecycle.
public class ToolEvent extends McpEvent {
public enum Type {
REGISTERED,
UNREGISTERED,
EXECUTED,
FAILED,
TIMEOUT
}
private final Type type;
private final Tool tool;
private final Map<String, Object> metadata;
public ToolEvent(Tool tool, Type type) {
this(tool, type, Map.of());
}
public ToolEvent(Tool tool, Type type, Map<String, Object> metadata) {
super(tool, "tool-registry");
this.type = type;
this.tool = tool;
this.metadata = metadata;
}
// Getters...
}PromptEvent
Events related to prompt operations.
public class PromptEvent extends McpEvent {
public enum Type {
RENDERED,
CACHED,
FAILED
}
private final Type type;
private final PromptTemplate template;
private final Map<String, Object> parameters;
public PromptEvent(PromptTemplate template, Type type, Map<String, Object> parameters) {
super(template, "prompt-renderer");
this.type = type;
this.template = template;
this.parameters = parameters;
}
// Getters...
}Event Listeners
@Component
public class McpEventListener implements ApplicationListener<McpEvent> {
private final MetricsService metricsService;
private final AuditService auditService;
@Override
public void onApplicationEvent(McpEvent event) {
switch (event) {
case ToolEvent toolEvent -> handleToolEvent(toolEvent);
case PromptEvent promptEvent -> handlePromptEvent(promptEvent);
default -> log.debug("Unhandled event: {}", event.getClass());
}
}
private void handleToolEvent(ToolEvent event) {
metricsService.recordToolEvent(
event.getTool().getName(),
event.getType(),
event.getTimestamp()
);
if (event.getType() == ToolEvent.Type.FAILED) {
auditService.logToolFailure(
event.getTool(),
event.getMetadata()
);
}
}
private void handlePromptEvent(PromptEvent event) {
if (event.getType() == PromptEvent.Type.CACHED) {
metricsService.incrementPromptCacheHit();
}
}
}Async Execution
AsyncToolExecutor
Asynchronous tool execution support.
public class AsyncToolExecutor {
private final ExecutorService executor;
private final ToolExecutor delegate;
public AsyncToolExecutor(ToolExecutor delegate, ExecutorService executor) {
this.delegate = delegate;
this.executor = executor;
}
public CompletableFuture<ToolResult> executeAsync(
String toolName,
Map<String, Object> arguments) {
return CompletableFuture.supplyAsync(() -> {
try {
return delegate.execute(toolName, arguments);
} catch (ToolExecutionException e) {
throw new CompletionException(e);
}
}, executor);
}
public ToolExecutionFuture executeWithTimeout(
String toolName,
Map<String, Object> arguments,
Duration timeout) {
CompletableFuture<ToolResult> future = executeAsync(toolName, arguments);
return new ToolExecutionFuture(future, timeout);
}
}
public class ToolExecutionFuture {
private final CompletableFuture<ToolResult> future;
private final Duration timeout;
public Optional<ToolResult> getResult() throws TimeoutException {
try {
return Optional.ofNullable(
future.get(timeout.toMillis(), TimeUnit.MILLISECONDS)
);
} catch (InterruptedException | ExecutionException e) {
return Optional.empty();
}
}
public boolean cancel() {
return future.cancel(true);
}
public boolean isDone() {
return future.isDone();
}
}Health Checks
McpHealthIndicator
Spring Boot actuator health check for MCP server.
@Component
public class McpHealthIndicator implements HealthIndicator {
private final McpServer mcpServer;
private final ToolRegistry toolRegistry;
@Override
public Health health() {
Health.Builder builder = new Health.Builder();
try {
// Check transport
Transport transport = mcpServer.getTransport();
builder.withDetail("transport", transport.getClass().getSimpleName());
builder.withDetail("connected", transport.isConnected());
// Check tools
List<Tool> tools = toolRegistry.listTools();
builder.withDetail("tools.count", tools.size());
// Sample tool execution
testToolExecution(builder, tools);
builder.status(Status.UP);
} catch (Exception e) {
builder.status(Status.DOWN)
.withDetail("error", e.getMessage());
}
return builder.build();
}
private void testToolExecution(Health.Builder builder, List<Tool> tools) {
if (!tools.isEmpty()) {
Tool sampleTool = tools.get(0);
try {
ToolResult result = sampleTool.execute(Map.of());
builder.withDetail("sampleTool.status", "success");
} catch (Exception e) {
builder.withDetail("sampleTool.status", "failed");
builder.withDetail("sampleTool.error", e.getMessage());
}
}
}
}Performance Metrics
McpMetrics
Micrometer-based metrics for MCP server.
@Component
public class McpMetrics {
private final MeterRegistry meterRegistry;
private Counter toolExecutionsCounter;
private Timer toolExecutionTimer;
private DistributionSummary toolArgumentSize;
private Counter toolFailuresCounter;
private Counter promptRenderCounter;
@PostConstruct
public void initialize() {
toolExecutionsCounter = Counter.builder("mcp.tool.executions")
.description("Number of tool executions")
.register(meterRegistry);
toolExecutionTimer = Timer.builder("mcp.tool.execution.time")
.description("Time taken for tool execution")
.register(meterRegistry);
toolArgumentSize = DistributionSummary.builder("mcp.tool.arguments.size")
.description("Size of tool arguments")
.register(meterRegistry);
toolFailuresCounter = Counter.builder("mcp.tool.failures")
.description("Number of tool failures")
.register(meterRegistry);
promptRenderCounter = Counter.builder("mcp.prompt.renders")
.description("Number of prompt renders")
.register(meterRegistry);
}
public void recordToolExecution(String toolName, long durationMs, boolean success) {
toolExecutionsCounter.increment();
toolExecutionTimer.record(durationMs, TimeUnit.MILLISECONDS);
if (!success) {
toolFailuresCounter.increment();
}
Tags tags = Tags.of("tool", toolName, "success", String.valueOf(success));
meterRegistry.counter("mcp.tool.executions.byTool", tags).increment();
}
public void recordPromptRender(String templateName) {
promptRenderCounter.increment();
Tags tags = Tags.of("template", templateName);
meterRegistry.counter("mcp.prompt.renders.byTemplate", tags).increment();
}
public void recordArgumentSize(int size) {
toolArgumentSize.record(size);
}
}Spring AI MCP Server Examples
Comprehensive examples for implementing MCP servers with Spring AI.
Table of Contents
1. Basic MCP Server Setup 2. Database Query Tools 3. API Integration Tools 4. File System Tools 5. Business Logic Tools 6. Multi-Modal Tools 7. Secure Enterprise Tools 8. Real-Time Streaming 9. Dynamic Tool Registration 10. Complete Application
Basic MCP Server Setup
Minimal Spring Boot MCP Server
@SpringBootApplication
@EnableMcpServer
public class SimpleMcpApplication {
public static void main(String[] args) {
SpringApplication.run(SimpleMcpApplication.class, args);
}
}
@Component
class CalculatorTools {
@Tool(description = "Add two numbers")
public double add(
@ToolParam("First number") double a,
@ToolParam("Second number") double b) {
return a + b;
}
@Tool(description = "Multiply two numbers")
public double multiply(
@ToolParam("First number") double a,
@ToolParam("Second number") double b) {
return a * b;
}
@Tool(description = "Calculate the square root")
public double sqrt(@ToolParam("Number") double x) {
if (x < 0) {
throw new IllegalArgumentException("Cannot calculate square root of negative number");
}
return Math.sqrt(x);
}
}
// application.properties
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.mcp.enabled=true
spring.ai.mcp.transport.type=stdioHTTP Transport Setup
@SpringBootApplication
public class HttpMcpApplication {
public static void main(String[] args) {
SpringApplication.run(HttpMcpApplication.class, args);
}
@Bean
public McpServer mcpServer(List<FunctionCallback> callbacks) {
return McpServer.builder()
.transport(HttpTransport.builder()
.port(8080)
.path("/mcp")
.cors(CorsConfig.builder()
.allowedOrigins("*")
.build())
.build())
.tools(callbacks.stream()
.map(Tool::fromFunctionCallback)
.toList())
.build();
}
}Multi-Transport Server
@Component
public class MultiTransportMcpServer {
private final McpServer stdioServer;
private final McpServer httpServer;
public MultiTransportMcpServer(List<Tool> tools) {
this.stdioServer = McpServer.builder()
.transport(new StdioTransport())
.tools(tools)
.build();
this.httpServer = McpServer.builder()
.transport(HttpTransport.builder()
.port(8081)
.path("/mcp")
.build())
.tools(tools)
.build();
}
@PostConstruct
public void start() {
// Start both servers
new Thread(stdioServer::start).start();
new Thread(httpServer::start).start();
}
}Database Query Tools
PostgreSQL Query Tool
@Component
public class PostgreSqlTools {
private final JdbcTemplate jdbcTemplate;
public PostgreSqlTools(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Tool(description = "Execute a read-only SQL query on PostgreSQL database")
public QueryResult executeReadOnlyQuery(
@ToolParam("SQL SELECT query") String query,
@ToolParam(value = "Query parameters as JSON object", required = false)
String paramsJson) {
// Security: Only allow SELECT queries
String normalizedQuery = query.trim().toUpperCase();
if (!normalizedQuery.startsWith("SELECT")) {
throw new SecurityException("Only SELECT queries are allowed");
}
// Security: Check for dangerous patterns
if (normalizedQuery.contains(";") && !normalizedQuery.endsWith(";")) {
throw new SecurityException("Multiple statements are not allowed");
}
try {
Map<String, Object> params = paramsJson != null && !paramsJson.isBlank()
? new ObjectMapper().readValue(paramsJson, Map.class)
: Map.of();
List<Map<String, Object>> results = jdbcTemplate.queryForList(query, params);
int rowCount = results.size();
return new QueryResult(true, results, null, "Query returned " + rowCount + " rows");
} catch (DataAccessException e) {
return new QueryResult(false, null, e.getMessage(), "Query execution failed");
} catch (JsonProcessingException e) {
return new QueryResult(false, null, e.getMessage(), "Invalid parameters JSON");
}
}
@Tool(description = "Get database schema information")
public SchemaInfo getDatabaseSchema(
@ToolParam(value = "Table name filter", required = false)
String tableFilter) {
String sql = """
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
""";
if (tableFilter != null && !tableFilter.isBlank()) {
sql += " AND table_name LIKE ?";
return new SchemaInfo(jdbcTemplate.queryForList(sql, "%" + tableFilter + "%"));
}
return new SchemaInfo(jdbcTemplate.queryForList(sql));
}
@Tool(description = "Get query execution plan")
public ExecutionPlan explainQuery(
@ToolParam("SQL query to analyze") String query) {
String explainSql = "EXPLAIN (FORMAT JSON, ANALYZE) " + query;
List<Map<String, Object>> plan = jdbcTemplate.queryForList(explainSql);
return new ExecutionPlan(query, plan);
}
@Tool(description = "Get database statistics")
public DatabaseStats getDatabaseStats() {
String sql = """
SELECT schemaname, tablename, n_tup_ins, n_tup_upd, n_tup_del
FROM pg_stat_user_tables
ORDER BY n_tup_ins DESC
LIMIT 10
""";
return new DatabaseStats(jdbcTemplate.queryForList(sql));
}
}
record QueryResult(boolean success, List<Map<String, Object>> data, String error, String message) {}
record SchemaInfo(List<Map<String, Object>> columns) {}
record ExecutionPlan(String query, List<Map<String, Object>> plan) {}
record DatabaseStats(List<Map<String, Object>> stats) {}MongoDB Query Tool
@Component
public class MongoDbTools {
private final MongoTemplate mongoTemplate;
public MongoDbTools(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
@Tool(description = "Execute a MongoDB find query")
public MongoResult findDocuments(
@ToolParam("Collection name") String collection,
@ToolParam(value = "Query filter as JSON", required = false)
String filterJson,
@ToolParam(value = "Maximum documents to return", required = false)
Integer limit) {
try {
Query query = new Query();
if (filterJson != null && !filterJson.isBlank()) {
Document filter = Document.parse(filterJson);
query.addCriteria(Criteria.byExample(filter));
}
if (limit != null) {
query.limit(limit);
}
List<Document> results = mongoTemplate.find(query, Document.class, collection);
return new MongoResult(true, results, null);
} catch (Exception e) {
return new MongoResult(false, null, e.getMessage());
}
}
@Tool(description = "Get collection statistics")
public CollectionStats getCollectionStats(
@ToolParam("Collection name") String collection) {
MongoCollection<Document> coll = mongoTemplate.getCollection(collection);
long count = coll.countDocuments();
return new CollectionStats(collection, count);
}
@Tool(description = "List all collections")
public List<String> listCollections() {
return mongoTemplate.getCollectionNames().stream()
.sorted()
.toList();
}
@Tool(description = "Get collection indexes")
public List<Document> getIndexes(
@ToolParam("Collection name") String collection) {
return mongoTemplate.getCollection(collection)
.listIndexes()
.into(new ArrayList<>());
}
}
record MongoResult(boolean success, List<Document> data, String error) {}
record CollectionStats(String collection, long count) {}Redis Query Tool
@Component
public class RedisTools {
private final RedisTemplate<String, Object> redisTemplate;
public RedisTools(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Tool(description = "Get value from Redis by key")
public RedisValue getValue(
@ToolParam("Redis key") String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
return new RedisValue(key, null, false);
}
String type = determineType(value);
return new RedisValue(key, value.toString(), type, true);
}
@Tool(description = "Get Redis key information")
public KeyInfo getKeyInfo(
@ToolParam("Redis key") String key) {
Long ttl = redisTemplate.getExpire(key);
String type = redisTemplate.type(key).code();
Long size = switch (type) {
case "string" -> redisTemplate.opsForValue().size(key);
case "list" -> redisTemplate.opsForList().size(key);
case "set" -> redisTemplate.opsForSet().size(key);
case "hash" -> (long) redisTemplate.opsForHash().size(key);
default -> 0L;
};
return new KeyInfo(key, type, ttl, size);
}
@Tool(description = "Search for keys by pattern")
public List<String> findKeys(
@ToolParam("Key pattern (e.g., user:*)") String pattern) {
Set<String> keys = redisTemplate.keys(pattern);
return keys != null ? new ArrayList<>(keys) : List.of();
}
private String determineType(Object value) {
if (value instanceof String) return "string";
if (value instanceof List) return "list";
if (value instanceof Set) return "set";
if (value instanceof Map) return "hash";
return "unknown";
}
}
record RedisValue(String key, String value, String type, boolean exists) {}
record KeyInfo(String key, String type, Long ttl, Long size) {}API Integration Tools
REST API Client Tool
@Component
public class RestApiTools {
private final WebClient webClient;
private final CircuitBreakerRegistry circuitBreakerRegistry;
public RestApiTools(WebClient.Builder builder, CircuitBreakerRegistry registry) {
this.webClient = builder
.defaultHeader(HttpHeaders.USER_AGENT, "Spring-AI-MCP-Client/1.0")
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.codecs(config -> config.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
.build();
this.circuitBreakerRegistry = registry;
}
@Tool(description = "Make HTTP GET request to a REST API")
public ApiResponse httpGet(
@ToolParam("URL to request") String url,
@ToolParam(value = "Headers as JSON object", required = false)
String headersJson,
@ToolParam(value = "Timeout in seconds", required = false)
Integer timeout) {
// Validate URL
if (!isValidUrl(url)) {
return new ApiResponse(0, null, Map.of(), "Invalid URL: " + url);
}
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("http-get");
return circuitBreaker.executeSupplier(() -> {
try {
WebClient.RequestHeadersSpec<?> request = webClient.get()
.uri(url)
.httpRequest(httpRequest -> {
if (timeout != null) {
httpRequest.headers(headers ->
headers.setReadTimeout(Duration.ofSeconds(timeout))
);
}
});
// Add custom headers if provided
if (headersJson != null && !headersJson.isBlank()) {
Map<String, String> headers = new ObjectMapper().readValue(headersJson, Map.class);
request.headers(httpHeaders -> headers.forEach(httpHeaders::add));
}
ResponseEntity<String> response = request
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
Mono.error(new ApiException("HTTP error: " + clientResponse.statusCode()))
)
.toEntity(String.class)
.block();
if (response == null) {
return new ApiResponse(500, null, Map.of(), "No response received");
}
String body = response.getBody();
Object parsedBody = parseResponseBody(body, response.getHeaders().getContentType());
return new ApiResponse(
response.getStatusCode().value(),
parsedBody,
response.getHeaders().toSingleValueMap(),
"Success"
);
} catch (Exception e) {
return new ApiResponse(500, null, Map.of(), "Error: " + e.getMessage());
}
});
}
@Tool(description = "Make HTTP POST request to a REST API")
public ApiResponse httpPost(
@ToolParam("URL to request") String url,
@ToolParam("Request body as JSON string") String bodyJson,
@ToolParam(value = "Headers as JSON object", required = false)
String headersJson) {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("http-post");
return circuitBreaker.executeSupplier(() -> {
try {
WebClient.RequestBodySpec request = webClient.post()
.uri(url);
// Add headers
if (headersJson != null && !headersJson.isBlank()) {
Map<String, String> headers = new ObjectMapper().readValue(headersJson, Map.class);
request.headers(httpHeaders -> headers.forEach(httpHeaders::add));
}
// Parse and set body
Object body = parseRequestBody(bodyJson);
Mono<Object> bodyMono = Mono.justOrEmpty(body);
ResponseEntity<String> response = request
.body(bodyMono, Object.class)
.retrieve()
.toEntity(String.class)
.block();
if (response == null) {
return new ApiResponse(500, null, Map.of(), "No response received");
}
Object parsedBody = parseResponseBody(response.getBody(), response.getHeaders().getContentType());
return new ApiResponse(
response.getStatusCode().value(),
parsedBody,
response.getHeaders().toSingleValueMap(),
"Success"
);
} catch (Exception e) {
return new ApiResponse(500, null, Map.of(), "Error: " + e.getMessage());
}
});
}
@Tool(description = "Get API status and health")
public HealthCheckResult checkApiHealth(
@ToolParam("Base URL of the API") String baseUrl) {
String healthUrl = baseUrl.endsWith("/") ? baseUrl + "health" : baseUrl + "/health";
try {
ResponseEntity<String> response = webClient.get()
.uri(healthUrl)
.retrieve()
.toEntity(String.class)
.block();
return new HealthCheckResult(
baseUrl,
response != null && response.getStatusCode().is2xxSuccessful(),
response != null ? response.getStatusCode().value() : 0,
response != null ? response.getBody() : "No response"
);
} catch (Exception e) {
return new HealthCheckResult(baseUrl, false, 0, e.getMessage());
}
}
private boolean isValidUrl(String url) {
try {
URL parsed = new URL(url);
String protocol = parsed.getProtocol();
return "http".equals(protocol) || "https".equals(protocol);
} catch (MalformedURLException e) {
return false;
}
}
private Object parseResponseBody(String body, MediaType contentType) {
if (body == null || body.isBlank()) {
return null;
}
try {
if (contentType != null && contentType.includes(MediaType.APPLICATION_JSON)) {
return new ObjectMapper().readValue(body, Object.class);
}
return body;
} catch (Exception e) {
return body; // Return raw body if parsing fails
}
}
private Object parseRequestBody(String bodyJson) throws JsonProcessingException {
if (bodyJson == null || bodyJson.isBlank()) {
return null;
}
return new ObjectMapper().readValue(bodyJson, Object.class);
}
}
record ApiResponse(int status, Object body, Map<String, String> headers, String message) {}
record HealthCheckResult(String url, boolean healthy, int statusCode, String response) {}
class ApiException extends RuntimeException {
public ApiException(String message) {
super(message);
}
}GraphQL API Tool
@Component
public class GraphQlTools {
private final WebClient webClient;
public GraphQlTools(WebClient.Builder builder) {
this.webClient = builder
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.build();
}
@Tool(description = "Execute GraphQL query")
public GraphQlResponse executeQuery(
@ToolParam("GraphQL endpoint URL") String endpoint,
@ToolParam("GraphQL query") String query,
@ToolParam(value = "Query variables as JSON", required = false)
String variablesJson) {
try {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("query", query);
if (variablesJson != null && !variablesJson.isBlank()) {
Map<String, Object> variables = new ObjectMapper().readValue(variablesJson, Map.class);
requestBody.put("variables", variables);
}
ResponseEntity<String> response = webClient.post()
.uri(endpoint)
.bodyValue(requestBody)
.retrieve()
.toEntity(String.class)
.block();
if (response == null) {
return new GraphQlResponse(null, List.of("No response received"));
}
Map<String, Object> responseBody = new ObjectMapper().readValue(response.getBody(), Map.class);
return new GraphQlResponse(
(Map<String, Object>) responseBody.get("data"),
(List<Map<String, Object>>) responseBody.get("errors")
);
} catch (Exception e) {
return new GraphQlResponse(null, List.of(Map.of("message", e.getMessage())));
}
}
@Tool(description = "Get GraphQL schema")
public String getSchema(
@ToolParam("GraphQL endpoint URL") String endpoint) {
String introspectionQuery = """
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
name
kind
description
}
}
}
""";
GraphQlResponse response = executeQuery(endpoint, introspectionQuery, null);
return new ObjectMapper().valueToTree(response).toPrettyString();
}
}
record GraphQlResponse(Map<String, Object> data, List<Map<String, Object>> errors) {}File System Tools
File Operations Tool
@Component
public class FileSystemTools {
private final Path baseDirectory;
public FileSystemTools(@Value("{mcp.filesystem.base-dir:/tmp/mcp") String baseDir) {
this.baseDirectory = Paths.get(baseDir).toAbsolutePath().normalize();
// Security: Create base directory if it doesn't exist
try {
Files.createDirectories(this.baseDirectory);
} catch (IOException e) {
throw new RuntimeException("Failed to create base directory", e);
}
}
@Tool(description = "Read file contents")
public FileReadResult readFile(
@ToolParam("Path to file, relative to base directory") String filePath) {
try {
Path file = resolveSafePath(filePath);
if (!Files.exists(file)) {
return new FileReadResult(false, null, "File does not exist: " + filePath);
}
if (!Files.isRegularFile(file)) {
return new FileReadResult(false, null, "Path is not a file: " + filePath);
}
// Security: Check file size
long size = Files.size(file);
if (size > 10 * 1024 * 1024) { // 10MB limit
return new FileReadResult(false, null, "File too large: " + size + " bytes");
}
String content = Files.readString(file);
String mimeType = Files.probeContentType(file);
return new FileReadResult(true, content, mimeType, filePath, size);
} catch (IOException e) {
return new FileReadResult(false, null, "Error reading file: " + e.getMessage());
}
}
@Tool(description = "Write content to file")
public FileWriteResult writeFile(
@ToolParam("Path to file, relative to base directory") String filePath,
@ToolParam("Content to write") String content) {
try {
Path file = resolveSafePath(filePath);
// Security: Don't allow writing outside base directory
if (!file.startsWith(baseDirectory)) {
return new FileWriteResult(false, filePath, "Invalid path");
}
// Create parent directories if needed
Files.createDirectories(file.getParent());
Files.writeString(file, content, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
return new FileWriteResult(true, filePath, "File written successfully");
} catch (IOException e) {
return new FileWriteResult(false, filePath, "Error writing file: " + e.getMessage());
}
}
@Tool(description = "List files in directory")
public ListFilesResult listFiles(
@ToolParam(value = "Directory path, relative to base directory", required = false)
String dirPath,
@ToolParam(value = "File pattern (e.g., *.txt)", required = false)
String pattern) {
try {
Path dir = dirPath != null && !dirPath.isBlank()
? resolveSafePath(dirPath)
: baseDirectory;
if (!Files.isDirectory(dir)) {
return new ListFilesResult(false, null, "Not a directory: " + dirPath);
}
try (var stream = Files.list(dir)) {
List<FileInfo> files = stream
.filter(path -> pattern == null ||
path.getFileName().toString().matches(pattern.replace("*", ".*")))
.map(path -> {
try {
return new FileInfo(
path.getFileName().toString(),
Files.isDirectory(path),
Files.size(path),
Files.getLastModifiedTime(path).toInstant()
);
} catch (IOException e) {
return null;
}
})
.filter(Objects::nonNull)
.sorted(Comparator.comparing(FileInfo::name))
.toList();
return new ListFilesResult(true, files, null);
}
} catch (IOException e) {
return new ListFilesResult(false, null, "Error listing files: " + e.getMessage());
}
}
@Tool(description = "Get file information")
public FileInfo getFileInfo(
@ToolParam("Path to file or directory") String path) {
try {
Path file = resolveSafePath(path);
if (!Files.exists(file)) {
return new FileInfo(path, false, -1, null, "File does not exist");
}
boolean isDirectory = Files.isDirectory(file);
long size = isDirectory ? -1 : Files.size(file);
Instant lastModified = Files.getLastModifiedTime(file).toInstant();
return new FileInfo(file.getFileName().toString(), isDirectory, size, lastModified, null);
} catch (IOException e) {
return new FileInfo(path, false, -1, null, "Error: " + e.getMessage());
}
}
private Path resolveSafePath(String path) throws IOException {
// Security: Prevent path traversal
Path file = baseDirectory.resolve(path).normalize();
if (!file.startsWith(baseDirectory)) {
throw new SecurityException("Invalid path");
}
return file;
}
}
record FileReadResult(boolean success, String content, String mimeType, String path, long size, String error) {
public FileReadResult(boolean success, String content, String error) {
this(success, content, null, null, 0, error);
}
}
record FileWriteResult(boolean success, String path, String message) {}
record ListFilesResult(boolean success, List<FileInfo> files, String error) {}
record FileInfo(String name, boolean isDirectory, long size, Instant lastModified, String error) {}CSV Processing Tool
@Component
public class CsvTools {
@Tool(description = "Read and analyze CSV file")
public CsvAnalysis analyzeCsv(
@ToolParam("Path to CSV file") String filePath,
@ToolParam(value = "Has header row", required = false)
Boolean hasHeader) {
boolean header = hasHeader != null ? hasHeader : true;
try (Reader reader = new FileReader(filePath);
CSVParser parser = new CSVParser(reader,
CSVFormat.DEFAULT.builder()
.setHeader()
.setSkipHeaderRecord(header)
.build())) {
List<CSVRecord> records = parser.getRecords();
Map<String, Integer> columnCount = new HashMap<>();
if (header) {
for (String column : parser.getHeaderNames()) {
columnCount.put(column, 0);
}
}
// Analyze data types
Map<String, Set<String>> columnTypes = new HashMap<>();
for (CSVRecord record : records) {
for (int i = 0; i < record.size(); i++) {
String column = header ? parser.getHeaderNames().get(i) : "col_" + i;
String value = record.get(i);
columnTypes.computeIfAbsent(column, k -> new HashSet<>())
.add(inferType(value));
}
}
return new CsvAnalysis(
filePath,
records.size(),
header ? parser.getHeaderNames() : null,
columnTypes,
header
);
} catch (IOException e) {
throw new RuntimeException("Failed to analyze CSV", e);
}
}
private String inferType(String value) {
if (value == null || value.isBlank()) return "empty";
if (value.matches("-?\\d+")) return "integer";
if (value.matches("-?\\d*\\.\\d+")) return "decimal";
if (value.matches("true|false", "true", "false")) return "boolean";
if (value.matches("\\d{4}-\\d{2}-\\d{2}")) return "date";
if (value.matches("\\d{4}(-\\d{2}){2}T\\d{2}(:\\d{2}){2}")) return "datetime";
return "string";
}
@Tool(description = "Convert CSV to JSON")
public List<Map<String, String>> csvToJson(
@ToolParam("Path to CSV file") String filePath) {
try (Reader reader = new FileReader(filePath);
CSVParser parser = new CSVParser(reader,
CSVFormat.DEFAULT.builder().setHeader().build())) {
return parser.getRecords().stream()
.map(record -> {
Map<String, String> json = new LinkedHashMap<>();
for (String header : parser.getHeaderNames()) {
json.put(header, record.get(header));
}
return json;
})
.toList();
} catch (IOException e) {
throw new RuntimeException("Failed to convert CSV to JSON", e);
}
}
}
record CsvAnalysis(
String filePath,
int rowCount,
List<String> headers,
Map<String, Set<String>> columnTypes,
boolean hasHeader
) {}Business Logic Tools
User Management Tools
@Component
public class UserManagementTools {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserManagementTools(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Tool(description = "Search users by criteria")
public List<UserInfo> searchUsers(
@ToolParam(value = "Email contains", required = false)
String email,
@ToolParam(value = "Name contains", required = false)
String name,
@ToolParam(value = "Role", required = false)
String role,
@ToolParam(value = "Active status", required = false)
Boolean active) {
List<User> users = userRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (email != null && !email.isBlank()) {
predicates.add(cb.like(root.get("email"), "%" + email + "%"));
}
if (name != null && !name.isBlank()) {
predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}
if (role != null && !role.isBlank()) {
predicates.add(cb.equal(root.get("role"), role));
}
if (active != null) {
predicates.add(cb.equal(root.get("active"), active));
}
return cb.and(predicates.toArray(new Predicate[0]));
});
return users.stream()
.map(user -> new UserInfo(
user.getId(),
user.getEmail(),
user.getName(),
user.getRole(),
user.isActive(),
user.getCreatedAt()
))
.toList();
}
@Tool(description = "Create a new user account")
public UserCreationResult createUser(
@ToolParam("User email") String email,
@ToolParam("User name") String name,
@ToolParam("User password") String password,
@ToolParam(value = "User role", required = false)
String role) {
// Validate input
if (!isValidEmail(email)) {
return new UserCreationResult(false, null, "Invalid email format");
}
if (password.length() < 8) {
return new UserCreationResult(false, null, "Password must be at least 8 characters");
}
// Check if user exists
if (userRepository.findByEmail(email).isPresent()) {
return new UserCreationResult(false, null, "User already exists: " + email);
}
try {
User user = new User();
user.setEmail(email);
user.setName(name);
user.setPassword(passwordEncoder.encode(password));
user.setRole(role != null ? role : "USER");
user.setActive(true);
user.setCreatedAt(LocalDateTime.now());
User saved = userRepository.save(user);
return new UserCreationResult(
true,
new UserInfo(
saved.getId(),
saved.getEmail(),
saved.getName(),
saved.getRole(),
saved.isActive(),
saved.getCreatedAt()
),
"User created successfully"
);
} catch (Exception e) {
return new UserCreationResult(false, null, "Error creating user: " + e.getMessage());
}
}
@Tool(description = "Get user statistics")
public UserStatistics getUserStatistics() {
long totalUsers = userRepository.count();
long activeUsers = userRepository.countByActive(true);
long inactiveUsers = userRepository.countByActive(false);
Map<String, Long> usersByRole = userRepository.findAll().stream()
.collect(Collectors.groupingBy(User::getRole, Collectors.counting()));
return new UserStatistics(
totalUsers,
activeUsers,
inactiveUsers,
usersByRole
);
}
private boolean isValidEmail(String email) {
return email != null && email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
}
}
record UserInfo(Long id, String email, String name, String role, boolean active, LocalDateTime createdAt) {}
record UserCreationResult(boolean success, UserInfo user, String message) {}
record UserStatistics(long totalUsers, long activeUsers, long inactiveUsers, Map<String, Long> usersByRole) {}Order Management Tools
@Component
public class OrderManagementTools {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
public OrderManagementTools(OrderRepository orderRepository, ProductRepository productRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
@Tool(description = "Create a new order")
public OrderCreationResult createOrder(
@ToolParam("Customer email") String customerEmail,
@ToolParam("Product IDs and quantities as JSON array")
String itemsJson) {
try {
List<OrderItemInput> items = new ObjectMapper().readValue(itemsJson,
new TypeReference<List<OrderItemInput>>() {});
// Validate items
List<OrderItem> orderItems = new ArrayList<>();
BigDecimal totalAmount = BigDecimal.ZERO;
for (OrderItemInput itemInput : items) {
Product product = productRepository.findById(itemInput.productId())
.orElseThrow(() -> new IllegalArgumentException(
"Product not found: " + itemInput.productId()));
if (product.getStock() < itemInput.quantity()) {
throw new IllegalArgumentException(
"Insufficient stock for product: " + product.getName());
}
OrderItem orderItem = new OrderItem();
orderItem.setProductId(product.getId());
orderItem.setProductName(product.getName());
orderItem.setQuantity(itemInput.quantity());
orderItem.setUnitPrice(product.getPrice());
orderItem.setSubtotal(product.getPrice().multiply(BigDecimal.valueOf(itemInput.quantity())));
orderItems.add(orderItem);
totalAmount = totalAmount.add(orderItem.getSubtotal());
}
// Create order
Order order = new Order();
order.setCustomerEmail(customerEmail);
order.setOrderItems(orderItems);
order.setTotalAmount(totalAmount);
order.setStatus("PENDING");
order.setCreatedAt(LocalDateTime.now());
Order saved = orderRepository.save(order);
return new OrderCreationResult(true, saved.getId(), saved.getTotalAmount(),
"Order created successfully");
} catch (Exception e) {
return new OrderCreationResult(false, null, null,
"Error: " + e.getMessage());
}
}
@Tool(description = "Search orders")
public List<OrderInfo> searchOrders(
@ToolParam(value = "Customer email", required = false)
String customerEmail,
@ToolParam(value = "Order status", required = false)
String status,
@ToolParam(value = "Start date (YYYY-MM-DD)", required = false)
String startDate,
@ToolParam(value = "End date (YYYY-MM-DD)", required = false)
String endDate) {
return orderRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (customerEmail != null && !customerEmail.isBlank()) {
predicates.add(cb.equal(root.get("customerEmail"), customerEmail));
}
if (status != null && !status.isBlank()) {
predicates.add(cb.equal(root.get("status"), status));
}
if (startDate != null && !startDate.isBlank()) {
predicates.add(cb.greaterThanOrEqualTo(
root.get("createdAt"), LocalDate.parse(startDate).atStartOfDay()));
}
if (endDate != null && !endDate.isBlank()) {
predicates.add(cb.lessThanOrEqualTo(
root.get("createdAt"), LocalDate.parse(endDate).atTime(LocalTime.MAX)));
}
return cb.and(predicates.toArray(new Predicate[0]));
}).stream().map(order -> new OrderInfo(
order.getId(),
order.getCustomerEmail(),
order.getStatus(),
order.getTotalAmount(),
order.getCreatedAt(),
order.getOrderItems().size()
)).toList();
}
@Tool(description = "Get order statistics")
public OrderStatistics getOrderStatistics(
@ToolParam(value = "Start date (YYYY-MM-DD)", required = false)
String startDate,
@ToolParam(value = "End date (YYYY-MM-DD)", required = false)
String endDate) {
LocalDateTime start = startDate != null ?
LocalDate.parse(startDate).atStartOfDay() :
LocalDateTime.now().minusDays(30);
LocalDateTime end = endDate != null ?
LocalDate.parse(endDate).atTime(LocalTime.MAX) :
LocalDateTime.now();
List<Order> orders = orderRepository.findByCreatedAtBetween(start, end);
BigDecimal totalRevenue = orders.stream()
.map(Order::getTotalAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Long> ordersByStatus = orders.stream()
.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));
Order lastOrder = orders.stream()
.max(Comparator.comparing(Order::getCreatedAt))
.orElse(null);
return new OrderStatistics(
orders.size(),
totalRevenue,
ordersByStatus,
lastOrder != null ? lastOrder.getCreatedAt() : null
);
}
}
record OrderItemInput(Long productId, int quantity) {}
record OrderCreationResult(boolean success, Long orderId, BigDecimal total, String message) {}
record OrderInfo(Long id, String customerEmail, String status, BigDecimal totalAmount,
LocalDateTime createdAt, int itemCount) {}
record OrderStatistics(int totalOrders, BigDecimal totalRevenue,
Map<String, Long> ordersByStatus, LocalDateTime lastOrderDate) {}Multi-Modal Tools
Image Processing Tools
@Component
public class ImageTools {
private final RestTemplate restTemplate;
public ImageTools(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
@Tool(description = "Download and analyze image")
public ImageAnalysis analyzeImage(@ToolParam("Image URL") String imageUrl) {
try {
// Download image
ResponseEntity<byte[]> response = restTemplate.getForEntity(imageUrl, byte[].class);
if (!response.getStatusCode().is2xxSuccessful()) {
return new ImageAnalysis(false, null, "Failed to download image");
}
byte[] imageData = response.getBody();
if (imageData == null) {
return new ImageAnalysis(false, null, "No image data");
}
// Analyze image
String contentType = response.getHeaders().getContentType() != null ?
response.getHeaders().getContentType().toString() : "unknown";
// Load image to get dimensions
InputStream is = new ByteArrayInputStream(imageData);
BufferedImage image = ImageIO.read(is);
Map<String, Object> metadata = Map.of(
"url", imageUrl,
"size", imageData.length,
"contentType", contentType,
"width", image != null ? image.getWidth() : -1,
"height", image != null ? image.getHeight() : -1
);
return new ImageAnalysis(true, metadata, null);
} catch (Exception e) {
return new ImageAnalysis(false, null, "Error: " + e.getMessage());
}
}
@Tool(description = "Convert image format")
public ImageConversionResult convertImage(
@ToolParam("Source image URL") String sourceUrl,
@ToolParam("Target format (jpg, png, gif)") String targetFormat) {
try {
// Download source image
ResponseEntity<byte[]> response = restTemplate.getForEntity(sourceUrl, byte[].class);
byte[] sourceImage = response.getBody();
if (sourceImage == null) {
return new ImageConversionResult(false, null, "No source image data");
}
// Convert image
InputStream is = new ByteArrayInputStream(sourceImage);
BufferedImage image = ImageIO.read(is);
String outputFileName = "converted_" + System.currentTimeMillis() + "." + targetFormat;
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(image, targetFormat, os);
byte[] convertedImage = os.toByteArray();
// Save to temp file
Path outputPath = Paths.get(System.getProperty("java.io.tmpdir"), outputFileName);
Files.write(outputPath, convertedImage);
return new ImageConversionResult(true, outputPath.toString(), null);
} catch (Exception e) {
return new ImageConversionResult(false, null, e.getMessage());
}
}
@Tool(description = "Generate QR code")
public QrCodeResult generateQrCode(
@ToolParam("Text or URL to encode") String content,
@ToolParam(value = "QR code size", required = false)
Integer size) {
int qrSize = size != null ? size : 200;
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(
content,
BarcodeFormat.QR_CODE,
qrSize,
qrSize
);
BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
String outputFileName = "qr_" + System.currentTimeMillis() + ".png";
Path outputPath = Paths.get(System.getProperty("java.io.tmpdir"), outputFileName);
ImageIO.write(image, "PNG", outputPath.toFile());
return new QrCodeResult(true, outputPath.toString(), content, qrSize, null);
} catch (Exception e) {
return new QrCodeResult(false, null, content, qrSize, e.getMessage());
}
}
}
record ImageAnalysis(boolean success, Map<String, Object> metadata, String error) {}
record ImageConversionResult(boolean success, String outputPath, String error) {}
record QrCodeResult(boolean success, String filePath, String content, int size, String error) {}Audio Processing Tools
@Component
public class AudioTools {
private final Logger log = LoggerFactory.getLogger(AudioTools.class);
@Tool(description = "Convert text to speech")
public TextToSpeechResult textToSpeech(
@ToolParam("Text to convert to speech") String text,
@ToolParam(value = "Voice (alloy, echo, fable, onyx, nova, shimmer)", required = false)
String voice,
@ToolParam(value = "Response format (mp3, opus, aac, flac)", required = false)
String responseFormat) {
String selectedVoice = voice != null ? voice : "alloy";
String format = responseFormat != null ? responseFormat : "mp3";
try {
// This would integrate with actual TTS service like OpenAI
// For demonstration, we'll create a dummy audio file
log.info("Converting text to speech: {} chars, voice: {}", text.length(), selectedVoice);
// Simulate processing time
Thread.sleep(1000);
// Create dummy audio file
String outputFileName = "speech_" + System.currentTimeMillis() + "." + format;
Path outputPath = Paths.get(System.getProperty("java.io.tmpdir"), outputFileName);
// Write dummy audio data
Files.write(outputPath, new byte[]{0, 1, 2, 3, 4, 5});
return new TextToSpeechResult(true, outputPath.toString(), text.length(), 1, selectedVoice, format);
} catch (Exception e) {
return new TextToSpeechResult(false, null, text.length(), 0, selectedVoice, format, e.getMessage());
}
}
@Tool(description = "Transcribe audio to text")
public SpeechToTextResult speechToText(
@ToolParam("Path to audio file") String audioFilePath,
@ToolParam(value = "Language (e.g., en, es, fr)", required = false)
String language) {
try {
Path audioPath = Paths.get(audioFilePath);
if (!Files.exists(audioPath)) {
return new SpeechToTextResult(false, null, null, "File not found");
}
long fileSize = Files.size(audioPath);
if (fileSize > 25 * 1024 * 1024) { // 25MB limit
return new SpeechToTextResult(false, null, null, "File too large (max 25MB)");
}
// This would integrate with actual STT service
log.info("Transcribing audio: {} bytes, language: {}", fileSize, language);
// Simulate transcription
Thread.sleep(2000);
String transcription = "This is a simulated transcription of the audio file. " +
"In a real implementation, this would be the actual transcribed text.";
return new SpeechToTextResult(true, transcription, language, null);
} catch (Exception e) {
return new SpeechToTextResult(false, null, language, e.getMessage());
}
}
@Tool(description = "Analyze audio file")
public AudioAnalysis analyzeAudio(@ToolParam("Path to audio file") String audioFilePath) {
try {
Path audioPath = Paths.get(audioFilePath);
if (!Files.exists(audioPath)) {
return new AudioAnalysis(false, null, "File not found");
}
long fileSize = Files.size(audioPath);
String fileName = audioPath.getFileName().toString();
String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
// Get audio format details
AudioFormat format = null;
long duration = 0;
if ("wav".equals(extension)) {
try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(audioPath.toFile())) {
format = audioInputStream.getFormat();
long frames = audioInputStream.getFrameLength();
duration = (long) (frames / format.getSampleRate());
}
}
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("fileName", fileName);
metadata.put("fileSize", fileSize);
metadata.put("fileSizeMB", String.format("%.2f", fileSize / (1024.0 * 1024.0)));
metadata.put("extension", extension);
if (format != null) {
metadata.put("sampleRate", format.getSampleRate());
metadata.put("channels", format.getChannels());
metadata.put("bitsPerSample", format.getSampleSizeInBits());
metadata.put("durationSeconds", duration);
metadata.put("durationFormatted", String.format("%d:%02d", duration / 60, duration % 60));
}
return new AudioAnalysis(true, metadata, null);
} catch (Exception e) {
return new AudioAnalysis(false, null, e.getMessage());
}
}
}
record TextToSpeechResult(
boolean success,
String outputFilePath,
int textLength,
int durationSeconds,
String voice,
String format,
String error
) {}
record SpeechToTextResult(boolean success, String transcription, String language, String error) {}
record AudioAnalysis(boolean success, Map<String, Object> metadata, String error) {}Secure Enterprise Tools
Secure Database Operations
@Component
public class SecureDatabaseTools {
private final JdbcTemplate jdbcTemplate;
private final SecurityService securityService;
public SecureDatabaseTools(JdbcTemplate jdbcTemplate, SecurityService securityService) {
this.jdbcTemplate = jdbcTemplate;
this.securityService = securityService;
}
@PreAuthorize("hasRole('ADMIN') or hasRole('DB_USER')")
@Tool(description = "Execute secure database query (requires authentication)")
public SecureQueryResult executeSecureQuery(
@ToolParam("SQL query") String query,
@ToolParam(value = "Query parameters", required = false)
String paramsJson) {
// Multi-factor authentication for sensitive operations
if (isSensitiveQuery(query)) {
if (!securityService.verifyMfaToken()) {
return new SecureQueryResult(false, null, null, "MFA verification required");
}
securityService.logSensitiveOperation("database_query", query);
}
// Query sanitization
String sanitizedQuery = sanitizeQuery(query);
if (sanitizedQuery == null) {
return new SecureQueryResult(false, null, null, "Query not allowed");
}
try {
Map<String, Object> params = paramsJson != null
? new ObjectMapper().readValue(paramsJson, Map.class)
: Map.of();
long startTime = System.currentTimeMillis();
List<Map<String, Object>> results = jdbcTemplate.queryForList(sanitizedQuery, params);
long duration = System.currentTimeMillis() - startTime;
User user = securityService.getCurrentUser();
securityService.auditQueryExecution(user, query, duration, results.size());
if (duration > 5000) {
log.warn("Slow query detected: {}ms by user {}", duration, user.getUsername());
}
return new SecureQueryResult(true, results, duration, null);
} catch (Exception e) {
securityService.logSecurityEvent("query_error", e.getMessage());
return new SecureQueryResult(false, null, null, "Query execution failed");
}
}
private boolean isSensitiveQuery(String query) {
String upper = query.toUpperCase();
return upper.contains("DELETE") || upper.contains("UPDATE") || upper.contains("DROP") ||
upper.contains("CREATE") || upper.contains("ALTER") || upper.contains("GRANT");
}
private String sanitizeQuery(String query) {
// Remove comments
String sanitized = query.replaceAll("\\/\\*.*?\\*\\/", "")
.replaceAll("--.*$", "")
.trim();
// Check for dangerous patterns
String upper = sanitized.toUpperCase();
if (upper.contains("UNION") || upper.contains(";/*") || upper.contains("xp_")) {
return null; // Potentially dangerous
}
return sanitized;
}
}
record SecureQueryResult(boolean success, List<Map<String, Object>> data, Long durationMs, String error) {}Real-Time Streaming
Streaming MCP Server
@Component
public class StreamingMcpServer {
private final SseEmitter emitter;
private final McpServer mcpServer;
public StreamingMcpServer(McpServer mcpServer) {
this.mcpServer = mcpServer;
this.emitter = new SseEmitter(600000L); // 10 minutes
}
@Tool(description = "Stream real-time data")
public void streamData(
@ToolParam("Data source") String source,
@ToolParam(value = "Stream interval (seconds)", required = false)
Integer interval) {
int seconds = interval != null ? interval : 5;
try {
emitter.send(SseEmitter.event()
.name("stream-start")
.data(Map.of("source", source, "interval", seconds)));
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
AtomicInteger counter = new AtomicInteger(0);
executor.scheduleAtFixedRate(() -> {
try {
Map<String, Object> data = fetchRealTimeData(source);
data.put("timestamp", Instant.now().toString());
data.put("sequence", counter.incrementAndGet());
emitter.send(SseEmitter.event()
.name("data-update")
.data(data));
} catch (IOException e) {
log.error("Failed to send stream data", e);
emitter.completeWithError(e);
executor.shutdown();
}
}, 0, seconds, TimeUnit.SECONDS);
// Stop after 100 updates or on client disconnect
emitter.onCompletion(() -> {
executor.shutdown();
log.info("Stream completed, shutting down executor");
});
emitter.onTimeout(() -> {
executor.shutdown();
emitter.complete();
log.warn("Stream timed out");
});
emitter.onError((ex) -> {
executor.shutdown();
log.error("Stream error occurred", ex);
});
} catch (IOException e) {
log.error("Failed to start stream", e);
emitter.completeWithError(e);
} finally {
// Ensure executor is shutdown in all cases
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (!executor.isShutdown()) {
executor.shutdown();
}
}));
}
}
private Map<String, Object> fetchRealTimeData(String source) {
// Simulate real-time data fetching
return Map.of(
"source", source,
"value", Math.random() * 100,
"unit", "metric",
"status", "active"
);
}
}Complete Application Example
Full Spring Boot MCP Application
@SpringBootApplication
@EnableMcpServer
public class EnterpriseMcpApplication {
public static void main(String[] args) {
SpringApplication.run(EnterpriseMcpApplication.class, args);
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/mcp/*")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
};
}
}Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/mcp/tools/secure*").hasRole("ADMIN")
.requestMatchers("/mcp/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}Production Configuration
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
mcp:
enabled: true
server:
name: enterprise-mcp-server
version: 1.0.0
transport: stdio
security:
enabled: true
authorization:
mode: role-based
audit:
enabled: true
logging:
enabled: true
level: DEBUG
metrics:
enabled: true
export:
prometheus:
enabled: true
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: trueThis comprehensive example demonstrates how to build a complete MCP server with Spring AI for various use cases and enterprise requirements.
Spring AI MCP Server — Implementation Patterns
Detailed patterns for creating tools, prompt templates, and configuring Spring Boot MCP servers.
Tool Creation Patterns
Database Tool
@Component
public class DatabaseTools {
private final JdbcTemplate jdbcTemplate;
public DatabaseTools(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Tool(description = "Execute a safe read-only SQL query")
public List<Map<String, Object>> executeQuery(
@ToolParam("SQL SELECT query") String query,
@ToolParam(value = "Query parameters", required = false) Map<String, Object> params) {
if (!query.trim().toUpperCase().startsWith("SELECT")) {
throw new IllegalArgumentException("Only SELECT queries are allowed");
}
return jdbcTemplate.queryForList(query, params);
}
@Tool(description = "Get table schema information")
public TableSchema getTableSchema(@ToolParam("Table name") String tableName) {
String sql = "SELECT column_name, data_type " +
"FROM information_schema.columns WHERE table_name = ?";
List<Map<String, Object>> columns = jdbcTemplate.queryForList(sql, tableName);
return new TableSchema(tableName, columns);
}
}
record TableSchema(String tableName, List<Map<String, Object>> columns) {}API Integration Tool
@Component
public class ApiTools {
private final WebClient webClient;
public ApiTools(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.build();
}
@Tool(description = "Make HTTP GET request to an API")
public ApiResponse callApi(
@ToolParam("API URL") String url,
@ToolParam(value = "Headers as JSON string", required = false) String headersJson) {
try { new URL(url); } catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid URL format");
}
HttpHeaders headers = new HttpHeaders();
if (headersJson != null && !headersJson.isBlank()) {
try {
Map<String, String> map = new ObjectMapper().readValue(headersJson, Map.class);
map.forEach(headers::add);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Invalid headers JSON");
}
}
return webClient.get()
.uri(url)
.headers(h -> h.addAll(headers))
.retrieve()
.bodyToMono(ApiResponse.class)
.block();
}
}
record ApiResponse(int status, Map<String, Object> body, HttpHeaders headers) {}File System Tool
@Component
public class FileSystemTools {
private final Path basePath;
public FileSystemTools(@Value("${mcp.file.base-path:/tmp}") String basePath) {
this.basePath = Paths.get(basePath).normalize();
}
@Tool(description = "List files in a directory")
public List<FileInfo> listFiles(
@ToolParam(value = "Directory path (relative to base)", required = false) String directory) {
Path targetPath = resolvePath(directory != null ? directory : "");
validatePath(targetPath);
try (Stream<Path> stream = Files.list(targetPath)) {
return stream.filter(Files::isRegularFile).map(this::toFileInfo).toList();
} catch (IOException e) {
throw new RuntimeException("Failed to list files", e);
}
}
@Tool(description = "Read file contents")
public FileContent readFile(
@ToolParam("File path (relative to base)") String filePath,
@ToolParam(value = "Maximum lines to read", required = false) Integer maxLines) {
Path targetPath = resolvePath(filePath);
validatePath(targetPath);
try {
List<String> lines = maxLines != null
? Files.lines(targetPath).limit(maxLines).toList()
: Files.readAllLines(targetPath);
return new FileContent(targetPath.toString(), lines);
} catch (IOException e) {
throw new RuntimeException("Failed to read file", e);
}
}
private Path resolvePath(String relativePath) {
return basePath.resolve(relativePath).normalize();
}
private void validatePath(Path path) {
if (!path.startsWith(basePath)) {
throw new SecurityException("Path traversal not allowed");
}
}
private FileInfo toFileInfo(Path path) {
try {
return new FileInfo(basePath.relativize(path).toString(),
Files.size(path), Files.getLastModifiedTime(path).toInstant());
} catch (IOException e) {
return new FileInfo(path.toString(), 0, Instant.now());
}
}
}
record FileInfo(String path, long size, Instant lastModified) {}
record FileContent(String path, List<String> lines) {}Prompt Template Patterns
@Component
public class CodeReviewPrompts {
@PromptTemplate(
name = "java-code-review",
description = "Review Java code for best practices and issues"
)
public Prompt createJavaCodeReviewPrompt(
@PromptParam("code") String code,
@PromptParam(value = "focusAreas", required = false) List<String> focusAreas) {
String focus = focusAreas != null ? String.join(", ", focusAreas) : "general best practices";
return Prompt.builder()
.system("You are an expert Java code reviewer with 20 years of experience.")
.user("""
Review the following Java code for %s:%s
Format: ## Critical Issues | ## Warnings | ## Suggestions | ## Positive Aspects
""".formatted(focus, code))
.build();
}
@PromptTemplate(
name = "generate-unit-tests",
description = "Generate comprehensive unit tests for Java code"
)
public Prompt createTestGenerationPrompt(
@PromptParam("code") String code,
@PromptParam("className") String className,
@PromptParam(value = "testingFramework", required = false) String framework) {
String testFramework = framework != null ? framework : "JUnit 5";
return Prompt.builder()
.system("You are an expert in test-driven development.")
.user("""
Generate comprehensive unit tests for class %s using %s:%s
Requirements: test all public methods, include edge cases, use AAA pattern, mock dependencies.
""".formatted(className, testFramework, code))
.build();
}
}FunctionCallback (Low-Level Pattern)
For low-level function calling without annotations:
@Configuration
public class FunctionConfig {
@Bean
public FunctionCallback weatherFunction() {
return FunctionCallback.builder()
.function("getCurrentWeather", new WeatherService())
.description("Get the current weather for a location")
.inputType(WeatherRequest.class)
.build();
}
@Bean
public FunctionCallback calculatorFunction() {
return FunctionCallbackWrapper.builder(new Calculator())
.withName("calculate")
.withDescription("Perform mathematical calculations")
.build();
}
}
class WeatherService implements Function<WeatherRequest, WeatherResponse> {
@Override
public WeatherResponse apply(WeatherRequest request) {
return new WeatherResponse(request.location(), 72, "Sunny");
}
}
record WeatherRequest(String location) {}
record WeatherResponse(String location, double temperature, String condition) {}Spring Boot Auto-Configuration
@Configuration
@AutoConfigureAfter({WebMvcAutoConfiguration.class})
@ConditionalOnClass({McpServer.class, ChatModel.class})
@ConditionalOnProperty(name = "spring.ai.mcp.enabled", havingValue = "true", matchIfMissing = true)
public class McpAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public McpServer mcpServer(
List<FunctionCallback> functionCallbacks,
List<PromptTemplate> promptTemplates,
McpServerProperties properties) {
McpServer.Builder builder = McpServer.builder()
.serverInfo("spring-ai-mcp", "1.0.0")
.transport(properties.getTransport().create());
functionCallbacks.forEach(cb -> builder.tool(Tool.fromFunctionCallback(cb)));
promptTemplates.forEach(t -> builder.prompt(Prompt.fromTemplate(t)));
return builder.build();
}
@Bean
@ConditionalOnProperty(name = "spring.ai.mcp.actuator.enabled", havingValue = "true")
public McpHealthIndicator mcpHealthIndicator(McpServer mcpServer) {
return new McpHealthIndicator(mcpServer);
}
}Application Properties Reference
application.yml (complete)
spring:
application:
name: my-mcp-server
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
mcp:
enabled: true
server:
name: my-mcp-server
version: 1.0.0
transport:
type: stdio # stdio | http | sse
http:
port: 8080
path: /mcp
cors:
enabled: true
allowed-origins: "*"
security:
enabled: true
authorization:
mode: role-based # none | role-based | permission-based | attribute-based
default-deny: true
audit:
enabled: true
tools:
package-scan: com.example.mcp.tools
validation:
enabled: true
max-execution-time: 30s
caching:
enabled: true
ttl: 5m
prompts:
package-scan: com.example.mcp.prompts
caching:
enabled: true
ttl: 1h
actuator:
enabled: true
rate-limiter:
enabled: true
requests-per-minute: 100
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
logging:
level:
com.example.mcp: DEBUG
org.springframework.ai: INFOCustom Server Configuration (Interceptors)
@Configuration
public class CustomMcpConfig {
@Bean
public McpServerCustomizer mcpServerCustomizer(MeterRegistry meterRegistry) {
return server -> {
server.addToolInterceptor((tool, args, chain) -> {
long start = System.currentTimeMillis();
Object result = chain.execute(tool, args);
long duration = System.currentTimeMillis() - start;
meterRegistry.timer("mcp.tool.duration", "tool", tool.name())
.record(duration, TimeUnit.MILLISECONDS);
return result;
});
};
}
}Related skills
Forks & variants (1)
Spring Ai Mcp Server Patterns has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
FAQ
What does spring ai mcp server patterns do?
Provides Spring Boot MCP server patterns that create Model Context Protocol servers with Spring AI by defining tool handlers, exposing resources, configuring prompt templates, and setting up transport
When should I invoke spring ai mcp server patterns?
Provides Spring Boot MCP server patterns that create Model Context Protocol servers with Spring AI by defining tool handlers, exposing resources, configuring prompt templates, and setting up transport
What are key capabilities?
Keep tools focused - one operation per tool
Is Spring Ai Mcp Server Patterns safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.