上传备份

master
王兵 4 weeks ago
commit 2dc4155d77

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,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</artifactId>
<version>0.1</version>
<packaging>pom</packaging>
<name>starter-mcp</name>
<description>MCP基本实现原理</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使
*
* @author wangbing
*/
@SpringBootApplication
public class BasicApplication {
public static void main(String[] args) {
SpringApplication.run(BasicApplication.class, args);
}
}

@ -0,0 +1,123 @@
package xyz.wbsite.mcp.basic.controller;
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.model.CallToolResult;
import xyz.wbsite.mcp.basic.model.InitializeResult;
import xyz.wbsite.mcp.basic.model.InputSchema;
import xyz.wbsite.mcp.basic.model.ListToolsResult;
import xyz.wbsite.mcp.basic.model.McpError;
import xyz.wbsite.mcp.basic.model.McpRequest;
import xyz.wbsite.mcp.basic.model.McpResponse;
import xyz.wbsite.mcp.basic.model.ServerCapabilities;
import xyz.wbsite.mcp.basic.model.TextContentData;
import xyz.wbsite.mcp.basic.model.ToolSpecificationData;
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,
"获取任意地区的天气预报.",
new InputSchema(
"object",
Map.of("location", Map.of(
"type", "string",
"description", "天气预报所在城市")
),
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,128 @@
package xyz.wbsite.mcp.basic.controller;
import cn.hutool.json.JSONUtil;
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 xyz.wbsite.mcp.basic.model.McpResponse;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* SSE广
* 广SSE
*
* @author wangbing
*/
@Service
public class SseBroadcaster {
private static final Logger log = LoggerFactory.getLogger(SseBroadcaster.class);
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
/**
* 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.controller;
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,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;
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.