几行代码实现MCP服务端/客户端(接入DeepSeek)

MCP介绍

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

参考 modelcontextprotocol.io/introductio...

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

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

springboot 3 MCP服务端

springboot需要3.4以上

核心依赖

xml 复制代码
   <dependency>
          <groupId>org.springframework.ai</groupId>
          <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
     </dependency>

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

xml 复制代码
  <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个返回固定值的 日本,中国,江苏城市天气用于测试。

java 复制代码
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;

/**
 * @author 海狸 Hally.W
 * Created on 2025/3/28 13:52
 * Updated on 2025/3/28 13:52
 */
@Slf4j
@Service
public 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 ([email protected])")
                .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) {
        }
    }

    /**
     * Get forecast for a specific latitude/longitude
     *
     * @param latitude  Latitude
     * @param longitude Longitude
     * @return The forecast for the given location
     */
    @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 "天气一般好";
    }



    /**
     * Get alerts for a specific area
     *
     * @param state Area code. Two-letter US state code (e.g. CA, NY)
     * @return Human readable alert information
     * @throws RestClientException if the request fails
     */
    @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

yaml 复制代码
# Using spring-ai-mcp-server-webmvc-spring-boot-starter
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

shell 复制代码
npm install -g tsx
npm install -g typescript

mcp client代码

client.ts

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';
// @ts-ignore
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);
        }
        // 调用OpenAI API
        const completion = await this.openai.chat.completions.create({
            model: model,
            messages,
            tools: availableTools,
            tool_choice: "auto"
        });
        const finalText: string[] = [];

        // 处理OpenAI的响应
        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

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

package.json

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

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的服务端后

在客户端工程目录运行
shell 复制代码
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...

以及网上其他代码片段

相关推荐
天下代码客18 分钟前
【八股】介绍Promise(ES6引入)
前端·ecmascript·es6
lb291731 分钟前
CSS 3D变换,transform:translateZ()
前端·css·3d
啊阿狸不会拉杆35 分钟前
第二十二章:Python-NLTK库:自然语言处理
前端·python·自然语言处理
萧寂17336 分钟前
html实现手势密码
前端·css·html
excel39 分钟前
webpack 核心编译器 八 节
前端
JoyZ42 分钟前
AI的时代学习还有意义吗?
前端
好_快43 分钟前
Lodash源码阅读-getSymbolsIn
前端·javascript·源码阅读
好_快44 分钟前
Lodash源码阅读-baseAssignValue
前端·javascript·源码阅读
好_快1 小时前
Lodash源码阅读-copyObject
前端·javascript·源码阅读