蔡蔡小棉袄 發表於 2025-9-30 21:37:00

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

<h1 id="9-spring-ai-当中对应-mcp-的操作">9. Spring AI 当中对应 MCP 的操作</h1>
<p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>9. Spring AI 当中对应 MCP 的操作<ul><li>MCP<ul><li>问题:</li><li>使用<ul><li><font style="color: rgba(42, 188, 191, 1)">MCP STDIO 输出配置实操</font><ul><li><font style="color: rgba(244, 170, 6, 1)">MCP Server</font><ul><li>现成共用MCP Server</li></ul></li><li><font style="color: rgba(244, 170, 6, 1)">MCP Client</font><ul><li>通过工具</li><li>通过 Spring AI 接入 第三方的 MCP Server</li><li>使用 Spring AI 接入 自定义MCP Server</li></ul></li></ul></li><li><font style="color: rgba(42, 188, 191, 1)">MCP SSE 输出配置实操(推荐 Web)</font><ul><li>MCP Server</li><li>MCP Client</li></ul></li><li>原理<ul><li>STDIO原理</li></ul></li><li>STDIO源码</li><li>MCP鉴权<ul><li><font style="color: rgba(6, 8, 31, 0.88)">STDIO</font></li><li><font style="color: rgba(6, 8, 31, 0.88)">SSE</font><ul><li>说明</li><li><font style="color: rgba(6, 8, 31, 0.88)">给 Spring MCP 服务器加上 OAuth2 支持</font></li><li><font style="color: rgba(6, 8, 31, 0.88)">为MCP Client设置请求头</font></li><li>重写源码</li></ul></li></ul></li></ul></li></ul></li></ul></li><li>最后:</li></ul></div><p></p>
<h2 id="mcp">MCP</h2>
<h3 id="问题">问题:</h3>
<ol>
<li>当有服务商需要将tools提供外部使用(比如高德地图提供了位置服务tools, 比如百度提供了联网搜索的tools...)</li>
<li>或者在企业级中, 有多个智能应用,想将通用的tools公共化</li>
</ol>
<p>怎么办?</p>
<p>可以把tools单独抽取出来, 由应用程序读取外部的tools。 那关键是怎么读呢? 怎么解析呢? 如果每个提供商各用一种规则你能想象有多麻烦! 所以MCP就诞生了, 他指定了标准规则, 以jsonrpc2.0的方式进行通讯。</p>
<p>那问题又来了, 以什么方式通讯呢? http? rpc? stdio? mcp提供了sse和stdio这2种方式。</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725273-1921338540.png" class="lazyload"></p>
<h3 id="使用">使用</h3>
<p>Streamable http目前springai1.0版本不支持(因为Streamable http 是 spring ai 1.0 之后说明的) 我们先掌握SSE和STDIO</p>
<p>分别说下STDIO和SSE的方式:</p>
<ul>
<li><strong>STDIO</strong>更适合客户端桌面应用和辅助工具</li>
<li><strong>SSE</strong>更适合web应用 、业务有关的公共tools</li>
</ul>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725244-887855626.png" class="lazyload"></p>
<h4 id="mcp-stdio-输出配置实操"><font style="color: rgba(42, 188, 191, 1)">MCP STDIO 输出配置实操</font></h4>
<h5 id="mcp-server"><font style="color: rgba(244, 170, 6, 1)">MCP Server</font></h5>
<h6 id="现成共用mcp-server">现成共用MCP Server</h6>
<p>现在有很多MCP 服务 给大家提供一个网站:MCP Server(MCP 服务器)</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725292-100756769.png" class="lazyload"></p>
<p><font style="color: rgba(51, 51, 51, 1)">那MCP有了, 怎么调用呢? 这里介绍2种使用方式:</font></p>
<h5 id="mcp-client"><font style="color: rgba(244, 170, 6, 1)">MCP Client</font></h5>
<h6 id="通过工具">通过工具</h6>
<p>CherryStudio、Cursor 、<font style="color: rgba(25, 27, 31, 1)">Claude Desktop、</font>Cline<font style="color: rgba(25, 27, 31, 1)"> 等等很多, 这里不一一演示, 不会的话自己找个文章, 工具使用都很简单!</font></p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725404-798686887.png" class="lazyload"></p>
<p>以Cline为例: 他是Vscode的插件</p>
<ol>
<li>
<p>安装VSCode</p>
</li>
<li>
<p>安装插件:</p>
</li>
</ol>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725248-598219279.png" class="lazyload"></p>
<ol>
<li>配置cline的模型:</li>
</ol>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725263-1822085841.png" class="lazyload"></p>
<ol>
<li>配置cline的mcpserver<img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213728732-2042516612.png" class="lazyload"></li>
</ol>
<pre><code class="language-json">{
    "mcpServers": {
      "baidu-map": {
            "command": "cmd",
            "args": [
                "/c",
                "npx",
                "-y",
                "@baidumap/mcp-server-baidu-map"
            ],
            "env": {
                "BAIDU_MAP_API_KEY": "LEyBQxG9UzR9C1GZ6zDHsFDVKvBem2do"
            }
      },
      "filesystem": {
            "command": "cmd",
            "args": [
                "/c",
                "npx",
                "-y",
                "@modelcontextprotocol/server-filesystem",
                "C:/Users/tuling/Desktop"
            ]
      },
      "mcp-server-weather": {
            "command": "java",
            "args": [
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=",
                "-jar",
                "D:\\ideaworkspace\\git_pull\\tuling-flight-booking_all\\mcp-stdio-server\\target\\mcp-stdio-server-xs-1.0.jar"
            ]
      }
    }
}
</code></pre>
<ol>
<li>开启cline权限</li>
</ol>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725217-1656218644.png" class="lazyload"><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725222-1386955657.png" class="lazyload"></p>
<p>6.测试:</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725250-674051652.png" class="lazyload"></p>
<h6 id="通过-spring-ai-接入-第三方的-mcp-server">通过 Spring AI 接入 第三方的 MCP Server</h6>
<ol>
<li>依赖</li>
</ol>
<pre><code class="language-xml">&lt;!--既支持sse\也支持Stdio--&gt;
&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
    &lt;artifactId&gt;spring-ai-starter-mcp-client-webflux&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<p>2 配置</p>
