mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-13 23:34:25 +00:00
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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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. |
|
||||
|
||||
@@ -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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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 />[](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. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user