How to Create a Java MCP Server: A Complete Guide
The Model Context Protocol (MCP) is revolutionizing how AI assistants integrate with external systems. If you're a Java developer looking to build MCP servers, this comprehensive guide walks you through everything you need to know—from foundational concepts to production-ready implementations.
What is MCP and Why Should Java Developers Care?
The Model Context Protocol is an open standard that enables seamless communication between AI models and external tools, databases, and services. Instead of struggling with custom API integrations or maintaining brittle LLM connectors, MCP provides a standardized, bidirectional communication protocol.
For Java developers, MCP opens powerful possibilities:
- Expose existing Java services to AI models without rewriting infrastructure
- Build intelligent tooling that combines your domain expertise with LLM capabilities
- Maintain type safety and performance while connecting to AI systems
- Leverage Spring Boot and existing ecosystems for rapid MCP server development
An MCP server acts as a bridge—it listens for requests from MCP clients (like Claude), executes Java logic, and returns results. Think of it as creating a specialized API endpoint that AI models can invoke with context-awareness.
Architecture Overview: How MCP Servers Work
Before diving into code, let's understand the architecture:
┌─────────────────┐
│ MCP Client │ (e.g., Claude desktop app)
│ (stdio/SSE) │
└────────┬────────┘
│
(JSON-RPC 2.0)
│
┌────────▼────────┐
│ Java MCP │
│ Server │
│ (Spring Boot) │
└────────┬────────┘
│
(Service calls)
│
┌────────▼────────┐
│ Business Logic │
│ Databases │
│ External APIs │
└─────────────────┘
Key concepts:
- Protocol: JSON-RPC 2.0 over stdio or Server-Sent Events (SSE)
- Resources: Data sources the MCP server exposes (database tables, APIs, files)
- Tools: Functions the server can execute for clients
- Prompts: Pre-defined AI prompts the server can provide
- Sampling: Optional sampling capabilities for advanced use cases
Setting Up Your Java MCP Server Project
Prerequisites
# Required
- Java 17+ (we'll use Java 21 LTS for this guide)
- Maven 3.8+ or Gradle 8.0+
- Spring Boot 3.2+
- An IDE (IntelliJ IDEA or VS Code with Java extensions)
# Optional but recommended
- Docker for containerization
- OCI CLI or AWS CLI for cloud deployment
Project Structure
Create a new Spring Boot project. Here's the recommended structure:
mcp-java-server/
├── src/main/java/com/example/mcp/
│ ├── McpServerApplication.java
│ ├── config/
│ │ ├── McpConfig.java
│ │ └── StdioTransportConfig.java
│ ├── protocol/
│ │ ├── JsonRpcRequest.java
│ │ ├── JsonRpcResponse.java
│ │ └── McpMessageHandler.java
│ ├── resources/
│ │ ├── ResourceProvider.java
│ │ └── DatabaseResourceProvider.java
│ ├── tools/
│ │ ├── ToolRegistry.java
│ │ └── QueryTool.java
│ └── handlers/
│ ├── InitializeHandler.java
│ ├── ResourceHandler.java
│ └── CallToolHandler.java
├── src/main/resources/
│ ├── application.yml
│ └── logback.xml
├── pom.xml
└── docker/
└── Dockerfile
Maven POM Configuration
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>mcp-java-server</artifactId>
<version>1.0.0</version>
<name>MCP Java Server</name>
<properties>
<java.version>21</java.version>
<jackson.version>2.17.0</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot Core -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Optional: Database Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Building Your First MCP Server
1. Core Data Models for JSON-RPC
package com.example.mcp.protocol;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonRpcRequest {
private String jsonrpc = "2.0";
private String method;
private Map<String, Object> params;
private String id;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonRpcResponse {
private String jsonrpc = "2.0";
private Object result;
private JsonRpcError error;
private String id;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class JsonRpcError {
private int code;
private String message;
private Object data;
}
2. Protocol Handler
package com.example.mcp.protocol;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class McpMessageHandler {
private final ObjectMapper objectMapper;
private final McpRequestDispatcher dispatcher;
public String handleMessage(String rawMessage) {
try {
JsonRpcRequest request = objectMapper.readValue(
rawMessage,
JsonRpcRequest.class
);
log.info("Received MCP request: method={}, id={}",
request.getMethod(),
request.getId()
);
JsonRpcResponse response = dispatcher.dispatch(request);
return objectMapper.writeValueAsString(response);
} catch (Exception e) {
log.error("Error processing MCP message", e);
return buildErrorResponse(e);
}
}
private String buildErrorResponse(Exception e) {
try {
JsonRpcResponse error = JsonRpcResponse.builder()
.error(JsonRpcError.builder()
.code(-32603)
.message("Internal error: " + e.getMessage())
.build())
.build();
return objectMapper.writeValueAsString(error);
} catch (Exception ex) {
return "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603," +
"\"message\":\"Serialization error\"}}";
}
}
}
3. Resource Provider Implementation
package com.example.mcp.resources;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@Builder
public class McpResource {
private String uri;
private String name;
private String mimeType;
private String description;
}
public interface ResourceProvider {
List<McpResource> listResources();
String getResourceContent(String uri);
}
package com.example.mcp.resources;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class InMemoryResourceProvider implements ResourceProvider {
private final Map<String, String> resources = new ConcurrentHashMap<>();
public InMemoryResourceProvider() {
// Initialize with sample resources
resources.put("database://users",
"{\n \"tables\": [\"users\", \"orders\", \"products\"]\n}");
resources.put("database://analytics",
"{\n \"metrics\": [\"daily_revenue\", \"user_growth\"]\n}");
}
@Override
public List<McpResource> listResources() {
List<McpResource> resourceList = new ArrayList<>();
resources.forEach((uri, content) -> {
resourceList.add(McpResource.builder()
.uri(uri)
.name(extractName(uri))
.mimeType("application/json")
.description("Resource: " + uri)
.build());
});
return resourceList;
}
@Override
public String getResourceContent(String uri) {
log.info("Retrieving resource: {}", uri);
String content = resources.get(uri);
if (content == null) {
throw new IllegalArgumentException("Resource not found: " + uri);
}
return content;
}
private String extractName(String uri) {
return uri.substring(uri.lastIndexOf('/') + 1);
}
}
4. Tool Registry and Execution
package com.example.mcp.tools;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
@Data
@AllArgsConstructor
@Builder
public class McpTool {
private String name;
private String description;
private Map<String, ToolParameter> inputSchema;
}
@Data
@AllArgsConstructor
@Builder
public class ToolParameter {
private String type;
private String description;
private boolean required;
}
package com.example.mcp.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
@Slf4j
@Component
public class ToolRegistry {
private final Map<String, ToolExecutor> executors = new HashMap<>();
public ToolRegistry() {
registerDefaultTools();
}
private void registerDefaultTools() {
// Register query tool
executors.put("query_database", new QueryDatabaseTool());
// Register analytics tool
executors.put("get_analytics", new AnalyticsTool());
// Register date formatting tool
executors.put("format_date", new DateFormattingTool());
}
public List<McpTool> getAvailableTools() {
return List.of(
McpTool.builder()
.name("query_database")
.description("Execute SQL queries against the database")
.inputSchema(Map.of(
"query", ToolParameter.builder()
.type("string")
.description("SQL query to execute")
.required(true)
.build()
))
.build(),
McpTool.builder()
.name("get_analytics")
.description("Fetch analytics metrics")
.inputSchema(Map.of(
"metric", ToolParameter.builder()
.type("string")
.description("Metric name")
.required(true)
.build()
))
.build()
);
}
public Object executeTool(String toolName, Map<String, Object> input) {
ToolExecutor executor = executors.get(toolName);
if (executor == null) {
throw new IllegalArgumentException("Tool not found: " + toolName);
}
log.info("Executing tool: {} with input: {}", toolName, input);
return executor.execute(input);
}
}
public interface ToolExecutor {
Object execute(Map<String, Object> input);
}
@Slf4j
class QueryDatabaseTool implements ToolExecutor {
@Override
public Object execute(Map<String, Object> input) {
String query = (String) input.get("query");
log.info("Executing database query: {}", query);
// Simulate query execution
return Map.of(
"rows", 5,
"query", query,
"status", "success"
);
}
}
@Slf4j
class AnalyticsTool implements ToolExecutor {
@Override
public Object execute(Map<String, Object> input) {
String metric = (String) input.get("metric");
log.info("Fetching analytics for: {}", metric);
return Map.of(
"metric", metric,
"value", Math.random() * 10000,
"unit", "USD"
);
}
}
@Slf4j
class DateFormattingTool implements ToolExecutor {
@Override
public Object execute(Map<String, Object> input) {
String date = (String) input.get("date");
return Map.of(
"original", date,
"formatted", "2026-01-15T10:30:00Z"
);
}
}
5. Request Dispatcher
package com.example.mcp.protocol;
import com.example.mcp.resources.ResourceProvider;
import com.example.mcp.tools.ToolRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class McpRequestDispatcher {
private final ResourceProvider resourceProvider;
private final ToolRegistry toolRegistry;
private boolean initialized = false;
public JsonRpcResponse dispatch(JsonRpcRequest request) {
try {
switch (request.getMethod()) {
case "initialize":
return handleInitialize(request);
case "resources/list":
return handleListResources(request);
case "resources/read":
return handleReadResource(request);
case "tools/list":
return handleListTools(request);
case "tools/call":
return handleCallTool(request);
default:
return buildErrorResponse(request.getId(), -32601,
"Method not found: " + request.getMethod());
}
} catch (Exception e) {
log.error("Error dispatching request", e);
return buildErrorResponse(request.getId(), -32603,
"Internal error: " + e.getMessage());
}
}
private JsonRpcResponse handleInitialize(JsonRpcRequest request) {
initialized = true;
Map<String, Object> result = new HashMap<>();
result.put("protocolVersion", "2024-11-05");
result.put("implementation", "java-mcp-server");
result.put("serverVersion", "1.0.0");
Map<String, Object> capabilities = new HashMap<>();
capabilities.put("resources", Map.of("subscribe", false));
capabilities.put("tools", new HashMap<>());
result.put("capabilities", capabilities);
return JsonRpcResponse.builder()
.result(result)
.id(request.getId())
.build();
}
private JsonRpcResponse handleListResources(JsonRpcRequest request) {
Map<String, Object> result = new HashMap<>();
result.put("resources", resourceProvider.listResources());
return JsonRpcResponse.builder()
.result(result)
.id(request.getId())
.build();
}
private JsonRpcResponse handleReadResource(JsonRpcRequest request) {
String uri = (String) request.getParams().get("uri");
String content = resourceProvider.getResourceContent(uri);
Map<String, Object> result = new HashMap<>();
result.put("contents", new Object[]{
Map.of(
"uri", uri,
"mimeType", "application/json",
"text", content
)
});
return JsonRpcResponse.builder()
.result(result)
.id(request.getId())
.build();
}
private JsonRpcResponse handleListTools(JsonRpcRequest request) {
Map<String, Object> result = new HashMap<>();
result.put("tools", toolRegistry.getAvailableTools());
return JsonRpcResponse.builder()
.result(result)
.id(request.getId())
.build();
}
private JsonRpcResponse handleCallTool(JsonRpcRequest request) {
String toolName = (String) request.getParams().get("name");
@SuppressWarnings("unchecked")
Map<String, Object> arguments = (Map<String, Object>) request.getParams().get("arguments");
Object result = toolRegistry.executeTool(toolName, arguments);
Map<String, Object> content = new HashMap<>();
content.put("type", "text");
content.put("text", result.toString());
Map<String, Object> responseResult = new HashMap<>();
responseResult.put("content", new Object[]{content});
return JsonRpcResponse.builder()
.result(responseResult)
.id(request.getId())
.build();
}
private JsonRpcResponse buildErrorResponse(String id, int code, String message) {
return JsonRpcResponse.builder()
.error(JsonRpcError.builder()
.code(code)
.message(message)
.build())
.id(id)
.build();
}
}
6. Stdio Transport Implementation
package com.example.mcp.transport;
import com.example.mcp.protocol.McpMessageHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
@RequiredArgsConstructor
public class StdioTransport {
private final McpMessageHandler messageHandler;
private PrintWriter out;
private BufferedReader in;
public void start() {
out = new PrintWriter(
new OutputStreamWriter(System.out, StandardCharsets.UTF_8),
true
);
in = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8)
);
log.info("Stdio transport started");
startListening();
}
private void startListening() {
Thread listenerThread = new Thread(() -> {
try {
String line;
while ((line = in.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
log.debug("Received: {}", line);
String response = messageHandler.handleMessage(line);
if (response != null && !response.isEmpty()) {
out.println(response);
log.debug("Sent: {}", response);
}
}
} catch (Exception e) {
log.error("Error in stdio listener", e);
}
});
listenerThread.setDaemon(true);
listenerThread.start();
}
public void send(String message) {
out.println(message);
log.debug("Sent message: {}", message);
}
}
7. Application Bootstrap
package com.example.mcp;
import com.example.mcp.transport.StdioTransport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j
@SpringBootApplication
@RequiredArgsConstructor
public class McpServerApplication implements CommandLineRunner {
private final StdioTransport stdioTransport;
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
log.info("Starting Java MCP Server...");
stdioTransport.start();
// Keep the application running
Thread.currentThread().join();
}
}
Deployment and Testing
Building and Running
# Build with Maven
mvn clean package
# Run locally
java -jar target/mcp-java-server-1.0.0.jar
# Or with Spring Boot Maven plugin
mvn spring-boot:run
Testing with MCP Inspector
# Install MCP Inspector
npm install -g @modelcontextprotocol/inspector
# Run your server with inspector
mcp-inspector node /path/to/your/server.js
# For Java:
mcp-inspector java -jar target/mcp-java-server-1.0.0.jar
Docker Deployment
FROM eclipse-temurin:21-jdk-jammy
WORKDIR /app
COPY target/mcp-java-server-1.0.0.jar .
ENTRYPOINT ["java", "-jar", "mcp-java-server-1.0.0.jar"]
Build and run:
docker build -t mcp-java-server .
docker run -it mcp-java-server
Production Considerations
Error Handling and Resilience
@Slf4j
@Component
public class ResilientMcpHandler {
public JsonRpcResponse executeWithRetry(
String method,
Map<String, Object> params) {
int maxRetries = 3;
int retryDelay = 1000;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return execute(method, params);
} catch (TemporaryException e) {
if (attempt == maxRetries) throw e;
log.warn("Attempt {} failed, retrying in {}ms",
attempt, retryDelay);
Thread.sleep(retryDelay);
retryDelay *= 2;
}
}
throw new RuntimeException("Max retries exceeded");
}
}
Monitoring and Logging
Configure structured logging in logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/mcp-server.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/mcp-server-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
Next Steps and Best Practices
- Authentication: Implement OAuth2 or API key validation for production servers
- Rate Limiting: Add request rate limiting to prevent abuse
- Caching: Implement caching for expensive resource operations
- Monitoring: Use Spring Boot Actuator for health checks and metrics
- Testing: Write comprehensive unit and integration tests for your tools
- Documentation: Generate OpenAPI/Swagger docs for your MCP server
- Versioning: Plan for API versioning as your MCP server evolves
Conclusion
Building Java MCP servers gives you the power to connect enterprise Java systems with AI capabilities while maintaining type safety, performance, and reliability. By following this guide, you've learned how to build a production-ready MCP server from scratch.
The beauty of this approach is that you're not tied to Python or JavaScript—you can leverage your existing Java expertise, frameworks, and libraries to create intelligent AI-integrated systems.
Start small: Build a simple MCP server exposing one resource or tool, test it thoroughly, and then expand based on your use cases.
Share this guide if you found it helpful! Connect with me on LinkedIn to discuss MCP implementations, Java architecture, or LLM integration patterns.
Top comments (0)