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
Low-level approach for registering MCP specifications as Spring beans. This provides maximum control over tool, resource, prompt, and completion registration.
Register tools using MCP tool specifications.
/**
* Synchronous tool specification (Java record)
* Create instances with: new SyncToolSpecification(tool, handler)
*/
record SyncToolSpecification(
/**
* The tool definition
*/
Tool tool,
/**
* The tool implementation handler
*/
BiFunction<McpSyncServerExchange, CallToolRequest, CallToolResult> handler
) {}
/**
* Asynchronous tool specification (Java record)
* Create instances with: new AsyncToolSpecification(tool, handler)
*/
record AsyncToolSpecification(
/**
* The tool definition
*/
Tool tool,
/**
* The async tool implementation handler
*/
BiFunction<McpAsyncServerExchange, CallToolRequest, Mono<CallToolResult>> handler
) {}Usage Examples:
Synchronous tool registration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springaicommunity.mcp.server.McpServerFeatures.SyncToolSpecification;
import org.springaicommunity.mcp.schema.McpSchema.*;
@Configuration
public class ToolConfig {
@Bean
public List<SyncToolSpecification> calculatorTools() {
// Define add tool
Tool addTool = new Tool(
"add",
"Add two numbers",
Map.of(
"type", "object",
"properties", Map.of(
"a", Map.of("type", "number", "description", "First number"),
"b", Map.of("type", "number", "description", "Second number")
),
"required", List.of("a", "b")
)
);
// Create specification with handler
SyncToolSpecification addSpec = new SyncToolSpecification(
addTool,
(exchange, request) -> {
double a = ((Number) request.arguments().get("a")).doubleValue();
double b = ((Number) request.arguments().get("b")).doubleValue();
double result = a + b;
return CallToolResult.builder()
.addTextContent("Result: " + result)
.build();
}
);
// Define multiply tool
Tool multiplyTool = new Tool(
"multiply",
"Multiply two numbers",
Map.of(
"type", "object",
"properties", Map.of(
"a", Map.of("type", "number"),
"b", Map.of("type", "number")
),
"required", List.of("a", "b")
)
);
SyncToolSpecification multiplySpec = new SyncToolSpecification(
multiplyTool,
(exchange, request) -> {
double a = ((Number) request.arguments().get("a")).doubleValue();
double b = ((Number) request.arguments().get("b")).doubleValue();
double result = a * b;
return CallToolResult.builder()
.addTextContent("Result: " + result)
.build();
}
);
return List.of(addSpec, multiplySpec);
}
}Tool with logging and progress:
@Bean
public List<SyncToolSpecification> advancedTools() {
Tool processTool = new Tool(
"process",
"Process data with logging and progress",
Map.of(
"type", "object",
"properties", Map.of(
"data", Map.of("type", "string", "description", "Data to process")
),
"required", List.of("data")
)
);
SyncToolSpecification spec = new SyncToolSpecification(
processTool,
(exchange, request) -> {
String data = (String) request.arguments().get("data");
// Log to client
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.logger("process-tool")
.data("Starting processing")
.build());
// Send progress
exchange.progressNotification(ProgressNotification.builder()
.progressToken(request.progressToken())
.progress(0.0)
.total(1.0)
.message("Starting")
.build());
// Process data
String result = processData(data);
exchange.progressNotification(ProgressNotification.builder()
.progressToken(request.progressToken())
.progress(1.0)
.total(1.0)
.message("Complete")
.build());
return CallToolResult.builder()
.addTextContent("Processed: " + result)
.build();
}
);
return List.of(spec);
}Asynchronous tool registration:
@Bean
public List<AsyncToolSpecification> asyncTools() {
Tool asyncTool = new Tool(
"async-process",
"Async data processing",
Map.of("type", "object", "properties", Map.of("input", Map.of("type", "string")))
);
AsyncToolSpecification spec = new AsyncToolSpecification(
asyncTool,
(exchange, request) -> {
String input = (String) request.arguments().get("input");
return exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Processing async")
.build())
.then(processDataAsync(input))
.map(result -> CallToolResult.builder()
.addTextContent("Result: " + result)
.build());
}
);
return List.of(spec);
}Register resources using MCP resource specifications.
/**
* Synchronous resource specification (Java record)
* Create instances with: new SyncResourceSpecification(resource, handler)
*/
record SyncResourceSpecification(
/**
* The resource definition
*/
Resource resource,
/**
* The resource read handler
*/
BiFunction<McpSyncServerExchange, ReadResourceRequest, ReadResourceResult> handler
) {}
/**
* Asynchronous resource specification (Java record)
* Create instances with: new AsyncResourceSpecification(resource, handler)
*/
record AsyncResourceSpecification(
/**
* The resource definition
*/
Resource resource,
/**
* The async resource read handler
*/
BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> handler
) {}Usage Example:
@Configuration
public class ResourceConfig {
@Bean
public List<SyncResourceSpecification> systemResources() {
// System info resource
Resource systemInfoResource = new Resource(
"system://info",
"System Information",
"Provides system information",
"application/json"
);
SyncResourceSpecification systemInfoSpec = new SyncResourceSpecification(
systemInfoResource,
(exchange, request) -> {
Map<String, Object> systemInfo = Map.of(
"os", System.getProperty("os.name"),
"javaVersion", System.getProperty("java.version"),
"processors", Runtime.getRuntime().availableProcessors()
);
String jsonContent = new ObjectMapper().writeValueAsString(systemInfo);
return new ReadResourceResult(
List.of(new TextResourceContents(
request.uri(),
"application/json",
jsonContent
))
);
}
);
return List.of(systemInfoSpec);
}
}Register resource templates with URI placeholders.
/**
* Synchronous resource template specification (Java record)
* Create instances with: new SyncResourceTemplateSpecification(template, handler)
*/
record SyncResourceTemplateSpecification(
/**
* The resource template with URI placeholders
*/
ResourceTemplate template,
/**
* The resource read handler
*/
BiFunction<McpSyncServerExchange, ReadResourceRequest, ReadResourceResult> handler
) {}
/**
* Asynchronous resource template specification (Java record)
* Create instances with: new AsyncResourceTemplateSpecification(template, handler)
*/
record AsyncResourceTemplateSpecification(
/**
* The resource template
*/
ResourceTemplate template,
/**
* The async resource read handler
*/
BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> handler
) {}Usage Example:
@Bean
public List<SyncResourceTemplateSpecification> templateResources() {
ResourceTemplate configTemplate = new ResourceTemplate(
"config://{key}",
"Configuration Value",
"Retrieve configuration by key",
"text/plain"
);
SyncResourceTemplateSpecification spec = new SyncResourceTemplateSpecification(
configTemplate,
(exchange, request) -> {
// Extract key from URI (e.g., "config://database" -> "database")
String key = extractKeyFromUri(request.uri());
String value = configurationService.get(key);
return new ReadResourceResult(
List.of(new TextResourceContents(
request.uri(),
"text/plain",
value
))
);
}
);
return List.of(spec);
}Register prompts using MCP prompt specifications.
/**
* Synchronous prompt specification (Java record)
* Create instances with: new SyncPromptSpecification(prompt, handler)
*/
record SyncPromptSpecification(
/**
* The prompt definition
*/
Prompt prompt,
/**
* The prompt handler
*/
BiFunction<McpSyncServerExchange, GetPromptRequest, GetPromptResult> handler
) {}
/**
* Asynchronous prompt specification (Java record)
* Create instances with: new AsyncPromptSpecification(prompt, handler)
*/
record AsyncPromptSpecification(
/**
* The prompt definition
*/
Prompt prompt,
/**
* The async prompt handler
*/
BiFunction<McpAsyncServerExchange, GetPromptRequest, Mono<GetPromptResult>> handler
) {}Usage Example:
@Configuration
public class PromptConfig {
@Bean
public List<SyncPromptSpecification> customPrompts() {
Prompt greetingPrompt = new Prompt(
"greeting",
"Generate a personalized greeting",
List.of(
new PromptArgument("name", "User's name", true),
new PromptArgument("language", "Language for greeting", false)
)
);
SyncPromptSpecification spec = new SyncPromptSpecification(
greetingPrompt,
(exchange, request) -> {
String name = request.arguments().get("name");
String language = request.arguments().getOrDefault("language", "en");
String greeting = switch (language) {
case "es" -> "¡Hola, " + name + "!";
case "fr" -> "Bonjour, " + name + "!";
case "de" -> "Guten Tag, " + name + "!";
default -> "Hello, " + name + "!";
};
return new GetPromptResult(
"Personalized greeting",
List.of(new PromptMessage(
Role.ASSISTANT,
new TextContent(greeting + " How can I help you today?")
))
);
}
);
return List.of(spec);
}
}Register completion providers using MCP completion specifications.
/**
* Synchronous completion specification (Java record)
* Create instances with: new SyncCompletionSpecification(ref, handler)
*/
record SyncCompletionSpecification(
/**
* The prompt reference for completions
*/
PromptReference ref,
/**
* The completion handler
*/
BiFunction<McpSyncServerExchange, CompleteRequest, CompleteResult> handler
) {}
/**
* Asynchronous completion specification (Java record)
* Create instances with: new AsyncCompletionSpecification(ref, handler)
*/
record AsyncCompletionSpecification(
/**
* The prompt reference
*/
PromptReference ref,
/**
* The async completion handler
*/
BiFunction<McpAsyncServerExchange, CompleteRequest, Mono<CompleteResult>> handler
) {}Usage Example:
@Configuration
public class CompletionConfig {
private final List<String> cities = List.of(
"New York", "Los Angeles", "Chicago", "Houston", "Phoenix",
"Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose"
);
@Bean
public List<SyncCompletionSpecification> completions() {
PromptReference cityPromptRef = new PromptReference(
"ref/prompt",
"city-search",
"City name completion"
);
SyncCompletionSpecification spec = new SyncCompletionSpecification(
cityPromptRef,
(exchange, request) -> {
String prefix = request.argument().value().toLowerCase();
List<String> matches = cities.stream()
.filter(city -> city.toLowerCase().startsWith(prefix))
.limit(10)
.toList();
return new CompleteResult(
new CompleteResult.CompleteCompletion(
matches,
matches.size(),
false
)
);
}
);
return List.of(spec);
}
}For stateless servers, use stateless specification variants:
/**
* Stateless synchronous tool specification
*/
interface StatelessSyncToolSpecification {
Tool tool();
BiFunction<McpTransportContext, CallToolRequest, CallToolResult> handler();
}
/**
* Stateless asynchronous tool specification
*/
interface StatelessAsyncToolSpecification {
Tool tool();
BiFunction<McpTransportContext, CallToolRequest, Mono<CallToolResult>> handler();
}
// Similar stateless variants exist for:
// - StatelessSyncResourceSpecification
// - StatelessAsyncResourceSpecification
// - StatelessSyncResourceTemplateSpecification
// - StatelessAsyncResourceTemplateSpecification
// - StatelessSyncPromptSpecification
// - StatelessAsyncPromptSpecificationUsage Example:
@Bean
public List<StatelessSyncToolSpecification> statelessTools() {
Tool simpleTool = new Tool("simple", "Simple stateless tool", simpleSchema);
StatelessSyncToolSpecification spec = new StatelessSyncToolSpecification(
simpleTool,
(context, request) -> {
// No bidirectional operations available
String input = (String) request.arguments().get("input");
return CallToolResult.builder()
.addTextContent("Processed: " + input)
.build();
}
);
return List.of(spec);
}Handlers receive exchange objects providing access to MCP operations:
/**
* Synchronous server exchange
*/
interface McpSyncServerExchange {
/**
* Send a logging notification to the client
*/
void loggingNotification(LoggingMessageNotification notification);
/**
* Send a progress notification to the client
*/
void progressNotification(ProgressNotification notification);
/**
* Ping the client
*/
void ping();
/**
* Request user input via elicitation (stateful only)
*/
<T> StructuredElicitResult<T> elicit(Class<T> type);
/**
* Request LLM sampling (stateful only)
*/
CreateMessageResult createMessage(CreateMessageRequest request);
}/**
* Asynchronous server exchange
*/
interface McpAsyncServerExchange {
/**
* Send a logging notification (reactive)
*/
Mono<Void> loggingNotification(LoggingMessageNotification notification);
/**
* Send a progress notification (reactive)
*/
Mono<Void> progressNotification(ProgressNotification notification);
/**
* Ping the client (reactive)
*/
Mono<Void> ping();
/**
* Request user input (reactive, stateful only)
*/
<T> Mono<StructuredElicitResult<T>> elicit(Class<T> type);
/**
* Request LLM sampling (reactive, stateful only)
*/
Mono<CreateMessageResult> createMessage(CreateMessageRequest request);
}@Configuration
public class McpSpecificationConfig {
@Bean
public List<SyncToolSpecification> tools() {
return List.of(
createCalculatorTool(),
createWeatherTool()
);
}
@Bean
public List<SyncResourceSpecification> resources() {
return List.of(
createSystemInfoResource(),
createStatusResource()
);
}
@Bean
public List<SyncResourceTemplateSpecification> resourceTemplates() {
return List.of(
createConfigResource()
);
}
@Bean
public List<SyncPromptSpecification> prompts() {
return List.of(
createGreetingPrompt(),
createSummaryPrompt()
);
}
@Bean
public List<SyncCompletionSpecification> completions() {
return List.of(
createCityCompletion()
);
}
private SyncToolSpecification createCalculatorTool() {
// Implementation...
}
// Other factory methods...
}| Feature | Programmatic | Annotations |
|---|---|---|
| Flexibility | ✓ Maximum control | Limited |
| Verbosity | More verbose | Concise |
| Type Safety | Runtime | Compile-time |
| Schema Control | Manual | Automatic |
| Dynamic Registration | ✓ Easy | Difficult |
| Learning Curve | Steeper | Gentle |
| Use Case | Complex requirements, dynamic tools | Standard use cases |
// Specification interfaces (synchronous)
import org.springaicommunity.mcp.server.SyncToolSpecification;
import org.springaicommunity.mcp.server.SyncResourceSpecification;
import org.springaicommunity.mcp.server.SyncResourceTemplateSpecification;
import org.springaicommunity.mcp.server.SyncPromptSpecification;
import org.springaicommunity.mcp.server.SyncCompletionSpecification;
// Specification interfaces (asynchronous)
import org.springaicommunity.mcp.server.AsyncToolSpecification;
import org.springaicommunity.mcp.server.AsyncResourceSpecification;
import org.springaicommunity.mcp.server.AsyncResourceTemplateSpecification;
import org.springaicommunity.mcp.server.AsyncPromptSpecification;
import org.springaicommunity.mcp.server.AsyncCompletionSpecification;
// Stateless specification variants (synchronous)
import org.springaicommunity.mcp.server.StatelessSyncToolSpecification;
import org.springaicommunity.mcp.server.StatelessSyncResourceSpecification;
import org.springaicommunity.mcp.server.StatelessSyncPromptSpecification;
// Stateless specification variants (asynchronous)
import org.springaicommunity.mcp.server.StatelessAsyncToolSpecification;
import org.springaicommunity.mcp.server.StatelessAsyncResourceSpecification;
import org.springaicommunity.mcp.server.StatelessAsyncPromptSpecification;
// Server exchange types
import org.springaicommunity.mcp.server.McpSyncServerExchange;
import org.springaicommunity.mcp.server.McpAsyncServerExchange;
// MCP schema types
import org.springaicommunity.mcp.schema.McpSchema.CallToolRequest;
import org.springaicommunity.mcp.schema.McpSchema.CallToolResult;
import org.springaicommunity.mcp.schema.McpSchema.ReadResourceResult;
import org.springaicommunity.mcp.schema.McpSchema.TextResourceContents;
import org.springaicommunity.mcp.schema.McpSchema.GetPromptResult;
import org.springaicommunity.mcp.schema.McpSchema.PromptMessage;
import org.springaicommunity.mcp.schema.McpSchema.CompleteResult;
import org.springaicommunity.mcp.schema.McpSchema.Tool;
// Spring framework
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// Reactive types (for async specifications)
import reactor.core.publisher.Mono;
// Java standard library
import java.util.List;
import java.util.Map;