An MCP server in Java: a minimal example with one tool
We write a minimal MCP server in Java with one tool, register it in Claude Code, and learn to find the cause when the agent doesn't see it.
6 min · · development · агенты
You need an MCP server in Java when you have JVM code the agent should be able to call: an environment check, a query to an internal service, reading data from a database. The server stays an ordinary program, and the agent (Claude Code, for example) launches it and talks to it over MCP. Below we write a minimal server with one tool, register it in an agent working through the Zukko gateway, and look at where to search for the problem when the agent doesn't see the tool.
How it works
The MCP documentation describes three roles: the host (the application with the model, such as Claude Code), the client (a component inside the host that holds the connection) and the server (your program that provides context). A server can offer three kinds of capabilities: tools (functions the model calls), resources (data) and prompts (templates). The minimal example needs only the first.
There are two transports: stdio, where the host launches your program as a child process and exchanges messages over stdin and stdout, and Streamable HTTP for remote servers. For a local Java server stdio is simpler: no port and no authorization. The protocol is built on JSON-RPC 2.0; the client requests the list of tools (tools/list) and calls the one it needs (tools/call).
An important consequence for stdio: standard output is taken by the protocol. Any System.out.println in your code will corrupt the stream. For debug messages use System.err or a logger configured for stderr.
Dependencies
You need Java 17 or newer. In the MCP Java SDK documentation the main artifact is io.modelcontextprotocol.sdk:mcp: it includes the core, JSON serialization and the stdio, SSE and Streamable HTTP transports with no external web frameworks.
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>PUT_THE_CURRENT_ONE_HERE</version>
</dependency>We deliberately don't pin a version: the SDK moves fast and has several release lines. Take a recent one from Maven Central (for several artifacts it is easier to import mcp-bom). The signatures below were checked against the sources of the 1.x and 2.x branches, where the stdio transport constructor takes McpJsonMapper. In older releases (0.x) it took an ObjectMapper, so examples found online may differ. In the 2.x line some of the methods used here (the no-argument Tool.builder() and the TextContent(String) constructor) are marked deprecated: the code compiles with warnings, and new code can switch to Tool.builder(name, jsonMapper, schema) and TextContent.builder(text).build(). If your IDE flags an error, open the server section of the MCP Java SDK documentation for your version.
An MCP server in Java: code with one tool
We'll write a check_port tool: it checks whether anything is listening on a given TCP port on the local machine. That is useful to an agent that starts a service and wants to confirm it came up. The host is deliberately fixed to localhost so the tool can't be used to scan other addresses.
package demo;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.List;What matters here:
- The input schema is plain JSON Schema. The model decides how to fill the arguments from the
descriptionand the types, so write them clearly. - In the handler you get
request.arguments()as aMap; numbers arrive asNumber, hence the cast throughNumberinstead of a direct(int). - The reply is built as a list of text blocks.
- The startup message goes to
System.err, notSystem.out. - The blocking call at the end keeps the JVM alive. If the process exits right after starting, check this part: the behavior depends on the SDK version.
The SDK API changes between releases, so build the example first and adjust the signatures to your version if needed.
Build and run
Build an executable jar with dependencies (for example with maven-shade-plugin or a Spring Boot build, as you prefer) and check the launch by hand:
java -cp target/port-check-server.jar demo.PortCheckServerSilence in the console is normal: a stdio server waits for the host to speak first. If you see an exception instead of waiting, fix it before connecting to the agent.
For checking without an agent, MCP Inspector, a tool from the protocol's developers, will do. It runs through npx and takes the server launch command; see its README for the exact syntax. Inspector shows the list of tools and lets you call check_port with a test value.
Connecting the server to the agent
In the Claude Code documentation a local stdio server is added with claude mcp add; everything after -- is treated as the server launch command:
claude mcp add port-check -- java -jar /absolute/path/to/port-check-server.jarIf you built the jar without a main-class manifest, use the -cp ... demo.PortCheckServer form. Make the path absolute: the host launches the server from its own working directory, and a relative path is the most common reason for "the server doesn't show up". You can check the status with claude mcp get port-check, and inside a session with /mcp.
To make Claude Code itself work through the gateway, set environment variables (details in the Claude Code guide):
export ANTHROPIC_BASE_URL=https://api.zukko.pro
export ANTHROPIC_AUTH_TOKEN=fetch_your_key
export ANTHROPIC_MODEL=claude-sonnet-5-5The address has no /v1: Claude Code adds it itself. Then ask in the session: "Check whether port 8080 is in use". The agent should call check_port and answer from the result. The protocol is described in more general terms in the articles what an MCP server is and MCP servers in Claude Code.
What it adds to spending
The description of every connected tool is sent to the model with the request. Suppose (a hypothetical assumption, not a measurement) your tool's description takes 150 tokens and the agent makes 40 requests per session. The extra input is then 150 × 40 = 6,000 tokens. For claude-sonnet-5-5 (input 0.4 per 1M tokens as of October 18, 2026): 6,000 × 0.4 / 1,000,000 = $0.0024. In the Anthropic format that Claude Code uses, the repeated prefix can be read from cache at the read price of 0.05 per 1M. The sum is small, but if you connect a dozen servers with dozens of tools, the descriptions start to take a noticeable share of the context.
If the agent doesn't see the tool
- Run the server command by hand: it should be quiet, with no stack traces.
- Check absolute paths to
javaand the jar. - Make sure nothing is written to stdout, including dependency logs.
- Check the Java version: 17 or newer is required.
- Restart the agent session after changing configuration.
- Open the server in MCP Inspector and see whether it returns the list of tools.
FAQ
Do I need Spring to write an MCP server in Java?
No, the example above does without it. Spring integrations exist in the SDK as optional modules and in the Spring AI project. If you're already on Spring, they're more convenient; for connecting Spring AI to the gateway see the note on Spring AI.
How do I make the server remote?
Instead of stdio you use the Streamable HTTP transport: the SDK has a servlet-based implementation and Spring integrations. Then, instead of a launch command, the agent is given a URL. Plan authorization in advance.
Which model should the agent use?
Choose by price, context and strength index and test on your own tasks. The model list and prices come from GET https://api.zukko.pro/v1/models.
Does this work from Russia?
Access from Russia is through the gateway, with payment by SBP, MasterCard or crypto. The MCP server runs locally and doesn't depend on that.
Create a key in the dashboard, connect Claude Code as described above and check that the agent calls your check_port.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Claude Code YOLO: Permission Modes and Working Without PromptsClaude Code has no mode literally called YOLO, but it has several permission modes and flexible rules. How to stop clicking Yes without losing files.
- Claude Code: a checklist of best practices for daily workHabits that make working with Claude Code predictable: a short CLAUDE.md, a plan before edits, small tasks, checking results and keeping spend under control.
- LangChain agents: tools, the loop and capping spendingWe build an agent with create_agent and two tools and run it on a model through an OpenAI-compatible gateway, then cap the steps and count tokens.