spring-ai-mcp-server-guide

joshiabhishek908/spring-ai-mcp-server-guide

3.2

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.

Tools
3
Resources
0
Prompts
0

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?

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

  1. Tool Not Found

    • Verify tool is properly annotated with @Component
    • Check tool name matches the request
  2. Resource Access Denied

    • Validate resource permissions
    • Check security configurations
  3. Timeout Issues

    • Increase timeout configurations
    • Implement proper async handling
  4. 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

  1. Fork the repository
  2. Create a feature branch
  3. Commit your changes
  4. Push to the branch
  5. 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