上传备份

master
王兵 1 month ago
commit 6f4ac0b9ee

16
.gitignore vendored

@ -0,0 +1,16 @@
Thumbs.db
.DS_Store
target/
out/
.micronaut/
.idea
*.iml
*.ipr
*.iws
.project
.settings
.classpath
.factorypath
.m2/
maven-wrapper.jar
maven-wrapper.properties

@ -0,0 +1,82 @@
# A simple MCP Server implemented with Micronaut
This project implements a simple MCP ([Model Context Protocol](https://modelcontextprotocol.io/introduction)) server,
with an HTTP SSE transport, using [Micronaut](https://micronaut.io/).
A test class uses [LangChain4j's MCP client](https://docs.langchain4j.dev/tutorials/mcp) support to call and interact with the Micronaut MCP server.
## What it does
The server provides dummy weather information ☀️ for a given city.
The MCP server implements a subset of the MCP protocol to handle requests for weather data.
In particular, it implements the following operations:
* `initialize`
* `notifications/initialize`
* `tools/list`
* `tools/call`
All the MCP protocol classes can be found in the `xyz.wbsite.mcp.basic.model` package.
There are two main controllers working together to implement the server-side of the MCP communication over HTTP/SSE:
### The `SseController` (serving `/mcp/sse`):
This controller acts as the entry point for the Server-Sent Events (SSE) connection.
When an MCP client wants to connect, it first makes an HTTP GET request to this endpoint (`/mcp/sse`).
How it works:
* It's annotated with `@Controller("/mcp/sse")`.
* It has a single method `connectSse()` annotated with `@Get(produces = MediaType.TEXT_EVENT_STREAM)`. This tells Micronaut that GET requests to `/mcp/sse` should be handled by this method and that the response will be an SSE stream.
* It injects the `SseBroadcaster` singleton bean.
* The `connectSse()` method simply calls `broadcaster.getEventsPublisher()` and returns the result.
What it does:
* It establishes the persistent SSE connection with the client.
* It delegates the responsibility of actually sending events over this connection to the SseBroadcaster. The broadcaster ensures the first event sent tells the client where to send POST requests (the endpoint event), and then sends subsequent responses or notifications.
### The `PostController` (`/mcp/post`):
This controller handles the incoming MCP command requests sent by the client after the SSE connection is established. The client learns the path for this controller (`/mcp/post`) from the initial endpoint event received via the SseController.
How it works:
* It's annotated with `@Controller("/mcp/post")`.
* It has a method `handleMcpPostRequest(@Body McpRequest request)` annotated with `@Post(consumes = MediaType.APPLICATION_JSON)`. This means it handles HTTP POST requests to `/mcp/post` where the body contains JSON data conforming to the `McpRequest` structure.
* It also injects the `SseBroadcaster`.
Inside `handleMcpPostRequest`:
* It deserializes the JSON request body into an `McpRequest` object.
* It calls a private helper method `processRequest(request)` to determine the appropriate action based on the `request.method()` (e.g., `initialize`, `tools/list`, `tools/call`).
* `processRequest` generates an `McpResponse` object containing the result (or an error, or `null` for notifications).
* If `processRequest` returns a response object, `handleMcpPostRequest` calls `broadcaster.broadcastResponse(mcpResponse)`. This sends the actual MCP result back to the client over the previously established SSE connection.
* Finally, it returns an immediate `HttpResponse.ok()` to the original POST request. This HTTP response simply acknowledges that the server received the POST request; it does not contain the actual MCP result.
What it does:
* It receives specific commands from the MCP client (like "list available tools" or "execute the weather tool"). It processes these commands, generates the corresponding MCP response, and uses the `SseBroadcaster` to send that response back asynchronously over the SSE channel managed initially by the `SseController`.
### The `SseBroadcaster`
The `SseBroadcaster` manages the SSE stream, sends the initial configuration (endpoint event), and provides a way for other parts of the server (like the `PostController`) to send JSON-formatted responses and notifications back to the connected client over that stream.
## The `McpWeatherClientTest` client
The `McpWeatherClientTest` class is an integration test that verifies the functionality of the MCP server.
You can run the test class with `./gradlew test`.
What it does:
* It starts a local server (via Micronaut).
* It sets up a LangChain4j AI assistant (`WeatherAssistant`) configured to use Google Cloud Vertex AI's Gemini 2.0 Flash mode.
* It configures this assistant to find and use tools provided by the local server via a specific protocol (MCP over HTTP/SSE).
* It tests if the client can discover the tools correctly.
* It tests if the assistant correctly uses the remote weather tool when asked about weather.
* It tests if the assistant correctly avoids using the weather tool for unrelated questions like simple greeting prompts.
---
>[!NOTE]
> This project is not an official Google project.

@ -0,0 +1,80 @@
<?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>
<groupId>xyz.wbsite</groupId>
<artifactId>starter-mcp-server</artifactId>
<version>0.1</version>
<packaging>pom</packaging>
<name>starter-mcp-server</name>
<description>Spring Boot MCP Server Demo</description>
<modules>
<module>starter-mcp-server-app</module>
<module>starter-mcp-server-basic</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<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>
</properties>
<repositories>
<!-- 将中央仓库地址指向阿里云聚合仓库,提高下载速度 -->
<repository>
<id>central</id>
<name>Central Repository</name>
<layout>default</layout>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<pluginRepositories>
<!-- 将插件的仓库指向阿里云聚合仓库解决低版本maven下载插件异常或提高下载速度 -->
<pluginRepository>
<id>central</id>
<name>Central Repository</name>
<url>https://maven.aliyun.com/repository/public</url>
<layout>default</layout>
</pluginRepository>
</pluginRepositories>
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-web</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 糊涂工具包含常用API避免重复造轮子 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-webflux</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webflux</artifactId>
<version>0.8.1</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,18 @@
package xyz.wbsite.mcp.basic;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* MCP
* mcp使@link{McpServerApplication}
*
* @author wangbing
*/
@SpringBootApplication
public class BasicApplication {
public static void main(String[] args) {
SpringApplication.run(BasicApplication.class, args);
}
}

@ -0,0 +1,50 @@
package xyz.wbsite.mcp.basic;
import java.util.List;
/**
*
*
*
* @author wangbing
*/
public class CallToolResult extends Data {
/**
*
*/
private List<TextContentData> content;
/**
*
*/
private Boolean isError;
public CallToolResult() {
}
public CallToolResult(List<TextContentData> content, Boolean isError) {
this.content = content;
this.isError = isError;
}
public CallToolResult(List<TextContentData> content) {
this(content, false);
}
public List<TextContentData> getContent() {
return content;
}
public void setContent(List<TextContentData> content) {
this.content = content;
}
public Boolean getIsError() {
return isError;
}
public void setIsError(Boolean isError) {
this.isError = isError;
}
}

@ -0,0 +1,18 @@
package xyz.wbsite.mcp.basic;
import java.io.Serial;
import java.io.Serializable;
/**
* .
*
* @author wangbing
* @version 0.0.1
* @since 1.8
*/
public class Data implements Serializable {
@Serial
private static final long serialVersionUID = -1L;
}

@ -0,0 +1,30 @@
package xyz.wbsite.mcp.basic;
/**
*
*
*
* @author wangbing
*/
public class InitializeResult extends Data {
/**
*
*/
private ServerCapabilities capabilities;
public InitializeResult() {
}
public InitializeResult(ServerCapabilities capabilities) {
this.capabilities = capabilities;
}
public ServerCapabilities getCapabilities() {
return capabilities;
}
public void setCapabilities(ServerCapabilities capabilities) {
this.capabilities = capabilities;
}
}

@ -0,0 +1,75 @@
package xyz.wbsite.mcp.basic;
import java.util.List;
import java.util.Map;
/**
*
*
*
* @author wangbing
*/
public class InputSchema extends Data {
/**
*
*/
private String type;
/**
*
*/
private Map<String, Object> properties;
/**
*
*/
private List<String> required;
/**
*
*/
private Boolean additionalProperties;
public InputSchema() {
}
public InputSchema(String type, Map<String, Object> properties, List<String> required, Boolean additionalProperties) {
this.type = type;
this.properties = properties;
this.required = required;
this.additionalProperties = additionalProperties;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Map<String, Object> getProperties() {
return properties;
}
public void setProperties(Map<String, Object> properties) {
this.properties = properties;
}
public List<String> getRequired() {
return required;
}
public void setRequired(List<String> required) {
this.required = required;
}
public Boolean getAdditionalProperties() {
return additionalProperties;
}
public void setAdditionalProperties(Boolean additionalProperties) {
this.additionalProperties = additionalProperties;
}
}

@ -0,0 +1,32 @@
package xyz.wbsite.mcp.basic;
import java.util.List;
/**
*
*
*
* @author wangbing
*/
public class ListToolsResult extends Data {
/**
*
*/
private List<ToolSpecificationData> tools;
public ListToolsResult() {
}
public ListToolsResult(List<ToolSpecificationData> tools) {
this.tools = tools;
}
public List<ToolSpecificationData> getTools() {
return tools;
}
public void setTools(List<ToolSpecificationData> tools) {
this.tools = tools;
}
}

@ -0,0 +1,36 @@
package xyz.wbsite.mcp.basic;
/**
*
*
*
* @author wangbing
*/
public class McpError extends Data {
private int code;
private String message;
public McpError() {
}
public McpError(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

@ -0,0 +1,58 @@
package xyz.wbsite.mcp.basic;
import com.fasterxml.jackson.databind.JsonNode;
/**
* MCP
* JSON-RPC
*
* @author wangbing
*/
public class McpRequest extends Data {
private String jsonrpc;
private Long id;
private String method;
private JsonNode params; // Use JsonNode for flexible params
public McpRequest() {
}
public McpRequest(String jsonrpc, Long id, String method, JsonNode params) {
this.jsonrpc = jsonrpc;
this.id = id;
this.method = method;
this.params = params;
}
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public JsonNode getParams() {
return params;
}
public void setParams(JsonNode params) {
this.params = params;
}
}

@ -0,0 +1,65 @@
package xyz.wbsite.mcp.basic;
/**
* MCP
* JSON-RPC
*
*
* @author wangbing
*/
public class McpResponse extends Data {
private String jsonrpc;
private Long id;
private Object result; // Can be ListToolsResult, CallToolResult, etc.
private McpError error; // Optional error field
public McpResponse() {
}
public McpResponse(String jsonrpc, Long id, Object result, McpError error) {
this.jsonrpc = jsonrpc;
this.id = id;
this.result = result;
this.error = error;
}
public McpResponse(Long id, Object result) {
this("2.0", id, result, null);
}
public McpResponse(Long id, McpError error) {
this("2.0", id, null, error);
}
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public McpError getError() {
return error;
}
public void setError(McpError error) {
this.error = error;
}
}

@ -0,0 +1,113 @@
package xyz.wbsite.mcp.basic;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
*
* MCP POST
*
* @author wangbing
*/
@RestController
@RequestMapping("/mcp/post")
public class PostController {
private static final Logger log = LoggerFactory.getLogger(PostController.class);
private static final String WEATHER_TOOL_NAME = "getWeatherForecast";
private static final String FAKE_WEATHER_JSON = "{\"forecast\": \"sunny\"}";
@Resource
SseBroadcaster broadcaster;
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> handleMcpPostRequest(@RequestBody McpRequest request) {
log.info("Received MCP POST Request: ID={}, Method={}", request.getId(), request.getMethod());
McpResponse mcpResponse = processRequest(request);
if (mcpResponse != null) {
// Send the response back over the SSE channel
broadcaster.broadcastResponse(mcpResponse);
} else {
// 处理可能不会生成McpResponse对象的通知等情况
// 在我们的例子中,'通知/初始化'落在这里。
log.debug("No explicit response object generated for method '{}', assuming notification ack.", request.getMethod());
}
// 立即返回HTTP 200 OK或202 Accepted以确认收到POST。
// 实际结果通过SSE异步发送。
// 200 OK可能更简单因为客户希望得到一些响应体即使是空的。
// 202 Accepted明确表示处理正在其他地方进行。让我们用200。
return ResponseEntity.ok().build();
}
private McpResponse processRequest(McpRequest request) {
switch (request.getMethod()) {
case "initialize":
log.info("Handling initialize request");
InitializeResult initResult = new InitializeResult(new ServerCapabilities());
return new McpResponse(request.getId(), initResult);
case "notifications/initialized":
log.info("Received initialized notification");
// 这是来自客户端的通知。MCP规范称通知
// 没有回应。所以我们在这里返回nullPOST处理程序
// 将只返回HTTP OK。
return null;
case "tools/list":
log.info("Handling tools/list request");
ToolSpecificationData weatherTool = new ToolSpecificationData(
WEATHER_TOOL_NAME,
"Gets the current weather forecast.",
new InputSchema(
"object",
Map.of("location", Map.of(
"type", "string",
"description", "Location to get the weather for")
),
List.of("location"),
false)
);
ListToolsResult listResult = new ListToolsResult(List.of(weatherTool));
return new McpResponse(request.getId(), listResult);
case "tools/call":
log.info("Handling tools/call request");
if (request.getParams() != null && request.getParams().has("name")) {
String toolName = request.getParams().get("name").asText();
if (WEATHER_TOOL_NAME.equals(toolName)) {
log.info("Executing tool: {}", toolName);
TextContentData textContent = new TextContentData(FAKE_WEATHER_JSON);
CallToolResult callResult = new CallToolResult(List.of(textContent));
return new McpResponse(request.getId(), callResult);
} else {
log.warn("Unknown tool requested: {}", toolName);
return new McpResponse(request.getId(), new McpError(-32601, "Method not found: " + toolName));
}
} else {
log.error("Invalid tools/call request: Missing 'name' in params");
return new McpResponse(request.getId(), new McpError(-32602, "Invalid params for tools/call"));
}
case "ping":
log.info("Handling ping request");
return new McpResponse(request.getId(), Collections.emptyMap());
default:
log.warn("Unsupported MCP method: {}", request.getMethod());
return new McpResponse(request.getId(), new McpError(-32601, "Method not found: " + request.getMethod()));
}
}
}

@ -0,0 +1,15 @@
package xyz.wbsite.mcp.basic;
import java.io.Serial;
/**
*
*
*
* @author wangbing
*/
public class ServerCapabilities extends Data {
@Serial
private static final long serialVersionUID = 1L;
}

@ -0,0 +1,139 @@
package xyz.wbsite.mcp.basic;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* SSE广
* 广SSE
*
* @author wangbing
*/
@Service // Manages the SSE emitters
public class SseBroadcaster {
private static final Logger log = LoggerFactory.getLogger(SseBroadcaster.class);
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
private final ObjectMapper objectMapper;
public SseBroadcaster(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* SSE
*
* @param emitter sse
*/
public void registerEmitter(SseEmitter emitter) {
// Add the emitter to the list
emitters.add(emitter);
log.info("New SSE emitter registered, total emitters: {}", emitters.size());
// Set up completion handlers to remove the emitter when the connection is closed
emitter.onCompletion(() -> {
emitters.remove(emitter);
log.info("SSE emitter completed, remaining emitters: {}", emitters.size());
});
emitter.onTimeout(() -> {
emitters.remove(emitter);
log.info("SSE emitter timed out, remaining emitters: {}", emitters.size());
});
emitter.onError((e) -> {
emitters.remove(emitter);
log.error("Error on SSE emitter: {}", e.getMessage(), e);
});
}
/**
*
*
* @param emitter sse
*/
public void sendEndpointEvent(SseEmitter emitter) {
try {
emitter.send(SseEmitter.event()
.name("endpoint")
.data("/mcp/post"));
log.info("Sent endpoint event to SSE emitter");
} catch (IOException e) {
log.error("Failed to send endpoint event: {}", e.getMessage(), e);
emitter.completeWithError(e);
}
}
/**
* POSTSSE
*
* @param response .
*/
public void broadcastResponse(McpResponse response) {
if (emitters.isEmpty()) {
log.warn("No active SSE emitters to broadcast response (ID: {})\n", response.getId());
return;
}
try {
// Responses are sent as events of type "message"
String jsonResponse = objectMapper.writeValueAsString(response);
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(jsonResponse));
log.debug("Successfully sent SSE message for Response ID: {}", response.getId());
} catch (IOException e) {
log.error("Failed to send SSE message (Response ID: {}): {}", response.getId(), e.getMessage(), e);
// 考虑移除有故障的发射器
emitter.completeWithError(e);
}
}
} catch (IOException e) {
log.error("Failed to serialize McpResponse (ID: {}) to JSON for SSE: {}", response.getId(), e.getMessage(), e);
}
}
/**
* Sends a simple notification (like log messages, tool updates) over SSE.
* This isn't used in the simple weather example but shows how non-response
* messages would be sent.
*
* @param notification An object representing the notification.
* @param eventName The SSE event name (e.g., "notifications/message").
*/
public void broadcastNotification(Object notification, String eventName) {
if (emitters.isEmpty()) {
log.warn("No active SSE emitters to broadcast notification ({})\n", eventName);
return;
}
try {
String jsonNotification = objectMapper.writeValueAsString(notification);
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(jsonNotification));
log.debug("Successfully sent SSE notification ({})\n", eventName);
} catch (IOException e) {
log.error("Failed to send SSE notification ({}): {}", eventName, e.getMessage(), e);
emitter.completeWithError(e);
}
}
} catch (IOException e) {
log.error("Failed to serialize notification ({}) to JSON for SSE: {}", eventName, e.getMessage(), e);
}
}
}

@ -0,0 +1,35 @@
package xyz.wbsite.mcp.basic;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* SSE
* SSE
*
* @author wangbing
*/
@RestController
@RequestMapping("/mcp/sse")
public class SseController {
private static final Logger log = LoggerFactory.getLogger(SseController.class);
@Resource
private SseBroadcaster broadcaster;
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connectSse() {
log.info("Client requesting SSE connection...");
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
broadcaster.registerEmitter(emitter);
broadcaster.sendEndpointEvent(emitter);
return emitter;
}
}

@ -0,0 +1,44 @@
package xyz.wbsite.mcp.basic;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
*
*
* @author wangbing
*/
public class TextContentData extends Data {
@JsonProperty("type")
private String type;
@JsonProperty("text")
private String text;
public TextContentData() {
}
public TextContentData(String type, String text) {
this.type = type;
this.text = text;
}
public TextContentData(String text) {
this("text", text);
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

@ -0,0 +1,46 @@
package xyz.wbsite.mcp.basic;
/**
*
*
*
* @author wangbing
*/
public class ToolSpecificationData extends Data {
private String name;
private String description;
private InputSchema inputSchema;
public ToolSpecificationData() {
}
public ToolSpecificationData(String name, String description, InputSchema inputSchema) {
this.name = name;
this.description = description;
this.inputSchema = inputSchema;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public InputSchema getInputSchema() {
return inputSchema;
}
public void setInputSchema(InputSchema inputSchema) {
this.inputSchema = inputSchema;
}
}

@ -0,0 +1,17 @@
package xyz.wbsite.mcp.easy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* MCP
*
* @author wangbing
*/
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}

@ -0,0 +1,23 @@
package xyz.wbsite.mcp.easy.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface P {
/**
* Description of a parameter
* @return the description of a parameter
*/
String value();
/**
* Whether the parameter is required
* @return true if the parameter is required, false otherwise
* Default is true.
*/
boolean required() default true;
}

@ -0,0 +1,28 @@
package xyz.wbsite.mcp.easy.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Tool {
/**
* Name of the tool. If not provided, method name will be used.
*
* @return name of the tool.
*/
String name() default "";
/**
* Description of the tool.
*
* @return description of the tool.
*/
String value() default "";
}

@ -0,0 +1,24 @@
package xyz.wbsite.mcp.easy.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
/**
* @author quanyu
* @date 2025/5/6 22:39
*/
@Configuration
class McpServerConfig {
@Bean
WebFluxSseServerTransportProvider webFluxSseServerTransportProvider() {
return new WebFluxSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
}
@Bean
RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransportProvider provider) {
return provider.getRouterFunction();
}
}

@ -0,0 +1,166 @@
package xyz.wbsite.mcp.easy.registrar;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import xyz.wbsite.mcp.easy.annotation.P;
import xyz.wbsite.mcp.easy.annotation.Tool;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
public class McpServerRegistrar {
@Resource
private ApplicationContext applicationContext;
@Resource
private WebFluxSseServerTransportProvider transport;
@PostConstruct
public void registerMcpServers() {
// 创建具有自定义配置的服务器
McpSyncServer syncServer = McpServer.sync(transport)
.serverInfo("Mcp Server", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder()
.resources(true, true) // 启用资源支持
.tools(true) // 启用工具支持
.prompts(true) // 启用提示支持
.logging() // 启用日志支持
.build())
.build();
// 查找方法级别的Tool注解
String[] beanNames = applicationContext.getBeanDefinitionNames();
for (String beanName : beanNames) {
// 跳过自身 Bean避免循环依赖
if (beanName.equals("mcpServerRegistrar")) {
continue;
}
Object bean = applicationContext.getBean(beanName);
for (Method method : bean.getClass().getMethods()) {
if (method.isAnnotationPresent(Tool.class)) {
Tool tool = method.getAnnotation(Tool.class);
// 处理参数
ObjectNode schema = new ObjectNode(JsonNodeFactory.instance);
schema.put("type", "object");
ArrayNode required = schema.putArray("required");
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
this.putProperties(schema, parameter);
required.add(parameter.getName());
}
McpServerFeatures.SyncToolSpecification syncToolSpecification = buildSyncToolSpecification(bean, method, tool.value(), schema.toPrettyString());
syncServer.addTool(syncToolSpecification);
}
}
}
// 发送日志通知
syncServer.loggingNotification(McpSchema.LoggingMessageNotification.builder()
.level(McpSchema.LoggingLevel.INFO)
.logger("Mcp Server Log")
.data("The server has been initialized")
.build());
}
private void putProperties(ObjectNode schema, Parameter parameter) {
// 处理参数
String typeName = jsonTypeMapper(parameter);
ObjectNode properties = (ObjectNode) schema.get("properties");
if (properties == null) {
properties = schema.putObject("properties");
}
ObjectNode paramSchema = properties.putObject(parameter.getName());
paramSchema.put("type", typeName);
if (parameter.isAnnotationPresent(P.class)) {
P p = parameter.getAnnotation(P.class);
paramSchema.put("description", p.value());
}
}
private String jsonTypeMapper(Parameter parameter) {
Class<?> paramType = parameter.getType();
Type genericType = parameter.getParameterizedType();
// 基础类型判断
if (paramType.isPrimitive()) {
if (paramType == boolean.class) {
return "boolean";
} else if (paramType == char.class) {
return "string";
} else {
return "number";
}
} else if (Number.class.isAssignableFrom(paramType)) {
return "number";
} else if (paramType == String.class) {
return "string";
} else if (paramType == Boolean.class) {
return "boolean";
}
// 复合类型判断
if (paramType.isArray() || Collection.class.isAssignableFrom(paramType)) {
return "array";
} else if (Map.class.isAssignableFrom(paramType)) {
return "object";
}
// 泛型类型处理
if (genericType instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) genericType).getRawType();
if (rawType == List.class || rawType == Set.class) {
return "array";
}
}
// 默认视为对象
return "object";
}
private McpServerFeatures.SyncToolSpecification buildSyncToolSpecification(Object instance, Method method, String toolDesc, String schema) {
// 定义Tool
McpSchema.Tool tool = new McpSchema.Tool(
method.getName(),
toolDesc,
schema
);
// 定义同步工具
return new McpServerFeatures.SyncToolSpecification(
tool,
(exchange, arguments) -> {
//执行函数
try {
Parameter[] parameters = method.getParameters();
Object[] argArr = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
argArr[i] = arguments.get(parameters[i].getName());
}
Object obj = method.invoke(instance, argArr);
String str = obj == null ? "" : JSONUtil.toJsonStr(obj);
return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(str)), false);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
);
}
}

@ -0,0 +1,17 @@
package xyz.wbsite.mcp.easy.tools;
import org.springframework.stereotype.Service;
import xyz.wbsite.mcp.easy.annotation.P;
import xyz.wbsite.mcp.easy.annotation.Tool;
/**
*
*/
@Service
public class WeatherService {
@Tool("Gets the current weather forecast.")
public String getWeatherForecast(@P("Location to get the weather for") String location) {
return location + "天气晴23℃~27℃";
}
}

@ -0,0 +1,3 @@
server.port=8080
# spring.jackson.serialization.fail-on-empty-beans=false

@ -0,0 +1,20 @@
<?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>
<parent>
<groupId>xyz.wbsite</groupId>
<artifactId>starter-mcp-server</artifactId>
<version>0.1</version>
</parent>
<artifactId>starter-mcp-server-app</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

@ -0,0 +1,17 @@
package xyz.wbsite.mcp.easy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* MCP
*
* @author wangbing
*/
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}

@ -0,0 +1,23 @@
package xyz.wbsite.mcp.easy.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface P {
/**
* Description of a parameter
* @return the description of a parameter
*/
String value();
/**
* Whether the parameter is required
* @return true if the parameter is required, false otherwise
* Default is true.
*/
boolean required() default true;
}

@ -0,0 +1,28 @@
package xyz.wbsite.mcp.easy.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Tool {
/**
* Name of the tool. If not provided, method name will be used.
*
* @return name of the tool.
*/
String name() default "";
/**
* Description of the tool.
*
* @return description of the tool.
*/
String value() default "";
}

@ -0,0 +1,24 @@
package xyz.wbsite.mcp.easy.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
/**
* @author quanyu
* @date 2025/5/6 22:39
*/
@Configuration
class McpServerConfig {
@Bean
WebFluxSseServerTransportProvider webFluxSseServerTransportProvider() {
return new WebFluxSseServerTransportProvider(new ObjectMapper(), "/mcp/message");
}
@Bean
RouterFunction<?> mcpRouterFunction(WebFluxSseServerTransportProvider provider) {
return provider.getRouterFunction();
}
}

@ -0,0 +1,166 @@
package xyz.wbsite.mcp.easy.registrar;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import xyz.wbsite.mcp.easy.annotation.P;
import xyz.wbsite.mcp.easy.annotation.Tool;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
public class McpServerRegistrar {
@Resource
private ApplicationContext applicationContext;
@Resource
private WebFluxSseServerTransportProvider transport;
@PostConstruct
public void registerMcpServers() {
// 创建具有自定义配置的服务器
McpSyncServer syncServer = McpServer.sync(transport)
.serverInfo("Mcp Server", "1.0.0")
.capabilities(McpSchema.ServerCapabilities.builder()
.resources(true, true) // 启用资源支持
.tools(true) // 启用工具支持
.prompts(true) // 启用提示支持
.logging() // 启用日志支持
.build())
.build();
// 查找方法级别的Tool注解
String[] beanNames = applicationContext.getBeanDefinitionNames();
for (String beanName : beanNames) {
// 跳过自身 Bean避免循环依赖
if (beanName.equals("mcpServerRegistrar")) {
continue;
}
Object bean = applicationContext.getBean(beanName);
for (Method method : bean.getClass().getMethods()) {
if (method.isAnnotationPresent(Tool.class)) {
Tool tool = method.getAnnotation(Tool.class);
// 处理参数
ObjectNode schema = new ObjectNode(JsonNodeFactory.instance);
schema.put("type", "object");
ArrayNode required = schema.putArray("required");
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
this.putProperties(schema, parameter);
required.add(parameter.getName());
}
McpServerFeatures.SyncToolSpecification syncToolSpecification = buildSyncToolSpecification(bean, method, tool.value(), schema.toPrettyString());
syncServer.addTool(syncToolSpecification);
}
}
}
// 发送日志通知
syncServer.loggingNotification(McpSchema.LoggingMessageNotification.builder()
.level(McpSchema.LoggingLevel.INFO)
.logger("Mcp Server Log")
.data("The server has been initialized")
.build());
}
private void putProperties(ObjectNode schema, Parameter parameter) {
// 处理参数
String typeName = jsonTypeMapper(parameter);
ObjectNode properties = (ObjectNode) schema.get("properties");
if (properties == null) {
properties = schema.putObject("properties");
}
ObjectNode paramSchema = properties.putObject(parameter.getName());
paramSchema.put("type", typeName);
if (parameter.isAnnotationPresent(P.class)) {
P p = parameter.getAnnotation(P.class);
paramSchema.put("description", p.value());
}
}
private String jsonTypeMapper(Parameter parameter) {
Class<?> paramType = parameter.getType();
Type genericType = parameter.getParameterizedType();
// 基础类型判断
if (paramType.isPrimitive()) {
if (paramType == boolean.class) {
return "boolean";
} else if (paramType == char.class) {
return "string";
} else {
return "number";
}
} else if (Number.class.isAssignableFrom(paramType)) {
return "number";
} else if (paramType == String.class) {
return "string";
} else if (paramType == Boolean.class) {
return "boolean";
}
// 复合类型判断
if (paramType.isArray() || Collection.class.isAssignableFrom(paramType)) {
return "array";
} else if (Map.class.isAssignableFrom(paramType)) {
return "object";
}
// 泛型类型处理
if (genericType instanceof ParameterizedType) {
Type rawType = ((ParameterizedType) genericType).getRawType();
if (rawType == List.class || rawType == Set.class) {
return "array";
}
}
// 默认视为对象
return "object";
}
private McpServerFeatures.SyncToolSpecification buildSyncToolSpecification(Object instance, Method method, String toolDesc, String schema) {
// 定义Tool
McpSchema.Tool tool = new McpSchema.Tool(
method.getName(),
toolDesc,
schema
);
// 定义同步工具
return new McpServerFeatures.SyncToolSpecification(
tool,
(exchange, arguments) -> {
//执行函数
try {
Parameter[] parameters = method.getParameters();
Object[] argArr = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
argArr[i] = arguments.get(parameters[i].getName());
}
Object obj = method.invoke(instance, argArr);
String str = obj == null ? "" : JSONUtil.toJsonStr(obj);
return new McpSchema.CallToolResult(Collections.singletonList(new McpSchema.TextContent(str)), false);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
);
}
}

