掘金 人工智能 03月31日
几行代码实现MCP服务端/客户端(接入DeepSeek)
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

Model Context Protocol (MCP) 是一个开放协议,旨在促进大型语言模型(LLM)应用与外部数据源和工具之间的无缝连接。文章介绍了如何利用Spring Boot搭建MCP服务端,并结合DeepSeek API实现天气查询功能,同时提供了Node.js实现的MCP客户端,演示了如何调用MCP服务端提供的工具。通过MCP,开发者可以构建更智能、更灵活的AI应用,例如AI驱动的IDE、改进的聊天交互或定制的AI工作流。

💡MCP 协议的核心作用在于,它定义了一种标准化的方式,使得LLM能够与外部数据源和工具进行交互,从而扩展LLM的功能。

🛠️文章详细介绍了使用Spring Boot 3构建MCP服务端的过程,包括所需的依赖、maven库配置以及天气查询服务的具体实现。WeatherService类中,通过@Tool注解定义了可供LLM调用的工具,例如获取天气预报和警报。

🌐为了演示MCP协议的实际应用,文章提供了使用Node.js搭建的MCP客户端示例,该客户端能够连接到MCP服务端,并通过OpenAI API调用天气查询工具。示例展示了如何通过MCP协议,从LLM应用中调用外部工具,实现更复杂的功能。

⚙️服务端代码中,定义了查询天气的方法,包括了对美国城市天气信息的查询,以及针对日本、中国和江苏等地的测试方法,通过模拟返回固定值,方便进行测试和验证。

MCP介绍

Model Context Protocol (MCP) 是一个开放协议,它使 LLM 应用与外部数据源和工具之间的无缝集成成为可能。无论你是构建 AI 驱动的 IDE、改善 chat 交互,还是构建自定义的 AI 工作流,MCP 提供了一种标准化的方式,将 LLM 与它们所需的上下文连接起来。

参考 modelcontextprotocol.io/introductio…

实现一个查询天气的简单功能

注册DeepSeek,获取apikey(需要充值)

springboot 3 MCP服务端

springboot需要3.4以上

核心依赖

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

以上依赖需要增加如下maven库到项目中

  <repository>            <name>Central Portal Snapshots</name>            <id>central-portal-snapshots</id>            <url>https://central.sonatype.com/repository/maven-snapshots/</url>            <releases>                <enabled>false</enabled>            </releases>            <snapshots>                <enabled>true</enabled>            </snapshots>        </repository>        <repository>            <id>spring-milestones</id>            <name>Spring Milestones</name>            <url>https://repo.spring.io/milestone</url>            <snapshots>                <enabled>false</enabled>            </snapshots>        </repository>        <repository>            <id>spring-snapshots</id>            <name>Spring Snapshots</name>            <url>https://repo.spring.io/snapshot</url>            <releases>                <enabled>false</enabled>            </releases>        </repository>

服务端代码

实现查询天气,api只支持美国城市,额外定义了3个返回固定值的 日本,中国,江苏城市天气用于测试。

