SpringAi+MCP实现sse及stdio服务调用

MCP简要说明

MCP(Model Context Protocol) 是一种由 Anthropic 于 2024 年 11 月发布的开源协议,旨在标准化大型语言模型(LLM)与外部数据源和工具的交互。它像"AI的USB-C接口",通过统一接口让 AI 模型无缝连接数据库、文件、API 等外部资源。

作用

  1. 简化集成:减少为每个工具或数据源编写自定义接口的工作,降低开发复杂性。
  2. 提供上下文:让 AI 访问实时、特定领域的外部数据,提升回答的准确性和相关性。
  3. 增强功能:支持 AI 执行操作(如发送邮件、查询数据库),而不仅是生成文本。
  4. 安全与灵活性:通过标准化协议确保安全访问,并允许动态发现和切换工具。

MCP 通过客户端-服务器架构(MCP Client 和 Server)实现,广泛应用于 AI 助手、开发工具等场景,助力构建更智能、互联的 AI 应用。

开发环境

JDK17、maven 3.8.6、springBoot 3.4.4、springAi 1.0.0-M6

效果展示

调用sse配置http服务

调用stdio配置mcp服务

快速接入百度地图MCP Server:lbs.baidu.com/faq/api?tit...

mcp-server

java代码:

McpServerApplication.java

typescript 复制代码
package com.ahucoding.rocket.mcpserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class McpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }
}

McpServerConfig.kava

typescript 复制代码
package com.ahucoding.rocket.mcpserver.cfg;

import com.ahucoding.rocket.mcpserver.service.BookService;
import com.ahucoding.rocket.mcpserver.service.WeatherService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Arrays;
import java.util.Map;

@Configuration
@EnableWebMvc
public class McpServerConfig implements WebMvcConfigurer {

    // 允许服务器公开可由语言模型调用的工具
    @Bean
    public ToolCallbackProvider openLibraryToolsOne(BookService bookService) {
        return MethodToolCallbackProvider.builder().toolObjects(bookService).build();
    }

    // 允许服务器公开可由语言模型调用的工具
    @Bean
    public ToolCallbackProvider openLibraryToolsTwo(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
    }

    // 允许服务器公开可由语言模型调用的工具
    @Bean
    public ToolCallbackProvider openLibraryToolsThree() {
        return ToolCallbackProvider.from(Arrays.asList(toolCallbackOne, toolCallbackTwo));
    }

