找回密码
 立即注册
首页 业界区 业界 9. Spring AI 当中对应 MCP 的操作

9. Spring AI 当中对应 MCP 的操作

丰江 2025-9-30 20:39:19
9. Spring AI 当中对应 MCP 的操作

@
目录

  • 9. Spring AI 当中对应 MCP 的操作

    • MCP

      • 问题:
      • 使用

        • MCP STDIO 输出配置实操

          • MCP Server

            • 现成共用MCP Server

          • MCP Client

            • 通过工具
            • 通过 Spring AI 接入 第三方的 MCP Server
            • 使用 Spring AI 接入 自定义MCP Server


        • MCP SSE 输出配置实操(推荐 Web)

          • MCP Server
          • MCP Client

        • 原理

          • STDIO原理

        • STDIO源码
        • MCP鉴权

          • STDIO
          • SSE

            • 说明
            • 给 Spring MCP 服务器加上 OAuth2 支持
            • 为MCP Client设置请求头
            • 重写源码





  • 最后:

MCP

问题:


  • 当有服务商需要将tools提供外部使用(比如高德地图提供了位置服务tools, 比如百度提供了联网搜索的tools...)
  • 或者在企业级中, 有多个智能应用,想将通用的tools公共化
怎么办?
可以把tools单独抽取出来, 由应用程序读取外部的tools。 那关键是怎么读呢? 怎么解析呢? 如果每个提供商各用一种规则你能想象有多麻烦! 所以MCP就诞生了, 他指定了标准规则, 以jsonrpc2.0的方式进行通讯。
那问题又来了, 以什么方式通讯呢? http? rpc? stdio? mcp提供了sse和stdio这2种方式。
1.png

使用

Streamable http目前springai1.0版本不支持(因为Streamable http 是 spring ai 1.0 之后说明的) 我们先掌握SSE和STDIO
分别说下STDIO和SSE的方式:

  • STDIO更适合客户端桌面应用和辅助工具
  • SSE更适合web应用 、业务有关的公共tools
2.png

MCP STDIO 输出配置实操

MCP Server

现成共用MCP Server

现在有很多MCP 服务 给大家提供一个网站:MCP Server(MCP 服务器)
3.png

那MCP有了, 怎么调用呢? 这里介绍2种使用方式:
MCP Client

通过工具

CherryStudio、Cursor 、Claude Desktop、Cline 等等很多, 这里不一一演示, 不会的话自己找个文章, 工具使用都很简单!
4.png

以Cline为例: 他是Vscode的插件

  • 安装VSCode
  • 安装插件:
5.png


  • 配置cline的模型:
6.png


  • 配置cline的mcpserver
    7.png

  1. {
  2.     "mcpServers": {
  3.         "baidu-map": {
  4.             "command": "cmd",
  5.             "args": [
  6.                 "/c",
  7.                 "npx",
  8.                 "-y",
  9.                 "@baidumap/mcp-server-baidu-map"
  10.             ],
  11.             "env": {
  12.                 "BAIDU_MAP_API_KEY": "LEyBQxG9UzR9C1GZ6zDHsFDVKvBem2do"
  13.             }
  14.         },
  15.         "filesystem": {
  16.             "command": "cmd",
  17.             "args": [
  18.                 "/c",
  19.                 "npx",
  20.                 "-y",
  21.                 "@modelcontextprotocol/server-filesystem",
  22.                 "C:/Users/tuling/Desktop"
  23.             ]
  24.         },
  25.         "mcp-server-weather": {
  26.             "command": "java",
  27.             "args": [
  28.                 "-Dspring.ai.mcp.server.stdio=true",
  29.                 "-Dlogging.pattern.console=",
  30.                 "-jar",
  31.                 "D:\\ideaworkspace\\git_pull\\tuling-flight-booking_all\\mcp-stdio-server\\target\\mcp-stdio-server-xs-1.0.jar"
  32.             ]
  33.         }
  34.     }
  35. }
复制代码

  • 开启cline权限
8.png
9.png

6.测试:
10.png

通过 Spring AI 接入 第三方的 MCP Server


  • 依赖
  1. <dependency>
  2.     <groupId>org.springframework.ai</groupId>
  3.     spring-ai-starter-mcp-client-webflux</artifactId>
  4. </dependency>
