A hello-world Model Context Protocol server in Java, built on Spring Boot and Spring AI, plus a console chat client that drives it with a small local LLM.
It is deliberately small, but it is not a toy. It uses the official Spring AI MCP integration, serves both transports (stdio and streamable HTTP), is covered by 92 automated tests including real protocol round-trips over a real pipe, and handles the things that actually break MCP servers in practice.
New to MCP? Start with GETTING-STARTED.md — it builds this entire project from an empty directory, one step at a time, explaining every dependency and every file.
$ java -jar target/hello-mcp.jar chat
hello-mcp-chat v1.0.0
a Model Context Protocol client for Java
Starting the MCP server ...
Server: hello-mcp-server v1.0.0 (4 tools)
Model: Ollama / phi3:latest / prompt-based tool selection
you> say hello to Kuldeep
[tool] say_hello {"name":"Kuldeep"} -> Hello, Kuldeep! Welcome to the Model Context Protocol.
bot> Hello, Kuldeep! Welcome to the Model Context Protocol.
you> what is 17.5 plus 24.25?
[tool] add {"a":17.5,"b":24.25} -> 41.75
bot> The sum of 17.5 and 24.25 is 41.75.
you> who wrote Hamlet?
bot> William Shakespeare wrote "Hamlet", around 1600-1602.
Note the third turn: the model correctly decided not to call a tool.
An LLM can only talk. The Model Context Protocol is how you let it do things: a server publishes a set of tools (functions with a name, a description and a JSON schema), and a client hands that list to a model. When the model decides a tool would help, the client calls it and feeds the result back. MCP standardises that conversation as JSON-RPC 2.0, so any client can use any server. This repo is one small server plus one small client, so you can see both ends.
Prerequisites: JDK 17 or newer. That is all — the Maven Wrapper (mvnw) downloads Maven itself.
git clone https://github.com/kuldeepcodes/hello-mcp-java.git
cd hello-mcp-java
./mvnw clean verify # build and run all 92 tests (Windows: .\mvnw.cmd clean verify)
java -jar target/hello-mcp.jar chatThe chat client launches the server itself, so there is nothing else to start.
No LLM installed? It still works:
java -jar target/hello-mcp.jar chat --provider noneThat uses deterministic keyword routing instead of a model — enough to prove the MCP wiring end to end. For real conversation, install Ollama and pull a small model:
ollama pull phi3 # ~2.2 GB, works with the prompt planner
ollama pull qwen2.5:1.5b # ~1 GB, supports native tool calling (more accurate)| Command | What it does |
|---|---|
java -jar target/hello-mcp.jar |
MCP server over stdio — what local clients spawn |
java -jar target/hello-mcp.jar --http |
MCP server over streamable HTTP on :5099/mcp |
java -jar target/hello-mcp.jar chat |
The chat client, which launches the server itself |
Chat client flags:
| Flag | Default | Meaning |
|---|---|---|
--ask "<question>" |
— | Ask one question, print the answer, exit |
--model <name> |
phi3 |
Which Ollama model to use |
--provider none |
auto |
Skip the LLM entirely and use keyword routing |
In-chat commands: /tools, /reset, /help, /exit.
Tools
| Tool | Purpose |
|---|---|
say_hello |
Greets someone by name, in any of 10 languages |
echo |
Returns your message unchanged — the simplest possible connectivity check |
get_server_time |
The server's clock, in any IANA time zone |
add |
Adds two numbers exactly, which is precisely what an LLM is bad at |
Prompts: friendly_greeting, summarize_capabilities — reusable prompt templates the client
can offer.
Resources: hello://server/info and the templated hello://greetings/{language} — read-only
data the client can pull in as context.
Any MCP client can spawn this server. For VS Code, create .vscode/mcp.json:
{
"servers": {
"hello-mcp-java": {
"type": "stdio",
"command": "java",
"args": ["-jar", "/absolute/path/to/hello-mcp-java/target/hello-mcp.jar"]
}
}
}For Claude Desktop, add the same block under mcpServers in claude_desktop_config.json.
Use an absolute path to the jar — the client will not run from your project directory.
These are the ones worth knowing before you write your own. Each is guarded by a test here.
The client reads JSON-RPC from the server's stdout. One stray System.out.println, one Spring
banner, one log line, and the stream is corrupt — the client disconnects with no useful error.
The fix is in logback.xml: every appender targets
System.err. Plus bannerMode(OFF) in
HelloMcpApplication.
StdioProtocolIT.stdoutIsPureJsonRpc exercises every endpoint, then asserts that every line the
server ever wrote to stdout parses as JSON-RPC 2.0. Adding a single println to main makes all
ten stdio tests fail.
If you build on a web application type, Spring binds a port even when the transport is stdio. Start
a second copy — as an MCP client legitimately might — and it dies with "address already in use".
The stdio branch uses WebApplicationType.NONE.
properties() registers default properties, which are the lowest-precedence source. A
spring.ai.mcp.server.stdio: true line in application.yml silently overrode the transport chosen
on the command line: --http bound its port, logged nothing unusual, and answered 404 to every
request. HttpProtocolIT now guards this.
A fourth, less dramatic one: the HTTP transport lives in a separate artifact,
spring-ai-starter-mcp-server-webflux. Without it you get the same silent 404.
Most small local models — phi3 among them — ship without a tool template, so Ollama rejects any
request carrying a tools array with HTTP 400. Refusing to run on the hardware people actually
have would make this a poor teaching repo, so the client detects the situation and adapts:
| Strategy | When | How |
|---|---|---|
| Native tool calling | Model supports it | Tools are sent in the API request; the model returns a real tool call |
| Prompt planner | Model does not | The tool catalogue goes in the prompt; the model replies with a small JSON decision object |
| Offline routing | No model at all | Deterministic keyword rules |
The strategy is chosen at startup by sending a throwaway request with a tool attached and
inspecting the failure. See
OllamaClient.probeToolSupport.
One design note worth stealing. get_server_time returns a human field — the local time
already written out as "Monday, 24 August 2026 at 3:34 PM". Without it, phi3 read
15:34 and reported "2:32 PM" every single time. Small models are unreliable at converting
anything; give them a finished sentence to copy instead.
src/main/java/dev/kuldeepcodes/hellomcp/
├── HelloMcpApplication.java entry point: stdio | --http | chat
├── server/
│ ├── tools/GreetingTools.java say_hello, echo
│ ├── tools/UtilityTools.java get_server_time, add
│ ├── prompts/HelloPrompts.java reusable prompt templates
│ └── resources/ServerResources.java
└── chat/
├── ChatRunner.java console UI, MCP client, the three strategies
├── llm/LlmClient.java the tiny interface a backend must satisfy
├── llm/OllamaClient.java Ollama HTTP API + tool-support probe
└── agent/
├── PromptToolPlanner.java tool selection for models without tool calling
├── JsonExtractor.java salvages JSON from untidy model output
└── OfflineToolRouter.java deterministic fallback
./mvnw test # 78 unit tests
./mvnw verify # + 14 integration tests against the packaged jarIntegration tests use Failsafe, not Surefire, because they launch target/hello-mcp.jar as a
child process and so can only run after package. They speak raw JSON-RPC over a real pipe, and
they hold the pipe open — writing requests and closing stdin makes the server shut down before
it can reply, which is a genuinely confusing way to watch a test fail.
This is one of three parallel implementations, same tools, same behaviour, same lessons:
- hello-mcp-dotnet — C# / .NET 10
- hello-mcp-java — Java 17 / Spring Boot (you are here)
- hello-mcp-python — Python 3.11+