> 技术文档 > MCP快速入门(for Java)_java mcp

MCP快速入门(for Java)_java mcp


什么是MCP

Model Control Protocol(MCP)是由AI研究机构Anthropic在2024年11月首次提出的新型协议规范,旨在解决大语言模型LLM应用中的上下文管理难题,MCP 标准化了如何将上下文和工具集成到 AI 应用程序的生态系统中。作为LLM交互领域的创新标准,MCP协议在发布后短短一年内已进行了多次更新,最近一次更新是在2025-03-26(Key Changes - Model Context Protocol),包含 添加了一个基于 OAuth 2.1 的全面的授权框架 等内容变更:

协议定义与核心价值

MCP是一套开放式的通信协议,它通过标准化:

  1. 上下文结构化表示(JSON Schema)
  2. 多轮对话状态跟踪机制
  3. 模型控制指令集

使开发者能够精准控制LLM的上下文窗口,解决传统对话系统中存在的:

  • 上下文丢失(Context Bleeding)
  • 指令冲突
  • 长文本处理低效等痛点

成熟度

目前MCP已被Claude系列模型原生支持,并在Llama 3、Mistral等开源模型中实现兼容。行业分析显示,超过43%的企业级LLM应用已开始采用MCP作为首选上下文管理方案(数据来源:2024 ML Stack调查报告)。

MCP的目标对象

刚开始接触MCP的开发者可能会进入一个误区,以为有了MCP后,LLM是不是可以直接调用MCP Server的能力(API)了,其实并不是,LLM实际上是无法感知MCP的存在的。

MCP(Model Control Protocol)的核心目标对象是LLM应用开发者,而不是LLM(大语言模型)本身。要理解这一点,我们需要明确LLM和LLM应用的区别:  

1. LLM vs. LLM应用

  • LLM(大语言模型):指底层的大模型(如GPT-4、Claude、Llama等),它们负责接收输入并生成文本输出,但本身不具备复杂的状态管理或上下文控制能力。  
  • LLM应用:指基于LLM构建的完整系统,如聊天机器人、AI助手、代码生成工具等。这些应用需要管理多轮对话、维护上下文、处理用户指令,并可能集成外部数据或API。  

2. 为什么LLM不能直接使用MCP

LLM本身是“无状态”的——它们仅对当前输入做出响应,而不会自动记住之前的交互。MCP的作用是让LLM应用能更高效地管理上下文,例如:

  • 维护对话历史(避免超出模型的上下文窗口限制)  
  • 动态调整提示词(prompt),确保LLM获得正确的背景信息  
  • 控制模型行为(如切换模式、调整生成参数)  

MCP协议定义了一套标准化的接口,让LLM应用能以结构化的方式与LLM交互,而不是让LLM自己去解析或执行这些逻辑。  

MCP作为外部协议而非模型内置机制,还有两大更关键原因:

  1. 训练成本问题:若MCP逻辑固化到LLM中,协议每次更新都需重新训练模型,带来极高的计算和迭代成本;

  2. 安全与权限挑战:若LLM直接通过MCP调用三方应用,权限管理(如数据访问、API鉴权)将难以控制,增加滥用风险。

因此,MCP设计为应用层协议,由外部系统管理上下文和资源,既保持LLM的通用性,又避免强耦合带来的运维负担。

3. MCP的目标用户

MCP主要面向:

  • LLM应用开发者:需要构建复杂对话系统或AI代理的工程师  
  • AI平台架构师:设计LLM基础设施,优化上下文管理和推理效率  
  • 企业级AI解决方案:需要稳定、可扩展的LLM交互协议  

简而言之,MCP不是让LLM自己“学会”管理上下文,而是为LLM应用提供一套标准化的控制机制,使开发者能更高效地构建可靠的AI系统。

MCP的主要角色和架构

MCP最核心的两个角色就是MCP客户端MCP服务端。参见:Model Context Protocol (MCP) :: Spring AI Reference

MCP Client

MCP 客户端是模型上下文协议 (MCP) 架构中的关键组件,负责建立和管理与 MCP 服务器的连接。对,你没听错,MCP体系中,客户端才是最复杂的,而且通常这个客户端也是由服务端应用(也就是前面说的LLM应用)来“使用”

它实现了协议的客户端功能,处理以下操作:

  • 协议版本协商以确保与服务器的兼容性
  • 能力协商以确定可用功能
  • 消息传输和 JSON-RPC 通信
  • 工具发现和执行
  • 资源访问和管理
  • 提示系统交互
  • 可选功能:
    • 根部管理
    • 采样支持
  • 同步和异步操作
  • 通信选择:
    • 基于 Stdio 的传输,用于基于进程的通信
    • 基于  SSE 客户端传输

