What is MCP (Model Context Protocol)? Explained for Java Devs
1. Overview
MCP (Model Context Protocol) is an open standard that helps LLMs (large language models) connect with data, tools, and external systems in a unified way, rather than each AI application having to write its own integration for each data source. This article explains what MCP is, how its architecture works, and walks you through writing a simple MCP server with Spring Boot (via Spring AI) to expose a tool for LLMs to call. After reading, you’ll understand when to use MCP and when it’s just over-engineering.
2. The Problem MCP Solves
Before MCP came along, each AI application (chatbot, agent) that wanted to access a database, file system, internal API, etc. had to write its own adapter. If you had M AI applications and N data sources, you’d need M × N different integration code.
MCP standardizes this communication layer: an AI application (called a Host) only needs to speak the MCP standard once, and any system that exposes an MCP server can “talk” to it. It’s like USB-C for electronics — one standard connector instead of dozens of different port types.
Note: MCP is not a framework to replace REST or gRPC for regular service-to-service communication. It was created for LLMs/agents to call tools, not for two backend microservices to talk to each other.
3. MCP Architecture
MCP has 3 main roles:
- Host: the application that contains the LLM (e.g., Claude Desktop, or a custom agent you write).
- Client: a component inside the Host that maintains a 1-to-1 connection with an MCP server.
- Server: a process that exposes capabilities (tools, resources, prompts) according to the MCP standard.
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Host │ 1-1 │ MCP Client │ JSON-RPC │ MCP Server │
│ (LLM-based) │◄──────►│ │◄──────────►│ (DB, API, file) │
└──────────────┘ └──────────────┘ └──────────────────┘
Communication between client and server uses JSON-RPC 2.0, transmitted over one of two common transports:
- stdio: the server runs as a child process, and client/server communicate via stdin/stdout. Suitable when the server runs locally.
- SSE/HTTP (Streamable HTTP): used when the server runs remotely, over the network.
3.1. Three Types of Capabilities an MCP Server Can Expose
| Capability | Meaning | Example |
|---|---|---|
| Tools | Functions the LLM can call to perform actions | getWeather(city), createOrder(...) |
| Resources | Data the LLM can read (similar to GET) | Log files, database records |
| Prompts | Prompt templates pre-defined by the server | “Summarize the order using template X” |
Most real-world use cases revolve around Tools, so the examples below focus on that.
4. Writing an MCP Server with Spring AI
Spring AI (from version 1.0.0-M6 onwards) supports MCP via a dedicated starter, allowing you to expose a Java bean as an MCP tool using an annotation, without having to implement JSON-RPC yourself.
4.1. Add Dependency
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
4.2. Define a Tool
@Service
public class WeatherService {
@Tool(description = "Get the current temperature of a city")
public String getTemperature(String city) {
return switch (city.toLowerCase()) {
case "ha noi" -> "28°C, cloudy";
case "sai gon" -> "32°C, sunny";
default -> "No data available for city: " + city;
};
}
}
@Toolmarks this method as an MCP tool. Thedescriptionattribute is very important — the LLM uses this description to decide whether to call the tool and what parameters to pass.getTemperaturetakes aString cityand returns a string describing the weather. The parameters and return type are automatically converted by the MCP server into a JSON schema that the LLM understands.
4.3. Register Tool with MCP Server
@Configuration
public class McpServerConfig {
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder()
.toolObjects(weatherService)
.build();
}
}
MethodToolCallbackProvider scans for methods with @Tool in weatherService and automatically registers them with the MCP server when the application context starts.
4.4. Configure the Server
spring.ai.mcp.server.name=weather-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=true
With stdio=true, the server runs as a standalone process, communicating via stdin/stdout — suitable for quick testing with Claude Desktop or a custom MCP client. If you want to deploy the server as a standalone service running remotely, you’d switch to SSE transport and expose it via an HTTP endpoint instead of stdio.
Start the server:
mvn spring-boot:run
Started WeatherMcpApplication in 1.124 seconds
Registered MCP tool: getTemperature
5. Calling an MCP Server from a Client
Suppose you write a Host application (e.g., an internal agent) that needs to call the getTemperature tool above.
5.1. Client Dependency
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
5.2. Call a Tool
@Component
public class WeatherAgent {
private final McpSyncClient mcpClient;
public WeatherAgent(McpSyncClient mcpClient) {
this.mcpClient = mcpClient;
}
public String askTemperature(String city) {
CallToolRequest request = new CallToolRequest(
"getTemperature",
Map.of("city", city));
CallToolResult result = mcpClient.callTool(request);
return result.content().get(0).text();
}
}
McpSyncClientis the Spring AI client that automatically creates a bean based on the transport configuration (stdio or SSE) declared inapplication.yml.CallToolRequestwraps the tool name (getTemperature) and parameters (city) into a JSON-RPC call.result.content()returns a list of response content items; here we take the firsttextpart.
Client configuration (example: connect to the server via stdio by running the server jar as a subprocess):
spring:
ai:
mcp:
client:
stdio:
connections:
weather-server:
command: java
args: -jar weather-mcp-server.jar
6. Testing Tools with JUnit 5 + AssertJ
Since @Tool is just an annotation on a regular Java method, you can test your business logic independently of MCP — no need to set up the whole server:
class WeatherServiceTest {
private final WeatherService weatherService = new WeatherService();
@Test
void shouldReturnTemperatureForKnownCity() {
String result = weatherService.getTemperature("Ha Noi");
assertThat(result).contains("28°C");
}
@Test
void shouldReturnFallbackMessageForUnknownCity() {
String result = weatherService.getTemperature("Tokyo");
assertThat(result)
.startsWith("No data available for city:")
.contains("Tokyo");
}
}
If you also want to test the MCP integration part (whether the tool is registered with the correct name and schema), you can write a test at the @SpringBootTest level and check the ToolCallbackProvider:
@SpringBootTest
class McpServerConfigTest {
@Autowired
private ToolCallbackProvider toolCallbackProvider;
@Test
void shouldExposeTemperatureTool() {
var toolNames = toolCallbackProvider.getToolCallbacks().stream()
.map(callback -> callback.getToolDefinition().name())
.toList();
assertThat(toolNames).contains("getTemperature");
}
}
This test ensures that whenever you refactor WeatherService, the tool is still exposed with the correct name — avoiding the case where you rename a method but forget to update places where the LLM references it.
7. Common Errors and When Not to Use MCP
Tool description too vague. If the description only says “get data”, the LLM will easily pick the wrong tool when there are many similar tools. Write a specific description that clearly states expected inputs and outputs.
Using MCP to replace internal REST/gRPC. MCP adds a layer of JSON-RPC and its own transport, optimized for the scenario where an LLM calls tools dynamically — not optimized for two backend services calling each other frequently with low latency. Using MCP here is over-engineering.
Not controlling tool permissions. An MCP server typically runs with the privileges of its host process. If you expose a tool that can delete files or run shell commands without restricting input parameters, the LLM (or prompt injection) could abuse that tool. Always validate parameters in your tools, don’t blindly trust input from the LLM.
Running stdio server in serverless environments. The stdio transport needs to spawn child processes — it doesn’t work well in serverless/short-lived container environments. In this case, you should use the SSE/HTTP transport.
Not syncing versions between client and server. MCP is still evolving rapidly (many breaking changes between spec versions). When upgrading the SDK, carefully review the changelog before deploying to production.
8. Conclusion
MCP standardizes how LLMs call tools and read external data, solving the M × N integration problem between AI applications and data sources. With Spring AI, you can expose a Java service as an MCP tool using just the @Tool annotation, without having to implement JSON-RPC yourself. Use MCP when building an agent/chatbot that needs to dynamically call tools based on the LLM’s decisions — and avoid using it for regular service-to-service communication, where REST or gRPC are still more suitable choices.