How MCP Works: Giving Claude a Set of Arms
// Published On: Jun 12, 2026
Claude is a powerful reasoning engine, but out of the box it knows only what was baked into it during training. It cannot check today’s weather, query your database, or call your internal APIs. The Model Context Protocol (MCP) is the standard that fixes this — and building a toy server in Spring Boot makes the mechanics concrete.
The core idea
Claude is a brain with no arms. MCP gives it arms.
More precisely, MCP is an open protocol that lets an AI model discover and call external tools in a structured, pluggable way. The model does not need to know how the tool is implemented. The tool does not need to know anything about the model. The protocol is the contract between them.
┌─────────────┐ MCP Protocol ┌──────────────────┐
│ Claude │ ◄─────────────────► │ MCP Server │
│ (the LLM) │ (JSON over stdio │ (your Java app) │
│ │ or HTTP/SSE) │ │
└─────────────┘ └──────────────────┘
The protocol defines three primitives:
- Tools — functions the model can call with typed inputs and outputs
- Resources — read-only data sources the model can access (files, database rows)
- Prompts — reusable prompt templates the server can expose
The most common use case is tools, so that is what the toy server focuses on.
The toy server
The server is a Spring Boot application with a single tool: getWeather. It takes a city name and returns fake-but-realistic weather data generated by JavaFaker. The point is not the weather data — it is the wiring.
The tool itself is a plain Java method:
@Tool(description = "Get weather summary for a city")
public String getWeather(
@ToolParam(description = "City name") String city) {
Weather weather = generateWeatherData();
Map<String, Object> result = Map.of(
"city", city,
"temperatureC", weather.temperatureC(),
"condition", weather.condition(),
"summary", city + " is " + weather.condition() + " at "
+ weather.temperatureC() + "°C");
return objectMapper.writeValueAsString(result);
}
The @Tool and @ToolParam annotations are picked up by Spring AI, which auto-generates the JSON schema that MCP needs to describe this tool to Claude. The app registration is minimal:
@Bean
ToolCallbackProvider weatherTools(FakeWeatherToolService service) {
return MethodToolCallbackProvider.builder()
.toolObjects(service)
.build();
}
And the transport is configured in application.yaml:
spring:
ai:
mcp:
server:
stdio: true # communicate over stdin/stdout
main:
web-application-type: none # no HTTP server needed
stdio transport means Claude launches the server as a subprocess and talks to it over standard input and output. No ports, no HTTP, no networking — just pipes.
What Claude actually sees
When Claude connects to the MCP server, the first thing that happens is tool discovery. The server responds with a JSON schema describing every tool it exposes:
{
"tools": [{
"name": "getWeather",
"description": "Get weather summary for a city",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
}
},
"required": ["city"]
}
}]
}
Claude now knows a tool exists, what it does, and what parameters to pass. From this point on, Claude can decide on its own when calling getWeather is the right move.
A single-city request
Prompt:
What's the weather like in Tokyo?
Claude recognises this needs live data, emits a tool call internally (not shown to the user), and the exchange looks like this:
Claude → server:
{
"type": "tool_use",
"name": "getWeather",
"input": { "city": "Tokyo" }
}
Server → Claude:
{
"city": "Tokyo",
"temperatureC": 22,
"condition": "Sunny",
"summary": "Tokyo is Sunny at 22°C"
}
Claude → user:
Tokyo is currently sunny and warm at 22°C — a great day to be outside.
A multi-city request
Prompt:
Compare the weather in NYC and LA.
Claude makes two sequential tool calls, collects both results, then synthesises a single answer.
Call 1:
{ "name": "getWeather", "input": { "city": "New York" } }
{ "city": "New York", "temperatureC": 9, "condition": "Rainy",
"summary": "New York is Rainy at 9°C" }
Call 2:
{ "name": "getWeather", "input": { "city": "Los Angeles" } }
{ "city": "Los Angeles", "temperatureC": 28, "condition": "Sunny",
"summary": "Los Angeles is Sunny at 28°C" }
Claude → user:
Quite a contrast today: New York is cold and rainy at 9°C,
while Los Angeles is warm and sunny at 28°C.
When Claude does not call the tool
Prompt:
What's 2 + 2?
No tool call. Claude answers directly — it only reaches for a tool when the question actually requires one. The model uses the tool description to make this decision. A description like “Get weather summary for a city” gives Claude enough signal to know when the tool is and is not relevant.
This is why tool descriptions matter. A vague description leads to missed calls or spurious calls. A precise description means Claude uses the tool exactly when it should.
How to connect it to Claude
Since the server uses stdio transport, you point Claude at the executable in your config:
{
"mcpServers": {
"weather": {
"command": "java",
"args": ["-jar", "/path/to/mcp-server.jar"]
}
}
}
Claude launches the process at startup, negotiates the protocol handshake, and from that point getWeather is available in every conversation — exactly like a built-in capability.
The pattern that scales
The toy only has one tool, but the pattern scales. You can add tools for querying a database, calling an internal REST API, reading files from S3, running a shell command — anything that can be expressed as a function. Spring AI picks up every method annotated with @Tool and exposes it automatically.
The separation is the point. The model does not need to know Java. The Java server does not need to know anything about language models. Each side evolves independently, and the protocol keeps them in sync.
The source for this server is at github.com/rupinr/mcp-server.