    private ToolCallback toolCallbackOne = new ToolCallback() {
        @Override
        public ToolDefinition getToolDefinition() {
            return ToolDefinition.builder()
                    .name("Weather1")
                    .description("大龙电台,根据城市名称获取天气预报,格式:西安、北京、上海等")
                    .inputSchema("""
                        {
                            "type": "object",
                            "properties": {
                                "cityName": {
                                    "type": "string",
                                    "description": "城市名称,例如:西安、北京、上海等"
                                }
                            },
                            "required": ["cityName"]
                        }
                        """)
                    .build();
        }
        public record TianQi(String cityName) {
        }

        @Override
        public String call(String toolInput) {
            ObjectMapper objectMapper = new ObjectMapper();
            TianQi tq= null;
            try {
                tq = objectMapper.readValue(toolInput,TianQi.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            Map<String, String> mockData = Map.of(
                    "西安", "晴天",
                    "北京", "小雨",
                    "上海", "大雨",
                    "河北", "阴天",
                    "邢台", "大暴雨",
                    "邯郸", "暴雪"
            );
            System.out.println("ToolCallback 大龙电台,天气服务,查询城市:"+tq.cityName);
            return mockData.getOrDefault(tq.cityName, "抱歉:未查询到对应城市!");
        }
    };

    private ToolCallback toolCallbackTwo = new ToolCallback() {
        @Override
        public ToolDefinition getToolDefinition() {
            return ToolDefinition.builder()
                    .name("Weather2")
                    .description("巨龙电台,根据城市名称获取天气预报,格式:西安、北京、上海等")
                    .inputSchema("""
                        {
                            "type": "object",
                            "properties": {
                                "cityName": {
                                    "type": "string",
                                    "description": "城市名称,例如:西安、北京、上海等"
                                }
                            },
                            "required": ["cityName"]
                        }
                        """)
                    .build();
        }
        public record TianQi(String cityName) {
        }

        @Override
        public String call(String toolInput) {
            ObjectMapper objectMapper = new ObjectMapper();
            TianQi tq= null;
            try {
                tq = objectMapper.readValue(toolInput,TianQi.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            Map<String, String> mockData = Map.of(
                    "西安", "晴天",
                    "北京", "小雨",
                    "上海", "大雨",
                    "河北", "阴天",
                    "邢台", "小雨",
                    "邯郸", "暴雪"
            );
            System.out.println("ToolCallback 巨龙电台,天气服务,查询城市:"+tq.cityName);
            return mockData.getOrDefault(tq.cityName, "抱歉:未查询到对应城市!");
        }
    };
}

BookService.java

typescript 复制代码
package com.ahucoding.rocket.mcpserver.service;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookService {


    public record Book(List<String> isbn, String title, List<String> authorName) {
    }

    @Tool(description = "通过书名获取数据列表")
    public List<Book> getBooks(String title) {
        System.out.printf("调用了 根据书名获取数的列表 > title=%s\n", title);
        // 这里模拟查询DB操作
        return List.of(new Book(List.of("ISBN-88888888888"), "SpringAI教程", List.of("红专写的书")));
    }

    @Tool(description = "通过作者名获取数据列表")
    public List<String> getBookTitlesByAuthor(String authorName) {
        System.out.printf("调用了 根据书作者获取数的列表 > title=%s\n", authorName);
        // 这里模拟查询DB操作
        return List.of(authorName+"SpringAI教程");
    }

}

WeatherService.java

typescript 复制代码
package com.ahucoding.rocket.mcpserver.service;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class WeatherService {
    /**
     * SpringAI @Tool注解,方便创建和维护MCP工具
     * name : 定义了工具的名称
     * description : 指明工具的功能
     */
    @Tool(name = "Weather0", description = "小龙电台,根据城市名称获取天气预报,格式:西安、北京、上海等")
    public String getWeatherByCity(String city) {
        System.out.println("MethodToolCallbackProvider 小龙电台,天气服务,查询城市:"+city);
        Map<String, String> mockData = Map.of(
                "西安", "晴天",
                "北京", "小雨",
                "上海", "大雨",
                "河北", "阴天",
                "邢台", "多云",
                "邯郸", "暴雪"
        );
        return mockData.getOrDefault(city, "抱歉:未查询到对应城市!");
    }
}

POM.XML

xml 复制代码
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <artifactId>mcp-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mcp-server</name>
    <description>mcp-server</description>

    <properties>
        <java.version>17</java.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.modelcontextprotocol.sdk</groupId>
                    <artifactId>mcp-spring-webmvc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--
        SSE 连接将在30秒后终止bug: https://github.com/modelcontextprotocol/java-sdk/pull/21
        -->
        <dependency>
            <groupId>io.modelcontextprotocol.sdk</groupId>
            <artifactId>mcp-spring-webmvc</artifactId>
            <version>0.8.1</version>
        </dependency>

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

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yaml

yaml 复制代码
spring:
  application:
    name: mcp-server
  ai:
    mcp:
      server:
        name: webmvc-mcp-server
        version: 1.0.0
        type: SYNC
        sse-message-endpoint: /mcp/messages

call-mcp-server

java代码

CallMcpServerApplication.java

typescript 复制代码
package com.ahucoding.rocket.callmcpserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CallMcpServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(CallMcpServerApplication.class, args);
    }

}

McpClientCfg.java

java 复制代码
package com.ahucoding.rocket.callmcpserver.cfg;

import io.modelcontextprotocol.client.McpClient;
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
public class McpClientCfg implements McpSyncClientCustomizer {

    @Override
    public void customize(String name, McpClient.SyncSpec spec) {
        // do nothing
        spec.requestTimeout(Duration.ofSeconds(30));
    }
}

ChatController.java

kotlin 复制代码
package com.ahucoding.rocket.callmcpserver.view;

import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import io.modelcontextprotocol.client.McpSyncClient;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * @author jianzhang
 * 2025/03/18/下午8:00
 */
@RestController
@RequestMapping("/dashscope/chat-client")
@CrossOrigin("*")
public class ChatController {

    private final ChatClient chatClient;

    private final ChatMemory chatMemory = new InMemoryChatMemory();


    public ChatController(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients, ToolCallbackProvider tools) {
        this.chatClient = chatClientBuilder
                // 它定义了聊天机器人在回答问题时应当遵循的风格和角色定位。
                // .defaultSystem("以专业天气预报主持人的风格回答问题。")
                // 指定聊天客户端可用的工具
                .defaultTools(tools)
                .defaultOptions(DashScopeChatOptions.builder().withTopP(0.7).build())
                .build();
    }


    @RequestMapping(value = "/generate_stream", method = RequestMethod.GET)
    public Flux<ChatResponse> generateStream(HttpServletResponse response, @RequestParam("id") String id, @RequestParam("prompt") String prompt) {
        response.setCharacterEncoding("UTF-8");
        var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, id, 10);
        return this.chatClient.prompt()
                .user(prompt)
                .advisors(messageChatMemoryAdvisor)
                .stream()
                .chatResponse()
                .onErrorResume(e -> {
                    System.out.println("Error: " + e.getMessage());
                    return Mono.empty();
                });
    }

    @GetMapping("/test")
    public String test() {
        return "hello world";
    }

}

IndexController.java

kotlin 复制代码
package com.ahucoding.rocket.callmcpserver.view;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping("/")
    public String chat(Model model) {
        return "index";
    }
}

