Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Minimal MCP Server in Java 17 (no SDK, from scratch)

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.

The protocol, in short

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.

Project layout

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

Full tool catalog (30 tools)

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

Security notes on the filesystem and network tools

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_get if you don't need it to reach the whole internet — reject requests to localhost, 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 open exec(cmd).

Adding a new tool

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.

Build

mvn clean package

This produces a single runnable jar (dependencies bundled via shade plugin): target/mcp-server.jar

Test it manually (no client needed)

Run the jar and paste JSON-RPC messages directly into stdin, one per line:

java -jar target/mcp-server.jar

Then 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).

Wire it into Claude Desktop

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.

Notes for production use

  • Sandboxing: ListFilesTool reads 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/ping aren'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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages