MCP(Model Context Protocol)好比大模型外挂!

1、导读

最近Anthropic主导发布了MCP(Model Context Protocol,模型上下文协议)后,着实真真火了一把。熟悉AI大模型的人对Anthropic应该不会陌生,Claude 3.5 Sonnet模型就是他们发布的,包括现在的最强编程AI模型 3.7 Sonnet。今天我们来刨析下什么是MCP,AI大模型下,需要MCP吗?

2、什么是MCP?

MCP(Model Context Protocol)模型上下文协议,是一种适用于AI大模型与数据源交互的标准协议。旨在实现跨模型、跨会话的上下文信息持久化与动态共享。它通过标准化接口实现模型间的上下文传递、版本控制和协同推理,解决复杂AI任务中的上下文碎片化问题。

那为什么需要重新定义AI的上下文管理? 在我们最开始使用AI大模型的时候,经常会遇到以下一些问题,如:上下文可能莫名的丢失,导致会话的语义出现断层。再如:不同模型之间,比如ChatGPT和DeepSeek之间记忆不互通,或者不同版本之间无法追踪上下文的变更历史等等,这些其实都是信息孤岛。

因此,我们需要有一个专门的协议或者说技术能跨模型、跨版本串联我们的上下文,解决记忆互通的问题。

如果我们观察整个AI自动化的发展过程,我们会发现AI自动化分三个阶段:AI Chat、AI Composer、AI Agent。

  • AI Chat 通常只是提供建议和代码片段,人机协作的模式通常需要用户手动进行复制,调试,集成。
  • AI Composer 相比AI Chat 支持了局部的代码自动修改,我们只需要确认接受或拒绝接受即可。但是也局限于代码的上下文操作。
  • AI Agent 可以完成端到端的任务闭环。他是一个完全的智能体,自主决策,修改,调整我们所需的代码块,或者自主提取需要的信息。我们只需要负责监控动作就行。但是跨系统的协作能力有所欠缺。

因此,进一步的演化路线自然是为了完善AI Agent的跨模型或跨系统而实现。他是一个中间层,可以作为AI Agent的智能路由中枢。

熟悉Java分布式的朋友应该会发现,这个其实很想分布式的发展进程。而MCP更多像是一个RPC的标准协议一样。不确定我这么比喻是否恰当。

3、MCP架构

模型上下文协议 (MCP) 遵循客户端-主机-服务器架构,其中每个主机可以运行多个客户端实例。这种架构使用户能够跨应用程序集成 AI 功能,同时保持明确的安全边界并隔离问题。MCP 基于 JSON-RPC 构建,提供有状态会话协议,专注于客户端和服务器之间的上下文交换和采样协调。

官方的MCP系统架构图:

可以发现 MCP 基于三层分层模式:

  • MCP Host(主机应用): 运行AI模型或代理的宿主程序,如Claude桌面版、某IDE中的AI助手等。主机应用通过内置的MCP客户端与外部建立连接,是连接的发起方。
  • MCP Client(客户端): 嵌入在主机应用中的协议客户端组件。每个MCP客户端与一个特定的MCP服务器保持一对一的连接,用于向服务器发送请求或接收响应。一个主机应用中可以运行多个MCP客户端,从而同时连接多个不同的服务器。
  • MCP Server(服务器): 独立运行的轻量程序,封装了某一数据源或服务的具体能力,通过标准化的MCP接口对外提供。每个MCP服务器相当于一个"适配器",将底层的数据源/工具(本地文件、数据库、第三方API等)的功能以统一格式暴露出来,供客户端调用。
  • 本地数据源: 部署在用户本地环境中的数据资源,例如本机文件系统、数据库、应用服务等。MCP服务器可以受控地访问这些资源,并将内容提供给AI模型使用。例如,一个本地MCP服务器可以读取电脑文件或查询本地SQLite数据库,然后将结果发给模型作为参考。
  • 远程服务: 通过网络提供的外部系统或在线服务(通常通过HTTP API访问)。MCP服务器也可以连接到这些远程服务获取数据。比如,可以有一个MCP服务器连接Slack的Web API,代表AI助手执行发送消息、读取频道记录等操作。

