
The java-mcp-server-generator skill generates complete, production-ready Model Context Protocol server projects in Java using the official MCP Java SDK with Maven or Gradle build configuration. It targets Java developers building MCP servers and solves the problem of setting up a properly structured Java MCP project from scratch by providing templates for server configuration, tool/resource/prompt handlers, and all necessary dependencies. The skill includes Maven and Gradle configuration examples, standard project structure with separate handler classes, and optional Spring Boot integration for developers who need it.
$ npx -y skills add github/awesome-copilot --skill java-mcp-server-generator --agent claude-codeInstalls into .claude/skills of the current project.
Create a Model Context Protocol (MCP) server in Java with the official MCP Java SDK 2.x. SDK 2.0.x supports the MCP specification revision 2025-11-25 and the earlier revisions. SDK 3.x will add the revision 2026-07-28.
Copy this checklist and track your progress:
Use Maven by default. If the project uses Gradle or the user asks for Gradle, read references/gradle.md. Use it for Step 1, Step 4, and Step 5.
Make this layout:
project-name/
├── pom.xml
└── src/
├── main/java/com/example/McpServerApp.java
└── test/java/com/example/McpServerAppTest.java
Write pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mcp-server-demo</artifactId>
<version>0.1.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-bom</artifactId>
<version>2.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.McpServerApp</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
mcp artifact contains mcp-core and the Jackson 3 JSON mapper mcp-json-jackson3.slf4j-simple writes the SDK logs to stderr.Start from this template:
package com.example;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.Tool;
public final class McpServerApp {
static SyncToolSpecification divideTool() {
Tool tool = Tool.builder("divide", Map.of(
"type", "object",
"properties", Map.of(
"a", Map.of("type", "number", "description", "Dividend"),
"b", Map.of("type", "number", "description", "Divisor")),
"required", List.of("a", "b")))
.description("Divide a by b.")
.build();
return SyncToolSpecification.builder()
.tool(tool)
.callHandler((exchange, request) -> {
double a = ((Number) request.arguments().get("a")).doubleValue();
double b = ((Number) request.arguments().get("b")).doubleValue();
if (b == 0) {
return CallToolResult.builder().addTextContent("b must not be zero.").isError(true).build();
}
return CallToolResult.builder().addTextContent(String.valueOf(a / b)).build();
})
.build();
}
public static void main(String[] args) {
McpServer.sync(new StdioServerTransportProvider(McpJsonDefaults.getMapper()))
.serverInfo("demo", "0.1.0")
.capabilities(ServerCapabilities.builder().tools(false).build())
.tools(divideTool())
.build();
}
}
Then change the template to match the request of the user:
divideTool() with the tools that the user asks for. Make one method for each tool.inputSchema and a description. The description tells the model when to use the tool..outputSchema(Map) to the tool. Return the data with CallToolResult.builder().structuredContent(map). Also add the same data as text content for older clients..annotations(ToolAnnotations.builder().readOnlyHint(true).build()). Use destructiveHint(true) for a destructive tool.ServerCapabilities.Use stdio by default. Read references/streamable-http.md only when the user asks for a remote server, an HTTP server, or Spring Boot.
The test starts the server as a subprocess and connects to it with the SDK client. Write a test for each tool and for each error result:
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path;
import java.util.Map;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
class McpServerAppTest {
private static McpSyncClient client;
@BeforeAll
static void startServer() {
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
ServerParameters server = ServerParameters.builder(java)
.args("-cp", System.getProperty("java.class.path"), McpServerApp.class.getName())
.build();
client = McpClient.sync(new StdioClientTransport(server, McpJsonDefaults.getMapper())).build();
client.initialize();
}
@AfterAll
static void stopServer() {
client.close();
}
@Test
void divide() {
CallToolResult result = client.callTool(
CallToolRequest.builder("divide").arguments(Map.of("a", 6, "b", 3)).build());
assertEquals("2.0", ((TextContent) result.content().get(0)).text());
}
@Test
void divideByZero() {
CallToolResult result = client.callTool(
CallToolRequest.builder("divide").arguments(Map.of("a", 1, "b", 0)).build());
assertTrue(result.isError());
assertTrue(((TextContent) result.content().get(0)).text().contains("must not be zero"));
}
}
java command of the JDK that runs Maven. Do not use the java command from PATH.mvn verify. This command compiles the code, runs the tests, and makes target/mcp-server-demo-0.1.0.jar.mvn verify again.For VS Code, write .vscode/mcp.json:
{
"servers": {
"demo": {
"type": "stdio",
"command": "java",
"args": ["-jar", "/absolute/path/to/project/target/mcp-server-demo-0.1.0.jar"]
}
}
}
For Claude Desktop, put the same command and args in claude_desktop_config.json, below the mcpServers key.
For an HTTP server, use the client configuration in references/streamable-http.md.
Tell the user these facts:
java -jar target/mcp-server-demo-0.1.0.jar starts the stdio server. The server waits for a host on stdin. The SDK logs go to stderr.SDK 2.x changed many patterns of SDK 0.x and 1.x. Do not copy examples from earlier versions. Read the migration guide for the full list.
Tool.builder(name, inputSchema), Resource.builder(uri, name), Prompt.builder(name), TextContent.builder(text), and ReadResourceResult.builder(contents). The builders without arguments are deprecated.null. A null value causes an IllegalArgumentException._, -, and .. The server builder rejects an incorrect name with an IllegalArgumentException. addTool() at runtime does not check the name.inputSchema and outputSchema are Map<String, Object> values. The JsonSchema record is deprecated.build() or addTool(). An incorrect schema causes an IllegalArgumentException.inputSchema. It also validates structuredContent against outputSchema. If the validation fails, the client gets a tool result with isError set to true. Thus, the handler gets only valid arguments.outputSchema, return structuredContent in each successful result. Without it, the SDK returns a tool result with isError set to true.CallToolResult with isError(true) for an error that the model must read.McpError for a tool failure. The client gets a JSON-RPC error, not a tool result. Many hosts do not show this error to the model.System.out.println().System.err.org.springframework.ai). Do not use the io.modelcontextprotocol.sdk:mcp-spring-* artifacts.exchange.createMessage()): Call the LLM provider API directly.exchange.loggingNotification()): Use SLF4J.exchange.listRoots()): Get paths from tool arguments, resource URIs, or the server configuration.Sponsors