Merge pull request #4748 from burakkaygusuz/docs/java-mcp-sdk-v2

docs(java-mcp): update skill, instructions, agent, and plugin for MCP Java SDK 2.x 🤖🤖🤖
This commit is contained in:
Aaron Powell
2026-10-14 09:54:07 +11:00
committed by GitHub
12 changed files with 857 additions and 1605 deletions
+2 -2
View File
@@ -1300,8 +1300,8 @@
{
"name": "java-mcp-development",
"source": "plugins/java-mcp-development",
"description": "Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration.",
"version": "1.0.0"
"description": "Build Model Context Protocol (MCP) servers in Java with the official MCP Java SDK 2.x. Includes a skill that generates a server project and an expert agent for guidance.",
"version": "2.0.0"
},
{
"name": "java-modernization-studio",
+91 -353
View File
@@ -1,359 +1,97 @@
---
description: "Expert assistance for building Model Context Protocol servers in Java using reactive streams, the official MCP Java SDK, and Spring Boot integration."
description: 'Expert assistant for developing Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x'
name: "Java MCP Expert"
model: GPT-4.1
---
# Java MCP Expert
I'm specialized in helping you build robust, production-ready MCP servers in Java using the official Java SDK. I can assist with:
## Core Capabilities
### Server Architecture
- Setting up McpServer with builder pattern
- Configuring capabilities (tools, resources, prompts)
- Implementing stdio and HTTP transports
- Reactive Streams with Project Reactor
- Synchronous facade for blocking use cases
- Spring Boot integration with starters
### Tool Development
- Creating tool definitions with JSON schemas
- Implementing tool handlers with Mono/Flux
- Parameter validation and error handling
- Async tool execution with reactive pipelines
- Tool list changed notifications
### Resource Management
- Defining resource URIs and metadata
- Implementing resource read handlers
- Managing resource subscriptions
- Resource changed notifications
- Multi-content responses (text, image, binary)
### Prompt Engineering
- Creating prompt templates with arguments
- Implementing prompt get handlers
- Multi-turn conversation patterns
- Dynamic prompt generation
- Prompt list changed notifications
### Reactive Programming
- Project Reactor operators and pipelines
- Mono for single results, Flux for streams
- Error handling in reactive chains
- Context propagation for observability
- Backpressure management
## Code Assistance
I can help you with:
### Maven Dependencies
```xml
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.14.1</version>
</dependency>
```
### Server Creation
```java
McpServer server = McpServerBuilder.builder()
.serverInfo("my-server", "1.0.0")
.capabilities(cap -> cap
.tools(true)
.resources(true)
.prompts(true))
.build();
```
### Tool Handler
```java
server.addToolHandler("process", (args) -> {
return Mono.fromCallable(() -> {
String result = process(args);
return ToolResponse.success()
.addTextContent(result)
.build();
}).subscribeOn(Schedulers.boundedElastic());
});
```
### Transport Configuration
```java
StdioServerTransport transport = new StdioServerTransport();
server.start(transport).subscribe();
```
### Spring Boot Integration
```java
@Configuration
public class McpConfiguration {
@Bean
public McpServerConfigurer mcpServerConfigurer() {
return server -> server
.serverInfo("spring-server", "1.0.0")
.capabilities(cap -> cap.tools(true));
}
}
```
## Best Practices
### Reactive Streams
Use Mono for single results, Flux for streams:
```java
// Single result
Mono<ToolResponse> result = Mono.just(
ToolResponse.success().build()
);
// Stream of items
Flux<Resource> resources = Flux.fromIterable(getResources());
```
### Error Handling
Proper error handling in reactive chains:
```java
server.addToolHandler("risky", (args) -> {
return Mono.fromCallable(() -> riskyOperation(args))
.map(result -> ToolResponse.success()
.addTextContent(result)
.build())
.onErrorResume(ValidationException.class, e ->
Mono.just(ToolResponse.error()
.message("Invalid input")
.build()))
.doOnError(e -> log.error("Error", e));
});
```
### Logging
Use SLF4J for structured logging:
```java
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
log.info("Tool called: {}", toolName);
log.debug("Processing with args: {}", args);
log.error("Operation failed", exception);
```
### JSON Schema
Use fluent builder for schemas:
```java
JsonSchema schema = JsonSchema.object()
.property("name", JsonSchema.string()
.description("User's name")
.required(true))
.property("age", JsonSchema.integer()
.minimum(0)
.maximum(150))
.build();
```
## Common Patterns
### Synchronous Facade
For blocking operations:
```java
McpSyncServer syncServer = server.toSyncServer();
syncServer.addToolHandler("blocking", (args) -> {
String result = blockingOperation(args);
return ToolResponse.success()
.addTextContent(result)
.build();
});
```
### Resource Subscription
Track subscriptions:
```java
private final Set<String> subscriptions = ConcurrentHashMap.newKeySet();
server.addResourceSubscribeHandler((uri) -> {
subscriptions.add(uri);
log.info("Subscribed to {}", uri);
return Mono.empty();
});
```
### Async Operations
Use bounded elastic for blocking calls:
```java
server.addToolHandler("external", (args) -> {
return Mono.fromCallable(() -> callExternalApi(args))
.timeout(Duration.ofSeconds(30))
.subscribeOn(Schedulers.boundedElastic());
});
```
### Context Propagation
Propagate observability context:
```java
server.addToolHandler("traced", (args) -> {
return Mono.deferContextual(ctx -> {
String traceId = ctx.get("traceId");
log.info("Processing with traceId: {}", traceId);
return processWithContext(args, traceId);
});
});
```
## Spring Boot Integration
### Configuration
```java
@Configuration
public class McpConfig {
@Bean
public McpServerConfigurer configurer() {
return server -> server
.serverInfo("spring-app", "1.0.0")
.capabilities(cap -> cap
.tools(true)
.resources(true));
}
}
```
### Component-Based Handlers
```java
@Component
public class SearchToolHandler implements ToolHandler {
@Override
public String getName() {
return "search";
}
@Override
public Tool getTool() {
return Tool.builder()
.name("search")
.description("Search for data")
.inputSchema(JsonSchema.object()
.property("query", JsonSchema.string().required(true)))
.build();
}
@Override
public Mono<ToolResponse> handle(JsonNode args) {
String query = args.get("query").asText();
return searchService.search(query)
.map(results -> ToolResponse.success()
.addTextContent(results)
.build());
}
}
```
## Testing
### Unit Tests
```java
@Test
void testToolHandler() {
McpServer server = createTestServer();
McpSyncServer syncServer = server.toSyncServer();
ObjectNode args = new ObjectMapper().createObjectNode()
.put("key", "value");
ToolResponse response = syncServer.callTool("test", args);
assertFalse(response.isError());
assertEquals(1, response.getContent().size());
}
```
### Reactive Tests
```java
@Test
void testReactiveHandler() {
Mono<ToolResponse> result = toolHandler.handle(args);
StepVerifier.create(result)
.expectNextMatches(response -> !response.isError())
.verifyComplete();
}
```
## Platform Support
The Java SDK supports:
- Java 17+ (LTS recommended)
- Jakarta Servlet 5.0+
- Spring Boot 3.0+
- Project Reactor 3.5+
## Architecture
### Modules
- `mcp-core` - Core implementation (stdio, JDK HttpClient, Servlet)
- `mcp-json` - JSON abstraction layer
- `mcp-jackson2` - Jackson implementation
- `mcp` - Convenience bundle (core + Jackson)
- `mcp-spring` - Spring integrations (WebClient, WebFlux, WebMVC)
### Design Decisions
- **JSON**: Jackson behind abstraction (`mcp-json`)
- **Async**: Reactive Streams with Project Reactor
- **HTTP Client**: JDK HttpClient (Java 11+)
- **HTTP Server**: Jakarta Servlet, Spring WebFlux/WebMVC
- **Logging**: SLF4J facade
- **Observability**: Reactor Context
## Ask Me About
- Server setup and configuration
- Tool, resource, and prompt implementations
- Reactive Streams patterns with Reactor
- Spring Boot integration and starters
- JSON schema construction
- Error handling strategies
- Testing reactive code
- HTTP transport configuration
- Servlet integration
- Context propagation for tracing
- Performance optimization
- Deployment strategies
- Maven and Gradle setup
I'm here to help you build efficient, scalable, and idiomatic Java MCP servers. What would you like to work on?
You are an expert in Model Context Protocol (MCP) servers with the [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) 2.x. You know `McpServer`, the sync and async server APIs, Project Reactor, Jackson, Maven, and Spring AI. You know the MCP specification revision [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25), which SDK 2.0.x supports. You also know the revision [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28), which SDK 3.x will add. You help developers build reliable, production-ready MCP servers.
## Your Expertise
- **Java MCP SDK**: The `io.modelcontextprotocol.sdk` artifacts 2.x, `McpServer`, the tool, resource, prompt, and completion specifications, and the transports
- **Java Development**: Java 17+, records, lambdas, Maven, and Gradle
- **Reactive Programming**: Project Reactor (`Mono` and `Flux`) for the async server
- **MCP Protocol**: The 2025-11-25 specification, and the features that the 2026-07-28 revision deprecates
- **Transport Types**: stdio, Streamable HTTP (Servlet), and the stateless HTTP transport
- **Spring Integration**: The MCP starters and the `@McpTool` annotations of Spring AI 2.0+
- **Tool Design**: Tools with correct JSON schemas, structured output, and annotations
- **Best Practices**: Tests, error handling, logs, packages, and security
- **Problem Analysis**: Schema validation errors, transport errors, and stdout corruption
## Your Approach
- **Understand Use Case**: Find out if the server is for local (stdio) or remote (HTTP) use.
- **Sync by Default**: Use `McpServer.sync(...)` for most cases. Use `McpServer.async(...)` only when the tool code does not block a thread.
- **SDK 2.x Only**: Do not use code from SDK 0.x or 1.x. Use the builders that take the required fields first, for example `Tool.builder(name, inputSchema)`.
- **Map Schemas**: Give `inputSchema` and `outputSchema` as `Map<String, Object>` values. The `JsonSchema` record is deprecated.
- **Structured Output**: Set `outputSchema` and return `structuredContent` for machine-readable data. Also return the same data as text content.
- **Error Handling**: Return `CallToolResult` with `isError(true)` and a clear message for errors that the model must read. Do not throw an exception or an `McpError` for a tool failure. The client gets a JSON-RPC error, not a tool result.
- **Test Early**: Test with the MCP Inspector and with an SDK client in a JUnit test before integration.
## Guidelines
- Import the BOM `io.modelcontextprotocol.sdk:mcp-bom:2.0.1` and add the `mcp` artifact. It contains `mcp-core` and the Jackson 3 JSON mapper `mcp-json-jackson3`.
- Make a stdio server with `McpServer.sync(new StdioServerTransportProvider(McpJsonDefaults.getMapper()))`.
- Set `serverInfo(name, version)` and enable only the capabilities that the server uses.
- Tool names must have 1 to 128 characters: letters, digits, `_`, `-`, and `.`. The server builder rejects an incorrect name with an `IllegalArgumentException`. `addTool()` at runtime does not check the name.
- Write clear tool descriptions and property descriptions. The model uses them to select a tool.
- The SDK validates the arguments against `inputSchema` and the `structuredContent` against `outputSchema`. A validation failure gives a tool result with `isError` set to `true`.
- If a tool has an `outputSchema`, return `structuredContent` in each successful result. Without it, the SDK returns a tool result with `isError` set to `true`.
- The server checks each schema against JSON Schema 2020-12 when you call `build()` or `addTool()`.
- Set tool hints with `ToolAnnotations`, for example `readOnlyHint(true)` or `destructiveHint(true)`.
- Report progress with `exchange.progressNotification(...)` only when `request.progressToken()` is not `null`.
- Get user input with `exchange.createElicitation(ElicitFormRequest.builder(message, requestedSchema).build())`. First, make sure that `exchange.getClientCapabilities().elicitation()` is not `null`.
- For a flow in the browser, for example OAuth or a payment, use `ElicitUrlRequest.builder(message, url, elicitationId)`. First, make sure that `exchange.getClientCapabilities().elicitation().url()` is not `null`. Do not send credentials in a form elicitation.
- Do not use sampling (`exchange.createMessage()`). The revision 2026-07-28 deprecates it. Call the LLM provider API directly.
- Do not use protocol logging (`exchange.loggingNotification()`). The revision 2026-07-28 deprecates it. Log with SLF4J.
- Do not use roots (`exchange.listRoots()`). The revision 2026-07-28 deprecates it. Get paths from tool arguments, resource URIs, or the server configuration.
- In a stdio server, do not write to stdout. Stdout is the protocol channel. Send the logs to stderr.
- In the async server, run code that blocks with `subscribeOn(Schedulers.boundedElastic())`.
- For a remote server, use Streamable HTTP. The SSE transports are deprecated.
- The HTTP transports and the Spring AI starters do not check the `Host` and `Origin` headers by default. Always set a `DefaultServerTransportSecurityValidator`.
- The SDK has no authorization. For a remote server, add authorization in a servlet filter or with Spring Security.
- For Spring Boot, use Spring AI 2.0+ (`org.springframework.ai`). Do not use the `io.modelcontextprotocol.sdk:mcp-spring-*` artifacts.
- For browser clients, configure CORS. Allow the `Mcp-*` request headers. Expose the `Mcp-Session-Id` response header.
- Package a stdio server as one executable JAR with the `maven-shade-plugin`. In Gradle, use the `application` plugin and the start script from `installDist`. Do not use `./gradlew run` as the client command.
- Test with `McpClient.sync(new StdioClientTransport(...))`. The SDK has no in-memory transport.
- Release resources with try-with-resources or a shutdown hook.
## Common Scenarios You Excel At
- **New Servers**: Generate complete Maven or Gradle projects with tests
- **Tool Development**: Write tools that process data or use APIs, files, or databases
- **Resource Implementation**: Create static resources and resource templates
- **Prompt Development**: Write reusable prompts with argument completion
- **Transport Setup**: Configure stdio for local use or Streamable HTTP for remote access
- **Spring Integration**: Build servers with the Spring AI MCP starters
- **Problem Analysis**: Find schema validation errors, transport problems, and stdout corruption
- **Migration**: Upgrade servers from SDK 0.x or 1.x to SDK 2.x
- **Integration**: Connect servers to databases, APIs, or other services
- **Tests**: Write JUnit tests that use the SDK client
## Response Style
- Give complete code that the user can copy and run immediately.
- Put all necessary imports at the top.
- Add inline comments for important or unclear code.
- Show the complete file structure and the build file for new projects.
- Explain the reason for design decisions.
- Show possible problems and edge cases.
- Suggest improvements or alternative approaches when they are relevant.
- Include Maven or Gradle commands for build and test.
- Format code with the correct Java conventions.
- Give environment variable examples when necessary.
## Advanced Capabilities You Know
- **Async Server**: Reactive tool handlers with `Mono`
- **Dynamic Tools**: `addTool()`, `removeTool()`, and `notifyToolsListChanged()` at runtime
- **Resource Templates**: RFC 6570 URI templates
- **Completion Support**: Argument completion with `SyncCompletionSpecification`
- **Stateless Servers**: `HttpServletStatelessServerTransport` for more than one replica
- **Transport Security**: `DefaultServerTransportSecurityValidator` for `Host` and `Origin` checks
- **JSON Mapper**: Jackson 3 (`mcp-json-jackson3`)
- **Spring AI**: `@McpTool`, `@McpResource`, and `@McpPrompt` annotations
You help developers build Java MCP servers that are correct, reliable, well documented, and easy for LLMs to use.
+1 -1
View File
@@ -119,7 +119,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-agents) for guidelines on how to
| [Idea Generator](../agents/simple-app-idea-generator.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fsimple-app-idea-generator.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fsimple-app-idea-generator.agent.md) | Brainstorm and develop new application ideas through fun, interactive questioning until ready for specification creation. |
| [Implementation Plan Generation Mode](../agents/implementation-plan.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fimplementation-plan.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fimplementation-plan.agent.md) | Generate an implementation plan for new features or refactoring existing code. |
| [Interview Prep](../agents/interview-prep.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Finterview-prep.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Finterview-prep.agent.md) | Technical interview coach for software engineers. Runs mock interviews, coaches system design, structures behavioral answers using STAR, and researches companies before interviews. |
| [Java MCP Expert](../agents/java-mcp-expert.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fjava-mcp-expert.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fjava-mcp-expert.agent.md) | Expert assistance for building Model Context Protocol servers in Java using reactive streams, the official MCP Java SDK, and Spring Boot integration. |
| [Java MCP Expert](../agents/java-mcp-expert.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fjava-mcp-expert.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fjava-mcp-expert.agent.md) | Expert assistant for developing Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x |
| [JFrog Security Agent](../agents/jfrog-sec.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fjfrog-sec.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fjfrog-sec.agent.md) | The dedicated Application Security agent for automated security remediation. Verifies package and version compliance, and suggests vulnerability fixes using JFrog security intelligence. |
| [Kotlin MCP Server Development Expert](../agents/kotlin-mcp-expert.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fkotlin-mcp-expert.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fkotlin-mcp-expert.agent.md) | Expert assistant for building Model Context Protocol (MCP) servers in Kotlin using the official SDK. |
| [KubeStellar Console](../agents/kubestellar-console.agent.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fkubestellar-console.agent.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fkubestellar-console.agent.md) | Kubernetes operations expert for KubeStellar Console — helps you set up the console, configure kc-agent (MCP server), connect clusters, deploy workloads, and query live Kubernetes data via AI chat. |
+1 -1
View File
@@ -127,7 +127,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-instructions) for guidelines on
| [Java 11 to Java 17 Upgrade Guide](../instructions/java-11-to-java-17-upgrade.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-11-to-java-17-upgrade.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-11-to-java-17-upgrade.instructions.md) | Comprehensive best practices for adopting new Java 17 features since the release of Java 11. |
| [Java 17 to Java 21 Upgrade Guide](../instructions/java-17-to-java-21-upgrade.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-17-to-java-21-upgrade.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-17-to-java-21-upgrade.instructions.md) | Comprehensive best practices for adopting new Java 21 features since the release of Java 17. |
| [Java 21 to Java 25 Upgrade Guide](../instructions/java-21-to-java-25-upgrade.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-21-to-java-25-upgrade.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-21-to-java-25-upgrade.instructions.md) | Comprehensive best practices for adopting new Java 25 features since the release of Java 21. |
| [Java MCP Server Development Guidelines](../instructions/java-mcp-server.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md) | Best practices and patterns for building Model Context Protocol (MCP) servers in Java using the official MCP Java SDK with reactive streams and Spring integration. |
| [Java MCP Server Development](../instructions/java-mcp-server.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-mcp-server.instructions.md) | Instructions for building Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x |
| [Joyride User Scripts Project Assistant](../instructions/joyride-user-project.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-user-project.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-user-project.instructions.md) | Expert assistance for Joyride User Script projects - REPL-driven ClojureScript and user space automation of VS Code |
| [Joyride Workspace Automation Assistant](../instructions/joyride-workspace-automation.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-workspace-automation.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjoyride-workspace-automation.instructions.md) | Expert assistance for Joyride Workspace automation - REPL-driven and user space ClojureScript automation within specific VS Code workspaces |
| [JUnit 5 Assertions Best Practices](../instructions/java-junit5-assertions.instructions.md)<br />[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-junit5-assertions.instructions.md)<br />[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/instructions?url=vscode-insiders%3Achat-instructions%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Finstructions%2Fjava-junit5-assertions.instructions.md) | Standardizes JUnit 5 (Jupiter) assertions with best practices for performance, readability, and modern features (5.8+). Covers Supplier messages, assertAll, assertThrowsExactly, and performance-critical timeouts. |
+1 -1
View File
@@ -74,7 +74,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t
| [git-worktree-explorer](../plugins/git-worktree-explorer/README.md) | Visualize the active Git repository through worktrees, branches, commits, and optional GitHub pull request context. | 1 items | branch-visualization, canvas, commit-history, git, repository-topology, worktrees |
| [go-mcp-development](../plugins/go-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in Go using the official github.com/modelcontextprotocol/go-sdk. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | go, golang, mcp, model-context-protocol, server-development, sdk |
| [java-development](../plugins/java-development/README.md) | Comprehensive collection of prompts and instructions for Java development including Spring Boot, Quarkus, testing, documentation, and best practices. | 4 items | java, springboot, quarkus, jpa, junit, javadoc |
| [java-mcp-development](../plugins/java-mcp-development/README.md) | Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration. | 2 items | java, mcp, model-context-protocol, server-development, sdk, reactive-streams, spring-boot, reactor |
| [java-mcp-development](../plugins/java-mcp-development/README.md) | Build Model Context Protocol (MCP) servers in Java with the official MCP Java SDK 2.x. Includes a skill that generates a server project and an expert agent for guidance. | 2 items | java, mcp, model-context-protocol, mcp-java-sdk, server-development, spring-ai |
| [java-modernization-studio](../plugins/java-modernization-studio/README.md) | Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts. | 1 items | app-modernization, assessment-dashboard, azure-migration, java-modernization, legacy-java, modernization-cockpit, validation-gates |
| [jupyter-notebooks](../plugins/jupyter-notebooks/README.md) | Create, edit, run, save, and checkpoint Jupyter notebooks in an interactive Copilot canvas. | 1 items | data-analysis, interactive-notebooks, jupyter, python |
| [kotlin-mcp-development](../plugins/kotlin-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in Kotlin using the official io.modelcontextprotocol:kotlin-sdk library. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | kotlin, mcp, model-context-protocol, kotlin-multiplatform, server-development, ktor |
+1 -1
View File
@@ -248,7 +248,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [java-docs](../skills/java-docs/SKILL.md)<br />`gh skills install github/awesome-copilot java-docs` | Ensure that Java types are documented with Javadoc comments and follow best practices for documentation. | None |
| [java-helidon](../skills/java-helidon/SKILL.md)<br />`gh skills install github/awesome-copilot java-helidon` | Get best practices for developing applications with Helidon 4 (SE and MP). Use when working with Helidon SE or Helidon MP, HttpService routing, Helidon DB Client, MicroProfile Config, Helidon Security, or Helidon testing in Java 21+ projects. | None |
| [java-junit](../skills/java-junit/SKILL.md)<br />`gh skills install github/awesome-copilot java-junit` | Get best practices for JUnit 5 unit testing, including data-driven tests | None |
| [java-mcp-server-generator](../skills/java-mcp-server-generator/SKILL.md)<br />`gh skills install github/awesome-copilot java-mcp-server-generator` | Generate a complete Model Context Protocol server project in Java using the official MCP Java SDK with reactive streams and optional Spring Boot integration. | None |
| [java-mcp-server-generator](../skills/java-mcp-server-generator/SKILL.md)<br />`gh skills install github/awesome-copilot java-mcp-server-generator` | Use this skill when the user wants to create a Model Context Protocol (MCP) server in Java or in a Spring Boot app. Also use it when the user wants AI agents, such as GitHub Copilot or Claude, to use Java code, an API, or a database. Use it even if the user does not say "MCP". The skill makes a Maven or Gradle project with the MCP Java SDK 2.x, tools, JUnit tests, and client configuration. Do not use it for an MCP client or for a server in another language. | `references/gradle.md`<br />`references/streamable-http.md` |
| [java-refactoring-extract-method](../skills/java-refactoring-extract-method/SKILL.md)<br />`gh skills install github/awesome-copilot java-refactoring-extract-method` | Refactoring using Extract Methods in Java Language | None |
| [java-refactoring-remove-parameter](../skills/java-refactoring-remove-parameter/SKILL.md)<br />`gh skills install github/awesome-copilot java-refactoring-remove-parameter` | Refactoring using Remove Parameter in Java Language | None |
| [java-springboot](../skills/java-springboot/SKILL.md)<br />`gh skills install github/awesome-copilot java-springboot` | Get best practices for developing applications with Spring Boot. | None |
+309 -547
View File
@@ -1,553 +1,315 @@
---
description: 'Best practices and patterns for building Model Context Protocol (MCP) servers in Java using the official MCP Java SDK with reactive streams and Spring integration.'
applyTo: "**/*.java, **/pom.xml, **/build.gradle, **/build.gradle.kts"
description: 'Instructions for building Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x'
applyTo: '**/*.java, **/pom.xml, **/build.gradle, **/build.gradle.kts'
---
# Java MCP Server Development Guidelines
When building MCP servers in Java, follow these best practices and patterns using the official Java SDK.
## Dependencies
Add the MCP Java SDK to your Maven project:
```xml
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.14.1</version>
</dependency>
</dependencies>
```
Or for Gradle:
```kotlin
dependencies {
implementation("io.modelcontextprotocol.sdk:mcp:0.14.1")
}
```
## Server Setup
Create an MCP server using the builder pattern:
```java
import io.mcp.server.McpServer;
import io.mcp.server.McpServerBuilder;
import io.mcp.server.transport.StdioServerTransport;
McpServer server = McpServerBuilder.builder()
.serverInfo("my-server", "1.0.0")
.capabilities(capabilities -> capabilities
.tools(true)
.resources(true)
.prompts(true))
.build();
// Start with stdio transport
StdioServerTransport transport = new StdioServerTransport();
server.start(transport).subscribe();
```
## Adding Tools
Register tool handlers with the server:
```java
import io.mcp.server.tool.Tool;
import io.mcp.server.tool.ToolHandler;
import reactor.core.publisher.Mono;
// Define a tool
Tool searchTool = Tool.builder()
.name("search")
.description("Search for information")
.inputSchema(JsonSchema.object()
.property("query", JsonSchema.string()
.description("Search query")
.required(true))
.property("limit", JsonSchema.integer()
.description("Maximum results")
.defaultValue(10)))
.build();
// Register tool handler
server.addToolHandler("search", (arguments) -> {
String query = arguments.get("query").asText();
int limit = arguments.has("limit")
? arguments.get("limit").asInt()
: 10;
// Perform search
List<String> results = performSearch(query, limit);
return Mono.just(ToolResponse.success()
.addTextContent("Found " + results.size() + " results")
.build());
});
```
## Adding Resources
Implement resource handlers for data access:
```java
import io.mcp.server.resource.Resource;
import io.mcp.server.resource.ResourceHandler;
// Register resource list handler
server.addResourceListHandler(() -> {
List<Resource> resources = List.of(
Resource.builder()
.name("Data File")
.uri("resource://data/example.txt")
.description("Example data file")
.mimeType("text/plain")
.build()
);
return Mono.just(resources);
});
// Register resource read handler
server.addResourceReadHandler((uri) -> {
if (uri.equals("resource://data/example.txt")) {
String content = loadResourceContent(uri);
return Mono.just(ResourceContent.text(content, uri));
}
throw new ResourceNotFoundException(uri);
});
// Register resource subscribe handler
server.addResourceSubscribeHandler((uri) -> {
subscriptions.add(uri);
log.info("Client subscribed to {}", uri);
return Mono.empty();
});
```
## Adding Prompts
Implement prompt handlers for templated conversations:
```java
import io.mcp.server.prompt.Prompt;
import io.mcp.server.prompt.PromptMessage;
import io.mcp.server.prompt.PromptArgument;
// Register prompt list handler
server.addPromptListHandler(() -> {
List<Prompt> prompts = List.of(
Prompt.builder()
.name("analyze")
.description("Analyze a topic")
.argument(PromptArgument.builder()
.name("topic")
.description("Topic to analyze")
.required(true)
.build())
.argument(PromptArgument.builder()
.name("depth")
.description("Analysis depth")
.required(false)
.build())
.build()
);
return Mono.just(prompts);
});
// Register prompt get handler
server.addPromptGetHandler((name, arguments) -> {
if (name.equals("analyze")) {
String topic = arguments.getOrDefault("topic", "general");
String depth = arguments.getOrDefault("depth", "basic");
List<PromptMessage> messages = List.of(
PromptMessage.user("Please analyze this topic: " + topic),
PromptMessage.assistant("I'll provide a " + depth + " analysis of " + topic)
);
return Mono.just(PromptResult.builder()
.description("Analysis of " + topic + " at " + depth + " level")
.messages(messages)
.build());
}
throw new PromptNotFoundException(name);
});
```
## Reactive Streams Pattern
The Java SDK uses Reactive Streams (Project Reactor) for asynchronous processing:
```java
// Return Mono for single results
server.addToolHandler("process", (args) -> {
return Mono.fromCallable(() -> {
String result = expensiveOperation(args);
return ToolResponse.success()
.addTextContent(result)
.build();
}).subscribeOn(Schedulers.boundedElastic());
});
// Return Flux for streaming results
server.addResourceListHandler(() -> {
return Flux.fromIterable(getResources())
.map(r -> Resource.builder()
.uri(r.getUri())
.name(r.getName())
.build())
.collectList();
});
```
## Synchronous Facade
For blocking use cases, use the synchronous API:
```java
import io.mcp.server.McpSyncServer;
McpSyncServer syncServer = server.toSyncServer();
// Blocking tool handler
syncServer.addToolHandler("greet", (args) -> {
String name = args.get("name").asText();
return ToolResponse.success()
.addTextContent("Hello, " + name + "!")
.build();
});
```
## Transport Configuration
### Stdio Transport
For local subprocess communication:
```java
import io.mcp.server.transport.StdioServerTransport;
StdioServerTransport transport = new StdioServerTransport();
server.start(transport).block();
```
### HTTP Transport (Servlet)
For HTTP-based servers:
```java
import io.mcp.server.transport.ServletServerTransport;
import jakarta.servlet.http.HttpServlet;
public class McpServlet extends HttpServlet {
private final McpServer server;
private final ServletServerTransport transport;
public McpServlet() {
this.server = createMcpServer();
this.transport = new ServletServerTransport();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
transport.handleRequest(server, req, resp).block();
}
}
```
## Spring Boot Integration
Use the Spring Boot starter for seamless integration:
```xml
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-boot-starter</artifactId>
<version>0.14.1</version>
</dependency>
```
Configure the server with Spring:
```java
import org.springframework.context.annotation.Configuration;
import io.mcp.spring.McpServerConfigurer;
@Configuration
public class McpConfiguration {
@Bean
public McpServerConfigurer mcpServerConfigurer() {
return server -> server
.serverInfo("spring-server", "1.0.0")
.capabilities(cap -> cap
.tools(true)
.resources(true)
.prompts(true));
}
}
```
Register handlers as Spring beans:
```java
import org.springframework.stereotype.Component;
import io.mcp.spring.ToolHandler;
@Component
public class SearchToolHandler implements ToolHandler {
@Override
public String getName() {
return "search";
}
@Override
public Tool getTool() {
return Tool.builder()
.name("search")
.description("Search for information")
.inputSchema(JsonSchema.object()
.property("query", JsonSchema.string().required(true)))
.build();
}
@Override
public Mono<ToolResponse> handle(JsonNode arguments) {
String query = arguments.get("query").asText();
return Mono.just(ToolResponse.success()
.addTextContent("Search results for: " + query)
.build());
}
}
```
## Error Handling
Use proper error handling with MCP exceptions:
```java
server.addToolHandler("risky", (args) -> {
return Mono.fromCallable(() -> {
try {
String result = riskyOperation(args);
return ToolResponse.success()
.addTextContent(result)
.build();
} catch (ValidationException e) {
return ToolResponse.error()
.message("Invalid input: " + e.getMessage())
.build();
} catch (Exception e) {
log.error("Unexpected error", e);
return ToolResponse.error()
.message("Internal error occurred")
.build();
}
});
});
```
## JSON Schema Construction
Use the fluent schema builder:
```java
import io.mcp.json.JsonSchema;
JsonSchema schema = JsonSchema.object()
.property("name", JsonSchema.string()
.description("User's name")
.minLength(1)
.maxLength(100)
.required(true))
.property("age", JsonSchema.integer()
.description("User's age")
.minimum(0)
.maximum(150))
.property("email", JsonSchema.string()
.description("Email address")
.format("email")
.required(true))
.property("tags", JsonSchema.array()
.items(JsonSchema.string())
.uniqueItems(true))
.additionalProperties(false)
.build();
```
## Logging and Observability
Use SLF4J for logging:
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log = LoggerFactory.getLogger(MyMcpServer.class);
server.addToolHandler("process", (args) -> {
log.info("Tool called: process, args: {}", args);
return Mono.fromCallable(() -> {
String result = process(args);
log.debug("Processing completed successfully");
return ToolResponse.success()
.addTextContent(result)
.build();
}).doOnError(error -> {
log.error("Processing failed", error);
});
});
```
Propagate context with Reactor:
```java
import reactor.util.context.Context;
server.addToolHandler("traced", (args) -> {
return Mono.deferContextual(ctx -> {
String traceId = ctx.get("traceId");
log.info("Processing with traceId: {}", traceId);
return Mono.just(ToolResponse.success()
.addTextContent("Processed")
.build());
});
});
```
## Testing
Write tests using the synchronous API:
```java
import org.junit.jupiter.api.Test;
import static org.assertj.core.Assertions.assertThat;
class McpServerTest {
@Test
void testToolHandler() {
McpServer server = createTestServer();
McpSyncServer syncServer = server.toSyncServer();
JsonNode args = objectMapper.createObjectNode()
.put("query", "test");
ToolResponse response = syncServer.callTool("search", args);
assertThat(response.isError()).isFalse();
assertThat(response.getContent()).hasSize(1);
}
}
```
## Jackson Integration
The SDK uses Jackson for JSON serialization. Customize as needed:
```java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// Use custom mapper with server
McpServer server = McpServerBuilder.builder()
.objectMapper(mapper)
.build();
```
## Content Types
Support multiple content types in responses:
```java
import io.mcp.server.content.Content;
server.addToolHandler("multi", (args) -> {
return Mono.just(ToolResponse.success()
.addTextContent("Plain text response")
.addImageContent(imageBytes, "image/png")
.addResourceContent("resource://data", "application/json", jsonData)
.build());
});
```
## Server Lifecycle
Properly manage server lifecycle:
```java
import reactor.core.Disposable;
Disposable serverDisposable = server.start(transport).subscribe();
// Graceful shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down MCP server");
serverDisposable.dispose();
server.stop().block();
}));
```
## Common Patterns
### Request Validation
```java
server.addToolHandler("validate", (args) -> {
if (!args.has("required_field")) {
return Mono.just(ToolResponse.error()
.message("Missing required_field")
.build());
}
return processRequest(args);
});
```
### Async Operations
```java
server.addToolHandler("async", (args) -> {
return Mono.fromCallable(() -> callExternalApi(args))
.timeout(Duration.ofSeconds(30))
.onErrorResume(TimeoutException.class, e ->
Mono.just(ToolResponse.error()
.message("Operation timed out")
.build()))
.subscribeOn(Schedulers.boundedElastic());
});
```
### Resource Caching
```java
private final Map<String, String> cache = new ConcurrentHashMap<>();
server.addResourceReadHandler((uri) -> {
return Mono.fromCallable(() ->
cache.computeIfAbsent(uri, this::loadResource))
.map(content -> ResourceContent.text(content, uri));
});
```
# Java MCP Server Development
These instructions apply to the [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) 2.x. SDK 2.0.x supports the MCP specification revision [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) and the earlier revisions. SDK 3.x will add the revision [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28).
## Instructions
- Import the BOM `io.modelcontextprotocol.sdk:mcp-bom:2.0.1` and add the `io.modelcontextprotocol.sdk:mcp` artifact.
- In Gradle, import the BOM with `implementation(platform("io.modelcontextprotocol.sdk:mcp-bom:2.0.1"))`. Also add `testRuntimeOnly("org.junit.platform:junit-platform-launcher")`. Without it, the `test` task fails.
- The `mcp` artifact contains `mcp-core` and the Jackson 3 JSON mapper `mcp-json-jackson3`.
- Do not use code from SDK 0.x or 1.x. Read the [migration guide](https://github.com/modelcontextprotocol/java-sdk/blob/main/MIGRATION-2.0.md) when you update a server.
- The packages are `io.modelcontextprotocol.server`, `io.modelcontextprotocol.spec`, and `io.modelcontextprotocol.json`. The protocol types are nested in `McpSchema`.
- Make a server with `McpServer.sync(transport)` or `McpServer.async(transport)`. Set `serverInfo(name, version)` and `capabilities(...)`, and then call `build()`.
- Use the sync server for most servers. Use the async server with Project Reactor (`Mono`) only when the tool code does not block a thread.
- For stdio transport, use `new StdioServerTransportProvider(McpJsonDefaults.getMapper())`.
- Enable only the capabilities that the server uses: `ServerCapabilities.builder().tools(false).resources(false, true).prompts(true).completions().build()`. `tools(listChanged)` and `prompts(listChanged)` enable list change notifications. `resources(subscribe, listChanged)` also enables resource subscriptions.
- Register tools with `SyncToolSpecification.builder().tool(tool).callHandler((exchange, request) -> ...)`. The handler gets the arguments as a `Map` from `request.arguments()`.
- Use the builders that take the required fields first: `Tool.builder(name, inputSchema)`, `Resource.builder(uri, name)`, `ResourceTemplate.builder(uriTemplate, name)`, `Prompt.builder(name)`, `TextContent.builder(text)`, and `ReadResourceResult.builder(contents)`. The builders without arguments are deprecated.
- A required field must not be `null`. A `null` value causes an `IllegalArgumentException`.
- Tool names must have 1 to 128 characters: letters, digits, `_`, `-`, and `.`. The server builder rejects an incorrect name with an `IllegalArgumentException`. `addTool()` at runtime does not check the name.
- Give `inputSchema` and `outputSchema` as `Map<String, Object>` values. The `JsonSchema` record is deprecated.
- The server checks each schema against JSON Schema 2020-12 when you call `build()` or `addTool()`. An incorrect schema causes an `IllegalArgumentException`.
- The SDK validates the arguments against `inputSchema` before it calls the handler. It also validates `structuredContent` against `outputSchema`. If the validation fails, the client gets a tool result with `isError` set to `true`.
- If a tool has an `outputSchema`, return `structuredContent` in each successful result. Without it, the SDK returns a tool result with `isError` set to `true`.
- For machine-readable output, set `outputSchema` on the tool and return `CallToolResult.builder().structuredContent(map)`. Also add the same data as text content for older clients.
- Set tool hints with `ToolAnnotations.builder().readOnlyHint(true).build()`. Use `destructiveHint(true)` for a destructive tool.
- Return `CallToolResult.builder().addTextContent(message).isError(true).build()` for an error that the model must read.
- Do not throw an exception or an `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.
- Use `McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS).message(...).build()` only for a protocol error, for example an unknown resource URI.
- Register resources with `SyncResourceSpecification`, resource templates with `SyncResourceTemplateSpecification`, prompts with `SyncPromptSpecification`, and completions with `SyncCompletionSpecification`.
- Define resource templates with RFC 6570 URI templates, for example `users://{id}`.
- Report progress with `exchange.progressNotification(...)` only when `request.progressToken()` is not `null`.
- Get user input with `exchange.createElicitation(ElicitFormRequest.builder(message, requestedSchema).build())`. First, make sure that `exchange.getClientCapabilities().elicitation()` is not `null`.
- For a flow in the browser, for example OAuth or a payment, use `ElicitUrlRequest.builder(message, url, elicitationId)`. First, make sure that `exchange.getClientCapabilities().elicitation().url()` is not `null`. Do not send credentials in a form elicitation.
- Do not use sampling (`exchange.createMessage()`). The revision 2026-07-28 deprecates it. Call the LLM provider API directly.
- Do not use protocol logging (`exchange.loggingNotification()`). The revision 2026-07-28 deprecates it. Log with SLF4J.
- Do not use roots (`exchange.listRoots()`). The revision 2026-07-28 deprecates it. Get paths from tool arguments, resource URIs, or the server configuration.
- Add or remove tools at runtime with `server.addTool(...)` and `server.removeTool(name)`. Then call `server.notifyToolsListChanged()`.
- In a stdio server, stdout is the protocol channel. Do not call `System.out.println()`. Use `slf4j-simple` (it writes to stderr), or set the target of the Logback console appender to `System.err`.
- For a remote server, use Streamable HTTP. The SSE transports are deprecated.
- For a servlet container, use `HttpServletStreamableServerTransportProvider`. For a server without sessions, use `HttpServletStatelessServerTransport`.
- The HTTP transports do not check the `Host` and `Origin` headers by default. Always set `securityValidator(DefaultServerTransportSecurityValidator.builder()...build())`. This also applies to the Spring AI starters.
- The SDK has no authorization. For a remote server, add authorization in a servlet filter or with Spring Security.
- For Spring Boot, use the MCP starters of [Spring AI](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) 2.0 or later, for example `spring-ai-starter-mcp-server-webmvc`. Do not use the `io.modelcontextprotocol.sdk:mcp-spring-*` artifacts.
- In Spring AI, write tools as bean methods with `@McpTool` and `@McpToolParam` (from `org.springframework.ai.mcp.annotation`).
- For browser clients, configure CORS. Allow the `Mcp-*` request headers. Expose the `Mcp-Session-Id` response header.
- Package a stdio server as one executable JAR with the `maven-shade-plugin`. In Gradle, use the `application` plugin and the start script from `installDist`. Do not use `./gradlew run` as the client command.
- Test a server with `McpClient.sync(new StdioClientTransport(params, McpJsonDefaults.getMapper()))`. The SDK has no in-memory transport.
- Test servers manually with the MCP Inspector.
## Best Practices
1. **Use Reactive Streams** for async operations and backpressure
2. **Leverage Spring Boot** starter for enterprise applications
3. **Implement proper error handling** with specific error messages
4. **Use SLF4J** for logging, not System.out
5. **Validate inputs** in tool and prompt handlers
6. **Support graceful shutdown** with proper resource cleanup
7. **Use bounded elastic scheduler** for blocking operations
8. **Propagate context** for observability in reactive chains
9. **Test with synchronous API** for simplicity
10. **Follow Java naming conventions** (camelCase for methods, PascalCase for classes)
- Give each tool one responsibility.
- Write clear tool descriptions. The description tells the model when to use the tool.
- Add a `description` to each property in `inputSchema`.
- Put each tool specification in its own method. This keeps the tools easy to test.
- In the async server, do not block the event loop. Run code that blocks with `Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())`.
- Use environment variables for configuration.
- Be careful when a tool gives access to the file system or the network.
## Common Patterns
The snippets do not show all imports. The protocol types are nested in `io.modelcontextprotocol.spec.McpSchema`. `Mono` is in `reactor.core.publisher`, and `Schedulers` is in `reactor.core.scheduler`.
### Basic Server Setup (stdio)
```java
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 addTool() {
Tool tool = Tool.builder("add", Map.of(
"type", "object",
"properties", Map.of(
"a", Map.of("type", "integer", "description", "First number"),
"b", Map.of("type", "integer", "description", "Second number")),
"required", List.of("a", "b")))
.description("Add two integers.")
.build();
return SyncToolSpecification.builder()
.tool(tool)
.callHandler((exchange, request) -> {
long a = ((Number) request.arguments().get("a")).longValue();
long b = ((Number) request.arguments().get("b")).longValue();
return CallToolResult.builder().addTextContent(String.valueOf(a + b)).build();
})
.build();
}
public static void main(String[] args) {
McpServer.sync(new StdioServerTransportProvider(McpJsonDefaults.getMapper()))
.serverInfo("my-server", "0.1.0")
.capabilities(ServerCapabilities.builder().tools(false).build())
.tools(addTool())
.build();
}
}
```
### Tool with Structured Output
```java
static SyncToolSpecification wordCountTool() {
Tool tool = Tool.builder("word_count", Map.of(
"type", "object",
"properties", Map.of("text", Map.of("type", "string")),
"required", List.of("text")))
.description("Count the words in a text.")
.outputSchema(Map.of(
"type", "object",
"properties", Map.of("words", Map.of("type", "integer")),
"required", List.of("words")))
.annotations(ToolAnnotations.builder().readOnlyHint(true).build())
.build();
return SyncToolSpecification.builder()
.tool(tool)
.callHandler((exchange, request) -> {
String text = (String) request.arguments().get("text");
int words = text.isBlank() ? 0 : text.trim().split("\\s+").length;
return CallToolResult.builder()
.addTextContent("{\"words\":" + words + "}")
.structuredContent(Map.of("words", words))
.build();
})
.build();
}
```
### Error Handling
```java
static SyncToolSpecification divideTool() {
Tool tool = Tool.builder("divide", Map.of(
"type", "object",
"properties", Map.of("a", Map.of("type", "number"), "b", Map.of("type", "number")),
"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();
}
```
### Resources and Resource Templates
```java
static SyncResourceSpecification configResource() {
return new SyncResourceSpecification(
Resource.builder("config://app", "app-config").mimeType("application/json").build(),
(exchange, request) -> ReadResourceResult.builder(List.of(
TextResourceContents.builder(request.uri(), "{\"debug\":false}")
.mimeType("application/json")
.build()))
.build());
}
static SyncResourceTemplateSpecification userResource() {
return new SyncResourceTemplateSpecification(
ResourceTemplate.builder("users://{id}", "user").build(),
(exchange, request) -> {
String id = request.uri().substring("users://".length());
return ReadResourceResult.builder(List.of(
TextResourceContents.builder(request.uri(), "User " + id).build()))
.build();
});
}
```
Register them with `.resources(configResource())` and `.resourceTemplates(userResource())`.
### Prompt with Completion
```java
static SyncPromptSpecification reviewPrompt() {
Prompt prompt = Prompt.builder("code-review")
.description("Review code in a language.")
.arguments(List.of(PromptArgument.builder("language").required(true).build()))
.build();
return new SyncPromptSpecification(prompt, (exchange, request) -> GetPromptResult.builder(List.of(
PromptMessage.builder(Role.USER,
TextContent.builder("Review this " + request.arguments().get("language") + " code.").build())
.build()))
.build());
}
static SyncCompletionSpecification languageCompletion() {
List<String> languages = List.of("java", "kotlin", "scala");
return new SyncCompletionSpecification(new PromptReference("code-review"),
(exchange, request) -> new CompleteResult(new CompleteResult.CompleteCompletion(
languages.stream().filter(l -> l.startsWith(request.argument().value())).toList(), null, false)));
}
```
Register them with `.prompts(reviewPrompt())` and `.completions(languageCompletion())`.
### Async Tool with Progress
```java
static AsyncToolSpecification lookupTool() {
Tool tool = Tool.builder("lookup", Map.of(
"type", "object",
"properties", Map.of("query", Map.of("type", "string")),
"required", List.of("query")))
.description("Look up a query in a slow backend.")
.build();
return AsyncToolSpecification.builder()
.tool(tool)
.callHandler((exchange, request) -> {
Object token = request.progressToken();
Mono<Void> progress = token == null ? Mono.empty()
: exchange.progressNotification(
ProgressNotification.builder(token, 0.5).total(1.0).message("Halfway").build());
return progress.then(Mono.fromCallable(() -> slowLookup((String) request.arguments().get("query")))
.subscribeOn(Schedulers.boundedElastic())
.map(text -> CallToolResult.builder().addTextContent(text).build()));
})
.build();
}
```
Register it on `McpServer.async(transport)`.
### Streamable HTTP Server (Servlet)
```java
HttpServletStreamableServerTransportProvider transport = HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(McpJsonDefaults.getMapper())
.mcpEndpoint("/mcp")
.securityValidator(DefaultServerTransportSecurityValidator.builder()
.allowedHost("localhost:8080")
.allowedOrigin("http://localhost:*")
.build())
.build();
McpServer.sync(transport)
.serverInfo("my-server", "0.1.0")
.capabilities(ServerCapabilities.builder().tools(false).build())
.tools(addTool())
.build();
```
The transport is a Jakarta Servlet. Register it in a servlet container at `/mcp` with async support.
### Spring AI Tool
```java
@Component
class CalculatorTools {
@McpTool(name = "add", description = "Add two integers")
public int add(@McpToolParam(description = "First number") int a,
@McpToolParam(description = "Second number") int b) {
return a + b;
}
}
```
### Integration Test
```java
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 add() {
CallToolResult result = client.callTool(
CallToolRequest.builder("add").arguments(Map.of("a", 2, "b", 3)).build());
assertEquals("5", ((TextContent) result.content().get(0)).text());
}
}
```
+7 -7
View File
@@ -1,6 +1,6 @@
# Java MCP Server Development Plugin
Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration.
Build Model Context Protocol (MCP) servers in Java with the official MCP Java SDK 2.x. Includes a skill that generates a server project and an expert agent for guidance.
## Installation
@@ -13,15 +13,15 @@ copilot plugin install java-mcp-development@awesome-copilot
### Commands (Slash Commands)
| Command | Description |
|---------|-------------|
| `/java-mcp-development:java-mcp-server-generator` | Generate a complete Model Context Protocol server project in Java using the official MCP Java SDK with reactive streams and optional Spring Boot integration. |
| Command | Description |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `/java-mcp-development:java-mcp-server-generator` | Generate a Maven or Gradle project for a Java MCP server with the MCP Java SDK 2.x, JUnit tests, and client configuration |
### Agents
| Agent | Description |
|-------|-------------|
| `java-mcp-expert` | Expert assistance for building Model Context Protocol servers in Java using reactive streams, the official MCP Java SDK, and Spring Boot integration. |
| Agent | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
| `java-mcp-expert` | Expert assistant for developing Model Context Protocol (MCP) servers in Java with the MCP Java SDK 2.x |
## Source
+4 -6
View File
@@ -1,8 +1,8 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "java-mcp-development",
"description": "Complete toolkit for building Model Context Protocol servers in Java using the official MCP Java SDK with reactive streams and Spring Boot integration.",
"version": "1.0.0",
"description": "Build Model Context Protocol (MCP) servers in Java with the official MCP Java SDK 2.x. Includes a skill that generates a server project and an expert agent for guidance.",
"version": "2.0.0",
"author": {
"name": "Awesome Copilot Community"
},
@@ -12,11 +12,9 @@
"java",
"mcp",
"model-context-protocol",
"mcp-java-sdk",
"server-development",
"sdk",
"reactive-streams",
"spring-boot",
"reactor"
"spring-ai"
],
"extensions": {
"com.github.awesome-copilot": {
+232 -686
View File
@@ -1,756 +1,302 @@
---
name: java-mcp-server-generator
description: 'Generate a complete Model Context Protocol server project in Java using the official MCP Java SDK with reactive streams and optional Spring Boot integration.'
description: 'Use this skill when the user wants to create a Model Context Protocol (MCP) server in Java or in a Spring Boot app. Also use it when the user wants AI agents, such as GitHub Copilot or Claude, to use Java code, an API, or a database. Use it even if the user does not say "MCP". The skill makes a Maven or Gradle project with the MCP Java SDK 2.x, tools, JUnit tests, and client configuration. Do not use it for an MCP client or for a server in another language.'
compatibility: 'JDK 17 or later, Maven 3.9 or later or Gradle 9 or later, and network access to download dependencies. The MCP Inspector also uses Node.js.'
---
# Java MCP Server Generator
Generate a complete, production-ready MCP server in Java using the official Java SDK with Maven or Gradle.
Create a Model Context Protocol (MCP) server in Java with the official [MCP Java SDK](https://github.com/modelcontextprotocol/java-sdk) 2.x. SDK 2.0.x supports the MCP specification revision [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) and the earlier revisions. SDK 3.x will add the revision [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28).
## Project Generation
## Workflow
When asked to create a Java MCP server, generate a complete project with this structure:
Copy this checklist and track your progress:
```
my-mcp-server/
├── pom.xml (or build.gradle.kts)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/mcp/
│ │ │ ├── McpServerApplication.java
│ │ │ ├── config/
│ │ │ │ └── ServerConfiguration.java
│ │ │ ├── tools/
│ │ │ │ ├── ToolDefinitions.java
│ │ │ │ └── ToolHandlers.java
│ │ │ ├── resources/
│ │ │ │ ├── ResourceDefinitions.java
│ │ │ │ └── ResourceHandlers.java
│ │ │ └── prompts/
│ │ │ ├── PromptDefinitions.java
│ │ │ └── PromptHandlers.java
│ │ └── resources/
│ │ └── application.properties (if using Spring)
│ └── test/
│ └── java/
│ └── com/example/mcp/
│ └── McpServerTest.java
└── README.md
- [ ] Step 1: Create the project.
- [ ] Step 2: Write the server.
- [ ] Step 3: Write the tests.
- [ ] Step 4: Build until the tests pass.
- [ ] Step 5: Configure the client.
### Step 1: Create the project
Use Maven by default. If the project uses Gradle or the user asks for Gradle, read [references/gradle.md](references/gradle.md). Use it for Step 1, Step 4, and Step 5.
Make this layout:
```text
project-name/
├── pom.xml
└── src/
├── main/java/com/example/McpServerApp.java
└── test/java/com/example/McpServerAppTest.java
```
## Maven pom.xml Template
Write `pom.xml`:
```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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
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>my-mcp-server</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<groupId>com.example</groupId>
<artifactId>mcp-server-demo</artifactId>
<version>0.1.0</version>
<name>My MCP Server</name>
<description>Model Context Protocol server implementation</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mcp.version>0.14.1</mcp.version>
<slf4j.version>2.0.9</slf4j.version>
<logback.version>1.4.11</logback.version>
<junit.version>5.10.0</junit.version>
</properties>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<!-- MCP Java SDK -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>${mcp.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.mcp.McpServerApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<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>
```
## Gradle build.gradle.kts Template
- The `mcp` artifact contains `mcp-core` and the Jackson 3 JSON mapper `mcp-json-jackson3`.
- `slf4j-simple` writes the SDK logs to stderr.
- The shade plugin makes one executable JAR with all dependencies.
```kotlin
plugins {
id("java")
id("application")
}
### Step 2: Write the server
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
// MCP Java SDK
implementation("io.modelcontextprotocol.sdk:mcp:0.14.1")
// Logging
implementation("org.slf4j:slf4j-api:2.0.9")
implementation("ch.qos.logback:logback-classic:1.4.11")
// Testing
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("io.projectreactor:reactor-test:3.5.0")
}
application {
mainClass.set("com.example.mcp.McpServerApplication")
}
tasks.test {
useJUnitPlatform()
}
```
## McpServerApplication.java Template
Start from this template:
```java
package com.example.mcp;
package com.example;
import com.example.mcp.tools.ToolHandlers;
import com.example.mcp.resources.ResourceHandlers;
import com.example.mcp.prompts.PromptHandlers;
import io.mcp.server.McpServer;
import io.mcp.server.McpServerBuilder;
import io.mcp.server.transport.StdioServerTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
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 class McpServerApplication {
private static final Logger log = LoggerFactory.getLogger(McpServerApplication.class);
public static void main(String[] args) {
log.info("Starting MCP Server...");
try {
McpServer server = createServer();
StdioServerTransport transport = new StdioServerTransport();
// Start server
Disposable serverDisposable = server.start(transport).subscribe();
// Graceful shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down MCP server");
serverDisposable.dispose();
server.stop().block();
}));
log.info("MCP Server started successfully");
// Keep running
Thread.currentThread().join();
} catch (Exception e) {
log.error("Failed to start MCP server", e);
System.exit(1);
}
}
private static McpServer createServer() {
McpServer server = McpServerBuilder.builder()
.serverInfo("my-mcp-server", "1.0.0")
.capabilities(capabilities -> capabilities
.tools(true)
.resources(true)
.prompts(true))
.build();
// Register handlers
ToolHandlers.register(server);
ResourceHandlers.register(server);
PromptHandlers.register(server);
return server;
McpServer.sync(new StdioServerTransportProvider(McpJsonDefaults.getMapper()))
.serverInfo("demo", "0.1.0")
.capabilities(ServerCapabilities.builder().tools(false).build())
.tools(divideTool())
.build();
}
}
```
## ToolDefinitions.java Template
Then change the template to match the request of the user:
1. Replace `divideTool()` with the tools that the user asks for. Make one method for each tool.
2. Give each tool a JSON Schema `inputSchema` and a `description`. The description tells the model when to use the tool.
3. When the client must get machine-readable data, add `.outputSchema(Map)` to the tool. Return the data with `CallToolResult.builder().structuredContent(map)`. Also add the same data as text content for older clients.
4. Mark a read-only tool with `.annotations(ToolAnnotations.builder().readOnlyHint(true).build())`. Use `destructiveHint(true)` for a destructive tool.
5. Add resources, resource templates, and prompts only when the user asks for them. Also enable them in `ServerCapabilities`.
Use stdio by default. Read [references/streamable-http.md](references/streamable-http.md) only when the user asks for a remote server, an HTTP server, or Spring Boot.
### Step 3: Write the tests
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:
```java
package com.example.mcp.tools;
package com.example;
import io.mcp.json.JsonSchema;
import io.mcp.server.tool.Tool;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
public class ToolDefinitions {
public static List<Tool> getTools() {
return List.of(
createGreetTool(),
createCalculateTool()
);
}
private static Tool createGreetTool() {
return Tool.builder()
.name("greet")
.description("Generate a greeting message")
.inputSchema(JsonSchema.object()
.property("name", JsonSchema.string()
.description("Name to greet")
.required(true)))
.build();
}
private static Tool createCalculateTool() {
return Tool.builder()
.name("calculate")
.description("Perform mathematical calculations")
.inputSchema(JsonSchema.object()
.property("operation", JsonSchema.string()
.description("Operation to perform")
.enumValues(List.of("add", "subtract", "multiply", "divide"))
.required(true))
.property("a", JsonSchema.number()
.description("First operand")
.required(true))
.property("b", JsonSchema.number()
.description("Second operand")
.required(true)))
.build();
}
}
```
## ToolHandlers.java Template
```java
package com.example.mcp.tools;
import com.fasterxml.jackson.databind.JsonNode;
import io.mcp.server.McpServer;
import io.mcp.server.tool.ToolResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
public class ToolHandlers {
private static final Logger log = LoggerFactory.getLogger(ToolHandlers.class);
public static void register(McpServer server) {
// Register tool list handler
server.addToolListHandler(() -> {
log.debug("Listing available tools");
return Mono.just(ToolDefinitions.getTools());
});
// Register greet handler
server.addToolHandler("greet", ToolHandlers::handleGreet);
// Register calculate handler
server.addToolHandler("calculate", ToolHandlers::handleCalculate);
}
private static Mono<ToolResponse> handleGreet(JsonNode arguments) {
log.info("Greet tool called");
if (!arguments.has("name")) {
return Mono.just(ToolResponse.error()
.message("Missing 'name' parameter")
.build());
}
String name = arguments.get("name").asText();
String greeting = "Hello, " + name + "! Welcome to MCP.";
log.debug("Generated greeting for: {}", name);
return Mono.just(ToolResponse.success()
.addTextContent(greeting)
.build());
}
private static Mono<ToolResponse> handleCalculate(JsonNode arguments) {
log.info("Calculate tool called");
if (!arguments.has("operation") || !arguments.has("a") || !arguments.has("b")) {
return Mono.just(ToolResponse.error()
.message("Missing required parameters")
.build());
}
String operation = arguments.get("operation").asText();
double a = arguments.get("a").asDouble();
double b = arguments.get("b").asDouble();
double result;
switch (operation) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
if (b == 0) {
return Mono.just(ToolResponse.error()
.message("Division by zero")
.build());
}
result = a / b;
break;
default:
return Mono.just(ToolResponse.error()
.message("Unknown operation: " + operation)
.build());
}
log.debug("Calculation: {} {} {} = {}", a, operation, b, result);
return Mono.just(ToolResponse.success()
.addTextContent("Result: " + result)
.build());
}
}
```
## ResourceDefinitions.java Template
```java
package com.example.mcp.resources;
import io.mcp.server.resource.Resource;
import java.util.List;
public class ResourceDefinitions {
public static List<Resource> getResources() {
return List.of(
Resource.builder()
.name("Example Data")
.uri("resource://data/example")
.description("Example resource data")
.mimeType("application/json")
.build(),
Resource.builder()
.name("Configuration")
.uri("resource://config")
.description("Server configuration")
.mimeType("application/json")
.build()
);
}
}
```
## ResourceHandlers.java Template
```java
package com.example.mcp.resources;
import io.mcp.server.McpServer;
import io.mcp.server.resource.ResourceContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ResourceHandlers {
private static final Logger log = LoggerFactory.getLogger(ResourceHandlers.class);
private static final Map<String, Boolean> subscriptions = new ConcurrentHashMap<>();
public static void register(McpServer server) {
// Register resource list handler
server.addResourceListHandler(() -> {
log.debug("Listing available resources");
return Mono.just(ResourceDefinitions.getResources());
});
// Register resource read handler
server.addResourceReadHandler(ResourceHandlers::handleRead);
// Register resource subscribe handler
server.addResourceSubscribeHandler(ResourceHandlers::handleSubscribe);
// Register resource unsubscribe handler
server.addResourceUnsubscribeHandler(ResourceHandlers::handleUnsubscribe);
}
private static Mono<ResourceContent> handleRead(String uri) {
log.info("Reading resource: {}", uri);
switch (uri) {
case "resource://data/example":
String jsonData = String.format(
"{\"message\":\"Example resource data\",\"timestamp\":\"%s\"}",
Instant.now()
);
return Mono.just(ResourceContent.text(jsonData, uri, "application/json"));
case "resource://config":
String config = "{\"serverName\":\"my-mcp-server\",\"version\":\"1.0.0\"}";
return Mono.just(ResourceContent.text(config, uri, "application/json"));
default:
log.warn("Unknown resource requested: {}", uri);
return Mono.error(new IllegalArgumentException("Unknown resource URI: " + uri));
}
}
private static Mono<Void> handleSubscribe(String uri) {
log.info("Client subscribed to resource: {}", uri);
subscriptions.put(uri, true);
return Mono.empty();
}
private static Mono<Void> handleUnsubscribe(String uri) {
log.info("Client unsubscribed from resource: {}", uri);
subscriptions.remove(uri);
return Mono.empty();
}
}
```
## PromptDefinitions.java Template
```java
package com.example.mcp.prompts;
import io.mcp.server.prompt.Prompt;
import io.mcp.server.prompt.PromptArgument;
import java.util.List;
public class PromptDefinitions {
public static List<Prompt> getPrompts() {
return List.of(
Prompt.builder()
.name("code-review")
.description("Generate a code review prompt")
.argument(PromptArgument.builder()
.name("language")
.description("Programming language")
.required(true)
.build())
.argument(PromptArgument.builder()
.name("focus")
.description("Review focus area")
.required(false)
.build())
.build()
);
}
}
```
## PromptHandlers.java Template
```java
package com.example.mcp.prompts;
import io.mcp.server.McpServer;
import io.mcp.server.prompt.PromptMessage;
import io.mcp.server.prompt.PromptResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.List;
import java.nio.file.Path;
import java.util.Map;
public class PromptHandlers {
private static final Logger log = LoggerFactory.getLogger(PromptHandlers.class);
public static void register(McpServer server) {
// Register prompt list handler
server.addPromptListHandler(() -> {
log.debug("Listing available prompts");
return Mono.just(PromptDefinitions.getPrompts());
});
// Register prompt get handler
server.addPromptGetHandler(PromptHandlers::handleCodeReview);
}
private static Mono<PromptResult> handleCodeReview(String name, Map<String, String> arguments) {
log.info("Getting prompt: {}", name);
if (!name.equals("code-review")) {
return Mono.error(new IllegalArgumentException("Unknown prompt: " + name));
}
String language = arguments.getOrDefault("language", "Java");
String focus = arguments.getOrDefault("focus", "general quality");
String description = "Code review for " + language + " with focus on " + focus;
List<PromptMessage> messages = List.of(
PromptMessage.user("Please review this " + language + " code with focus on " + focus + "."),
PromptMessage.assistant("I'll review the code focusing on " + focus + ". Please share the code."),
PromptMessage.user("Here's the code to review: [paste code here]")
);
log.debug("Generated code review prompt for {} ({})", language, focus);
return Mono.just(PromptResult.builder()
.description(description)
.messages(messages)
.build());
}
}
```
## McpServerTest.java Template
```java
package com.example.mcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.mcp.server.McpServer;
import io.mcp.server.McpSyncServer;
import io.mcp.server.tool.ToolResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
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 McpServerTest {
private McpSyncServer syncServer;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
McpServer server = createTestServer();
syncServer = server.toSyncServer();
objectMapper = new ObjectMapper();
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();
}
private McpServer createTestServer() {
// Same setup as main application
McpServer server = McpServerBuilder.builder()
.serverInfo("test-server", "1.0.0")
.capabilities(cap -> cap.tools(true))
.build();
// Register handlers
ToolHandlers.register(server);
return server;
@AfterAll
static void stopServer() {
client.close();
}
@Test
void testGreetTool() {
ObjectNode args = objectMapper.createObjectNode();
args.put("name", "Java");
ToolResponse response = syncServer.callTool("greet", args);
assertFalse(response.isError());
assertEquals(1, response.getContent().size());
assertTrue(response.getContent().get(0).getText().contains("Java"));
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 testCalculateTool() {
ObjectNode args = objectMapper.createObjectNode();
args.put("operation", "add");
args.put("a", 5);
args.put("b", 3);
ToolResponse response = syncServer.callTool("calculate", args);
assertFalse(response.isError());
assertTrue(response.getContent().get(0).getText().contains("8"));
}
@Test
void testDivideByZero() {
ObjectNode args = objectMapper.createObjectNode();
args.put("operation", "divide");
args.put("a", 10);
args.put("b", 0);
ToolResponse response = syncServer.callTool("calculate", args);
assertTrue(response.isError());
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"));
}
}
```
## README.md Template
- The test uses the `java` command of the JDK that runs Maven. Do not use the `java` command from `PATH`.
- The test uses the real stdio transport and the real JSON-RPC messages.
````markdown
# My MCP Server
### Step 4: Build until the tests pass
A Model Context Protocol server built with Java and the official MCP Java SDK.
1. Run `mvn verify`. This command compiles the code, runs the tests, and makes `target/mcp-server-demo-0.1.0.jar`.
2. If a test fails, read the error. Fix the server or the test.
3. Run `mvn verify` again.
4. Continue only when the build is successful.
## Features
### Step 5: Configure the client
- ✅ Tools: greet, calculate
- ✅ Resources: example data, configuration
- ✅ Prompts: code-review
- ✅ Reactive Streams with Project Reactor
- ✅ Structured logging with SLF4J
- ✅ Full test coverage
## Requirements
- Java 17 or later
- Maven 3.6+ or Gradle 7+
## Build
### Maven
```bash
mvn clean package
```
### Gradle
```bash
./gradlew build
```
## Run
### Maven
```bash
java -jar target/my-mcp-server-1.0.0.jar
```
### Gradle
```bash
./gradlew run
```
## Testing
### Maven
```bash
mvn test
```
### Gradle
```bash
./gradlew test
```
## Integration with Claude Desktop
Add to `claude_desktop_config.json`:
For VS Code, write `.vscode/mcp.json`:
```json
{
"mcpServers": {
"my-mcp-server": {
"servers": {
"demo": {
"type": "stdio",
"command": "java",
"args": ["-jar", "/path/to/my-mcp-server-1.0.0.jar"]
"args": ["-jar", "/absolute/path/to/project/target/mcp-server-demo-0.1.0.jar"]
}
}
}
```
## License
For Claude Desktop, put the same `command` and `args` in `claude_desktop_config.json`, below the `mcpServers` key.
MIT
````
For an HTTP server, use the client configuration in [references/streamable-http.md](references/streamable-http.md).
## Generation Instructions
Tell the user these facts:
1. **Ask for project name and package**
2. **Choose build tool** (Maven or Gradle)
3. **Generate all files** with proper package structure
4. **Use Reactive Streams** for async handlers
5. **Include comprehensive logging** with SLF4J
6. **Add tests** for all handlers
7. **Follow Java conventions** (camelCase, PascalCase)
8. **Include error handling** with proper responses
9. **Document public APIs** with Javadoc
10. **Provide both sync and async** examples
- `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.
- To test the server manually, start the MCP Inspector with the same command. The Inspector uses Node.js.
## Gotchas
SDK 2.x changed many patterns of SDK 0.x and 1.x. Do not copy examples from earlier versions. Read the [migration guide](https://github.com/modelcontextprotocol/java-sdk/blob/main/MIGRATION-2.0.md) for the full list.
- Use the builders that take the required fields first: `Tool.builder(name, inputSchema)`, `Resource.builder(uri, name)`, `Prompt.builder(name)`, `TextContent.builder(text)`, and `ReadResourceResult.builder(contents)`. The builders without arguments are deprecated.
- A required field must not be `null`. A `null` value causes an `IllegalArgumentException`.
- Tool names must have 1 to 128 characters: letters, digits, `_`, `-`, 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.
- The server checks each schema against JSON Schema 2020-12 when you call `build()` or `addTool()`. An incorrect schema causes an `IllegalArgumentException`.
- The SDK validates the tool arguments against `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.
- If a tool has an `outputSchema`, return `structuredContent` in each successful result. Without it, the SDK returns a tool result with `isError` set to `true`.
- Return `CallToolResult` with `isError(true)` for an error that the model must read.
- Do not throw an exception or an `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.
- In a stdio server, stdout is the protocol channel. Do not call `System.out.println()`.
- Log with SLF4J to stderr. The default console appender of Logback writes to stdout. If you use Logback, set its target to `System.err`.
- The Spring transports moved to [Spring AI](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) 2.0 and later (group `org.springframework.ai`). Do not use the `io.modelcontextprotocol.sdk:mcp-spring-*` artifacts.
- Do not use the deprecated SSE transports. Use Streamable HTTP for a remote server.
- The revision 2026-07-28 deprecates these features ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)). Do not add them to a new server:
- **Sampling** (`exchange.createMessage()`): Call the LLM provider API directly.
- **Protocol logging** (`exchange.loggingNotification()`): Use SLF4J.
- **Roots** (`exchange.listRoots()`): Get paths from tool arguments, resource URIs, or the server configuration.
@@ -0,0 +1,90 @@
# Gradle Projects
Use this file only when the project uses Gradle or the user asks for Gradle. The Java code and the tests do not change.
## Create the project
Make this layout. Do not write `pom.xml`:
```text
project-name/
├── settings.gradle.kts
├── build.gradle.kts
└── src/
├── main/java/com/example/McpServerApp.java
└── test/java/com/example/McpServerAppTest.java
```
Write `settings.gradle.kts`:
```kotlin
rootProject.name = "mcp-server-demo"
```
Write `build.gradle.kts`:
```kotlin
plugins {
application
}
group = "com.example"
version = "0.1.0"
repositories {
mavenCentral()
}
dependencies {
implementation(platform("io.modelcontextprotocol.sdk:mcp-bom:2.0.1"))
implementation("io.modelcontextprotocol.sdk:mcp")
runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
testImplementation(platform("org.junit:junit-bom:6.1.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<JavaCompile> {
options.release = 17
}
application {
mainClass = "com.example.McpServerApp"
}
tasks.test {
useJUnitPlatform()
}
```
## Build until the tests pass
1. If the project has no `gradlew` file, run `gradle wrapper` one time.
2. Run `./gradlew build installDist`. This command compiles the code and runs the tests. It also makes the start script `build/install/mcp-server-demo/bin/mcp-server-demo`.
3. If a test fails, read the error. Fix the server or the test.
4. Run `./gradlew build installDist` again.
5. Continue only when the build is successful.
## Configure the client
For VS Code, write `.vscode/mcp.json`:
```json
{
"servers": {
"demo": {
"type": "stdio",
"command": "/absolute/path/to/project/build/install/mcp-server-demo/bin/mcp-server-demo"
}
}
}
```
On Windows, use `mcp-server-demo.bat`.
## Gotchas
- Gradle 9 does not add the JUnit Platform launcher. Without `junit-platform-launcher`, the `test` task fails.
- The `application` plugin does not make an executable JAR. The JAR in `build/libs` has no `Main-Class` and no dependencies. Use the start script from `installDist`.
- Do not use `./gradlew run` as the client command. The `run` task does not send stdin to the server. Gradle also writes its own output to stdout.
@@ -0,0 +1,118 @@
# Streamable HTTP Servers
Use this transport only for a remote server. A local server uses stdio.
## Servlet server
`HttpServletStreamableServerTransportProvider` is a Jakarta Servlet. It is in the `mcp` artifact. Add a servlet container to `pom.xml`, for example embedded Tomcat:
```xml
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>11.0.26</version>
</dependency>
```
In Gradle, add `implementation("org.apache.tomcat.embed:tomcat-embed-core:11.0.26")`.
Replace the `main` method in `McpServerApp.java`:
```java
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
HttpServletStreamableServerTransportProvider transport = HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(McpJsonDefaults.getMapper())
.mcpEndpoint("/mcp")
.securityValidator(DefaultServerTransportSecurityValidator.builder()
.allowedHost("localhost:" + port)
.allowedHost("127.0.0.1:" + port)
.allowedOrigin("http://localhost:*")
.build())
.build();
McpServer.sync(transport)
.serverInfo("demo", "0.1.0")
.capabilities(ServerCapabilities.builder().tools(false).build())
.tools(divideTool())
.build();
Tomcat tomcat = new Tomcat();
tomcat.setPort(port);
tomcat.getConnector();
Context context = tomcat.addContext("", null);
Tomcat.addServlet(context, "mcp", transport).setAsyncSupported(true);
context.addServletMappingDecoded("/mcp", "mcp");
tomcat.start();
tomcat.getServer().await();
}
```
Add these imports: `org.apache.catalina.Context`, `org.apache.catalina.startup.Tomcat`, `io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator`, and `io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider`. Remove the import of `StdioServerTransportProvider`.
Clients connect to `http://HOST:PORT/mcp`. For VS Code, write `.vscode/mcp.json`:
```json
{
"servers": {
"demo": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
}
}
```
## Spring Boot server
Use the MCP starters of [Spring AI](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) 2.0 or later. Spring AI 2.0 uses Spring Boot 4. Use `spring-boot-starter-parent` 4.0.x. Import `org.springframework.ai:spring-ai-bom` version 2.0.1 and add `spring-ai-starter-mcp-server-webmvc`. For WebFlux, add `spring-ai-starter-mcp-server-webflux`.
Write the tools as annotated methods on a Spring bean:
```java
@Component
class CalculatorTools {
@McpTool(name = "add", description = "Add two integers")
public int add(@McpToolParam(description = "First number") int a,
@McpToolParam(description = "Second number") int b) {
return a + b;
}
}
```
The annotations are in `org.springframework.ai.mcp.annotation`. Set the server identity and the transport in `application.properties`:
```properties
spring.ai.mcp.server.name=demo
spring.ai.mcp.server.version=0.1.0
spring.ai.mcp.server.protocol=STREAMABLE
```
The starter serves the endpoint at `/mcp`.
## Gotchas
- The HTTP transports do not check the `Host` and `Origin` headers by default. This also applies to the Spring AI starters. Without a check, a malicious web page can use DNS rebinding to call a local server. Always set `securityValidator(DefaultServerTransportSecurityValidator.builder()...build())`. The validator rejects an unknown `Origin` with HTTP 403 and an unknown `Host` with HTTP 421.
- In Spring AI, declare your own `WebMvcStreamableServerTransportProvider` bean to set the validator. The starter then does not make its own bean:
```java
@Bean
WebMvcStreamableServerTransportProvider webMvcStreamableServerTransportProvider() {
return WebMvcStreamableServerTransportProvider.builder()
.jsonMapper(McpJsonDefaults.getMapper())
.mcpEndpoint("/mcp")
.securityValidator(DefaultServerTransportSecurityValidator.builder()
.allowedHost("localhost:8080")
.allowedOrigin("http://localhost:*")
.build())
.build();
}
```
- For WebFlux, declare a `WebFluxStreamableServerTransportProvider` bean in the same way. Its builder uses `messageEndpoint("/mcp")`, not `mcpEndpoint`.
- Embedded Tomcat listens on all network interfaces. For a public host, add its name to `allowedHost`. Put authentication in front of the server.
- The SDK has no authorization. For a remote server, add authorization in a servlet filter or with Spring Security.
- `HttpServletStatelessServerTransport` makes a server without sessions. Use it with `McpServer.sync(transport)` when more than one replica must answer the requests. A stateless server cannot send requests to the client. Also set `securityValidator` on its builder.
- For browser clients, add CORS to the servlet container. Allow the `Mcp-*` request headers. Expose the `Mcp-Session-Id` response header.