4、MCP & JAVA支持

MCP提供了多种编程语言的支持,如Python,Java,Kotlin,TypeScript等。这里以Java SDK为例,官方也提供了相关文档:modelcontextprotocol.io/sdk/java/mc...

Spring AI也已经支持了MCP了。

4.1、多种传输方式

MCP提供了两种不同的传输实现。

  • 默认传输方式:基于Stdio,HTTP SSE
  • 基于Spring的传输:WebFlux SSE,WebMVC SSE

4.2、JAVA应用的基础架构

JAVA SDK也是遵循分层结构:

  • 客户端/服务器层 (McpClient/McpServer): 两者都使用 McpSession 进行同步/异步作,其中 McpClient 处理客户端协议作,McpServer 管理服务器端协议作。
  • 会话层 (McpSession): 使用 DefaultMcpSession 实现管理通信模式和状态。
  • 传输层 (McpTransport): 通过以下方式处理 JSON-RPC 消息序列化/反序列化:
  • 核心模块中的 StdioTransport (stdin/stdout)
  • 专用传输模块(Java HttpClient、Spring WebFlux、Spring WebMVC)中的 HTTP SSE 传输

MCP 客户端是模型上下文协议 (MCP) 架构中的关键组件,负责建立和管理与 MCP 服务器的连接。它实现协议的客户端。

MCP 服务器是模型上下文协议 (MCP) 架构中的基础组件,用于为客户提供工具、资源和功能。它实现协议的服务器端。

4.3、简单构建自己的MCP Server

MCP 服务器可以提供三种主要类型的功能:

  1. **Resources: 资源。**客户端可以读取的类似文件的数据(如 API 响应或文件内容)。
  2. **Tools: 工具。**可由 LLM(经用户批准)调用的函数。
  3. **Prompts: 提示。**帮助用户完成特定任务的预先编写的模板。

由于Spring AI已经同步支持了MCP,因此我们这里使用Spring AI MCP自动换配来快速入门。环境要求:Java 17+,Spring Boot 3.3.x+。

4.3.1、构建Spring Boot初始化工程

和常规的构建Spring Boot工程一样,只是这里选择MCP Server依赖。如果没有勾选,后续自己添加相应依赖即可。

MCP的maven依赖:

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

4.3.2、实现MCP Server

让我们实现一个使用 REST 客户端查询 National Weather Service API 数据的气象服务。创建WeatherService.java:

java 复制代码
package org.example.mcpserverspringai;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientResponseException;

@Service
public class WeatherService {