POM.XML

xml 复制代码
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ahucoding.rocket</groupId>
    <artifactId>call-mcp-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>call-mcp-server</name>
    <description>call-mcp-server</description>

    <properties>
        <java.version>17</java.version>
        <spring.ai.alibaba>1.0.0-M6.1</spring.ai.alibaba>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
            <version>1.0.0-M6</version>
        </dependency>

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

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>${spring.ai.alibaba}</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yaml

yaml 复制代码
server:
  port: 9999

# https://docs.spring.io/spring-ai/reference/1.0/api/mcp/mcp-client-boot-starter-docs.html
spring:
  ai:
    mcp:
      client:
        enabled: true # 是否启用MCP客户端,默认:true
        request-timeout: 60s # MCP客户端请求超时时间,默认:20s
        type: SYNC # MCP客户端类型,默认:SYNC(同步)
        name: call-mcp-server # MCP客户端名称,默认:spring-ai-mcp-client
        stdio:
          servers-configuration: classpath:mcp-server.json
        # SSE配置
        sse:
          connections:
            server1:
              url: http://127.0.0.1:8080
    # 通义大模型配置
    dashscope:
      api-key: sk-XXXXXXXXXX
      chat:
        options:
          model: qwen-max
        workspace-id: "llm-XXXXXXXXXX"

mcp-server.json

json 复制代码
{
  "mcpServers": {
    "baidu-map": {
      "command": "uv",
      "args": [
        "run",
        "--with",
        "mcp[cli]",
        "mcp",
        "run",
        "D:\anzhuang\baidu_map_mcp_server\map.py"
      ],
      "env": {
        "BAIDU_MAPS_API_KEY": "RZ3K***********xA9amF"
      }
    }
  }
}

AI交互页面

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI 对话助手</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto p-4 max-w-3xl">
    <!-- 标题 -->
    <div class="text-center mb-8">
        <h1 class="text-3xl font-bold text-gray-800">AI 对话助手</h1>
        <p class="text-gray-600 mt-2">基于 Spring AI 的流式对话系统 By AhuCodingBeast</p>
    </div>

    <!-- 聊天容器 -->
    <div id="chat-container" class="bg-white rounded-xl shadow-lg p-4 mb-4 h-[500px] overflow-y-auto space-y-4">
        <!-- 初始欢迎消息 -->
        <div class="ai-message flex items-start gap-3">
            <div class="bg-green-100 p-3 rounded-lg max-w-[85%]">
                <span class="text-gray-800">您好!我是AI助手,有什么可以帮您?</span>
            </div>
        </div>
    </div>

    <!-- 输入区域 -->
    <div class="flex gap-2">
        <input type="text" id="message-input"
               class="flex-1 border border-gray-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
               placeholder="输入您的问题...">
        <button id="send-button"
                class="bg-blue-500 text-white px-6 py-3 rounded-xl hover:bg-blue-600 transition-colors flex items-center">
            <span>发送</span>
            <svg id="loading-spinner" class="hidden w-4 h-4 ml-2 animate-spin" fill="none" viewBox="0 0 24 24">
                <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
            </svg>
        </button>
    </div>
