commit
6f4ac0b9ee
@ -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,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,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,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,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,3 @@
|
||||
server.port=8080
|
||||
|
||||
# spring.jackson.serialization.fail-on-empty-beans=false
|
@ -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,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…
Reference in new issue