@ -0,0 +1,17 @@
package xyz.wbsite.mcp.easy.tools;
import org.springframework.stereotype.Service;
import xyz.wbsite.mcp.easy.annotation.P;
import xyz.wbsite.mcp.easy.annotation.Tool;
/**
*
*/
@Service
public class WeatherService {
@Tool("获取天气预报.")
public String getWeatherForecast(@P("天气预报所在城市") String location) {
return location + "天气晴23℃~27℃";
}
}

@ -0,0 +1,3 @@
server.port=8080
# spring.jackson.serialization.fail-on-empty-beans=false

@ -0,0 +1,61 @@
<?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>
<groupId>xyz.wbsite</groupId>
<artifactId>starter-mcp-server-basic</artifactId>
<version>0.1</version>
<packaging>jar</packaging>
<name>starter-mcp-server</name>
<description>Spring Boot MCP Server Demo</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<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>
</properties>
<repositories>
<!-- 将中央仓库地址指向阿里云聚合仓库,提高下载速度 -->
<repository>
<id>central</id>
<name>Central Repository</name>
<layout>default</layout>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<pluginRepositories>
<!-- 将插件的仓库指向阿里云聚合仓库解决低版本maven下载插件异常或提高下载速度 -->
<pluginRepository>
<id>central</id>
<name>Central Repository</name>
<url>https://maven.aliyun.com/repository/public</url>
<layout>default</layout>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 糊涂工具包含常用API避免重复造轮子 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,18 @@
package xyz.wbsite.mcp.basic;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* MCP
* mcp使@link{McpServerApplication}
*
* @author wangbing
*/
@SpringBootApplication
public class BasicApplication {
public static void main(String[] args) {
SpringApplication.run(BasicApplication.class, args);
}
}

