MCP (Model Context Protocol) is a standard way for an AI client (Claude Desktop, Claude Code, an IDE, etc.) to discover and call tools exposed by a separate server process. This project implements the protocol by hand over the stdio transport — the simplest transport, where the client launches your program as a subprocess and talks JSON-RPC 2.0 to it over stdin/stdout.
Client (Claude Desktop, etc.) Your MCP Server (this project)
│ spawns process │
│───────── initialize ─────────────────►│ "hi, I speak protocol X, here's who I am"
│◄──────── result: capabilities ─────────│ "hi, I support 'tools', here's my name"
│──── notifications/initialized ────────►│ (no reply expected)
│ │
│───────── tools/list ──────────────────►│ "what tools do you have?"
│◄──────── result: [tool defs] ──────────│ name + description + JSON-schema per tool
│ │
│───────── tools/call ──────────────────►│ {"name":"calculator","arguments":{...}}
│◄──────── result: content[] ────────────│ the tool's output
Every message is one line of JSON on stdout/stdin. Nothing else may be
printed to stdout — any logging must go to System.err, otherwise you
corrupt the protocol stream. This project logs every message it sends/receives
to stderr so you can watch it work.
src/main/java/com/example/mcp/
├── McpServer.java the JSON-RPC loop: reads stdin, dispatches, writes stdout
├── McpTool.java interface every tool implements
└── tools/ 30 tools, grouped below
Filesystem
| Tool | What it does |
|---|---|
list_files |
Lists files/folders in a directory |
read_file |
Reads a text file's contents (size-capped) |
write_file |
Creates/overwrites a text file |
append_file |
Appends text to the end of a file |
delete_file |
Deletes a file |
copy_file |
Copies a file to a new path |
move_file |
Moves/renames a file |
create_directory |
Creates a directory (and missing parents) |
file_info |
Size, last-modified time, readable/writable, file vs directory |
search_text |
grep-style regex search within a file, with line numbers |
Network / system
| Tool | What it does |
|---|---|
http_get |
Fetches a URL over HTTP(S), returns the response body |
system_info |
OS, Java version, CPU cores, heap memory |
get_current_time |
Current date/time in any IANA timezone |
Text & data utilities
| Tool | What it does |
|---|---|
calculator |
Exact arithmetic (add/subtract/multiply/divide) |
base64_encode / base64_decode |
Base64 conversion |
hash_text |
MD5 / SHA-1 / SHA-256 / SHA-512 of a string |
uuid_generate |
Generates one or more random UUIDs |
json_format |
Validates JSON and pretty-prints it |
word_count |
Line/word/character counts for a block of text |
string_replace |
Literal or regex find-and-replace |
url_encode / url_decode |
Percent-encoding conversion |
sort_lines |
Sorts lines of text, ascending or descending |
dedupe_lines |
Removes duplicate lines, keeps first-seen order |
regex_extract |
Pulls all matches (and capture groups) out of text |
text_diff |
Line-level added/removed/common comparison of two texts (set-based, not positional) |
random_number |
Random integer in a given range |
Date & conversion
| Tool | What it does |
|---|---|
date_add |
Adds/subtracts days, hours, or minutes from an ISO-8601 date |
unit_convert |
Converts length, weight, or temperature between units |
read_file, write_file, and search_text will touch any path the JVM process
has permission to, and http_get will fetch any http(s) URL — including
internal/private addresses if this server ever runs somewhere network-reachable.
That's fine for local experimentation, but before using these beyond your own
machine:
- Allow-list a base directory for the file tools — resolve the given path
against a fixed root and reject anything that escapes it (watch for
..and symlinks), rather than accepting arbitrary absolute paths. - Allow-list hosts for
http_getif you don't need it to reach the whole internet — reject requests tolocalhost,127.0.0.1,169.254.169.254(cloud metadata endpoints), and other private IP ranges to prevent SSRF. - Deliberately not included: a generic "run any shell command" tool. That
pattern hands the calling AI unrestricted code execution — a single
prompt-injected instruction (e.g. from a file the AI reads with
read_file) could get it to run something destructive. If you need command execution, prefer a narrow tool that runs one specific, hardcoded, allow-listed command rather than an openexec(cmd).
Implement McpTool and register it in McpServer's constructor:
public class MyTool implements McpTool {
public String getName() { return "my_tool"; }
public String getDescription() { return "..."; }
public Map<String,Object> getInputSchema() { /* JSON schema */ }
public String execute(JsonNode arguments) { /* do the work */ }
}// in McpServer's constructor:
registerTool(new MyTool());That's it — it will automatically show up in tools/list and become callable.
mvn clean packageThis produces a single runnable jar (dependencies bundled via shade plugin):
target/mcp-server.jar
Run the jar and paste JSON-RPC messages directly into stdin, one per line:
java -jar target/mcp-server.jarThen paste (each on one line, press Enter after each):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-test","version":"0.0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"calculator","arguments":{"operation":"multiply","a":6,"b":7}}}You should see a result line printed back on stdout after each request
(server logs go to stderr so they won't clutter the JSON output).
Add it to Claude Desktop's MCP config file
(claude_desktop_config.json — on macOS at
~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"my-java-server": {
"command": "java",
"args": ["-jar", "/absolute/path/to/mcp-server.jar"]
}
}
}Restart Claude Desktop and the calculator / list_files tools will show up
as available tools in a new conversation.
- Sandboxing:
ListFilesToolreads from any path it's given — allow-list directories before shipping anything like this for real. - Concurrency: this loop handles one message at a time, in order, which is
fine for stdio (the client also talks to you strictly sequentially per
connection). If a tool call is slow, consider running it on a separate
thread and replying asynchronously so
tools/list/pingaren't blocked. - Other transports: MCP also supports Streamable HTTP for remote servers (instead of a local subprocess talking over stdio). That's a bigger lift — happy to build that version too if you need a remotely hosted MCP server rather than a local one.
- Official SDKs: for anything beyond a learning example, consider the
official MCP Java SDK (
io.modelcontextprotocol.sdk) or Spring AI's MCP starter, which handle protocol-version negotiation, schema validation, and the HTTP transport for you. This project intentionally avoids them so you can see the wire protocol directly.