    private final RestClient restClient;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public WeatherService() {
        this.restClient = RestClient.builder()
                .baseUrl("https://api.weather.gov")
                .defaultHeader("Accept", "application/geo+json")
                .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])")
                .build();
    }

    @Tool(name="getWeatherForecastByLocation", description = "Get weather forecast for a specific latitude/longitude")
    public String getWeatherForecastByLocation(
            double latitude,
            double longitude
    ) {
        try {
            // Step 1: 获取点位信息
            String pointsResponse = restClient.get()
                    .uri("/points/{lat},{lon}", latitude, longitude)
                    .retrieve()
                    .onStatus(s -> s.is4xxClientError() || s.is5xxServerError(), (req, res) -> {
                        throw new WeatherApiException("Failed to get location data: " + res.getStatusText());
                    })
                    .body(String.class);

            JsonNode pointsRoot = objectMapper.readTree(pointsResponse);
            String forecastUrl = pointsRoot.path("properties")
                    .path("forecast")
                    .asText();

            // Step 2: 获取预报数据
            String forecastResponse = restClient.get()
                    .uri(forecastUrl)
                    .retrieve()
                    .body(String.class);

            return parseForecastData(forecastResponse);

        } catch (RestClientResponseException e) {
            throw new WeatherApiException("Weather API error: " + e.getResponseBodyAsString());
        } catch (Exception e) {
            throw new WeatherApiException("Failed to retrieve forecast", e);
        }
    }

    private String parseForecastData(String json) throws Exception {
        StringBuilder result = new StringBuilder();
        JsonNode root = objectMapper.readTree(json);
        JsonNode periods = root.path("properties").path("periods");

        result.append("## Weather Forecast\n");
        for (JsonNode period : periods) {
            result.append(String.format(
                    "### %s\n- Temperature: %.1f°F / %.1f°C\n- Wind: %s %s\n- Details: %s\n\n",
                    period.path("name").asText(),
                    period.path("temperature").asDouble(),
                    fahrenheitToCelsius(period.path("temperature").asDouble()),
                    period.path("windDirection").asText(),
                    period.path("windSpeed").asText(),
                    period.path("detailedForecast").asText()
            ));
        }
        return result.toString();
    }

    @Tool(name="getAlerts", description = "Get weather alerts for a US state")
    public String getAlerts(@ToolParam(description = "Two-letter US state code (e.g. CA, NY)") String state) {
        if (state == null || state.length() != 2) {
            throw new IllegalArgumentException("Invalid state code format");
        }

        try {
            String alertResponse = restClient.get()
                    .uri("/alerts/active?area={state}", state.toUpperCase())
                    .retrieve()
                    .body(String.class);

            return parseAlertData(alertResponse);

        } catch (RestClientResponseException e) {
            throw new WeatherApiException("Weather API error: " + e.getResponseBodyAsString());
        } catch (Exception e) {
            throw new WeatherApiException("Failed to retrieve alerts", e);
        }
    }

    private String parseAlertData(String json) throws Exception {
        StringBuilder result = new StringBuilder();
        JsonNode root = objectMapper.readTree(json);
        JsonNode features = root.path("features");

        result.append("## Weather Alerts\n");
        for (JsonNode feature : features) {
            JsonNode properties = feature.path("properties");
            result.append(String.format(
                    "### %s\n- Severity: %s\n- Areas: %s\n- Effective: %s\n- Instructions: %s\n\n",
                    properties.path("event").asText(),
                    properties.path("severity").asText(),
                    properties.path("areaDesc").asText(),
                    properties.path("effective").asText(),
                    properties.path("instruction").asText()
            ));
        }

        if (result.isEmpty()) {
            return "No active alerts for this area";
        }
        return result.toString();
    }

    private double fahrenheitToCelsius(double f) {
        return (f - 32) * 5 / 9;
    }

    public static class WeatherApiException extends RuntimeException {
        public WeatherApiException(String message) {
            super(message);
        }

        public WeatherApiException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

@Service 注解会在您的应用程序上下文中自动注册服务。Spring AI @Tool 注释,使创建和维护 MCP 工具变得容易。使用Spring Boot自动配置将自动向 MCP 服务器注册这些工具。

4.3.3、创建Server启动器

java 复制代码
@SpringBootApplication
public class McpServerSpringAiApplication {

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

    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return  MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
    }

}

这里使用 MethodToolCallbackProvider 实用程序将 @Tools 转换为 MCP 服务器使用的可作回调。

4.3.4、打包MCP Server

接着我们运行mvn clean package,将上面的服务打成jar包。

4.3.5、MCP Client调用

我们可以使用一些桌面工具如Claude Desktop、Cursor等来引入我们的MCP Server。我自己试了下Claude,但是Claude需要国外的手机号进行注册才行,咋没这个条件啊。又试了下Cursor,发现这货只支持Http SSE方式。我这里只是为了做演示,所以就算了,不想封装了。 因此这里我直接编写一个MCP Client来做测试。

我们创建一个MCP Client工程:mcp-client-spring-ai,引入maven依赖:

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

创建MCP Client测试类:

java 复制代码
package org.example.mcpclientspringai;

import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Map;

@SpringBootTest
class McpClientSpringAiApplicationTests {