@ -0,0 +1,132 @@
package xyz.wbsite.mcp.basic;
import cn.hutool.json.JSONUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import xyz.wbsite.mcp.basic.model.McpResponse;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* SSE广
* 广SSE
*
* @author wangbing
*/
@Service // Manages the SSE emitters
public class SseBroadcaster {
private static final Logger log = LoggerFactory.getLogger(SseBroadcaster.class);
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
// private final ObjectMapper objectMapper;
// public SseBroadcaster(ObjectMapper objectMapper) {
// this.objectMapper = objectMapper;
// }
/**
* SSE
*
* @param emitter sse
*/
public void registerEmitter(SseEmitter emitter) {
// Add the emitter to the list
emitters.add(emitter);
log.info("New SSE emitter registered, total emitters: {}", emitters.size());
// Set up completion handlers to remove the emitter when the connection is closed
emitter.onCompletion(() -> {
emitters.remove(emitter);
log.info("SSE emitter completed, remaining emitters: {}", emitters.size());
});
emitter.onTimeout(() -> {
emitters.remove(emitter);
log.info("SSE emitter timed out, remaining emitters: {}", emitters.size());
});
emitter.onError((e) -> {
emitters.remove(emitter);
log.error("Error on SSE emitter: {}", e.getMessage(), e);
});
}
/**
*
*
* @param emitter sse
*/
public void sendEndpointEvent(SseEmitter emitter) {
try {
emitter.send(SseEmitter.event()
.name("endpoint")
.data("/mcp/post"));
log.info("Sent endpoint event to SSE emitter");
} catch (IOException e) {
log.error("Failed to send endpoint event: {}", e.getMessage(), e);
emitter.completeWithError(e);
}
}
/**
* POSTSSE
*
* @param response .
*/
public void broadcastResponse(McpResponse response) {
if (emitters.isEmpty()) {
log.warn("No active SSE emitters to broadcast response (ID: {})\n", response.getId());
return;
}
// Responses are sent as events of type "message"
String jsonResponse = JSONUtil.toJsonStr(response);
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(jsonResponse));
log.debug("Successfully sent SSE message for Response ID: {}", response.getId());
} catch (IOException e) {
log.error("Failed to send SSE message (Response ID: {}): {}", response.getId(), e.getMessage(), e);
// 考虑移除有故障的发射器
emitter.completeWithError(e);
}
}
}
/**
* Sends a simple notification (like log messages, tool updates) over SSE.
* This isn't used in the simple weather example but shows how non-response
* messages would be sent.
*
* @param notification An object representing the notification.
* @param eventName The SSE event name (e.g., "notifications/message").
*/
public void broadcastNotification(Object notification, String eventName) {
if (emitters.isEmpty()) {
log.warn("No active SSE emitters to broadcast notification ({})\n", eventName);
return;
}
String jsonNotification = JSONUtil.toJsonStr(notification);
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(jsonNotification));
log.debug("Successfully sent SSE notification ({})\n", eventName);
} catch (IOException e) {
log.error("Failed to send SSE notification ({}): {}", eventName, e.getMessage(), e);
emitter.completeWithError(e);
}
}
}
}