package com.hally.ai.mcp.mcpserverspringboot.services;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;import com.fasterxml.jackson.annotation.JsonProperty;import lombok.extern.slf4j.Slf4j;import org.springframework.ai.tool.annotation.Tool;import org.springframework.stereotype.Service;import org.springframework.web.client.RestClient;import java.util.List;import java.util.Map;import java.util.stream.Collectors;@Slf4j@Servicepublic class WeatherService {    private static final String BASE_URL = "https://api.weather.gov";    private final RestClient restClient;    public WeatherService() {        this.restClient = RestClient.builder()                .baseUrl(BASE_URL)                .defaultHeader("Accept", "application/geo+json")                .defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")                .build();    }    @JsonIgnoreProperties(ignoreUnknown = true)    public record Points(@JsonProperty("properties") Props properties) {        @JsonIgnoreProperties(ignoreUnknown = true)        public record Props(@JsonProperty("forecast") String forecast) {        }    }    @JsonIgnoreProperties(ignoreUnknown = true)    public record Forecast(@JsonProperty("properties") Props properties) {        @JsonIgnoreProperties(ignoreUnknown = true)        public record Props(@JsonProperty("periods") List<Period> periods) {        }        @JsonIgnoreProperties(ignoreUnknown = true)        public record Period(@JsonProperty("number") Integer number, @JsonProperty("name") String name,                             @JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime,                             @JsonProperty("isDaytime") Boolean isDayTime,                             @JsonProperty("temperature") Integer temperature,                             @JsonProperty("temperatureUnit") String temperatureUnit,                             @JsonProperty("temperatureTrend") String temperatureTrend,                             @JsonProperty("probabilityOfPrecipitation") Map probabilityOfPrecipitation,                             @JsonProperty("windSpeed") String windSpeed,                             @JsonProperty("windDirection") String windDirection,                             @JsonProperty("icon") String icon, @JsonProperty("shortForecast") String shortForecast,                             @JsonProperty("detailedForecast") String detailedForecast) {        }    }    @JsonIgnoreProperties(ignoreUnknown = true)    public record Alert(@JsonProperty("features") List<Feature> features) {        @JsonIgnoreProperties(ignoreUnknown = true)        public record Feature(@JsonProperty("properties") Properties properties) {        }        @JsonIgnoreProperties(ignoreUnknown = true)        public record Properties(@JsonProperty("event") String event, @JsonProperty("areaDesc") String areaDesc,                                 @JsonProperty("severity") String severity,                                 @JsonProperty("description") String description,                                 @JsonProperty("instruction") String instruction) {        }    }        @Tool(description = "Get weather forecast for a specific latitude/longitude")    public String getWeatherForecastByLocation(double latitude, double longitude) {        var points = restClient.get()                .uri("/points/{latitude},{longitude}", latitude, longitude)                .retrieve()                .body(Points.class);        var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class);        String forecastText = forecast.properties().periods().stream().map(p -> {            return String.format("""                            %s:                            Temperature: %s %s                            Wind: %s %s                            Forecast: %s                            """, p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(),                    p.detailedForecast());        }).collect(Collectors.joining());        return forecastText;    }    @Tool(description = "日本城市的天气")    public String getJapanWeatherForecastByLocation(double latitude, double longitude) {        System.out.println("日本城市的天气");        return "天气一塌糊涂";    }    @Tool(description = "中国城市的天气")    public String getChinaWeatherForecastByLocation(double latitude, double longitude) {        System.out.println("中国城市的天气:"+longitude+","+latitude);        return "天气非常好";    }    @Tool(description = "江苏城市的天气")    public String getChinajsWeatherForecastByLocation(double latitude, double longitude) {        System.out.println("江苏城市的天气:"+longitude+","+latitude);        return "天气一般好";    }        @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)")    public String getAlerts(String state) {        Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class);        return alert.features()                .stream()                .map(f -> String.format("""                                Event: %s                                Area: %s                                Severity: %s                                Description: %s                                Instructions: %s                                """, f.properties().event(), f.properties.areaDesc(), f.properties.severity(),                        f.properties.description(), f.properties.instruction()))                .collect(Collectors.joining("\n"));    }    public static void main(String[] args) {        WeatherService client = new WeatherService();        System.out.println(client.getWeatherForecastByLocation(47.6062, -122.3321));        System.out.println(client.getAlerts("NY"));    }}

application.yml

spring:  application:    name: mcp-server-springboot  ai:    mcp:      server:        name: weather-mcp-server        version: 1.0.0  main:    banner-mode: off

启动服务端后,默认sse服务地址:http://localhost:8080/sse

nodejs实现MCP客户端

搭建typescpit环境

node版本建议 v22.14.0

npm install -g tsxnpm install -g typescript

mcp client代码

client.ts

import {Client} from "@modelcontextprotocol/sdk/client/index.js";import {StdioClientTransport, StdioServerParameters} from "@modelcontextprotocol/sdk/client/stdio.js";import {SSEClientTransport} from "@modelcontextprotocol/sdk/client/sse.js";import OpenAI from "openai";import {Tool} from "@modelcontextprotocol/sdk/types.js";import {ChatCompletionMessageParam} from "openai/resources/chat/completions.js";import {createInterface} from "readline";import {homedir} from 'os';import config from "./config/mcp-server-config.js";const DEEPSEEK_API_KEY = "sk-xxx";const model = "deepseek-chat";if (!DEEPSEEK_API_KEY) {    throw new Error("DEEPSEEK_API_KEY   is required");}interface MCPToolResult {    content: string;}interface ServerConfig {    name: string;    type: 'command' | 'sse';    command?: string;    url?: string;    isOpen?: boolean;}class MCPClient {    static getOpenServers(): string[] {        return config.filter((cfg: { isOpen: any; }) => cfg.isOpen).map((cfg: { name: any; }) => cfg.name);    }    private sessions: Map<string, Client> = new Map();    private transports: Map<string, StdioClientTransport | SSEClientTransport> = new Map();    private openai: OpenAI;    constructor() {        this.openai = new OpenAI({            apiKey: DEEPSEEK_API_KEY,            baseURL: 'https://api.deepseek.com'        });    }    async connectToServer(serverName: string): Promise<void> {        const serverConfig = config.find((cfg: { name: string; }) => cfg.name === serverName) as ServerConfig;        if (!serverConfig) {            throw new Error(`Server configuration not found for: ${serverName}`);        }        let transport: StdioClientTransport | SSEClientTransport;        if (serverConfig.type === 'command' && serverConfig.command) {            transport = await this.createCommandTransport(serverConfig.command);        } else if (serverConfig.type === 'sse' && serverConfig.url) {            transport = await this.createSSETransport(serverConfig.url);        } else {            throw new Error(`Invalid server configuration for: ${serverName}`);        }        const client = new Client(            {                name: "mcp-client",                version: "1.0.0"            },            {                capabilities: {                    prompts: {},                    resources: {},                    tools: {}                }            }        );        await client.connect(transport);        this.sessions.set(serverName, client);        this.transports.set(serverName, transport);                const response = await client.listTools();        console.log(`\nConnected to server '${serverName}' with tools:`, response.tools.map((tool: Tool) => tool.name));    }    private async createCommandTransport(shell: string): Promise<StdioClientTransport> {        const [command, ...shellArgs] = shell.split(' ');        if (!command) {            throw new Error("Invalid shell command");        }                const args = shellArgs.map(arg => {            if (arg.startsWith('~/')) {                return arg.replace('~', homedir());            }            return arg;        });        const serverParams: StdioServerParameters = {            command,            args,            env: Object.fromEntries(                Object.entries(process.env).filter(([_, v]) => v !== undefined)            ) as Record<string, string>        };        return new StdioClientTransport(serverParams);    }    private async createSSETransport(url: string): Promise<SSEClientTransport> {        return new SSEClientTransport(new URL(url));    }    async processQuery(query: string): Promise<string> {        if (this.sessions.size === 0) {            throw new Error("Not connected to any server");        }        const messages: ChatCompletionMessageParam[] = [            {                role: "user",                content: query            }        ];                const availableTools: any[] = [];        for (const [serverName, session] of this.sessions) {            const response = await session.listTools();            const tools = response.tools.map((tool: Tool) => ({                type: "function" as const,                function: {                    name: `${serverName}__${tool.name}`,                    description: `[${serverName}] ${tool.description}`,                    parameters: tool.inputSchema                }            }));            availableTools.push(...tools);        }                const completion = await this.openai.chat.completions.create({            model: model,            messages,            tools: availableTools,            tool_choice: "auto"        });        const finalText: string[] = [];                for (const choice of completion.choices) {            const message = choice.message;            if (message.content) {                finalText.push(message.content);            }            console.log("message====" + JSON.stringify(message));            if (message.tool_calls) {                for (const toolCall of message.tool_calls) {                    const [serverName, toolName] = toolCall.function.name.split('__');                    const session = this.sessions.get(serverName);                    if (!session) {                        finalText.push(`[Error: Server ${serverName} not found]`);                        continue;                    }                    const toolArgs = JSON.parse(toolCall.function.arguments);                                        const result = await session.callTool({                        name: toolName,                        arguments: toolArgs                    });                    const toolResult = result as unknown as MCPToolResult;                    finalText.push(`[Calling tool ${toolName} on server ${serverName} with args ${JSON.stringify(toolArgs)}]`);                    console.log(`[Calling tool ${toolName} on server ${serverName} with args ${JSON.stringify(toolArgs)}]`)                    console.log("toolResult.content====" + JSON.stringify(toolResult.content));                    finalText.push(toolResult.content);                                        messages.push({                        role: "assistant",                        content: "",                        tool_calls: [toolCall]                    });                    messages.push({                        role: "tool",                        tool_call_id: toolCall.id,                        content: JSON.stringify(toolResult.content)                    });                    console.log(`messages: ${JSON.stringify(messages)}`)                                        const nextCompletion = await this.openai.chat.completions.create({                        model: model,                        messages,                        tools: availableTools,                        tool_choice: "auto"                    });                    if (nextCompletion.choices[0].message.content) {                        finalText.push(nextCompletion.choices[0].message.content);                    }                }            }        }        return finalText.join("\n");    }    async chatLoop(): Promise<void> {        console.log("\nMCP Client Started!");        console.log("Type your queries or 'quit' to exit.");        const readline = createInterface({            input: process.stdin,            output: process.stdout        });        const askQuestion = () => {            return new Promise<string>((resolve) => {                readline.question("\nQuery: ", resolve);            });        };        try {            while (true) {                const query = (await askQuestion()).trim();                if (query.toLowerCase() === 'quit') {                    break;                }                try {                    const response = await this.processQuery(query);                    console.log("\n======" + response);                } catch (error) {                    console.error("\nError:", error);                }            }        } finally {            readline.close();        }    }    async cleanup(): Promise<void> {        for (const transport of this.transports.values()) {            await transport.close();        }        this.transports.clear();        this.sessions.clear();    }    hasActiveSessions(): boolean {        return this.sessions.size > 0;    }}async function main() {    const openServers = MCPClient.getOpenServers();    console.log("Connecting to servers:", openServers.join(", "));    const client = new MCPClient();    try {                for (const serverName of openServers) {            try {                await client.connectToServer(serverName);            } catch (error) {                console.error(`Failed to connect to server '${serverName}':`, error);            }        }        if (!client.hasActiveSessions()) {            throw new Error("Failed to connect to any server");        }        await client.chatLoop();    } finally {        await client.cleanup();    }}main().catch(console.error);

MCP服务端的配置放在统一的 config.js

const config = [    {        name: 'spingboot-sse',        type: 'sse',        url: 'http://localhost:8080/sse',        isOpen: true    }];export default config;

package.json

  {  "name": "mcp-test",  "version": "1.0.0",  "main": "index.js",  "type": "module",  "scripts": {    "start": "npx tsx --env-file=.env --watch  src/index.ts",    "client": "npx tsx --env-file=.env --watch  src/client.ts",    "build": "tsc"  },  "author": "",  "license": "ISC",  "description": "",  "dependencies": {    "@modelcontextprotocol/sdk": "^1.8.0",    "express": "^5.0.1",    "openai": "^4.90.0"  },  "devDependencies": {    "@types/express": "^5.0.1",    "@types/node": "^22.13.14"  }}

tsconfig.json

  {    "compilerOptions": {      "target": "ES2022",      "module": "Node16",      "moduleResolution": "Node16",      "outDir": "./build",      "rootDir": "./src",      "strict": true,      "esModuleInterop": true,      "skipLibCheck": true,      "forceConsistentCasingInFileNames": true    },    "include": ["src/**/*"],    "exclude": ["node_modules"]  }  

运行效果

启动springboot的服务端后

在客户端工程目录运行
npm run client

打印如下信息,说明已经连接上MCP服务

Query 输入 纽约天气、南京天气、北京天气、东京天气,查看服务端日志,发现调用了不同的方法,输出了不同的结果

自动匹配后端方法:中国城市的天气

输入南京天气

自动匹配调用后端江苏城市天气方法

完整代码

springboot服务端 github.com/hallywang/m…

ts客户端 github.com/hallywang/m…

参考

springai github.com/spring-proj…mcp tssdk: github.com/modelcontex…

以及网上其他代码片段

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

MCP LLM Spring Boot DeepSeek AI工具
相关文章