Spring Boot Starter for building Model Context Protocol (MCP) servers with auto-configuration, annotation-based tool/resource/prompt definitions, and support for STDIO, SSE, and Streamable-HTTP transports
Handle complex edge cases and advanced scenarios in MCP servers.
When you need to accept arbitrary JSON structures:
@McpTool(name = "process_dynamic", description = "Process dynamic JSON data")
public CallToolResult processDynamic(CallToolRequest request) {
Map<String, Object> args = request.arguments();
// Handle unknown structure
StringBuilder result = new StringBuilder("Processed fields:\n");
for (Map.Entry<String, Object> entry : args.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Type-safe handling
if (value instanceof String) {
result.append(key).append(": ").append(value).append(" (string)\n");
} else if (value instanceof Number) {
result.append(key).append(": ").append(value).append(" (number)\n");
} else if (value instanceof Boolean) {
result.append(key).append(": ").append(value).append(" (boolean)\n");
} else if (value instanceof Map) {
result.append(key).append(": [nested object]\n");
} else if (value instanceof List) {
result.append(key).append(": [array with ")
.append(((List<?>) value).size()).append(" items]\n");
}
}
return CallToolResult.builder()
.addTextContent(result.toString())
.build();
}@McpTool(name = "export_large_dataset", description = "Export large dataset")
public CallToolResult exportLargeDataset(
McpSyncRequestContext context,
@McpToolParam(description = "Table name", required = true) String table) {
try {
context.info("Starting export of table: " + table);
// Stream to temporary file
Path tempFile = Files.createTempFile("export-", ".json");
try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) {
writer.write("[");
AtomicInteger count = new AtomicInteger(0);
AtomicInteger lastProgress = new AtomicInteger(0);
jdbcTemplate.query(
"SELECT * FROM " + table,
rs -> {
if (count.get() > 0) {
writer.write(",");
}
// Convert row to JSON
Map<String, Object> row = new HashMap<>();
ResultSetMetaData metaData = rs.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
row.put(metaData.getColumnName(i), rs.getObject(i));
}
writer.write(new ObjectMapper().writeValueAsString(row));
// Update progress every 100 rows
int current = count.incrementAndGet();
if (current % 100 == 0) {
int progress = Math.min(90, current / 100);
if (progress > lastProgress.get()) {
context.progress(progress);
lastProgress.set(progress);
}
}
}
);
writer.write("]");
}
context.progress(100);
context.info("Export complete: " + count.get() + " rows");
// Return file path or upload to storage
return CallToolResult.builder()
.addTextContent("Export complete. File: " + tempFile.toString())
.build();
} catch (Exception e) {
context.error("Export failed: " + e.getMessage());
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
}
}@McpTool(name = "process_in_chunks", description = "Process large data in chunks")
public CallToolResult processInChunks(
McpSyncRequestContext context,
@McpToolParam(description = "Data array", required = true) List<String> data) {
int chunkSize = 100;
int totalChunks = (data.size() + chunkSize - 1) / chunkSize;
context.info("Processing " + data.size() + " items in " + totalChunks + " chunks");
List<String> results = new ArrayList<>();
for (int i = 0; i < data.size(); i += chunkSize) {
int end = Math.min(i + chunkSize, data.size());
List<String> chunk = data.subList(i, end);
// Process chunk
String chunkResult = processChunk(chunk);
results.add(chunkResult);
// Update progress
int progress = ((i + chunkSize) * 100) / data.size();
context.progress(Math.min(progress, 100));
context.info("Processed chunk " + (i / chunkSize + 1) + "/" + totalChunks);
}
return CallToolResult.builder()
.addTextContent("Processed " + results.size() + " chunks successfully")
.build();
}@Component
public class ConcurrentTools {
private final ConcurrentHashMap<String, AtomicInteger> counters = new ConcurrentHashMap<>();
private final ReentrantLock lock = new ReentrantLock();
@McpTool(name = "increment_counter", description = "Thread-safe counter increment")
public CallToolResult incrementCounter(
@McpToolParam(description = "Counter name", required = true) String name) {
AtomicInteger counter = counters.computeIfAbsent(name, k -> new AtomicInteger(0));
int newValue = counter.incrementAndGet();
return CallToolResult.builder()
.addTextContent("Counter '" + name + "' is now: " + newValue)
.build();
}
@McpTool(name = "critical_section", description = "Execute with exclusive lock")
public CallToolResult criticalSection(
McpSyncRequestContext context,
@McpToolParam(description = "Operation", required = true) String operation) {
try {
if (!lock.tryLock(5, TimeUnit.SECONDS)) {
return CallToolResult.builder()
.addTextContent("Error: Could not acquire lock within timeout")
.isError(true)
.build();
}
try {
context.info("Lock acquired, executing operation");
// Critical section
Thread.sleep(1000); // Simulate work
return CallToolResult.builder()
.addTextContent("Operation completed: " + operation)
.build();
} finally {
lock.unlock();
context.info("Lock released");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return CallToolResult.builder()
.addTextContent("Error: Operation interrupted")
.isError(true)
.build();
}
}
}@McpTool(name = "long_running_task", description = "Task with timeout")
public CallToolResult longRunningTask(
McpSyncRequestContext context,
@McpToolParam(description = "Duration in seconds", required = true) int duration) {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<String> future = executor.submit(() -> {
for (int i = 0; i < duration; i++) {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Task cancelled");
}
Thread.sleep(1000);
context.progress((i + 1) * 100 / duration);
}
return "Task completed after " + duration + " seconds";
});
// Wait with timeout
String result = future.get(30, TimeUnit.SECONDS);
return CallToolResult.builder()
.addTextContent(result)
.build();
} catch (TimeoutException e) {
context.error("Task timed out after 30 seconds");
executor.shutdownNow();
return CallToolResult.builder()
.addTextContent("Error: Task timed out")
.isError(true)
.build();
} catch (Exception e) {
context.error("Task failed: " + e.getMessage());
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
} finally {
executor.shutdown();
}
}@Component
public class CompatibleTools {
// Works in both modes - uses transport context
@McpTool(name = "simple_calc", description = "Simple calculation")
public int simpleCalc(
McpTransportContext context,
@McpToolParam(description = "Value", required = true) int value) {
// Can access transport metadata
String requestId = context.getRequestId();
return value * 2;
}
// Only works in stateful mode - uses full context
@McpTool(name = "calc_with_progress", description = "Calculation with progress")
public int calcWithProgress(
McpSyncRequestContext context,
@McpToolParam(description = "Value", required = true) int value) {
context.info("Starting calculation");
context.progress(50);
int result = value * 2;
context.progress(100);
return result;
}
// Conditional feature usage
@McpTool(name = "smart_calc", description = "Calculation with optional progress")
public CallToolResult smartCalc(
McpSyncRequestContext context,
@McpToolParam(description = "Value", required = true) int value) {
// Check if progress is available
try {
context.progress(50);
} catch (Exception e) {
// Progress not available in stateless mode, continue anyway
}
int result = value * 2;
try {
context.progress(100);
} catch (Exception e) {
// Ignore
}
return CallToolResult.builder()
.addTextContent("Result: " + result)
.build();
}
}@McpTool(name = "process_image", description = "Process image data")
public CallToolResult processImage(
@McpToolParam(description = "Base64 encoded image", required = true) String base64Image) {
try {
// Decode base64
byte[] imageBytes = Base64.getDecoder().decode(base64Image);
// Process image
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
if (image == null) {
return CallToolResult.builder()
.addTextContent("Error: Invalid image format")
.isError(true)
.build();
}
// Get image info
String info = String.format(
"Image: %dx%d pixels, type: %s",
image.getWidth(),
image.getHeight(),
image.getType()
);
// Return processed image
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
String processedBase64 = Base64.getEncoder().encodeToString(baos.toByteArray());
return CallToolResult.builder()
.addTextContent(info)
.addImageContent(processedBase64, "image/png")
.build();
} catch (IllegalArgumentException e) {
return CallToolResult.builder()
.addTextContent("Error: Invalid base64 encoding")
.isError(true)
.build();
} catch (IOException e) {
return CallToolResult.builder()
.addTextContent("Error: Cannot process image - " + e.getMessage())
.isError(true)
.build();
}
}@Component
public class AdvancedResources {
// Multiple path segments
@McpResource(
uri = "api://{service}/{version}/{endpoint}",
name = "API Endpoint",
description = "Access API endpoints")
public ReadResourceResult getApiEndpoint(
String service,
String version,
String endpoint) {
String url = String.format(
"https://%s.api.example.com/%s/%s",
service, version, endpoint);
String response = restTemplate.getForObject(url, String.class);
return new ReadResourceResult(
List.of(new TextResourceContents(
String.format("api://%s/%s/%s", service, version, endpoint),
"application/json",
response
))
);
}
// Query parameters in URI
@McpResource(
uri = "search://{query}",
name = "Search Results",
description = "Search with query")
public ReadResourceResult search(String query) {
// Parse query parameters from URI
Map<String, String> params = parseQueryParams(query);
String searchTerm = params.getOrDefault("q", "");
int limit = Integer.parseInt(params.getOrDefault("limit", "10"));
List<String> results = searchService.search(searchTerm, limit);
return new ReadResourceResult(
List.of(new TextResourceContents(
"search://" + query,
"application/json",
new ObjectMapper().writeValueAsString(results)
))
);
}
}@McpTool(name = "enhanced_query", description = "Query with LLM enhancement and retry")
public CallToolResult enhancedQuery(
McpSyncRequestContext context,
@McpToolParam(description = "Query", required = true) String query) {
if (!context.sampleEnabled()) {
return CallToolResult.builder()
.addTextContent("Error: Sampling not available")
.isError(true)
.build();
}
int maxRetries = 3;
CreateMessageResult llmResult = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
context.info("Attempt " + attempt + " to enhance query");
llmResult = context.sample(builder -> builder
.messages(List.of(new PromptMessage(
Role.USER,
new TextContent("Enhance this query: " + query)
)))
.maxTokens(100)
.temperature(0.7)
);
break; // Success
} catch (Exception e) {
context.warn("Attempt " + attempt + " failed: " + e.getMessage());
if (attempt == maxRetries) {
return CallToolResult.builder()
.addTextContent("Error: Failed to enhance query after " + maxRetries + " attempts")
.isError(true)
.build();
}
// Wait before retry
try {
Thread.sleep(1000 * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
String enhancedQuery = llmResult.content();
return CallToolResult.builder()
.addTextContent("Enhanced query: " + enhancedQuery)
.build();
}