复制代码
2 配置
  1. spring:
  2.   ai:
  3.     mcp:
  4.       client:
  5.       # 连接超时时间设置
  6.         request-timeout: 60000
  7.         stdio: # 设置 sse 输出方式
  8.         # 配置Mcp 方式2: 将 mcp的配置 单独放在一个 Json 文件当中读取,推荐,利用维护
  9.         # classpath 是指:项目resources
  10.           servers-configuration: classpath:/mcp-servers-config.json
  11.         # 配置MCP 方式2: 直接将 mcp 配置全局配置文件中(mcp 配置太多不利于维护)
  12.           # connections:
  13.           #   server1:
  14.           #     command: /path/to/server
  15.           #     args:
  16.           #       - --port=8080
  17.           #       - --mode=production
  18.           #     env:
  19.           #       API_KEY: your-api-key
  20.           #       DEBUG: "true"
复制代码

  • mcp-servers-config.json:
获取Baidu地图key: 控制台 | 百度地图开放平台
  1. {
  2.     "mcpServers": {
  3.         "baidu-map": {
  4.             "command": "cmd",
  5.             "args": [
  6.                 "/c",
  7.                 "npx",
  8.                 "-y",
  9.                 "@baidumap/mcp-server-baidu-map"
  10.             ],
  11.             "env": {
  12.                 "BAIDU_MAP_API_KEY": "xxxx"
  13.             }
  14.         },
  15.         "filesystem": {
  16.             "command": "cmd",
  17.             "args": [
  18.                 "/c",
  19.                 "npx",
  20.                 "-y",
  21.                 "@modelcontextprotocol/server-filesystem",
  22.                 "C:/Users/tuling/Desktop"
  23.             ]
  24.         },
  25.         "mcp-server-weather": {
  26.             "command": "java",
  27.             "args": [
  28.                 "-Dspring.ai.mcp.server.stdio=true",
  29.                 "-Dlogging.pattern.console=",
  30.                 "-jar",
  31.                 "D:\\xxx\\target\\mcp-stdio-server-xs-1.0.jar"
  32.             ]
  33.         }
  34.     }
  35. }
复制代码
  1. {
  2.     "mcpServers": {
  3.       // 外部第三方的
  4.         "baidu-map": {
  5.             "command": "cmd",
  6.             "args": [
  7.                 "/c",
  8.                 "npx",
  9.                 "-y",
  10.                 "@baidumap/mcp-server-baidu-map"
  11.             ],
  12.             "env": {
  13.                 "BAIDU_MAP_API_KEY": "xxxx"
  14.             }
  15.         },
  16.        // 外部第三方的
  17.         "filesystem": {
  18.             "command": "cmd",  // 指明使用 cmd 命令执行
  19.             "args": [
  20.                 "/c",
  21.                 "npx",
  22.                 "-y",
  23.                 "@modelcontextprotocol/server-filesystem",
  24.                 "C:/Users/tuling/Desktop"
  25.             ]
  26.         },
  27.        // 自定义的 mcp 服务
  28.         "mcp-server-weather": {  // 对应的项目名 application的 name
  29.             "command": "java", // 指明通过 java 命令执行,java 解析可以直接识别到
  30.             "args": [
  31.                 "-Dspring.ai.mcp.server.stdio=true",
  32.                 "-Dlogging.pattern.console=", // 清空控制台,不然会输入很多信息
  33.                 "-jar", // -jar 启动 Spring Boot
  34.                 "D:\\xxx\\target\\mcp-stdio-server-xs-1.0.jar" // 自定义的mcp服务的jar路径
  35.             ]
  36.         }
  37.     }
  38. }
