joshiabhishek908/spring-ai-mcp-server-guide
If you are the rightful owner of spring-ai-mcp-server-guide and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to henry@mcphub.com.
This guide provides a comprehensive overview of building Model Context Protocol (MCP) servers using the Spring AI framework.
Spring AI MCP Server Guide
A comprehensive guide to building Model Context Protocol (MCP) servers using Spring AI framework.
Table of Contents
- What is MCP?
- Prerequisites
- Project Setup
- Creating Your First MCP Server
- Advanced Features
- Examples
- Best Practices
- Troubleshooting
What is MCP?
Model Context Protocol (MCP) is a standard protocol for connecting AI assistants with external systems and data sources. It enables AI models to:
- Access real-time data
- Interact with external APIs
- Perform actions in connected systems
- Maintain context across interactions
Prerequisites
- Java 17 or higher
- Maven 3.6+ or Gradle 7+
- Spring Boot 3.2+
- Spring AI 0.8.0+
- IDE (IntelliJ IDEA, VS Code, etc.)
Project Setup
1. Initialize Spring Boot Project
Create a new Spring Boot project with the following dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
2. Application Configuration
# application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: https://api.openai.com
server:
port: 8080
mcp:
server:
name: "Spring AI MCP Server"
version: "1.0.0"
description: "MCP Server built with Spring AI"
Creating Your First MCP Server
1. MCP Server Configuration
@Configuration
@EnableConfigurationProperties(McpServerProperties.class)
public class McpServerConfig {
@Bean
public McpServer mcpServer(McpServerProperties properties) {
return McpServer.builder()
.name(properties.getName())
.version(properties.getVersion())
.description(properties.getDescription())
.build();
}
}
2. MCP Properties
@ConfigurationProperties(prefix = "mcp.server")
@Data
public class McpServerProperties {
private String name = "Spring AI MCP Server";
private String version = "1.0.0";
private String description = "MCP Server implementation";
private Map<String, String> capabilities = new HashMap<>();
}
3. MCP Tool Definition
@Component
public class WeatherTool implements McpTool {
private final WebClient webClient;
public WeatherTool(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.build();
}
@Override
public String getName() {
return "get_weather";
}
@Override
public String getDescription() {
return "Get current weather information for a location";
}
@Override
public JsonSchema getInputSchema() {
return JsonSchema.builder()
.type("object")
.properties(Map.of(
"location", JsonSchema.builder()
.type("string")
.description("City name or coordinates")
.build()
))
.required(List.of("location"))
.build();
}
@Override
public CompletableFuture<ToolResult> execute(Map<String, Object> arguments) {
String location = (String) arguments.get("location");
return webClient.get()
.uri("https://api.openweathermap.org/data/2.5/weather?q={location}&appid={apiKey}",
location, "your-api-key")
.retrieve()
.bodyToMono(String.class)
.map(response -> ToolResult.success(response))
.onErrorReturn(ToolResult.error("Failed to fetch weather data"))
.toFuture();
}
}
4. MCP Resource Handler
@Component
public class DatabaseResource implements McpResource {
private final JdbcTemplate jdbcTemplate;
public DatabaseResource(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public String getUri() {
return "database://users";
}
@Override
public String getName() {
return "User Database";
}
@Override
public String getDescription() {
return "Access to user database records";
}
@Override
public String getMimeType() {
return "application/json";
}
@Override
public CompletableFuture<ResourceContent> read(Map<String, String> parameters) {
try {
List<Map<String, Object>> users = jdbcTemplate.queryForList("SELECT * FROM users");
String jsonContent = objectMapper.writeValueAsString(users);
return CompletableFuture.completedFuture(
ResourceContent.text(jsonContent, getMimeType())
);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
5. MCP Server Controller
@RestController
@RequestMapping("/mcp")
@Slf4j
public class McpServerController {
private final McpServer mcpServer;
private final List<McpTool> tools;
private final List<McpResource> resources;
public McpServerController(McpServer mcpServer,
List<McpTool> tools,
List<McpResource> resources) {
this.mcpServer = mcpServer;
this.tools = tools;
this.resources = resources;
}
@PostMapping("/initialize")
public ResponseEntity<McpResponse> initialize(@RequestBody McpRequest request) {
log.info("Initializing MCP server with request: {}", request);
McpCapabilities capabilities = McpCapabilities.builder()
.tools(true)
.resources(true)
.prompts(false)
.build();
McpServerInfo serverInfo = McpServerInfo.builder()
.name(mcpServer.getName())
.version(mcpServer.getVersion())
.description(mcpServer.getDescription())
.build();
McpResponse response = McpResponse.builder()
.capabilities(capabilities)
.serverInfo(serverInfo)
.build();
return ResponseEntity.ok(response);
}
@PostMapping("/tools/list")
public ResponseEntity<McpToolsListResponse> listTools() {
List<McpToolInfo> toolInfos = tools.stream()
.map(tool -> McpToolInfo.builder()
.name(tool.getName())
.description(tool.getDescription())
.inputSchema(tool.getInputSchema())
.build())
.toList();
return ResponseEntity.ok(
McpToolsListResponse.builder()
.tools(toolInfos)
.build()
);
}
@PostMapping("/tools/call")
public ResponseEntity<CompletableFuture<McpToolCallResponse>> callTool(
@RequestBody McpToolCallRequest request) {
String toolName = request.getParams().getName();
Map<String, Object> arguments = request.getParams().getArguments();
Optional<McpTool> tool = tools.stream()
.filter(t -> t.getName().equals(toolName))
.findFirst();
if (tool.isEmpty()) {
return ResponseEntity.badRequest().build();
}
CompletableFuture<McpToolCallResponse> response = tool.get()
.execute(arguments)
.thenApply(result -> McpToolCallResponse.builder()
.content(List.of(TextContent.builder()
.type("text")
.text(result.getContent())
.build()))
.isError(result.isError())
.build());
return ResponseEntity.ok(response);
}
@PostMapping("/resources/list")
public ResponseEntity<McpResourcesListResponse> listResources() {
List<McpResourceInfo> resourceInfos = resources.stream()
.map(resource -> McpResourceInfo.builder()
.uri(resource.getUri())
.name(resource.getName())
.description(resource.getDescription())
.mimeType(resource.getMimeType())
.build())
.toList();
return ResponseEntity.ok(
McpResourcesListResponse.builder()
.resources(resourceInfos)
.build()
);
}
}
Advanced Features
1. Custom Tool with Spring AI Integration
@Component
public class AiAnalysisTool implements McpTool {
private final ChatClient chatClient;
public AiAnalysisTool(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@Override
public String getName() {
return "analyze_text";
}
@Override
public String getDescription() {
return "Analyze text using AI and return insights";
}
@Override
public CompletableFuture<ToolResult> execute(Map<String, Object> arguments) {
String text = (String) arguments.get("text");
String analysisType = (String) arguments.getOrDefault("type", "general");
String prompt = String.format(
"Analyze the following text for %s insights: %s",
analysisType, text
);
return CompletableFuture.supplyAsync(() -> {
try {
String analysis = chatClient.prompt(prompt)
.call()
.content();
return ToolResult.success(analysis);
} catch (Exception e) {
return ToolResult.error("Analysis failed: " + e.getMessage());
}
});
}
}
2. Streaming Response Tool
@Component
public class StreamingTool implements McpTool {
private final SseEmitter createStreamingResponse(String query) {
SseEmitter emitter = new SseEmitter();
CompletableFuture.runAsync(() -> {
try {
// Simulate streaming response
for (int i = 0; i < 10; i++) {
Thread.sleep(500);
emitter.send(SseEmitter.event()
.data("Chunk " + (i + 1) + " of response to: " + query));
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
3. Database Integration Tool
@Component
public class DatabaseQueryTool implements McpTool {
private final JdbcTemplate jdbcTemplate;
private final SqlParser sqlParser;
@Override
public CompletableFuture<ToolResult> execute(Map<String, Object> arguments) {
String query = (String) arguments.get("query");
// Validate and sanitize query
if (!sqlParser.isSafeQuery(query)) {
return CompletableFuture.completedFuture(
ToolResult.error("Unsafe query detected")
);
}
return CompletableFuture.supplyAsync(() -> {
try {
List<Map<String, Object>> results = jdbcTemplate.queryForList(query);
return ToolResult.success(objectMapper.writeValueAsString(results));
} catch (Exception e) {
return ToolResult.error("Query execution failed: " + e.getMessage());
}
});
}
}
Examples
Example 1: Simple Calculator Tool
@Component
public class CalculatorTool implements McpTool {
@Override
public String getName() {
return "calculate";
}
@Override
public CompletableFuture<ToolResult> execute(Map<String, Object> arguments) {
String expression = (String) arguments.get("expression");
try {
// Use a safe expression evaluator
double result = evaluateExpression(expression);
return CompletableFuture.completedFuture(
ToolResult.success(String.valueOf(result))
);
} catch (Exception e) {
return CompletableFuture.completedFuture(
ToolResult.error("Invalid expression: " + e.getMessage())
);
}
}
}
Example 2: File System Tool
@Component
public class FileSystemTool implements McpTool {
@Value("${app.file.base-path}")
private String basePath;
@Override
public CompletableFuture<ToolResult> execute(Map<String, Object> arguments) {
String operation = (String) arguments.get("operation");
String path = (String) arguments.get("path");
Path safePath = Paths.get(basePath, path).normalize();
// Security check
if (!safePath.startsWith(Paths.get(basePath))) {
return CompletableFuture.completedFuture(
ToolResult.error("Access denied: Path outside allowed directory")
);
}
return switch (operation) {
case "read" -> readFile(safePath);
case "list" -> listDirectory(safePath);
case "exists" -> checkExists(safePath);
default -> CompletableFuture.completedFuture(
ToolResult.error("Unsupported operation: " + operation)
);
};
}
}
Best Practices
1. Security Considerations
- Always validate and sanitize input parameters
- Implement proper authentication and authorization
- Use parameterized queries for database operations
- Limit file system access to safe directories
- Implement rate limiting for API calls
2. Error Handling
@Component
public class RobustTool implements McpTool {
@Override
public CompletableFuture<ToolResult> execute(Map<String, Object> arguments) {
return CompletableFuture.supplyAsync(() -> {
try {
// Tool implementation
return ToolResult.success("Result");
} catch (ValidationException e) {
log.warn("Validation error: {}", e.getMessage());
return ToolResult.error("Invalid input: " + e.getMessage());
} catch (Exception e) {
log.error("Unexpected error in tool execution", e);
return ToolResult.error("Internal error occurred");
}
});
}
}
3. Performance Optimization
- Use async processing for long-running operations
- Implement caching for frequently accessed data
- Use connection pooling for external services
- Monitor and log performance metrics
4. Configuration Management
@ConfigurationProperties(prefix = "mcp.tools")
@Data
public class ToolsConfiguration {
private Map<String, ToolConfig> configs = new HashMap<>();
@Data
public static class ToolConfig {
private boolean enabled = true;
private Map<String, String> parameters = new HashMap<>();
private RateLimitConfig rateLimit = new RateLimitConfig();
}
}
Testing
Unit Testing
@ExtendWith(MockitoExtension.class)
class WeatherToolTest {
@Mock
private WebClient webClient;
@InjectMocks
private WeatherTool weatherTool;
@Test
void testGetWeather() {
// Test implementation
Map<String, Object> arguments = Map.of("location", "New York");
CompletableFuture<ToolResult> result = weatherTool.execute(arguments);
assertThat(result).succeedsWithin(Duration.ofSeconds(5));
assertThat(result.join().isSuccess()).isTrue();
}
}
Integration Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class McpServerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testToolExecution() {
McpToolCallRequest request = McpToolCallRequest.builder()
.method("tools/call")
.params(McpToolCallParams.builder()
.name("get_weather")
.arguments(Map.of("location", "London"))
.build())
.build();
ResponseEntity<McpToolCallResponse> response = restTemplate.postForEntity(
"/mcp/tools/call", request, McpToolCallResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
}
}
Troubleshooting
Common Issues
-
Tool Not Found
- Verify tool is properly annotated with
@Component
- Check tool name matches the request
- Verify tool is properly annotated with
-
Resource Access Denied
- Validate resource permissions
- Check security configurations
-
Timeout Issues
- Increase timeout configurations
- Implement proper async handling
-
Memory Issues
- Monitor resource usage
- Implement streaming for large responses
Debugging Tips
- Enable debug logging:
logging.level.com.yourpackage=DEBUG
- Use Spring Boot Actuator for monitoring
- Implement health checks for external dependencies
Contributing
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
License
This project is licensed under the MIT License - see the file for details.
Resources
Support
For questions and support, please:
- Check the documentation
- Open an issue in this repository
- Join the Spring AI community discussions