Spring AI 2.0 MCP服务器搭建与使用

0 阅读7分钟

利用Spring AI 能快速搭建MCP服务器,通过内置的客户端能方便的与服务器进行工具调用、日志传输、进度通知、采样、启发式请求以及资源管理等。

一、以webmvc 方式启动MCP服务器

1. 添加依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

2. 以STREAMABLE模式配置服务器协议

spring:
  application:
    name: smart-mcp-server
  ai:
    mcp:
      server:
        enabled: true
        type: SYNC
        protocol: STREAMABLE
        annotation-scanner:
          enabled: true
        name: my-mcp-server
        version: 1.0.0
server:
  port: 7000

两种协议STATELESSSTREAMABLE的主要区别是前者不支持双向操作,后者支持双向操作。这意味着在STATELESS协议下,不能在服务器执行工具调用的同时向客户端传输日志、进度信息、请求客户端采样,也不能ping客户端。

二、配置客户端

1. 添加依赖:

...
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
...

2. 修改配置文件

spring:
  application:
    name: smart-mcp-client
  ai:
    mcp:
      client:
        type: SYNC
        streamable-http:
          connections:
          my-server-1:
              url: http://localhost:7000

在不修改访问路径或端点时,两端默认的端点是/mcp

三、使用MCP服务器中的工具

1. 在客户端中使用工具

当配置正确后启动客户端程序,程序会自动去连接MCP服务器,如果连接不上会导致启动失败,通常会报404错误。要在ChatClient中使用MCP提供的工具,需要用到ToolCallbackProvider,所有MCP客户端注册的MCP工具将使用它的实例来提供,MCP 客户端会自动创建这个它的bean

@Bean
ChatClient defaultChatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) {
    return builder
            .defaultTools(toolCallbackProvider)
            .build();
}

可以通过如下方式看到客户端注册的全部工具

ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();

2. 在MCP服务器中声明工具

MCP相关注解都以Mcp开头,MCP工具的声明跟普通工具的声明几乎一样:

@Component
public class CalculatorTools {
    @McpTool(name = "getCurrentDataTime", description = "Get the  current date and time,in the format: yyyy‑MM‑dd HH:mm:ss")
    public String getCurrentDataTime() {
        LocalDateTime now = LocalDateTime.now();
        return now.format(DateTimeFormatter.ofPattern("yyyy‑MM‑dd HH:mm:ss"));
    }

    @McpTool(name = "add", description = "Add two numbers together")
    public int add(
            @McpToolParam(description = "First number", required = true) int a,
            @McpToolParam(description = "Second number", required = true) int b) {
        return a + b;
    }

}

工具的声明需要框架能够扫描到,一般放在@Component标记的类里面。

3. 日志传输

在MCP服务端运行工具时,可以向客户端传输日志来记录工具的执行情况。

(1) 服务端发送日志

在服务器的工具参数定义中使用McpSyncRequestContext

@McpTool(name = "getCurrentDataTime", description = "Get the  current date and time,in the format: yyyy‑MM‑dd HH:mm:ss")
public String getCurrentDataTime(
        McpSyncRequestContext context
) {
    LocalDateTime now = LocalDateTime.now();
    context.info("handling tool calling: getCurrentDataTime");
    return now.format(DateTimeFormatter.ofPattern("yyyy‑MM‑dd HH:mm:ss"));
}

McpSyncRequestContext有info、warning、debug、error 不同的日志级别可以使用。

(2)客户端接收日志

@Component
public class LoggingHandler {
    @McpLogging(clients = "my-server-1")
    public void handleLoggingMessage(McpSchema.LoggingMessageNotification notification) {
        System.out.println("Received log: " + notification.level() +
                " - " + notification.data());
    }

这里要注意McpLogging的参数clients是指客户端配置中的连接名称,这里是my-server-1。如果使用错误不会报错,但不会收到日志信息。

4. 采样

MCP采样的逻辑是MCP在执行工具过程中,需要再次调用大模型。这里模拟一个实用场景,向大模型输入一个连接地址,大模型需要给出连接内容的总结。

(1) MCP服务器发送采样请求

MCP调用summarizeArticle工具,使用大模型传输过来的link参数,下载网页内容,然后传回客户端请求大模型总结。

@McpTool(name = "summarizeArticle",description = "Summarize the articles connected by the specified link.")
String summarizeArticle(
        @McpToolParam(description = "The link address of the article",required = true) 
        String link,
        McpSyncRequestContext context
) throws IOException {
    //爬取网页内容
    ...
    //把网页内容传回客户端请求采样,设置特别的温度值
    McpSchema.CreateMessageResult samplingResult =context.sample(samplingSpec -> {
        ...
    });
    return samplingResult.content().toString();
}

调用context.sample()方法请求客户端采样。

(2)客户端处理采样请求

@McpSampling(clients = "my-server-1")
public McpSchema.CreateMessageResult handleSamplingRequest(McpSchema.CreateMessageRequest request) {
    // 将不同的SampleingMessage转换为Message,这里只处理text类型
    List<Message> list= request.messages().stream().map(
            samplingMessage -> {
               ...
            }
    ).toList();
   
     //用 request.temperature(),request.systemPrompt(),list 请求大模型,并返回结果str给MCP服务器
    ...
    
    return McpSchema.CreateMessageResult.builder(McpSchema.Role.ASSISTANT, str, "deepseek")
            .build();
}

采样请求中的消息是SamplingMessage类型,需要转换成ChatClient能接收的Message类型,然后通过McpSchema.CreateMessageResult回传结果给MCP服务端。

5. 启发式请求

启发式请求是MCP服务器工具希望得到用户的更多信息,通常由用户输入。

(1) MCP服务器发送启发式请求

@McpTool(name = "getUserInfo",description = "Get user name and email")
String getUserInfo(
        McpSyncRequestContext context
) {
    ...
    HashMap<String,Object> requestedSchema=new HashMap<>();
    ...
    McpSchema.ElicitFormRequest elicitFormRequest=McpSchema.ElicitFormRequest
            .builder("input user`s name and email",requestedSchema)
            .build();

    McpSchema.ElicitResult result=context.elicit(elicitFormRequest);
    ...

    //处理结果
    if(result.action()==McpSchema.ElicitResult.Action.ACCEPT){
       ...
    }
    else if(result.action()==McpSchema.ElicitResult.Action.DECLINE){
       ...
    }

}

关键步骤是调用context.elicit方法,需要传入一个McpSchema.ElicitRequest类型的参数,它目前有两个实现类,McpSchema.ElicitFormRequest McpSchema.ElicitUrlRequest , 它们的model类型分别是"form""url"。在这里使用前者,它的builder方法的第一次参数是传入需要提示用户的消息,第二个参数是一个Map对象,表示需要用户输入的相关项目。处理结果时间需要判断McpSchema.ElicitResultaction值。

(2)客户端处理启发式请求

@McpElicitation(clients = "my-server-1")
public McpSchema.ElicitResult handleElicitationRequest(McpSchema.ElicitRequest request) {
    Map<String, Object> userData = new HashMap<String,Object>();
    ...
    if(request.mode().equals("form")){
       ...
       //装入用户输入的信息
    }
    ...
    if (userData != null) {
        return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, userData);
    } else {
        return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.DECLINE, null);
    }
}

关键步骤是根据不同的mode来处理不同的用户输入,根据MCP传过来的schema获得需要输入的项目,最后设置acionMap类型的用户信息返回结果给服务端。

6. 进度通知

(1)MCP服务端发送进度通知

@McpTool(name = "addFromTo", description = "Summation of a sequence of natural numbers,the start number of the sequence must be smaller than the end number. ")
String addFromTo(
        @McpToolParam(description = "this parameter represents the start of the sequence.") int start,
        @McpToolParam(description = "this parameter represents the end of the sequence.") int end,
        McpSyncRequestContext context
){
    ...
    McpSchema.ProgressNotification notification=McpSchema.ProgressNotification.builder("111-11111-111",50).message("now progress is ").total(100.0).build();
    context.progress(notification);
   ...
    return "the result is "+result;
}

关键步骤是调用context.progress()方法,需要传教McpSchema.ProgressNotification对象,特别注意要传入一个进度token,如果没有这个token,客户端无法收到进度通知。这里的token是"111-11111-111"

(2)客户端接收进度通知

@McpProgress(clients = "my-server-1")
public void progressNotification(McpSchema.ProgressNotification notification){
    ...
}

四、MCP资源管理

1. 服务端资源管理

MCP可以存放资源,包括文字或者二进制资源,通过MCP客户端可以很方便的读取,下面以文字资源为例,模拟服务器端资源管理。用@McpResource注解定义资源方法,通过参数uri定义的位置访问资源,注意这里的路径变量key的使用。

    ...
    @McpResource(
            name = "poemResource",
            description = "the poem libaray",
            uri = "poem://{key}"
    )
    String poemResource(String key){
        return poems.get(key);
    }
    ...

2. 客户端获取资源

首先要获取客户端实例,Spring会自动创建客户端bean,这里需要注意,注入的MCP客户端实例是一个List集合对象,不是单个对象。

@Resource
List<McpSyncClient> mcpClients;

创建McpSchema.Resource对象,传入资源的uri,然后调用客户端的readResource()获取资源

String uri=String.format("poem://%s","静夜思");
resource=McpSchema.Resource.builder(uri,"poemResource").build();
McpSchema.TextResourceContents contents= (McpSchema.TextResourceContents) mcpClients.getFirst().readResource(resource).contents().getFirst();
String poemText=contents.text();

这里的"poemResource"是资源的名称,不能缺少,可以是任意字符串。Spring MCP定义了两种资源:BlobResourceContents二进制大对象资源和 TextResourceContents文本资源。

五、提示词管理

1.服务端提示词管理

@McpPrompt注解定义提示词方法,@McpArg注解定义获取提示词需要的参数,通常用于替换提示词里面的变量。用McpSchema.PromptMessage对象封装提示词内容,用McpSchema.GetPromptResult构造提示词返回结果。

@McpPrompt(name ="travelGuideSystemPrompt",description = "Travel guide system prompt")
public McpSchema.GetPromptResult travelGuideSystemPrompt(
        @McpArg (description = "the name of the robot",required = true)
        String robotName){
    ...
    List<McpSchema.PromptMessage> list=List.of(
           ...
    );
    return McpSchema.GetPromptResult.builder(list).description("Travel guide system prompt").build();
}

2. 客户端获取提示词

Map<String,Object> argMap= Map.of("robotName","peter");
McpSchema.GetPromptRequest request= McpSchema.GetPromptRequest.builder("travelGuideSystemPrompt").arguments(argMap).build();
McpSchema.TextContent content= (McpSchema.TextContent) mcpClients.getFirst().getPrompt(request).messages().getFirst().content();
return content.text();

McpSchema.GetPromptRequest构造提示词获取请求,需要传入服务端提示词方法定义的name,用Map对象进行传参。

六、补全

MCP补全是让MCP服务器能为客户端的输入提供自动补全建议,类似 IDE 里的代码提示。Spring AI 支持提示词补全和资源补全,补全方法需要与存在的提示词和资源对应,客户端请求补全时需要提供提示词或资源方法的参数,来获取提示词和资源集合。

以上代码会逐步整理上传至gitee。