Awesome-omni-skill java-mcp-server
Best practices and patterns for building Model Context Protocol (MCP) servers in Java using the official MCP Java SDK with reactive streams and Spring integration. Triggers on: **/*.java, **/pom.xml, **/build.gradle, **/build.gradle.kts
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/backend/java-mcp-server" ~/.claude/skills/diegosouzapw-awesome-omni-skill-java-mcp-server && rm -rf "$T"
skills/backend/java-mcp-server/SKILL.mdJava MCP Server Development Guidelines
When building MCP servers in Java, follow these best practices and patterns using the official Java SDK.
Dependencies
Add the MCP Java SDK to your Maven project:
<dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> <version>0.14.1</version> </dependency> </dependencies>
Or for Gradle:
dependencies { implementation("io.modelcontextprotocol.sdk:mcp:0.14.1") }
Server Setup
Create an MCP server using the builder pattern:
import io.mcp.server.McpServer; import io.mcp.server.McpServerBuilder; import io.mcp.server.transport.StdioServerTransport; McpServer server = McpServerBuilder.builder() .serverInfo("my-server", "1.0.0") .capabilities(capabilities -> capabilities .tools(true) .resources(true) .prompts(true)) .build(); // Start with stdio transport StdioServerTransport transport = new StdioServerTransport(); server.start(transport).subscribe();
Adding Tools
Register tool handlers with the server:
import io.mcp.server.tool.Tool; import io.mcp.server.tool.ToolHandler; import reactor.core.publisher.Mono; // Define a tool Tool searchTool = Tool.builder() .name("search") .description("Search for information") .inputSchema(JsonSchema.object() .property("query", JsonSchema.string() .description("Search query") .required(true)) .property("limit", JsonSchema.integer() .description("Maximum results") .defaultValue(10))) .build(); // Register tool handler server.addToolHandler("search", (arguments) -> { String query = arguments.get("query").asText(); int limit = arguments.has("limit") ? arguments.get("limit").asInt() : 10; // Perform search List<String> results = performSearch(query, limit); return Mono.just(ToolResponse.success() .addTextContent("Found " + results.size() + " results") .build()); });
Adding Resources
Implement resource handlers for data access:
import io.mcp.server.resource.Resource; import io.mcp.server.resource.ResourceHandler; // Register resource list handler server.addResourceListHandler(() -> { List<Resource> resources = List.of( Resource.builder() .name("Data File") .uri("resource://data/example.txt") .description("Example data file") .mimeType("text/plain") .build() ); return Mono.just(resources); }); // Register resource read handler server.addResourceReadHandler((uri) -> { if (uri.equals("resource://data/example.txt")) { String content = loadResourceContent(uri); return Mono.just(ResourceContent.text(content, uri)); } throw new ResourceNotFoundException(uri); }); // Register resource subscribe handler server.addResourceSubscribeHandler((uri) -> { subscriptions.add(uri); log.info("Client subscribed to {}", uri); return Mono.empty(); });
Adding Prompts
Implement prompt handlers for templated conversations:
import io.mcp.server.prompt.Prompt; import io.mcp.server.prompt.PromptMessage; import io.mcp.server.prompt.PromptArgument; // Register prompt list handler server.addPromptListHandler(() -> { List<Prompt> prompts = List.of( Prompt.builder() .name("analyze") .description("Analyze a topic") .argument(PromptArgument.builder() .name("topic") .description("Topic to analyze") .required(true) .build()) .argument(PromptArgument.builder() .name("depth") .description("Analysis depth") .required(false) .build()) .build() ); return Mono.just(prompts); }); // Register prompt get handler server.addPromptGetHandler((name, arguments) -> { if (name.equals("analyze")) { String topic = arguments.getOrDefault("topic", "general"); String depth = arguments.getOrDefault("depth", "basic"); List<PromptMessage> messages = List.of( PromptMessage.user("Please analyze this topic: " + topic), PromptMessage.assistant("I'll provide a " + depth + " analysis of " + topic) ); return Mono.just(PromptResult.builder() .description("Analysis of " + topic + " at " + depth + " level") .messages(messages) .build()); } throw new PromptNotFoundException(name); });
Reactive Streams Pattern
The Java SDK uses Reactive Streams (Project Reactor) for asynchronous processing:
// Return Mono for single results server.addToolHandler("process", (args) -> { return Mono.fromCallable(() -> { String result = expensiveOperation(args); return ToolResponse.success() .addTextContent(result) .build(); }).subscribeOn(Schedulers.boundedElastic()); }); // Return Flux for streaming results server.addResourceListHandler(() -> { return Flux.fromIterable(getResources()) .map(r -> Resource.builder() .uri(r.getUri()) .name(r.getName()) .build()) .collectList(); });
Synchronous Facade
For blocking use cases, use the synchronous API:
import io.mcp.server.McpSyncServer; McpSyncServer syncServer = server.toSyncServer(); // Blocking tool handler syncServer.addToolHandler("greet", (args) -> { String name = args.get("name").asText(); return ToolResponse.success() .addTextContent("Hello, " + name + "!") .build(); });
Transport Configuration
Stdio Transport
For local subprocess communication:
import io.mcp.server.transport.StdioServerTransport; StdioServerTransport transport = new StdioServerTransport(); server.start(transport).block();
HTTP Transport (Servlet)
For HTTP-based servers:
import io.mcp.server.transport.ServletServerTransport; import jakarta.servlet.http.HttpServlet; public class McpServlet extends HttpServlet { private final McpServer server; private final ServletServerTransport transport; public McpServlet() { this.server = createMcpServer(); this.transport = new ServletServerTransport(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { transport.handleRequest(server, req, resp).block(); } }
Spring Boot Integration
Use the Spring Boot starter for seamless integration:
<dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-spring-boot-starter</artifactId> <version>0.14.1</version> </dependency>
Configure the server with Spring:
import org.springframework.context.annotation.Configuration; import io.mcp.spring.McpServerConfigurer; @Configuration public class McpConfiguration { @Bean public McpServerConfigurer mcpServerConfigurer() { return server -> server .serverInfo("spring-server", "1.0.0") .capabilities(cap -> cap .tools(true) .resources(true) .prompts(true)); } }
Register handlers as Spring beans:
import org.springframework.stereotype.Component; import io.mcp.spring.ToolHandler; @Component public class SearchToolHandler implements ToolHandler { @Override public String getName() { return "search"; } @Override public Tool getTool() { return Tool.builder() .name("search") .description("Search for information") .inputSchema(JsonSchema.object() .property("query", JsonSchema.string().required(true))) .build(); } @Override public Mono<ToolResponse> handle(JsonNode arguments) { String query = arguments.get("query").asText(); return Mono.just(ToolResponse.success() .addTextContent("Search results for: " + query) .build()); } }
Error Handling
Use proper error handling with MCP exceptions:
server.addToolHandler("risky", (args) -> { return Mono.fromCallable(() -> { try { String result = riskyOperation(args); return ToolResponse.success() .addTextContent(result) .build(); } catch (ValidationException e) { return ToolResponse.error() .message("Invalid input: " + e.getMessage()) .build(); } catch (Exception e) { log.error("Unexpected error", e); return ToolResponse.error() .message("Internal error occurred") .build(); } }); });
JSON Schema Construction
Use the fluent schema builder:
import io.mcp.json.JsonSchema; JsonSchema schema = JsonSchema.object() .property("name", JsonSchema.string() .description("User's name") .minLength(1) .maxLength(100) .required(true)) .property("age", JsonSchema.integer() .description("User's age") .minimum(0) .maximum(150)) .property("email", JsonSchema.string() .description("Email address") .format("email") .required(true)) .property("tags", JsonSchema.array() .items(JsonSchema.string()) .uniqueItems(true)) .additionalProperties(false) .build();
Logging and Observability
Use SLF4J for logging:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger log = LoggerFactory.getLogger(MyMcpServer.class); server.addToolHandler("process", (args) -> { log.info("Tool called: process, args: {}", args); return Mono.fromCallable(() -> { String result = process(args); log.debug("Processing completed successfully"); return ToolResponse.success() .addTextContent(result) .build(); }).doOnError(error -> { log.error("Processing failed", error); }); });
Propagate context with Reactor:
import reactor.util.context.Context; server.addToolHandler("traced", (args) -> { return Mono.deferContextual(ctx -> { String traceId = ctx.get("traceId"); log.info("Processing with traceId: {}", traceId); return Mono.just(ToolResponse.success() .addTextContent("Processed") .build()); }); });
Testing
Write tests using the synchronous API:
import org.junit.jupiter.api.Test; import static org.assertj.core.Assertions.assertThat; class McpServerTest { @Test void testToolHandler() { McpServer server = createTestServer(); McpSyncServer syncServer = server.toSyncServer(); JsonNode args = objectMapper.createObjectNode() .put("query", "test"); ToolResponse response = syncServer.callTool("search", args); assertThat(response.isError()).isFalse(); assertThat(response.getContent()).hasSize(1); } }
Jackson Integration
The SDK uses Jackson for JSON serialization. Customize as needed:
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); // Use custom mapper with server McpServer server = McpServerBuilder.builder() .objectMapper(mapper) .build();
Content Types
Support multiple content types in responses:
import io.mcp.server.content.Content; server.addToolHandler("multi", (args) -> { return Mono.just(ToolResponse.success() .addTextContent("Plain text response") .addImageContent(imageBytes, "image/png") .addResourceContent("resource://data", "application/json", jsonData) .build()); });
Server Lifecycle
Properly manage server lifecycle:
import reactor.core.Disposable; Disposable serverDisposable = server.start(transport).subscribe(); // Graceful shutdown Runtime.getRuntime().addShutdownHook(new Thread(() -> { log.info("Shutting down MCP server"); serverDisposable.dispose(); server.stop().block(); }));
Common Patterns
Request Validation
server.addToolHandler("validate", (args) -> { if (!args.has("required_field")) { return Mono.just(ToolResponse.error() .message("Missing required_field") .build()); } return processRequest(args); });
Async Operations
server.addToolHandler("async", (args) -> { return Mono.fromCallable(() -> callExternalApi(args)) .timeout(Duration.ofSeconds(30)) .onErrorResume(TimeoutException.class, e -> Mono.just(ToolResponse.error() .message("Operation timed out") .build())) .subscribeOn(Schedulers.boundedElastic()); });
Resource Caching
private final Map<String, String> cache = new ConcurrentHashMap<>(); server.addResourceReadHandler((uri) -> { return Mono.fromCallable(() -> cache.computeIfAbsent(uri, this::loadResource)) .map(content -> ResourceContent.text(content, uri)); });
Best Practices
- Use Reactive Streams for async operations and backpressure
- Leverage Spring Boot starter for enterprise applications
- Implement proper error handling with specific error messages
- Use SLF4J for logging, not System.out
- Validate inputs in tool and prompt handlers
- Support graceful shutdown with proper resource cleanup
- Use bounded elastic scheduler for blocking operations
- Propagate context for observability in reactive chains
- Test with synchronous API for simplicity
- Follow Java naming conventions (camelCase for methods, PascalCase for classes)