</div>

<script>
    const chatContainer = document.getElementById('chat-container');
    const messageInput = document.getElementById('message-input');
    const sendButton = document.getElementById('send-button');
    const loadingSpinner = document.getElementById('loading-spinner');

    // 发送消息处理
    function handleSend() {
        const message = messageInput.value.trim();
        if (!message) return;

        // 添加用户消息
        addMessage(message, 'user');
        messageInput.value = '';

        // 构建API URL
        const apiUrl = new URL('http://localhost:9999/dashscope/chat-client/generate_stream');
        apiUrl.searchParams.append('id', '01');
        apiUrl.searchParams.append('prompt', message);

        // 显示加载状态
        sendButton.disabled = true;
        loadingSpinner.classList.remove('hidden');

        // 创建EventSource连接
        const eventSource = new EventSource(apiUrl);
        let aiMessageElement = null;

        eventSource.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data);
                console.log(data);
                const content = data.result?.output?.text || '';
                const finishReason = data.result?.metadata?.finishReason;

                // 创建消息容器(如果不存在)
                if (!aiMessageElement) {
                    aiMessageElement = addMessage('', 'ai');
                }

                // 追加内容
                if (content) {
                    aiMessageElement.querySelector('.message-content').textContent += content;
                    autoScroll();
                }

                // 处理结束
                if (finishReason === 'STOP') {
                    eventSource.close();
                    sendButton.disabled = false;
                    loadingSpinner.classList.add('hidden');
                }
            } catch (error) {
                console.error('解析错误:', error);
            }
        };

        eventSource.onerror = (error) => {
            console.error('连接错误:', error);
            eventSource.close();
            sendButton.disabled = false;
            loadingSpinner.classList.add('hidden');
            addMessage('对话连接异常,请重试', 'ai', true);
        };
    }

    // 添加消息到容器
    function addMessage(content, type, isError = false) {
        const messageDiv = document.createElement('div');
        messageDiv.className = `${type}-message flex items-start gap-3`;

        const bubble = document.createElement('div');
        bubble.className = `p-3 rounded-lg max-w-[85%] ${
            type === 'user'
                ? 'bg-blue-500 text-white ml-auto'
                : `bg-green-100 ${isError ? 'text-red-500' : 'text-gray-800'}`
        }`;

        const contentSpan = document.createElement('span');
        contentSpan.className = 'message-content';
        contentSpan.textContent = content;

        bubble.appendChild(contentSpan);
        messageDiv.appendChild(bubble);
        chatContainer.appendChild(messageDiv);

        autoScroll();
        return bubble;
    }

    // 自动滚动到底部
    function autoScroll() {
        chatContainer.scrollTop = chatContainer.scrollHeight;
    }

    // 事件监听
    sendButton.addEventListener('click', handleSend);
    messageInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            handleSend();
        }
    });
</script>
</body>
</html>

至此结束!!!

相关推荐
ZHOU_WUYI13 分钟前
使用Docker Compose部署Dify
docker·容器·dify
长天一色6 小时前
Docker 镜像调试最佳实践
运维·docker·容器
甘北11 小时前
docker commit除了提交容器成镜像,还能搞什么之修改cmd命令
linux·运维·docker
徐子宸14 小时前
Docker面试题(1)
java·docker·容器
莱茵不哈哈17 小时前
Docker:容器化技术
运维·docker·容器
morliz子轩19 小时前
部署Gitlab-CE with Docker私有云环境
docker·容器·gitlab
缘友一世21 小时前
开篇:MCP理论理解和学习
学习·mcp·模型上下文协议
demon755200321 小时前
Maven插件之docker-maven-plugin
docker·maven
亚里随笔21 小时前
多场景游戏AI新突破!Divide-Fuse-Conquer如何激发大模型“顿悟时刻“?
人工智能·深度学习·游戏·llm·agent
巅峰程序1 天前
[docker]更新容器中镜像版本
docker