<pre><code class="language-yaml">spring:
ai:
    mcp:
      client:
      # 连接超时时间设置
      request-timeout: 60000
      stdio: # 设置 sse 输出方式
      # 配置Mcp 方式2: 将 mcp的配置 单独放在一个 Json 文件当中读取,推荐,利用维护
      # classpath 是指:项目resources
          servers-configuration: classpath:/mcp-servers-config.json
      # 配置MCP 方式2: 直接将 mcp 配置全局配置文件中(mcp 配置太多不利于维护)
          # connections:
          #   server1:
          #   command: /path/to/server
          #   args:
          #       - --port=8080
          #       - --mode=production
          #   env:
          #       API_KEY: your-api-key
          #       DEBUG: "true"
</code></pre>
<ol start="2">
<li>mcp-servers-config.json:</li>
</ol>
<p>获取Baidu地图key: 控制台 | 百度地图开放平台</p>
<pre><code class="language-json">{
    "mcpServers": {
      "baidu-map": {
            "command": "cmd",
            "args": [
                "/c",
                "npx",
                "-y",
                "@baidumap/mcp-server-baidu-map"
            ],
            "env": {
                "BAIDU_MAP_API_KEY": "xxxx"
            }
      },
      "filesystem": {
            "command": "cmd",
            "args": [
                "/c",
                "npx",
                "-y",
                "@modelcontextprotocol/server-filesystem",
                "C:/Users/tuling/Desktop"
            ]
      },
      "mcp-server-weather": {
            "command": "java",
            "args": [
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=",
                "-jar",
                "D:\\xxx\\target\\mcp-stdio-server-xs-1.0.jar"
            ]
      }
    }
}
</code></pre>
<pre><code class="language-json">{
    "mcpServers": {
      // 外部第三方的
      "baidu-map": {
            "command": "cmd",
            "args": [
                "/c",
                "npx",
                "-y",
                "@baidumap/mcp-server-baidu-map"
            ],
            "env": {
                "BAIDU_MAP_API_KEY": "xxxx"
            }
      },
       // 外部第三方的
      "filesystem": {
            "command": "cmd",// 指明使用 cmd 命令执行
            "args": [
                "/c",
                "npx",
                "-y",
                "@modelcontextprotocol/server-filesystem",
                "C:/Users/tuling/Desktop"
            ]
      },
       // 自定义的 mcp 服务
      "mcp-server-weather": {// 对应的项目名 application的 name
            "command": "java", // 指明通过 java 命令执行,java 解析可以直接识别到
            "args": [
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=", // 清空控制台,不然会输入很多信息
                "-jar", // -jar 启动 Spring Boot
                "D:\\xxx\\target\\mcp-stdio-server-xs-1.0.jar" // 自定义的mcp服务的jar路径
            ]
      }
    }
}
</code></pre>
<ol start="3">
<li>绑定到Chatclient</li>
</ol>
<pre><code class="language-java">/**
* @description: 智能航空助手:
*/
@RestController
@CrossOrigin
public class OpenAiController {
   
    private final ChatClient chatClient;
   
