MCP Quickstart
Get a working Model Context Protocol (MCP) server and client talking to each other in about 5 minutes, using MCP revision 2026-07-28 (the current revision — see Choosing your MCP setup if you need 2025-06-18 instead).
1. Add the dependency
<properties>
<juneau.version>10.0.0-SNAPSHOT</juneau.version>
</properties>
<repositories>
<repository>
<id>apache-snapshots</id>
<url>https://repository.apache.org/snapshots/</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-mcp-v20260728</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-client-mcp-v20260728</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-microservice-jetty-bundle</artifactId>
<version>${juneau.version}</version>
<type>pom</type>
</dependency>
</dependencies>
10.0.0-SNAPSHOT needs the Apache snapshots repository; Maven Central has only the 9.2.0 release today (see Install Juneau and Downloads).
juneau-microservice-jetty-bundle adds the embedded Jetty server used in step 2. To manage versions with the BOM instead, see Install Juneau.
The server artifact transitively pulls in the revision-neutral juneau-rest-server-mcp core plus the 2026-07-28 wire beans (juneau-bean-mcp-v20260728). The client artifact does the same on the client side.
2. Write a minimal server
Extend the concrete, revision-bound McpRestServlet and register one tool. McpToolHandler.of(...) wires a descriptor and a call body together in one expression — McpToolHandler has two abstract methods, so it can't be satisfied by a bare lambda:
package com.example;
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.mcp.*;
import org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet;
@Rest(path="/mcp")
public class EchoMcpServlet extends McpRestServlet {
@Override
protected McpServerConfig createMcpConfig() {
return new McpServerConfig()
.setName("echo-server")
.setVersion("1.0.0")
.addTool(McpToolHandler.of(
new McpToolSpec().setName("echo").setDescription("Echoes the supplied text back."),
(arguments, ctx) -> McpToolOutcome.text(String.valueOf(arguments.getOrDefault("text", "")))
));
}
}
Start it on the Jetty microservice. Create src/main/java/com/example/App.java:
package com.example;
import org.apache.juneau.microservice.jetty.*;
public class App {
public static void main(String[] args) throws Exception {
JettyMicroservice.run(args, new EchoMcpServlet(), false).join();
}
}
mvn compile exec:java -Dexec.mainClass=com.example.App
The server listens on port 10000 (see Ports used in these docs) and serves the endpoint at http://localhost:10000/mcp, the @Rest(path="/mcp") path. Leave it running for steps 3 and 4. To mount it with web.xml or Spring Boot instead, see Choosing your MCP setup.
3. Write a minimal client
McpClient.connect(endpoint) builds a client and performs its one mandatory server/discover handshake in a single call — 2026-07-28 has no initialize method, so this is the call that replaces it:
package com.example;
import java.util.*;
import org.apache.juneau.rest.client.mcp.v20260728.McpClient;
public class EchoMcpClientDemo {
public static void main(String[] args) throws Exception {
try (var client = McpClient.connect("http://localhost:10000/mcp")) {
System.out.println(client.listTools());
System.out.println(client.callTool("echo", Map.of("text", "hello")));
}
}
}
McpClient implements Closeable, so try-with-resources closes the underlying transport for you. Run it from a second terminal with mvn compile exec:java -Dexec.mainClass=com.example.EchoMcpClientDemo.
4. Talk to it with raw curl / JSON-RPC
MCP 2026-07-28 (SEP-2243) requires every request to carry Mcp-Method and Mcp-Name HTTP headers alongside the JSON-RPC body — Mcp-Method echoes the JSON-RPC method, and Mcp-Name is the routed tool/prompt name (for tools/call/prompts/get) or resource URI (for resources/read); it's empty for every other method:
curl -s http://localhost:10000/mcp \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: echo" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {"text": "hello"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'
The Accept: application/json header asks for JSON; without it a bare curl gets Juneau's default HTML view.
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": {"name": "echo-server", "version": "1.0.0"}
},
"content": [{"type": "text", "text": "hello"}]
}
}
A request missing the required _meta.protocolVersion/clientCapabilities, or missing the Mcp-Method/Mcp-Name headers, is rejected with a JSON-RPC -32600 error before it ever reaches your tool handler.
Next steps
- Choosing your MCP setup —
2025-06-18vs2026-07-28, dedicated servlet vs mixin, plain vs Spring Boot. - MCP Recipes — copy-pasteable snippets for typed tools, prompts, resources, resource templates, completions, error handling, elicitation, subscriptions, and the client side.
- juneau-rest-server-mcp reference and juneau-rest-client-mcp reference — full API reference for both sides.