    @Test
    void contextLoads() {
        // 这里直接指定我们刚mcp server打包的jar的位置
        var stdioParams = ServerParameters.builder("java")
                .args("-jar", "E:\\idea_projects\\mcp-server-spring-ai\\target\\mcp-server-spring-ai-0.0.1-SNAPSHOT.jar")
                .build();
        var stdioTransport = new StdioClientTransport(stdioParams);
        var mcpClient = McpClient.sync(stdioTransport).build();
        mcpClient.initialize();
        
        McpSchema.ListToolsResult toolsList = mcpClient.listTools();
        System.out.println("MCP tools集合:" + toolsList.tools());
        
        // 这里随机给一些经纬度参数
        McpSchema.CallToolResult weather = mcpClient.callTool(
                new McpSchema.CallToolRequest("getWeatherForecastByLocation",
                        Map.of("latitude", "47.6062", "longitude", "-122.3321")));
        System.out.println("根据经纬度查询天气信息:" + weather.content());
        
        McpSchema.CallToolResult alert = mcpClient.callTool(
                new McpSchema.CallToolRequest("getAlerts", Map.of("state", "NY")));
        System.out.println("获取天气状态信息:" + alert.content());
        mcpClient.closeGracefully();
    }

}

4.3.6、运行结果

从打印的信息可以看出,成功获取到了我们通过@Tool注解的两个MCP Server的工具,同时也成功查询到了当地的气象信息。就是这么简单。

4.4、既然如此?

可见Java通过Spring AI进行MCP的集成,也是相当简单。想象一下,如果我们写好了能够满足需求的MCP Server,然后通过Client自己完成监管以及代码check,甚至可以集成视觉模型进行样式调整?那么岂不是一个活脱脱的智能程序员就出现了?当然可能想的比较简单。

其实由此可见,MCP也并不是什么新鲜玩意,他就只是个大家共识的标准协议而已。

5、MCP & function calling

有人肯定会说,MCP这样的方式和function calling有什么区别吗? function calling同样也具备调用外部接口的能力。为什么要需要MCP。

  • MAP:
    1. MCP是一个更底层,通用的标准协议。
    2. MCP更倾向于抽象和通用。
    3. MCP通常支持多数据源。
  • function calling:
    1. function calling是大模型专用的特性,或者说实现方式。
    2. function calling更倾向于具体的实现调用。
    3. function calling适用于特定场景,单一数据源。

6、小结

MCP正在重塑AI系统的构建范式,其核心价值体现在他的认知连续性,让AI真正具备"记忆传承"能力。以及系统协作性,构建模型间的认知协作网络。还有工程标准化,上下文管理从定制开发走向协议规范。

随着V1.2协议标准即将发布,MCP将成为智能系统的基础设施标配。我相信未来已来,唯智者先见;上下文革命,从MCP开始。

相关推荐
ice_junjun4 分钟前
OpenCV Video 模块使用指南(Python 版)
人工智能·python·opencv
景联文科技38 分钟前
景联文科技:以高质量数据标注推动人工智能领域创新与发展
人工智能·科技·数据标注
仙人掌_lz42 分钟前
RAG(Retrieval-Augmented Generation)基建之PDF解析的“魔法”与“陷阱”
人工智能·深度学习·ai·pdf·rag
赛卡1 小时前
自动驾驶背后的数学:ReLU,Sigmoid, Leaky ReLU, PReLU,Swish等激活函数解析
人工智能·pytorch·python·神经网络·机器学习·数学建模·自动驾驶
訾博ZiBo1 小时前
AI日报 - 2025年3月25日
人工智能
小白的高手之路1 小时前
Pytorch中的数据加载
开发语言·人工智能·pytorch·python·深度学习·机器学习
Fansv5871 小时前
深度学习框架PyTorch——从入门到精通(6.2)自动微分机制
人工智能·pytorch·经验分享·python·深度学习·机器学习
墨绿色的摆渡人2 小时前
用 pytorch 从零开始创建大语言模型(六):对分类进行微调
人工智能·pytorch·python·深度学习·语言模型·embedding
猎人everest2 小时前
机器学习之概率论
人工智能·机器学习·概率论
豆芽8192 小时前
二项式分布(Binomial Distribution)
人工智能·python·机器学习·numpy·概率论