    public OpenAiController(
            DashScopeChatModel dashScopeChatModel,
                            // 配置引入 外部 mcp tools
                            ToolCallbackProvider mcpTools) {
      this.chatClient =ChatClient.builder(dashScopeChatModel)
      .defaultToolCallbacks(mcpTools)// 将外部的 mcop tools 对大模型进行绑定,这里是构造器的绑定,不是单个对话的绑定
      .build();
    }
   

@CrossOrigin
@GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux&lt;String&gt; generateStreamAsString(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {

    Flux&lt;String&gt; content = chatClient.prompt()
            .user(message)
            .stream()
            .content();

    returncontent;

    }
</code></pre>
<pre><code class="language-yaml"># 调试日志
logging:
level:
    io:
      modelcontextprotocol:
      client: DEBUG
      spec: DEBUG
</code></pre>
<h6 id="使用-spring-ai-接入-自定义mcp-server">使用 Spring AI 接入 自定义MCP Server</h6>
<p>创建一个spring ai项目</p>
<ol>
<li>依赖</li>
</ol>
<pre><code class="language-xml">&lt;!--mcp-server--&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
&lt;artifactId&gt;spring-ai-starter-mcp-server&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependencyManagement&gt;
      &lt;dependencies&gt;
            &lt;!--spring ai 包管理依赖 --&gt;
            &lt;dependency&gt;
                &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
                &lt;artifactId&gt;spring-ai-bom&lt;/artifactId&gt;
                &lt;version&gt;${spring-ai.version}&lt;/version&gt;
                &lt;type&gt;pom&lt;/type&gt;
                &lt;scope&gt;import&lt;/scope&gt;
            &lt;/dependency&gt;
      &lt;/dependencies&gt;
&lt;/dependencyManagement&gt;

&lt;!-- 打包 --&gt;
&lt;build&gt;
      &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
                &lt;executions&gt;
                  &lt;execution&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;repackage&lt;/goal&gt;
                        &lt;/goals&gt;
                  &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
      &lt;/plugins&gt;
    &lt;/build&gt;
</code></pre>
<ol start="2">
<li>添加工具</li>
</ol>
<pre><code class="language-java">@Service
public class UserToolService {

    Map&lt;String,Double&gt; userScore = Map.of(
      "xushu",99.0,
      "zhangsan",2.0,
      "lisi",3.0);
    @Tool(description = "获取用户分数")
    public String getScore(String username) { // 也可以添加上 @ToolParam(description=“” )告诉大模型这个参数的描述是做什么的
      if(userScore.containsKey(userName)){
            return userScore.get(userName).toString();
      }

      return "未检索到当前用户"+userName;
    }
}
</code></pre>
<ol start="3">
<li>暴露工具</li>
</ol>
<pre><code class="language-java">@Bean// 将我们编写的 tools 对外的UserToolService 绑定上去
public ToolCallbackProvider weatherTools(UserToolService userToolService) {
    return MethodToolCallbackProvider.builder().toolObjects(userToolService).build();
}
</code></pre>
<ol start="4">
<li>配置</li>
</ol>
<pre><code class="language-yaml">spring:
main:
    banner-mode: off
ai:
    mcp:
      server:
      name: my-weather-server
      version: 0.0.1
</code></pre>
<p><font style="background-color: rgba(248, 206, 211, 0.5)"># 注意:您必须禁用横幅和控制台日志记录,以允许 STDIO 传输!!工作 banner-mode: off</font></p>
<ol start="5">
<li>打包 mvn package</li>
</ol>
<p>此时target/生成了jar则成功!</p>
<ol start="6">
<li>在我们需要的用到我们自定义的 mcp 的项目当中,加上我们自行定义的 MCP 服务。如下,我们是将其统一放到了一个配置的 json 文件当中。去了</li>
</ol>
<pre><code class="language-json">{
    "mcpServers": {
      // 外部第三方的
      "baidu-map": {
            "command": "cmd",
            "args": [
                "/c",
                "npx",
                "-y",
                "@baidumap/mcp-server-baidu-map"
            ],
            "env": {
                "BAIDU_MAP_API_KEY": "xxxx"
            }
      },
       // 外部第三方的
      "filesystem": {
            "command": "cmd",// 指明使用 cmd 命令执行
            "args": [
                "/c",
                "npx",
                "-y",
                "@modelcontextprotocol/server-filesystem",
                "C:/Users/tuling/Desktop"
            ]
      },
       // 自定义的 mcp 服务
      "mcp-server-weather": {// 对应的项目名 application的 name
            "command": "java", // 指明通过 java 命令执行,java 解析可以直接识别到
            "args": [
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=", // 清空控制台,不然会输入很多信息
                "-jar", // -jar 启动 Spring Boot
                "D:\\xxx\\target\\mcp-stdio-server-xs-1.0.jar" // 自定义的mcp服务的jar路径
            ]
      }
    }
}
</code></pre>
<h4 id="mcp-sse-输出配置实操推荐-web"><font style="color: rgba(42, 188, 191, 1)">MCP SSE 输出配置实操(推荐 Web)</font></h4>
<h5 id="mcp-server-1">MCP Server</h5>
<p>这种方式需要将部署为Web服务</p>
<ol>
<li>依赖</li>
</ol>
<pre><code class="language-xml">      &lt;!--mcp服务器核心依赖— 响应式--&gt;
      &lt;dependency&gt;
      &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
      &lt;artifactId&gt;spring-ai-starter-mcp-server-webflux&lt;/artifactId&gt;
      &lt;/dependency&gt;
      &lt;!-- 这个 SSE 是需要 Web 的 --&gt;
      &lt;dependency&gt;
      &lt;groupId&gt;org.springframework&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
      &lt;/dependency&gt;
</code></pre>
<ol start="2">
<li>定义外部工具</li>
</ol>
<pre><code class="language-java">@Service
public class UserToolService {

    Map&lt;String,Double&gt; userScore = Map.of(
            "xushu",99.0,
            "zhangsan",2.0,
            "lisi",3.0);
    @Tool(description = "获取用户分数")
    public String getScore(String username) {
      if(userScore.containsKey(username)){
            return userScore.get(username).toString();
      }

      return "未检索到当前用户";
    }
}
</code></pre>
<ol start="3">
<li>暴露工具</li>
</ol>
<pre><code class="language-java">@Bean
    public ToolCallbackProvider weatherToolCallbackProvider(WeatherService weatherService,
                                                            UserToolService userToolService) {
      return MethodToolCallbackProvider.builder().toolObjects(userToolService).build();
    }
</code></pre>
<ol start="4">
<li>配置(需要用 web 启动)</li>
</ol>
<pre><code class="language-yaml">server:
port: 8088
</code></pre>
<h5 id="mcp-client-1">MCP Client</h5>
<p>将上面 通过 SSE 方式创建的自定义 MCP Server 配置进来</p>
<ol>
<li>添加依赖</li>
</ol>
<pre><code class="language-xml">&lt;!--既支持sse\也支持Stdio--&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
&lt;artifactId&gt;spring-ai-starter-mcp-client-webflux&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<ol>
<li>配置</li>
</ol>
<pre><code class="language-yaml">spring:
ai:
    mcp:
      client:
      enabled: true
      name: my-mcp-client
      version: 1.0.0
      request-timeout: 30s
      type: ASYNC# or SYNC
      sse: # 设置 sse 输出方式
          connections:
            server1:
            url: http://localhost:8088
</code></pre>
<ol>
<li>代码</li>
</ol>
<pre><code class="language-java">/**
* @author wx:程序员徐庶
* @version 1.0
* @description: 智能航空助手:需要一对一解答关注wx: 程序员徐庶
*/
@RestController
@CrossOrigin
public class OpenAiController {

    private final ChatClient chatClient;

    public OpenAiController(
      DashScopeChatModel dashScopeChatModel,
      // 外部 mcp tools
      ToolCallbackProvider mcpTools) {
      this.chatClient =ChatClient.builder(dashScopeChatModel)
      .defaultToolCallbacks(mcpTools)
      .build();
    }


    @CrossOrigin
    @GetMapping(value = "/ai/generateStreamAsString", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux&lt;String&gt; generateStreamAsString(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {

      Flux&lt;String&gt; content = chatClient.prompt()
      .user(message)
      .stream()
      .content();

      returncontent;

    }
</code></pre>
<h4 id="原理">原理</h4>
<ol>
<li>STDIO 是基于标准输入\输出流的方式, 需要在MCP 客户端安装一个包(可以是jar包、python包、npm包等..). 它是“客户端”的MCP Server。</li>
</ol>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725319-1943519937.png" class="lazyload"></p>
<ol>
<li>SSE 是基于Http的方式进行通讯, 需要将MCP Server部署为一个web服务. 它是服务端的MCP Server</li>
</ol>
<h5 id="stdio原理">STDIO原理</h5>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725264-586886496.png" class="lazyload"></p>
<p>很多人不理解stdio到底什么意思, 为什么一定要把stdio server的banner关掉, 还要清空控制台?</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725317-453461788.png" class="lazyload"></p>
<ol>
<li>首先SpringAi底层会读取到mcp-servers-config.json的信息</li>
<li>然后执行命令(其实聪明的小伙伴早就发现了,mcp-servers-config.json文件中就是一堆shell命令)
<ol>
<li>怎么执行? 熟悉java的同学应该知道,java里面有一个对象用于执行命令:</li>
</ol>
</li>
</ol>
<pre><code class="language-java">ProcessBuilder processBuilder = new ProcessBuilder();
      processBuilder.command("java","-version");

      Process process = processBuilder.start();

      process.errorReader().lines().forEach(System.out::println);
</code></pre>
<ol>
<li>所以springAi底层相当于读取到信息后, 会通过processBuilder去执行命令</li>
</ol>
<pre><code class="language-java">String[] commands={"java",
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=",
                "-jar",
                "D:\\ideaworkspace\\git_pull\\tuling-flight-booking_all\\mcp-stdio-server\\target\\mcp-stdio-server-xs-1.0.jar"};

      ProcessBuilder processBuilder = new ProcessBuilder();
      processBuilder.command(commands);
      // processBuilder.environment().put("username","xushu");

      Process process = processBuilder.start();
</code></pre>
<p>其实你也完全可以自己通过mcd去执行命令</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725221-1969095872.png" class="lazyload"></p>
<ol>
<li>运行jar -jar mcp-stdio-server.jar</li>
<li>输入<font style="color: rgba(50, 50, 50, 1); background-color: rgba(255, 255, 255, 0.01)">{"jsonrpc":"2.0","method":"tools/list","id":"3b3f3431-1","params":{}}</font></li>
<li>输出tools列表</li>
</ol>
<p>这就是标准输入输出流! 看到这里你应该知道, 为什么需要-Dlogging.pattern.console= 完全是为了清空控制台,才能读取信息!</p>
<p>所以利用java也是一样的原理:</p>
<pre><code class="language-java">@Test
    public void test() throws IOException, InterruptedException {
      String[] commands={"java",
                "-Dspring.ai.mcp.server.stdio=true",
                "-Dlogging.pattern.console=",
                "-jar",
                "D:\\ideaworkspace\\git_pull\\tuling-flight-booking_all\\mcp-stdio-server\\target\\mcp-stdio-server-xs-1.0.jar"};

      ProcessBuilder processBuilder = new ProcessBuilder();
      processBuilder.command(commands);
      processBuilder.environment().put("username","xushu");

      Process process = processBuilder.start();

      Thread thread = new Thread(() -&gt; {
            try (BufferedReader processReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line=processReader.readLine())!=null) {
                        System.out.println(line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
      });
      thread.start();


      Thread.sleep(1000);

      new Thread(() -&gt; {

            try {
                //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\"}}}";
                String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":\"3b3f3431-1\",\"params\":{}}";

                jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n");

                var os = process.getOutputStream();
                synchronized (os) {
                  os.write(jsonMessage.getBytes(StandardCharsets.UTF_8));
                  os.write("\n".getBytes(StandardCharsets.UTF_8));
                  os.flush();
                }
                System.out.println("写入完成!");
            }catch (IOException e){
                e.printStackTrace();
            }
      }).start();


      thread.join();
      /*JSONRPCRequest,
      clientInfo=Implementation]]*/
    }
</code></pre>
<ol>
<li>通过ProcessBuilder执行命令</li>
<li>通过子线程轮询 process.getInputStream 获取输出流</li>
<li>通过process.getOutputStream(); 进行写入流</li>
</ol>
<p>所以整个过程是这样的:再回顾上面的图</p>
<p>启动程序---&gt;读取mcpjson---&gt;通过ProcessBuilder启动命令---&gt; 写入初始化jsonrpc----&gt;写入获取tools列表jsonrpc----&gt;请求大模型(携带tools)----&gt;写入请求外部tool的jsonrpc----&gt;获取数据---&gt;发送给大模型----&gt;响应。</p>
<h4 id="stdio源码">STDIO源码</h4>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725366-618850753.png" class="lazyload"></p>
<h4 id="mcp鉴权">MCP鉴权</h4>
<p><font style="color: rgba(6, 8, 31, 0.88)">在做MCP企业级方案落地时, 我们可能不想让没有权限的人访问MCP Server, 或者需要根据不同的用户返回不同的数据, 这里就涉及到MCP Server授权操作。</font></p>
<p><font style="color: rgba(6, 8, 31, 0.88)">那MCP Server有2种传输方式, 实现起来不一样:</font></p>
<h5 id="stdio"><font style="color: rgba(6, 8, 31, 0.88)">STDIO</font></h5>
<p><font style="color: rgba(6, 8, 31, 0.88)">这种方式在本地运行,</font><strong><font style="color: rgba(223, 42, 63, 1)">它 将MCP Server作为子进程启动</font></strong><font style="color: rgba(62, 62, 62, 1)">。</font><font style="color: rgba(6, 8, 31, 0.88)"> 我们称为标准输入输出, 其实就是利用运行命令的方式写入和读取控制台的信息,以达到传输。 </font></p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725204-1326935249.png" class="lazyload"></p>
<p><font style="color: rgba(6, 8, 31, 0.88)">通常我们会配置一段json,比如这里的百度地图MCP Server :</font></p>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88)">其中command和args代表运行的命令和参数。</font></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">其实env中的节点BAIDU_MAP_API_KEY就是做授权的。</font></li>
</ul>
<p><font style="color: rgba(6, 8, 31, 0.88)">如果你传入的BAIDU_MAP_API_KEY不对, 就没有使用权限。</font></p>
<pre><code class="language-json">"baidu-map": {
"command": "cmd",
"args": [
    "/c",
    "npx",
    "-y",
    "@baidumap/mcp-server-baidu-map"
],
"env": {
    "BAIDU_MAP_API_KEY": "LEyBQxG9UzR9C1GZ6zDHsFDVKvBem2do"
}
},
</code></pre>
<p><font style="color: rgba(6, 8, 31, 0.88)">所以STDIO做授权的方式很明确, 就是通过env【环境变量】,实现步骤如下:</font></p>
<ol>
<li>服务端发放一个用户的凭证(可以是秘钥、token) 这步不细讲,需要有一个授权中心发放凭证。</li>
<li>通过mcp client通过env传入凭证</li>
<li>mcp server通过<font style="color: rgba(6, 8, 31, 0.88)">环境变量鉴权</font></li>
</ol>
<p><font style="color: rgba(6, 8, 31, 0.88)">所以在MCP Server端就可以通过获取环境变量的方式获取env里面的变量:</font></p>
<p><font style="color: rgba(6, 8, 31, 0.88)">也可以通过AOP的方式统一处理</font></p>
<pre><code class="language-java">@Tool(description = "获取用户余额")
    public String getScore() {
      String userName = System.getenv("API_KEY");
      // todo .. 鉴权处理
      return "未检索到当前用户"+userName;
    }
</code></pre>
<p><font style="color: rgba(6, 8, 31, 0.88); background-color: rgba(248, 206, 211, 0.5)">这种方式要注意: 他不支持动态鉴权, 也就是动态更换环境变量, 因为STDIO是本地运行方式,</font><strong><font style="color: rgba(223, 42, 63, 1); background-color: rgba(248, 206, 211, 0.5)">它 将MCP Server作为子进程启动, </font></strong><font style="color: rgba(6, 8, 31, 0.88); background-color: rgba(248, 206, 211, 0.5)">如果是多个用户动态切换凭证, 会对共享的环境变量造成争抢, 最终只能存储一个。 除非一个用户对应一个STDIO MCP Server. 但是这样肯定很吃性能! 如果要多用户动态切换授权, 可以用SSE的方式;</font></p>
<h5 id="sse"><font style="color: rgba(6, 8, 31, 0.88)">SSE</font></h5>
<h6 id="说明">说明</h6>
<p><font style="color: rgba(6, 8, 31, 0.88)">不过,如果你想把 MCP 服务器开放给外部使用,就需要暴露一些标准的 HTTP 接口。对于私有场景,MCP 服务器可能并不需要严格的身份认证,但在企业级部署下,对这些接口的安全和权限把控就非常重要了。为了解决这个问题,</font>2025 年 3 月发布的最新 MCP 规范<font style="color: rgba(6, 8, 31, 0.88)">引入了安全基础,借助了广泛使用的 </font>OAuth2 框架<font style="color: rgba(6, 8, 31, 0.88)">。</font></p>
<font style="color: rgba(0, 0, 0, 0.9)">
</font>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725271-1803636783.png" class="lazyload"></p>
<font style="color: rgba(0, 0, 0, 0.9)">
</font>
<p><font style="color: rgba(6, 8, 31, 0.88)">本文不会详细介绍 OAuth2 的所有内容,不过简单回顾一下还是很有帮助。</font></p>
<p><strong><font style="color: rgba(223, 42, 63, 1)">在规范的草案中,MCP 服务器既是资源服务器,也是授权服务器。</font></strong></p>
<ul>
<li>作为资源服务器,MCP 负责检查每个请求中的 Authorization请求头。这个请求头必须包括一个 OAuth2access_token(令牌),它代表客户端的“权限”。这个令牌通常是一个 JWT(JSON Web Token),也可能只是一个不可读的随机字符串。如果令牌缺失或无效(无法解析、已过期、不是发给本服务器的等),请求会被拒绝。正常情况下,调用示例如下:</li>
</ul>
<pre><code class="language-plain">curl https://mcp.example.com/sse -H "Authorization: Bearer &lt;有效的 access token&gt;"
</code></pre>
<ul>
<li>作为授权服务器,MCP 还需要有能力为客户端安全地签发access_token。在发放令牌前,服务器会校验客户端的凭据,有时还需要校验访问用户的身份。授权服务器决定令牌的有效期、权限范围、目标受众等特性。</li>
</ul>
<p><font style="color: rgba(6, 8, 31, 0.88)">用 Spring Security 和 Spring Authorization Server,可以方便地为现有的 Spring MCP 服务器加上这两大安全能力。</font></p>
<font style="color: rgba(0, 0, 0, 0.9)">
</font>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725277-1734165758.png" class="lazyload"></p>
<font style="color: rgba(0, 0, 0, 0.9)">
</font>
<h6 id="给-spring-mcp-服务器加上-oauth2-支持"><font style="color: rgba(6, 8, 31, 0.88)">给 Spring MCP 服务器加上 OAuth2 支持</font></h6>
<p><font style="color: rgba(6, 8, 31, 0.88)">这里以官方例子仓库的【天气】MCP 工具演示如何集成 OAuth2,主要是让服务器端能签发和校验令牌。</font></p>
<p>首先,pom.xml里添加必要的依赖:</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-oauth2-resource-server&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-oauth2-authorization-server&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<p>接着,在application.properties配置里加上简易的 OAuth2 客户端信息,便于请求令牌:</p>
<pre><code class="language-properties">spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-id=xushu
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-secret={noop}xushu666
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-authentication-methods=client_secret_basic
spring.security.oauth2.authorizationserver.client.oidc-client.registration.authorization-grant-types=client_credentials
</code></pre>
<p>这样定义后,你可以直接通过 POST 请求和授权服务器交互,无需浏览器,用配置好的/secret作为固定凭据。 比如 最后一步是开启授权服务器和资源服务器功能。通常会新增一个安全配置类,比如SecurityConfiguration,如下:</p>
<pre><code class="language-java">import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer.authorizationServer;

@Configuration
@EnableWebSecurity
class SecurityConfiguration {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      return http.authorizeHttpRequests(auth -&gt; auth.anyRequest().authenticated())
      .with(authorizationServer(), Customizer.withDefaults())
      .oauth2ResourceServer(resource -&gt; resource.jwt(Customizer.withDefaults()))
      .csrf(CsrfConfigurer::disable)
      .cors(Customizer.withDefaults())
      .build();
    }
}
</code></pre>
<p><font style="color: rgba(6, 8, 31, 0.88)">这个过滤链主要做了这些事情:</font></p>
<ul>
<li><font style="color: rgba(6, 8, 31, 0.88)">要求所有请求都要经过身份认证。也就是访问 MCP 的接口,必须带上 access_token。</font></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">同时启用了授权服务器和资源服务器两大能力。</font></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">关闭了 CSRF(跨站请求伪造防护),因为 MCP 不是给浏览器直接用的,这部分无需开启。</font></li>
<li><font style="color: rgba(6, 8, 31, 0.88)">打开了 CORS(跨域资源共享),方便用 MCP inspector 测试。</font></li>
</ul>
<p><font style="color: rgba(6, 8, 31, 0.88)">这样配置之后,只有带 access_token 的访问才会被接受,否则会直接返回 401 未授权错误,例如:</font></p>
<pre><code class="language-powershell">curl http://localhost:8080/sse --fail-with-body
# 返回:
# curl: (22) The requested URL returned error: 401
</code></pre>
<p>要使用 MCP 服务器,先要获取一个 access_token。可通过client_credentials授权方式(用于机器到机器、服务账号的场景):</p>
<pre><code class="language-powershell">curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
# 返回:
# {"access_token":"&lt;YOUR-ACCESS-TOKEN&gt;","token_type":"Bearer","expires_in":299}
</code></pre>
<p><font style="color: rgba(6, 8, 31, 0.88)">把返回的 access_token 记下来(它一般以 “ey” 开头),之后就可以用它来正常请求服务器了:</font></p>
<pre><code class="language-powershell">curl http://localhost:8080/sse -H"Authorization: Bearer YOUR_ACCESS_TOKEN"
# 服务器响应内容
</code></pre>
<p><font style="color: rgba(6, 8, 31, 0.88)">你还可以直接在</font><font style="color: rgba(9, 105, 218, 1)">MCP inspector</font><font style="color: rgba(6, 8, 31, 0.88)">工具里用这个 access_token。从菜单的 Authentication &gt; Bearer 处粘贴令牌并连接即可。</font></p>
<font style="color: rgba(0, 0, 0, 0.9)">
</font>
<h6 id="为mcp-client设置请求头"><font style="color: rgba(6, 8, 31, 0.88)">为MCP Client设置请求头</font></h6>
<p>目前, mcp 的java sdk 没有提供api直接调用, 经过徐庶老师研究源码后, 你只能通过2种方式实现:</p>
<h6 id="重写源码">重写源码</h6>
<p>扩展mcp 的sse方式java sdk的源码, 整个重写一遍。 工作量较大, 并且我预计过不了多久, spring ai和mcp协议都会更新这块。 看你的紧急程度, 如果考虑整体扩展性维护性,可以整体重写一遍:</p>
<p>提供一个重写思路</p>
<p><strong>重写McpSseClientProperties</strong></p>
<p>MCPSse客户端属性配置:新增请求头字段</p>
<pre><code class="language-java">package org.springframework.ai.autoconfigure.mcp.client.properties;

@ConfigurationProperties("spring.ai.mcp.client.sse")
public class McpSseClientProperties {
    public static final String CONFIG_PREFIX = "spring.ai.mcp.client.sse";
    private final Map&lt;String, SseParameters&gt; connections = new HashMap();
   
    private final Map&lt;String, String&gt; headersMap = new HashMap&lt;&gt;();
    private String defaultHeaderName;
    private String defaultHeaderValue;
    private boolean enableCompression = false;
    private int connectionTimeout = 5000;

    public McpSseClientProperties() {
    }

    public Map&lt;String, SseParameters&gt; getConnections() {
      return this.connections;
    }

    public Map&lt;String, String&gt; getHeadersMap() {
      return this.headersMap;
    }

    public String getDefaultHeaderName() {
      return this.defaultHeaderName;
    }

    public void setDefaultHeaderName(String defaultHeaderName) {
      this.defaultHeaderName = defaultHeaderName;
    }

    public String getDefaultHeaderValue() {
      return this.defaultHeaderValue;
    }

    public void setDefaultHeaderValue(String defaultHeaderValue) {
      this.defaultHeaderValue = defaultHeaderValue;
    }

    public boolean isEnableCompression() {
      return this.enableCompression;
    }

    public void setEnableCompression(boolean enableCompression) {
      this.enableCompression = enableCompression;
    }

    public int getConnectionTimeout() {
      return this.connectionTimeout;
    }

    public void setConnectionTimeout(int connectionTimeout) {
      this.connectionTimeout = connectionTimeout;
    }

    public static record SseParameters(String url) {
      public SseParameters(String url) {
            this.url = url;
      }

      public String url() {
            return this.url;
      }
    }
}
</code></pre>
<p><strong>重写SseWebFluxTransportAutoConfiguration</strong></p>
<p><strong>自动装配添加请求头配置信息</strong></p>
<pre><code class="language-java">package org.springframework.ai.autoconfigure.mcp.client;

@AutoConfiguration
@ConditionalOnClass({WebFluxSseClientTransport.class})
@EnableConfigurationProperties({McpSseClientProperties.class, McpClientCommonProperties.class})
@ConditionalOnProperty(
      prefix = "spring.ai.mcp.client",
      name = {"enabled"},
      havingValue = "true",
      matchIfMissing = true
)
public class SseWebFluxTransportAutoConfiguration {
    public SseWebFluxTransportAutoConfiguration() {
    }

    @Bean
    public List&lt;NamedClientMcpTransport&gt; webFluxClientTransports(McpSseClientProperties sseProperties, WebClient.Builder webClientBuilderTemplate, ObjectMapper objectMapper) {
      List&lt;NamedClientMcpTransport&gt; sseTransports = new ArrayList();
      Iterator var5 = sseProperties.getConnections().entrySet().iterator();
      Map&lt;String, String&gt; headersMap = sseProperties.getHeadersMap();
      while(var5.hasNext()) {
            Map.Entry&lt;String, McpSseClientProperties.SseParameters&gt; serverParameters = (Map.Entry)var5.next();
            WebClient.Builder webClientBuilder = webClientBuilderTemplate.clone()
                  .defaultHeaders(headers -&gt; {
                        if (headersMap != null &amp;&amp; !headersMap.isEmpty()) {
                            headersMap.forEach(headers::add);
                        }
                  })
                  .baseUrl(((McpSseClientProperties.SseParameters)serverParameters.getValue()).url());
            WebFluxSseClientTransport transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper);
            sseTransports.add(new NamedClientMcpTransport((String)serverParameters.getKey(), transport));
      }

      return sseTransports;
    }

    @Bean
    @ConditionalOnMissingBean
    public WebClient.Builder webClientBuilder() {
      return WebClient.builder();
    }

    @Bean
    @ConditionalOnMissingBean
    public ObjectMapper objectMapper() {
      return new ObjectMapper();
    }
}
</code></pre>
<p>使用:</p>
<p><img alt="" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725279-777969789.png" class="lazyload"></p>
<p><strong>设置WebClientCustomizer</strong></p>
<p>在用Spring-ai-M8版本的时候, 发现提供了WebClientCustomizer进行扩展。 可以尝试:</p>
<ol>
<li>根据用户凭证进行授权</li>
</ol>
<pre><code class="language-java">curl -XPOST http://localhost:8080/oauth2/token --data grant_type=client_credentials --user xushu:xushu666
</code></pre>
<ol>
<li>根据授权后的token进行请求:</li>
</ol>
<pre><code class="language-java">@Bean
public WebClientCustomizer webClientCustomizer() {
    // 认证 mcp server/oauth?username:password   --&gt; access_token
    return (builder) -&gt; {
      builder.defaultHeader("Authorization","Bearer eyJraWQiOiIzYmMzMDRmZC02NzcyLTRkYTItODJiMy1hNTEwNGExMDBjNTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ4dXNodSIsImF1ZCI6Inh1c2h1IiwibmJmIjoxNzQ2NzE4MjE5LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE3NDY3MTg1MTksImlhdCI6MTc0NjcxODIxOSwianRpIjoiM2VhMzIyODctNTQ5NC00NWZlLThlZDItZGY1MjViNmIwNzkxIn0.Q-zWBZxa2CeFZo2YinenyaLb8KBMMua40X8YSs4n2fez7ODihtoVuCeJQnd2Q6qV2Pa8Z3cfH4QcMUuxMJ-_sLtZaSXpbCThH5q3KoQZ8C4MLJRTpuRqv4z1n7uLNXiVG2rya5hGwjTxu5qzHuBa2ri9pamRwmsjTz4vLHBJ1ILxDJcTkZUFuV1ExQJViewGt_7KMYcFqzGyRPiS4mm4wVvJTDjqcEGwMelu51L44K1DDYgt29vVLRVQEmnUtbBzePAxRqfw_HWJdhRSeQNiqRYCYhdAlPr3QZUFJa54GpuZn3CNyaXFoL7mENSR7wCYWx6wi--_REw6oaIfeSm-Xg");
    };
}
</code></pre>
<p><font style="background-color: rgba(248, 206, 211, 0.5)">SSE是支持动态切换token的, 因为一个请求就是一个新的http请求, 不会出现多线程争抢。 </font></p>
<p><font style="background-color: rgba(248, 206, 211, 0.5)">但是需要动态请求:</font></p>
<p><font style="background-color: rgba(248, 206, 211, 0.5)">curl -XPOST </font><font style="background-color: rgba(248, 206, 211, 0.5)">http://localhost:8080/oauth2/token</font><font style="background-color: rgba(248, 206, 211, 0.5)"> --data grant_type=client_credentials --user xushu:xushu666 进行重新授权</font></p>
<h1 id="最后">最后:</h1>
<blockquote>
<p>“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”</p>
<p><img alt="在这里插入图片描述" loading="lazy" src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250930213725359-715230519.gif" class="lazyload"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/TheMagicalRainbowSea/p/19121820
頁: [1]
查看完整版本: 9. Spring AI 当中对应 MCP 的操作