@ -0,0 +1,35 @@
package xyz.wbsite.mcp.basic;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* SSE
* SSE
*
* @author wangbing
*/
@RestController
@RequestMapping("/mcp/sse")
public class SseController {
private static final Logger log = LoggerFactory.getLogger(SseController.class);
@Resource
private SseBroadcaster broadcaster;
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connectSse() {
log.info("Client requesting SSE connection...");
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
broadcaster.registerEmitter(emitter);
broadcaster.sendEndpointEvent(emitter);
return emitter;
}
}

@ -0,0 +1,50 @@
package xyz.wbsite.mcp.basic.model;
import java.util.List;
/**
*
*
*
* @author wangbing
*/
public class CallToolResult extends Data {
/**
*
*/
private List<TextContentData> content;
/**
*
*/
private Boolean isError;
public CallToolResult() {
}
public CallToolResult(List<TextContentData> content, Boolean isError) {
this.content = content;
this.isError = isError;
}
public CallToolResult(List<TextContentData> content) {
this(content, false);
}
public List<TextContentData> getContent() {
return content;
}
public void setContent(List<TextContentData> content) {
this.content = content;
}
public Boolean getIsError() {
return isError;
}
public void setIsError(Boolean isError) {
this.isError = isError;
}
}

@ -0,0 +1,18 @@
package xyz.wbsite.mcp.basic.model;
import java.io.Serial;
import java.io.Serializable;
/**
* .
*
* @author wangbing
* @version 0.0.1
* @since 1.8
*/
public class Data implements Serializable {
@Serial
private static final long serialVersionUID = -1L;
}