你可以看到,通常是MCP客户端在“调用” LLM 和 MCP服务端 

MCP Server

MCP 服务器是模型上下文协议 (MCP) 架构中的基础组件,为客户端提供:

  • 资源:上下文和数据,供用户或 AI 模型使用。它们通常是 只读的,AI 模型可以读取资源数据,但不会直接修改或执行它们,所以一般是指数据库查询。
  • 提示:为用户提供模板化的消息和工作流程
  • 工具:AI模型执行的功能。它们通常是 动态的、有副作用的(如修改数据、发送消息、触发操作),所以一般是指AI调用

它实现了协议的服务器端,负责:

  • 服务器端协议操作实现

    • 工具曝光和发现

    • 基于 URI 访问的资源管理

    • 及时提供和处理模板

    • 与客户进行能力谈判

    • 结构化日志记录和通知

  • 并发客户端连接管理

  • 同步和异步 API 支持

  • 通信实施:

    • 基于 Stdio 的传输,用于基于进程的通信

    • 基于 SSE 服务器传输

通常来说MCP服务器是不直接使用LLM的(当然,如果你这个MCP服务器也是另一些MCP服务器的MCP客户端时也有可能需要直接使用LLM),它通常是我们常见的一个个业务系统。

这里需要特殊说明的是如果MCP客户端和MCP服务器是部署在一起的,那么可以使用Stdio( Standard Input/Output(标准输入/输出) 的缩写,指计算机程序与外部环境(如终端、文件或其他程序)进行数据交互的标准方式。它是操作系统提供的基础通信机制,几乎所有编程语言都支持 stdio 操作)的方式来进行通信。

一言以蔽之

通过MCP的角色、架构分析,大家应该有所感知,MCP就是通过定义一套标准协议,同时标准化通过LLM来使用应用能力来解决我们之前直接使用LLM去编排Function Call难度大、复杂度高以及效率低的问题

实战:构建MCP客户端和服务器(for Java)

说的再多都是虚的,得眼见为实,这里我们通过构建两个Spring应用来演示如何在本地来使用MCP。Spring框架早已提供对调用LLM能力的封装:Spring AI API :: Spring AI Reference,这里我们就使用Spring框架来构建MCP客户端和MCP服务器。

构建MCP服务器

MCP服务器相对比较简单,它负责把业务功能封装成一个个原子能力,供MCP客户端来使用,新建一个Maven项目mcp-server,pom.xml如下:

 4.0.0  com.manzhizhen.mcp mcp-study 0.0.1-SNAPSHOT  mcp-server 0.0.1-SNAPSHOT jar mcp-server Demo project for mcp  21 1.0.0-M7    org.springframework.ai spring-ai-starter-mcp-server-webmvc   org.projectlombok lombok true   ch.qos.logback logback-classic   org.springframework.boot spring-boot-starter-test test  

这里和常规的Spring Boot项目不同的是加了spring-ai的依赖,而且我们计划使用SSE来进行通信(Stdio方式生产不常用):

  org.springframework.ai spring-ai-starter-mcp-server-webmvc 

接着,我们Copy官方的天气预报例子,构建一个通过经纬度查询天气预报(getWeatherForecastByLocation),另一个通过美国州代码来查询天气预报的接口(getAlerts),注意,这属于上面提到的MCP服务器提供的“工具Tool”类型。

我们先通过 org.springframework.ai.tool.annotation.Tool 注解完成工具的定义(参考 https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webmvc-server/src/main/java/org/springframework/ai/mcp/sample/server):

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.ai.tool.annotation.ToolParam;import org.springframework.stereotype.Service;import org.springframework.web.client.RestClient;import org.springframework.web.client.RestClientException;import java.util.List;import java.util.Map;import java.util.stream.Collectors;@Slf4j@Servicepublic class WeatherService { private final RestClient restClient; public WeatherService() { this.restClient = RestClient.builder() .baseUrl(\"https://api.weather.gov\") .defaultHeader(\"Accept\", \"application/geo+json\") .defaultHeader(\"User-Agent\", \"WeatherApiClient/1.0 (835576511@qq.com)\") .build(); } /** * Get forecast for a specific latitude/longitude * @param latitude Latitude * @param longitude Longitude * @return The forecast for the given location * @throws RestClientException if the request fails */ @Tool(description = \"Get weather forecast for a specific latitude/longitude\") public String getWeatherForecastByLocation( double latitude, // Latitude coordinate double longitude // Longitude coordinate ) { log.info(\"Fetching forecast for coordinates: {}, {}\", latitude, 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; } /** * 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\") public String getAlerts( @ToolParam(description = \"Two-letter US state code (e.g. CA, NY)\") String state) { log.info(\"Fetching alerts for state: {}\", state); Alert alert = restClient.get().uri(\"/alerts/active/area/{state}\", state).retrieve().body(Alert.class); return alert.features() .stream() .map(f -> String.format(\"\"\"Event: %sArea: %sSeverity: %sDescription: %sInstructions: %s\"\"\", f.properties().event(), f.properties.areaDesc(), f.properties.severity(), f.properties.description(), f.properties.instruction())) .collect(Collectors.joining(\"\\n\")); } @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 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 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是 Spring AI 项目中的一个注解,用于将 Java 方法标记为可由 AI 模型(如 OpenAI、Azure OpenAI 或其他支持的 AI 服务)调用的工具(Tool)。它的主要作用是将你的方法暴露给 MCP客户端,使它够动态调用这些方法来完成特定任务。其中 @Tool 完成方法整体的功能描述,而@ToolParam 可以进一步完成参数的说明,如果你参数名足够清晰,也可以不用加@ToolParam。

同时在我们的Spring Boot启动类中增加ToolCallbackProvider,这样才能真正把Tool暴露出去给MCP Client调用:

import lombok.extern.slf4j.Slf4j;import org.springframework.ai.tool.ToolCallbackProvider;import org.springframework.ai.tool.method.MethodToolCallbackProvider;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;@Slf4j@SpringBootApplicationpublic class McpServerApplication {public static void main(String[] args) {log.info(\"McpServerApplication 启动啦\");SpringApplication.run(McpServerApplication.class, args);}/** * Tools * Allows servers to expose tools that can be invoked by language models. * The auto-configuration will automatically register the tool callbacks as MCP tools. * You can have multiple beans producing ToolCallbacks. The auto-configuration will merge them. * @param weatherService * @return */@Beanpublic ToolCallbackProvider weatherTools(WeatherService weatherService) {return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();}}

细心的朋友会发现,前面不是说有Stdio和SSE两种方式来暴露吗?目前看代码上没有体现?其实前面在pom.xml中我们依赖的spring-ai-starter-mcp-server-webmvc里面就依赖了:

  org.springframework.boot spring-boot-starter-web 

这里面就支持了SSE的实现,我们只需要在application.yml配置文件中配置一下即可:

server: port: 8090spring: application: name: mcp-server main: banner-mode: off ai: mcp: server: name: mcp-server version: 1.0.0 type: SYNC sse-message-endpoint: /mcp/messages

这里的 sse-message-endpoint 就是配置SSE的path。注意这里暴露的是8090端口。到这里,一个简单的MCP Server就构建完成了。

构建MCP客户端

前面说过通常MCP客户端是需要使用LLM的,这里我们首选DeepSeek,然后在DeepSeek开放平台官网创建Api Keys 再充10块钱,这样你就有了可用DeepSeek Api Key了:

同样,我们建一个Maven项目叫做mcp-client,pom.xml配置如下:

 4.0.0  com.manzhizhen.mcp mcp-study 0.0.1-SNAPSHOT  mcp-client 0.0.1-SNAPSHOT jar mcp-client  UTF-8 22    org.springframework.ai spring-ai-starter-mcp-client   org.springframework.boot spring-boot-starter-web   org.springframework.ai spring-ai-starter-model-openai   org.projectlombok lombok true   ch.qos.logback logback-classic   junit junit 3.8.1 test   com.google.guava guava 33.3.1-jre compile  

其中和其他项目不同的是我们依赖了:

  org.springframework.ai spring-ai-starter-mcp-client   org.springframework.ai spring-ai-starter-model-openai 

一个是为了支持mcp-client,一个是为了支持我们调用DeepSeek。由于只是做一个简单的演示,我们决定使用IDEA的控制台作为输入,来当做一个天气预报咨询平台。我们只需要修改启动类即可:

import io.modelcontextprotocol.client.McpSyncClient;import lombok.extern.slf4j.Slf4j;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.tool.ToolCallbackProvider;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.context.annotation.Bean;import java.util.List;import java.util.Scanner;@Slf4j@SpringBootApplicationpublic class McpClientSseApplication {public static void main(String[] args) {SpringApplication.run(McpClientSseApplication.class, args);}@Beanpublic CommandLineRunner interactiveChatRunner(ChatClient.Builder chatClientBuilder,ToolCallbackProvider tools,ConfigurableApplicationContext context) {return args -> {var chatClient = chatClientBuilder.defaultTools(tools).build();Scanner scanner = new Scanner(System.in); // 用于读取控制台输入System.out.println(\"=== 聊天模式已启动(输入 \'exit\' 退出) ===\");while (true) {System.out.print(\"\\n>>> 你的问题: \");String userInput = scanner.nextLine().trim();// 输入 \"exit\" 退出聊天if (\"exit\".equalsIgnoreCase(userInput)) {System.out.println(\">>> 聊天结束,程序退出。\");break;}// 空输入则跳过if (userInput.isEmpty()) {continue;}// 调用 AI 并打印回复System.out.println(\"\\n>>> AI 回复: \" + chatClient.prompt(userInput).call().content());}context.close(); // 关闭 Spring 上下文};}}

可以看到,为了能不断的在控制台提问,我们写了一个while循环,当你输入exit才退出。

这里有一个关键,就是不管我们mcp-client连接了多少个mcp-server以及使用了多少个mcp-server的能力(例如Tool),我们都不需要去太关心细节,一行代码“chatClient.prompt(userInput).call().content()”就搞定,mcp内置的库类会去调用AI来根据你的问题“合理”的使用mcp-server提供的一个或者多个能力来完成任务,之前用Function Call可没有这么丝滑

对,mcp-client修改这一个类即可,接下来我们配置下application.yml:

spring: application: name: mcp-client ai: mcp: client: toolcallback: enabled: true enabled: true name: my-mcp-client version: 1.0.0 request-timeout: 30s type: SYNC # or ASYNC for reactive applications sse: connections: server1:  url: http://localhost:8090 openai: api-key: 这里填写你的DeepSeek api key base-url: https://api.deepseek.com chat: options: model: deepseek-chat temperature: 0.7 embedding: enabled: false

可以看到,我们最重要的是要配置mcp-server的地址(这里由于mcp-server和mcp-client部署在一起,所以用的是localhost)以及我们的openai的类型和对于的api key。到这里,整个mcp-client示例我们也写完了。

看看效果

先运行mcp-server,再运行mcp-client,如果启动都没问题,那么我们在mcp-client的控制台可以进行天气预报咨询了:

>>> 你的问题: 帮我查下 纬度 (lat): 40.7128,经度 (lon): -74.0060 的天气>>> AI 回复: 以下是纬度 40.7128,经度 -74.0060 的天气预报:### 今天- **温度**: 80°F- **风速**: 10 到 23 mph,西南风- **预报**: 上午8点前可能有零星小雨,随后可能有零星阵雨和雷暴。部分晴天,最高气温接近80°F。西南风10至23 mph,阵风高达39 mph。降水概率20%。### 今晚- **温度**: 57°F- **风速**: 15 到 22 mph,西风- **预报**: 凌晨2点前可能有零星阵雨和雷暴。大部分多云,最低气温约57°F,夜间气温升至62°F左右。西风15至22 mph,阵风高达38 mph。降水概率20%。### 周日- **温度**: 63°F- **风速**: 15 到 20 mph,西北风- **预报**: 大部分晴天,最高气温接近63°F。西北风15至20 mph。...

可以看到我的问题携带了经纬度,所以最终是mcp-server的WeatherService#getWeatherForecastByLocation 被调用了。

>>> 你的问题: 美国 WA 州 天气预报>>> AI 回复: 以下是美国华盛顿州(WA)的天气预报:### 今晚- **温度**: 44°F - **风速**: 9 mph 西北风 - **预报**: 多云,最低气温约44°F。西北风约9 mph,阵风高达22 mph。### 周六- **温度**: 54°F - **风速**: 9至13 mph 西北风 - **预报**: 大部分晴天,最高气温约54°F,下午气温降至约52°F。西北风9至13 mph,阵风高达29 mph。### 周六晚- **温度**: 38°F - **风速**: 10 mph 西北风 - **预报**: 部分多云,最低气温约38°F。西北风约10 mph,阵风高达28 mph。...

这个问题很明显想引导mcp-client去使用mcp-server的WeatherService#getAlerts,但由于getAlerts没有查到华盛顿州的数据,所以mcp-client最终通过DeepSeek拿到华盛顿州的经纬度47.7511, -120.7401 去调用了getWeatherForecastByLocation方法,真聪明!!!

总结

欢迎加作者WX sugarmq 一起来探讨技术话题。