复制代码

  • 绑定到Chatclient
  1. /**
  2. * @description: 智能航空助手:
  3. */
  4. @RestController
  5. @CrossOrigin
  6. public class OpenAiController {
  7.    
  8.     private final ChatClient chatClient;
  9.    
  10.     public OpenAiController(
  11.             DashScopeChatModel dashScopeChatModel,
  12.                             // 配置引入 外部 mcp tools
  13.                             ToolCallbackProvider mcpTools) {
  14.         this.chatClient =ChatClient.builder(dashScopeChatModel)
  15.         .defaultToolCallbacks(mcpTools)  // 将外部的 mcop tools 对大模型进行绑定,这里是构造器的绑定,不是单个对话的绑定
  16.         .build();
  17.     }
  18.    
  19. @CrossOrigin
  20. @GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  21. public Flux<String> generateStreamAsString(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
  22.     Flux<String> content = chatClient.prompt()
  23.             .user(message)
  24.             .stream()
  25.             .content();
  26.     return  content;
  27.     }
复制代码
  1. # 调试日志
  2. logging:
  3.   level:
  4.     io:
  5.       modelcontextprotocol:
  6.         client: DEBUG
  7.         spec: DEBUG
复制代码
使用 Spring AI 接入 自定义MCP Server

创建一个spring ai项目

  • 依赖
  1. <dependency>
  2.   <groupId>org.springframework.ai</groupId>
  3.   spring-ai-starter-mcp-server</artifactId>
  4. </dependency>
  5. <dependencyManagement>
  6.         <dependencies>
  7.             
  8.             <dependency>
  9.                 <groupId>org.springframework.ai</groupId>
  10.                 spring-ai-bom</artifactId>
  11.                 <version>${spring-ai.version}</version>
  12.                 <type>pom</type>
  13.                 <scope>import</scope>
  14.             </dependency>
  15.         </dependencies>
  16.   </dependencyManagement>
  17. <build>
  18.         <plugins>
  19.             <plugin>
  20.                 <groupId>org.springframework.boot</groupId>
  21.                 spring-boot-maven-plugin</artifactId>
  22.                 <executions>
  23.                     <execution>
  24.                         <goals>
  25.                             <goal>repackage</goal>
  26.                         </goals>
  27.                     </execution>
  28.                 </executions>
  29.             </plugin>
  30.         </plugins>
  31.     </build>
复制代码

  • 添加工具
  1. @Service
  2. public class UserToolService {
  3.     Map<String,Double> userScore = Map.of(
  4.         "xushu",99.0,
  5.         "zhangsan",2.0,
  6.         "lisi",3.0);
  7.     @Tool(description = "获取用户分数")
  8.     public String getScore(String username) { // 也可以添加上 @ToolParam(description=“” )告诉大模型这个参数的描述是做什么的
  9.         if(userScore.containsKey(userName)){
  10.             return userScore.get(userName).toString();
  11.         }  
  12.         return "未检索到当前用户"+userName;
  13.     }
  14. }
复制代码

  • 暴露工具
  1. @Bean  // 将我们编写的 tools 对外的UserToolService 绑定上去
  2. public ToolCallbackProvider weatherTools(UserToolService userToolService) {
  3.     return MethodToolCallbackProvider.builder().toolObjects(userToolService).build();
  4. }
复制代码

  • 配置
  1. spring:
  2.   main:
  3.     banner-mode: off
  4.   ai:
  5.     mcp:
  6.       server:
  7.         name: my-weather-server
  8.         version: 0.0.1
复制代码
# 注意:您必须禁用横幅和控制台日志记录,以允许 STDIO 传输!!工作 banner-mode: off

  • 打包 mvn package
此时target/生成了jar则成功!

  • 在我们需要的用到我们自定义的 mcp 的项目当中,加上我们自行定义的 MCP 服务。如下,我们是将其统一放到了一个配置的 json 文件当中。去了
  1. {
  2.     "mcpServers": {
  3.       // 外部第三方的
  4.         "baidu-map": {
  5.             "command": "cmd",
  6.             "args": [
  7.                 "/c",
  8.                 "npx",
  9.                 "-y",
  10.                 "@baidumap/mcp-server-baidu-map"
  11.             ],
  12.             "env": {
  13.                 "BAIDU_MAP_API_KEY": "xxxx"
  14.             }
  15.         },
  16.        // 外部第三方的
  17.         "filesystem": {
  18.             "command": "cmd",  // 指明使用 cmd 命令执行
  19.             "args": [
  20.                 "/c",
  21.                 "npx",
  22.                 "-y",
  23.                 "@modelcontextprotocol/server-filesystem",
  24.                 "C:/Users/tuling/Desktop"
  25.             ]
  26.         },
  27.        // 自定义的 mcp 服务
  28.         "mcp-server-weather": {  // 对应的项目名 application的 name
  29.             "command": "java", // 指明通过 java 命令执行,java 解析可以直接识别到
  30.             "args": [
  31.                 "-Dspring.ai.mcp.server.stdio=true",
  32.                 "-Dlogging.pattern.console=", // 清空控制台,不然会输入很多信息
  33.                 "-jar", // -jar 启动 Spring Boot
  34.                 "D:\\xxx\\target\\mcp-stdio-server-xs-1.0.jar" // 自定义的mcp服务的jar路径
  35.             ]
  36.         }
  37.     }
  38. }
复制代码
MCP SSE 输出配置实操(推荐 Web)

MCP Server

这种方式需要将部署为Web服务

  • 依赖
  1.       
  2.       <dependency>
  3.         <groupId>org.springframework.ai</groupId>
  4.         spring-ai-starter-mcp-server-webflux</artifactId>
  5.       </dependency>
  6.       
  7.       <dependency>
  8.         <groupId>org.springframework</groupId>
  9.         spring-boot-starter-web</artifactId>
  10.       </dependency>
复制代码

  • 定义外部工具
  1. @Service
  2. public class UserToolService {
  3.     Map<String,Double> userScore = Map.of(
  4.             "xushu",99.0,
  5.             "zhangsan",2.0,
  6.             "lisi",3.0);
  7.     @Tool(description = "获取用户分数")
  8.     public String getScore(String username) {
  9.         if(userScore.containsKey(username)){
  10.             return userScore.get(username).toString();
  11.         }
  12.         return "未检索到当前用户";
  13.     }
  14. }
复制代码

  • 暴露工具
  1. @Bean
  2.     public ToolCallbackProvider weatherToolCallbackProvider(WeatherService weatherService,
  3.                                                             UserToolService userToolService) {
  4.         return MethodToolCallbackProvider.builder().toolObjects(userToolService).build();
  5.     }
复制代码

  • 配置(需要用 web 启动)
  1. server:
  2.   port: 8088
复制代码
MCP Client

将上面 通过 SSE 方式创建的自定义 MCP Server 配置进来

  • 添加依赖
  1. <dependency>
  2.   <groupId>org.springframework.ai</groupId>
  3.   spring-ai-starter-mcp-client-webflux</artifactId>
  4. </dependency>
复制代码

  • 配置
  1. spring:
  2.   ai:
  3.     mcp:
  4.       client:
  5.         enabled: true
  6.         name: my-mcp-client
  7.         version: 1.0.0
  8.         request-timeout: 30s
  9.         type: ASYNC  # or SYNC
  10.         sse: # 设置 sse 输出方式
  11.           connections:
  12.             server1:
  13.               url: http://localhost:8088
复制代码

  • 代码
  1. /**
  2. * @author wx:程序员徐庶
  3. * @version 1.0
  4. * @description: 智能航空助手:需要一对一解答关注wx: 程序员徐庶
  5. */
  6. @RestController
  7. @CrossOrigin
  8. public class OpenAiController {
  9.     private final ChatClient chatClient;
  10.     public OpenAiController(
  11.         DashScopeChatModel dashScopeChatModel,
  12.         // 外部 mcp tools
  13.         ToolCallbackProvider mcpTools) {
  14.         this.chatClient =ChatClient.builder(dashScopeChatModel)
  15.         .defaultToolCallbacks(mcpTools)
  16.         .build();
  17.     }
  18.     @CrossOrigin
  19.     @GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  20.     public Flux<String> generateStreamAsString(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
  21.         Flux<String> content = chatClient.prompt()
  22.         .user(message)
  23.         .stream()
  24.         .content();
  25.         return  content;
  26.     }
复制代码
原理


  • STDIO 是基于标准输入\输出流的方式, 需要在MCP 客户端安装一个包(可以是jar包、python包、npm包等..). 它是“客户端”的MCP Server。
11.png


  • SSE 是基于Http的方式进行通讯, 需要将MCP Server部署为一个web服务. 它是服务端的MCP Server
STDIO原理

12.png

很多人不理解stdio到底什么意思, 为什么一定要把stdio server的banner关掉, 还要清空控制台?
13.png


  • 首先SpringAi底层会读取到mcp-servers-config.json的信息
  • 然后执行命令(其实聪明的小伙伴早就发现了,mcp-servers-config.json文件中就是一堆shell命令)

    • 怎么执行? 熟悉java的同学应该知道,java里面有一个对象用于执行命令:

  1. ProcessBuilder processBuilder = new ProcessBuilder();
  2.         processBuilder.command("java","-version");
  3.         Process process = processBuilder.start();
  4.         process.errorReader().lines().forEach(System.out::println);
复制代码

  • 所以springAi底层相当于读取到信息后, 会通过processBuilder去执行命令
  1. String[] commands={"java",
  2.                 "-Dspring.ai.mcp.server.stdio=true",
  3.                 "-Dlogging.pattern.console=",
  4.                 "-jar",
  5.                 "D:\\ideaworkspace\\git_pull\\tuling-flight-booking_all\\mcp-stdio-server\\target\\mcp-stdio-server-xs-1.0.jar"};
  6.         ProcessBuilder processBuilder = new ProcessBuilder();
  7.         processBuilder.command(commands);
  8.         // processBuilder.environment().put("username","xushu");
  9.         Process process = processBuilder.start();
复制代码
其实你也完全可以自己通过mcd去执行命令
14.png


  • 运行jar -jar mcp-stdio-server.jar
  • 输入{"jsonrpc":"2.0","method":"tools/list","id":"3b3f3431-1","params":{}}
  • 输出tools列表
这就是标准输入输出流! 看到这里你应该知道, 为什么需要-Dlogging.pattern.console= 完全是为了清空控制台,才能读取信息!
所以利用java也是一样的原理:
  1. @Test
  2.     public void test() throws IOException, InterruptedException {
  3.         String[] commands={"java",
  4.                 "-Dspring.ai.mcp.server.stdio=true",
  5.                 "-Dlogging.pattern.console=",
  6.                 "-jar",
  7.                 "D:\\ideaworkspace\\git_pull\\tuling-flight-booking_all\\mcp-stdio-server\\target\\mcp-stdio-server-xs-1.0.jar"};
  8.         ProcessBuilder processBuilder = new ProcessBuilder();
  9.         processBuilder.command(commands);
  10.         processBuilder.environment().put("username","xushu");
  11.         Process process = processBuilder.start();
  12.         Thread thread = new Thread(() -> {
  13.             try (BufferedReader processReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
  14.                 String line;
  15.                 while ((line=processReader.readLine())!=null) {
  16.                         System.out.println(line);
  17.                 }
  18.             } catch (IOException e) {
  19.                 e.printStackTrace();
  20.             }
  21.         });
  22.         thread.start();
  23.         Thread.sleep(1000);
  24.         new Thread(() -> {
  25.             try {
  26.                 //String jsonMessage="{"jsonrpc":"2.0","method":"initialize","id":"3670122a-0","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"spring-ai-mcp-client","version":"1.0.0"}}}";
  27.                 String jsonMessage = "{"jsonrpc":"2.0","method":"tools/list","id":"3b3f3431-1","params":{}}";
  28.                 jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n");
  29.                 var os = process.getOutputStream();
  30.                 synchronized (os) {
  31.                     os.write(jsonMessage.getBytes(StandardCharsets.UTF_8));
  32.                     os.write("\n".getBytes(StandardCharsets.UTF_8));
  33.                     os.flush();
  34.                 }
  35.                 System.out.println("写入完成!");
  36.             }catch (IOException e){
  37.                 e.printStackTrace();
  38.             }
  39.         }).start();
  40.         thread.join();
  41.         /*JSONRPCRequest[jsonrpc=2.0, method=initialize, id=5d83d0d1-0, params=InitializeRequest[protocolVersion=2024-11-05, capabilities=ClientCapabilities[experimental=null, roots=null, sampling=null],
  42.         clientInfo=Implementation[name=spring-ai-mcp-client, version=1.0.0]]]*/
  43.     }
复制代码

  • 通过ProcessBuilder执行命令
  • 通过子线程轮询 process.getInputStream 获取输出流
  • 通过process.getOutputStream(); 进行写入流
所以整个过程是这样的:再回顾上面的图
启动程序--->读取mcpjson--->通过ProcessBuilder启动命令---> 写入初始化jsonrpc---->写入获取tools列表jsonrpc---->请求大模型(携带tools)---->写入请求外部tool的jsonrpc---->获取数据--->发送给大模型---->响应。
STDIO源码

15.png

MCP鉴权

在做MCP企业级方案落地时, 我们可能不想让没有权限的人访问MCP Server, 或者需要根据不同的用户返回不同的数据, 这里就涉及到MCP Server授权操作。
那MCP Server有2种传输方式, 实现起来不一样:
STDIO

这种方式在本地运行,它 将MCP Server作为子进程启动。 我们称为标准输入输出, 其实就是利用运行命令的方式写入和读取控制台的信息,以达到传输。
16.png

通常我们会配置一段json,比如这里的百度地图MCP Server :

  • 其中command和args代表运行的命令和参数。
  • 其实env中的节点BAIDU_MAP_API_KEY就是做授权的。
如果你传入的BAIDU_MAP_API_KEY不对, 就没有使用权限。
  1. "baidu-map": {
  2.   "command": "cmd",
  3.   "args": [
  4.     "/c",
  5.     "npx",
  6.     "-y",
  7.     "@baidumap/mcp-server-baidu-map"
  8.   ],
  9.   "env": {
  10.     "BAIDU_MAP_API_KEY": "LEyBQxG9UzR9C1GZ6zDHsFDVKvBem2do"
  11.   }
  12. },
复制代码
所以STDIO做授权的方式很明确, 就是通过env【环境变量】,实现步骤如下:

  • 服务端发放一个用户的凭证(可以是秘钥、token) 这步不细讲,需要有一个授权中心发放凭证。
  • 通过mcp client通过env传入凭证
  • mcp server通过环境变量鉴权
所以在MCP Server端就可以通过获取环境变量的方式获取env里面的变量:
也可以通过AOP的方式统一处理
  1. @Tool(description = "获取用户余额")
  2.     public String getScore() {
  3.         String userName = System.getenv("API_KEY");
  4.         // todo .. 鉴权处理
  5.         return "未检索到当前用户"+userName;
  6.     }
复制代码
这种方式要注意: 他不支持动态鉴权, 也就是动态更换环境变量, 因为STDIO是本地运行方式,它 将MCP Server作为子进程启动, 如果是多个用户动态切换凭证, 会对共享的环境变量造成争抢, 最终只能存储一个。 除非一个用户对应一个STDIO MCP Server. 但是这样肯定很吃性能! 如果要多用户动态切换授权, 可以用SSE的方式;
SSE

说明

不过,如果你想把 MCP 服务器开放给外部使用,就需要暴露一些标准的 HTTP 接口。对于私有场景,MCP 服务器可能并不需要严格的身份认证,但在企业级部署下,对这些接口的安全和权限把控就非常重要了。为了解决这个问题,2025 年 3 月发布的最新 MCP 规范引入了安全基础,借助了广泛使用的 OAuth2 框架。
  
17.png

  本文不会详细介绍 OAuth2 的所有内容,不过简单回顾一下还是很有帮助。
在规范的草案中,MCP 服务器既是资源服务器,也是授权服务器。

  • 作为资源服务器,MCP 负责检查每个请求中的 Authorization请求头。这个请求头必须包括一个 OAuth2access_token(令牌),它代表客户端的“权限”。这个令牌通常是一个 JWT(JSON Web Token),也可能只是一个不可读的随机字符串。如果令牌缺失或无效(无法解析、已过期、不是发给本服务器的等),请求会被拒绝。正常情况下,调用示例如下:
  1. curl https://mcp.example.com/sse -H "Authorization: Bearer <有效的 access token>"
复制代码

  • 作为授权服务器,MCP 还需要有能力为客户端安全地签发access_token。在发放令牌前,服务器会校验客户端的凭据,有时还需要校验访问用户的身份。授权服务器决定令牌的有效期、权限范围、目标受众等特性。
用 Spring Security 和 Spring Authorization Server,可以方便地为现有的 Spring MCP 服务器加上这两大安全能力。
  
18.png

  给 Spring MCP 服务器加上 OAuth2 支持

这里以官方例子仓库的【天气】MCP 工具演示如何集成 OAuth2,主要是让服务器端能签发和校验令牌。
首先,pom.xml里添加必要的依赖:
  1. <dependency>
  2.   <groupId>org.springframework.boot</groupId>
  3.   spring-boot-starter-oauth2-resource-server</artifactId>
  4. </dependency>
  5. <dependency>
  6.   <groupId>org.springframework.boot</groupId>
  7.   spring-boot-starter-oauth2-authorization-server</artifactId>
  8. </dependency>
复制代码
接着,在application.properties配置里加上简易的 OAuth2 客户端信息,便于请求令牌:
  1. spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-id=xushu
  2. spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-secret={noop}xushu666
  3. spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-authentication-methods=client_secret_basic
  4. spring.security.oauth2.authorizationserver.client.oidc-client.registration.authorization-grant-types=client_credentials
复制代码
这样定义后,你可以直接通过 POST 请求和授权服务器交互,无需浏览器,用配置好的/secret作为固定凭据。 比如 最后一步是开启授权服务器和资源服务器功能。通常会新增一个安全配置类,比如SecurityConfiguration,如下:
  1. import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer.authorizationServer;
  2. @Configuration
  3. @EnableWebSecurity
  4. class SecurityConfiguration {
  5.     @Bean
  6.     SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  7.         return http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
  8.         .with(authorizationServer(), Customizer.withDefaults())
  9.         .oauth2ResourceServer(resource -> resource.jwt(Customizer.withDefaults()))
  10.         .csrf(CsrfConfigurer::disable)
  11.         .cors(Customizer.withDefaults())
  12.         .build();
  13.     }
  14. }
复制代码
这个过滤链主要做了这些事情:

  • 要求所有请求都要经过身份认证。也就是访问 MCP 的接口,必须带上 access_token。
  • 同时启用了授权服务器和资源服务器两大能力。
  • 关闭了 CSRF(跨站请求伪造防护),因为 MCP 不是给浏览器直接用的,这部分无需开启。
  • 打开了 CORS(跨域资源共享),方便用 MCP inspector 测试。
这样配置之后,只有带 access_token 的访问才会被接受,否则会直接返回 401 未授权错误,例如:
  1. curl http://localhost:8080/sse --fail-with-body
  2. # 返回:
  3. # curl: (22) The requested URL returned error: 401
复制代码
要使用 MCP 服务器,先要获取一个 access_token。可通过client_credentials授权方式(用于机器到机器、服务账号的场景):
  1. curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
  2. # 返回:
  3. # {"access_token":"<YOUR-ACCESS-TOKEN>","token_type":"Bearer","expires_in":299}
复制代码
把返回的 access_token 记下来(它一般以 “ey” 开头),之后就可以用它来正常请求服务器了:
  1. curl http://localhost:8080/sse -H"Authorization: Bearer YOUR_ACCESS_TOKEN"
  2. # 服务器响应内容
复制代码
你还可以直接在MCP inspector工具里用这个 access_token。从菜单的 Authentication > Bearer 处粘贴令牌并连接即可。
  为MCP Client设置请求头

目前, mcp 的java sdk 没有提供api直接调用, 经过徐庶老师研究源码后, 你只能通过2种方式实现:
重写源码

扩展mcp 的sse方式java sdk的源码, 整个重写一遍。 工作量较大, 并且我预计过不了多久, spring ai和mcp协议都会更新这块。 看你的紧急程度, 如果考虑整体扩展性维护性,可以整体重写一遍:
提供一个重写思路
重写McpSseClientProperties
MCPSse客户端属性配置:新增请求头字段
  1. package org.springframework.ai.autoconfigure.mcp.client.properties;
  2. @ConfigurationProperties("spring.ai.mcp.client.sse")
  3. public class McpSseClientProperties {
  4.     public static final String CONFIG_PREFIX = "spring.ai.mcp.client.sse";
  5.     private final Map<String, SseParameters> connections = new HashMap();
  6.    
  7.     private final Map<String, String> headersMap = new HashMap<>();
  8.     private String defaultHeaderName;
  9.     private String defaultHeaderValue;
  10.     private boolean enableCompression = false;
  11.     private int connectionTimeout = 5000;
  12.     public McpSseClientProperties() {
  13.     }
  14.     public Map<String, SseParameters> getConnections() {
  15.         return this.connections;
  16.     }
  17.     public Map<String, String> getHeadersMap() {
  18.         return this.headersMap;
  19.     }
  20.     public String getDefaultHeaderName() {
  21.         return this.defaultHeaderName;
  22.     }
  23.     public void setDefaultHeaderName(String defaultHeaderName) {
  24.         this.defaultHeaderName = defaultHeaderName;
  25.     }
  26.     public String getDefaultHeaderValue() {
  27.         return this.defaultHeaderValue;
  28.     }
  29.     public void setDefaultHeaderValue(String defaultHeaderValue) {
  30.         this.defaultHeaderValue = defaultHeaderValue;
  31.     }
  32.     public boolean isEnableCompression() {
  33.         return this.enableCompression;
  34.     }
  35.     public void setEnableCompression(boolean enableCompression) {
  36.         this.enableCompression = enableCompression;
  37.     }
  38.     public int getConnectionTimeout() {
  39.         return this.connectionTimeout;
  40.     }
  41.     public void setConnectionTimeout(int connectionTimeout) {
  42.         this.connectionTimeout = connectionTimeout;
  43.     }
  44.     public static record SseParameters(String url) {
  45.         public SseParameters(String url) {
  46.             this.url = url;
  47.         }
  48.         public String url() {
  49.             return this.url;
  50.         }
  51.     }
  52. }
复制代码
重写SseWebFluxTransportAutoConfiguration
自动装配添加请求头配置信息
  1. package org.springframework.ai.autoconfigure.mcp.client;
  2. @AutoConfiguration
  3. @ConditionalOnClass({WebFluxSseClientTransport.class})
  4. @EnableConfigurationProperties({McpSseClientProperties.class, McpClientCommonProperties.class})
  5. @ConditionalOnProperty(
  6.         prefix = "spring.ai.mcp.client",
  7.         name = {"enabled"},
  8.         havingValue = "true",
  9.         matchIfMissing = true
  10. )
  11. public class SseWebFluxTransportAutoConfiguration {
  12.     public SseWebFluxTransportAutoConfiguration() {
  13.     }
  14.     @Bean
  15.     public List<NamedClientMcpTransport> webFluxClientTransports(McpSseClientProperties sseProperties, WebClient.Builder webClientBuilderTemplate, ObjectMapper objectMapper) {
  16.         List<NamedClientMcpTransport> sseTransports = new ArrayList();
  17.         Iterator var5 = sseProperties.getConnections().entrySet().iterator();
  18.         Map<String, String> headersMap = sseProperties.getHeadersMap();
  19.         while(var5.hasNext()) {
  20.             Map.Entry<String, McpSseClientProperties.SseParameters> serverParameters = (Map.Entry)var5.next();
  21.             WebClient.Builder webClientBuilder = webClientBuilderTemplate.clone()
  22.                     .defaultHeaders(headers -> {
  23.                         if (headersMap != null && !headersMap.isEmpty()) {
  24.                             headersMap.forEach(headers::add);
  25.                         }
  26.                     })
  27.                     .baseUrl(((McpSseClientProperties.SseParameters)serverParameters.getValue()).url());
  28.             WebFluxSseClientTransport transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
  29.             sseTransports.add(new NamedClientMcpTransport((String)serverParameters.getKey(), transport));
  30.         }
  31.         return sseTransports;
  32.     }
  33.     @Bean
  34.     @ConditionalOnMissingBean
  35.     public WebClient.Builder webClientBuilder() {
  36.         return WebClient.builder();
  37.     }
  38.     @Bean
  39.     @ConditionalOnMissingBean
  40.     public ObjectMapper objectMapper() {
  41.         return new ObjectMapper();
  42.     }
  43. }
复制代码
使用:
19.png

设置WebClientCustomizer
在用Spring-ai-M8版本的时候, 发现提供了WebClientCustomizer进行扩展。 可以尝试:

  • 根据用户凭证进行授权
  1. curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
复制代码

  • 根据授权后的token进行请求:
  1. @Bean
  2. public WebClientCustomizer webClientCustomizer() {
  3.     // 认证 mcp server  /oauth?username:password   --> access_token
  4.     return (builder) -> {
  5.         builder.defaultHeader("Authorization","Bearer eyJraWQiOiIzYmMzMDRmZC02NzcyLTRkYTItODJiMy1hNTEwNGExMDBjNTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ4dXNodSIsImF1ZCI6Inh1c2h1IiwibmJmIjoxNzQ2NzE4MjE5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE3NDY3MTg1MTksImlhdCI6MTc0NjcxODIxOSwianRpIjoiM2VhMzIyODctNTQ5NC00NWZlLThlZDItZGY1MjViNmIwNzkxIn0.Q-zWBZxa2CeFZo2YinenyaLb8KBMMua40X8YSs4n2fez7ODihtoVuCeJQnd2Q6qV2Pa8Z3cfH4QcMUuxMJ-_sLtZaSXpbCThH5q3KoQZ8C4MLJRTpuRqv4z1n7uLNXiVG2rya5hGwjTxu5qzHuBa2ri9pamRwmsjTz4vLHBJ1ILxDJcTkZUFuV1ExQJViewGt_7KMYcFqzGyRPiS4mm4wVvJTDjqcEGwMelu51L44K1DDYgt29vVLRVQEmnUtbBzePAxRqfw_HWJdhRSeQNiqRYCYhdAlPr3QZUFJa54GpuZn3CNyaXFoL7mENSR7wCYWx6wi--_REw6oaIfeSm-Xg");
  6.     };
  7. }
复制代码
SSE是支持动态切换token的, 因为一个请求就是一个新的http请求, 不会出现多线程争抢。
但是需要动态请求:
curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666 进行重新授权
最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
20.gif


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册