@ -0,0 +1,30 @@
package xyz.wbsite.mcp.basic.model;
/**
*
*
*
* @author wangbing
*/
public class InitializeResult extends Data {
/**
*
*/
private ServerCapabilities capabilities;
public InitializeResult() {
}
public InitializeResult(ServerCapabilities capabilities) {
this.capabilities = capabilities;
}
public ServerCapabilities getCapabilities() {
return capabilities;
}
public void setCapabilities(ServerCapabilities capabilities) {
this.capabilities = capabilities;
}
}

@ -0,0 +1,75 @@
package xyz.wbsite.mcp.basic.model;
import java.util.List;
import java.util.Map;
/**
*
*
*
* @author wangbing
*/
public class InputSchema extends Data {
/**
*
*/
private String type;
/**
*
*/
private Map<String, Object> properties;
/**
*
*/
private List<String> required;
/**
*
*/
private Boolean additionalProperties;
public InputSchema() {
}
public InputSchema(String type, Map<String, Object> properties, List<String> required, Boolean additionalProperties) {
this.type = type;
this.properties = properties;
this.required = required;
this.additionalProperties = additionalProperties;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Map<String, Object> getProperties() {
return properties;
}
public void setProperties(Map<String, Object> properties) {
this.properties = properties;
}
public List<String> getRequired() {
return required;
}
public void setRequired(List<String> required) {
this.required = required;
}
public Boolean getAdditionalProperties() {
return additionalProperties;
}
public void setAdditionalProperties(Boolean additionalProperties) {
this.additionalProperties = additionalProperties;
}
}

@ -0,0 +1,32 @@
package xyz.wbsite.mcp.basic.model;
import java.util.List;
/**
*
*
*
* @author wangbing
*/
public class ListToolsResult extends Data {
/**
*
*/
private List<ToolSpecificationData> tools;
public ListToolsResult() {
}
public ListToolsResult(List<ToolSpecificationData> tools) {
this.tools = tools;
}
public List<ToolSpecificationData> getTools() {
return tools;
}
public void setTools(List<ToolSpecificationData> tools) {
this.tools = tools;
}
}

@ -0,0 +1,36 @@
package xyz.wbsite.mcp.basic.model;
/**
*
*
*
* @author wangbing
*/
public class McpError extends Data {
private int code;
private String message;
public McpError() {
}
public McpError(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

@ -0,0 +1,58 @@
package xyz.wbsite.mcp.basic.model;
import com.fasterxml.jackson.databind.JsonNode;
/**
* MCP
* JSON-RPC
*
* @author wangbing
*/
public class McpRequest extends Data {
private String jsonrpc;
private Long id;
private String method;
private JsonNode params; // Use JsonNode for flexible params
public McpRequest() {
}
public McpRequest(String jsonrpc, Long id, String method, JsonNode params) {
this.jsonrpc = jsonrpc;
this.id = id;
this.method = method;
this.params = params;
}
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public JsonNode getParams() {
return params;
}
public void setParams(JsonNode params) {
this.params = params;
}
}

@ -0,0 +1,65 @@
package xyz.wbsite.mcp.basic.model;
/**
* MCP
* JSON-RPC
*
*
* @author wangbing
*/
public class McpResponse extends Data {
private String jsonrpc;
private Long id;
private Object result; // Can be ListToolsResult, CallToolResult, etc.
private McpError error; // Optional error field
public McpResponse() {
}
public McpResponse(String jsonrpc, Long id, Object result, McpError error) {
this.jsonrpc = jsonrpc;
this.id = id;
this.result = result;
this.error = error;
}
public McpResponse(Long id, Object result) {
this("2.0", id, result, null);
}
public McpResponse(Long id, McpError error) {
this("2.0", id, null, error);
}
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public McpError getError() {
return error;
}
public void setError(McpError error) {
this.error = error;
}
}

@ -0,0 +1,114 @@
package xyz.wbsite.mcp.basic.model;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.wbsite.mcp.basic.SseBroadcaster;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
*
* MCP POST
*
* @author wangbing
*/
@RestController
@RequestMapping("/mcp/post")
public class PostController {
private static final Logger log = LoggerFactory.getLogger(PostController.class);
private static final String WEATHER_TOOL_NAME = "getWeatherForecast";
private static final String FAKE_WEATHER_JSON = "{\"forecast\": \"sunny\"}";
@Resource
SseBroadcaster broadcaster;
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> handleMcpPostRequest(@RequestBody McpRequest request) {
log.info("Received MCP POST Request: ID={}, Method={}", request.getId(), request.getMethod());
McpResponse mcpResponse = processRequest(request);
if (mcpResponse != null) {
// Send the response back over the SSE channel
broadcaster.broadcastResponse(mcpResponse);
} else {
// 处理可能不会生成McpResponse对象的通知等情况
// 在我们的例子中,'通知/初始化'落在这里。
log.debug("No explicit response object generated for method '{}', assuming notification ack.", request.getMethod());
}
// 立即返回HTTP 200 OK或202 Accepted以确认收到POST。
// 实际结果通过SSE异步发送。
// 200 OK可能更简单因为客户希望得到一些响应体即使是空的。
// 202 Accepted明确表示处理正在其他地方进行。让我们用200。
return ResponseEntity.ok().build();
}
private McpResponse processRequest(McpRequest request) {
switch (request.getMethod()) {
case "initialize":
log.info("Handling initialize request");
InitializeResult initResult = new InitializeResult(new ServerCapabilities());
return new McpResponse(request.getId(), initResult);
case "notifications/initialized":
log.info("Received initialized notification");
// 这是来自客户端的通知。MCP规范称通知
// 没有回应。所以我们在这里返回nullPOST处理程序
// 将只返回HTTP OK。
return null;
case "tools/list":
log.info("Handling tools/list request");
ToolSpecificationData weatherTool = new ToolSpecificationData(
WEATHER_TOOL_NAME,
"Gets the current weather forecast.",
new InputSchema(
"object",
Map.of("location", Map.of(
"type", "string",
"description", "Location to get the weather for")
),
List.of("location"),
false)
);
ListToolsResult listResult = new ListToolsResult(List.of(weatherTool));
return new McpResponse(request.getId(), listResult);
case "tools/call":
log.info("Handling tools/call request");
if (request.getParams() != null && request.getParams().has("name")) {
String toolName = request.getParams().get("name").asText();
if (WEATHER_TOOL_NAME.equals(toolName)) {
log.info("Executing tool: {}", toolName);
TextContentData textContent = new TextContentData(FAKE_WEATHER_JSON);
CallToolResult callResult = new CallToolResult(List.of(textContent));
return new McpResponse(request.getId(), callResult);
} else {
log.warn("Unknown tool requested: {}", toolName);
return new McpResponse(request.getId(), new McpError(-32601, "Method not found: " + toolName));
}
} else {
log.error("Invalid tools/call request: Missing 'name' in params");
return new McpResponse(request.getId(), new McpError(-32602, "Invalid params for tools/call"));
}
case "ping":
log.info("Handling ping request");
return new McpResponse(request.getId(), Collections.emptyMap());
default:
log.warn("Unsupported MCP method: {}", request.getMethod());
return new McpResponse(request.getId(), new McpError(-32601, "Method not found: " + request.getMethod()));
}
}
}

@ -0,0 +1,15 @@
package xyz.wbsite.mcp.basic.model;
import java.io.Serial;
/**
*
*
*
* @author wangbing
*/
public class ServerCapabilities extends Data {
@Serial
private static final long serialVersionUID = 1L;
}

@ -0,0 +1,44 @@
package xyz.wbsite.mcp.basic.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
*
*
*
* @author wangbing
*/
public class TextContentData extends Data {
@JsonProperty("type")
private String type;
@JsonProperty("text")
private String text;
public TextContentData() {
}
public TextContentData(String type, String text) {
this.type = type;
this.text = text;
}
public TextContentData(String text) {
this("text", text);
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

@ -0,0 +1,46 @@
package xyz.wbsite.mcp.basic.model;
/**
*
*
*
* @author wangbing
*/
public class ToolSpecificationData extends Data {
private String name;
private String description;
private InputSchema inputSchema;
public ToolSpecificationData() {
}
public ToolSpecificationData(String name, String description, InputSchema inputSchema) {
this.name = name;
this.description = description;
this.inputSchema = inputSchema;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public InputSchema getInputSchema() {
return inputSchema;
}
public void setInputSchema(InputSchema inputSchema) {
this.inputSchema = inputSchema;
}
}

@ -0,0 +1,3 @@
server.port=8080
# spring.jackson.serialization.fail-on-empty-beans=false
Loading…
Cancel
Save

Powered by TurnKey Linux.