一、MCP 必知必会
什么是 MCP?
MCP(Model Context Protocol,模型上下文协议)是一种开放标准,目的是增强 AI 与外部系统的交互能力。MCP 为 AI 提供了与外部工具、资源和服务交互的标准化方式,让 AI 能够访问最新数据、执行复杂操作,并与现有系统集成。
根据 官方定义,MCP 是一种开放协议,它标准化了应用程序如何向大模型提供上下文的方式。可以将 MCP 想象成 AI 应用的 USB 接口。就像 USB 为设备连接各种外设和配件提供了标准化方式一样,MCP 为 AI 模型连接不同的数据源和工具提供了标准化的方法。
我们一定要记住 MCP 它是个协议或者标准,它本身并不提供什么服务,只是定义好了一套规范,让服务提供者和服务使用者去遵守。这样的好处显而易见,就像 HTTP 协议一样,现在前端向后端发送请求基本都是用 HTTP 协议,什么 get / post 请求类别、什么 401、404 状态码,这些标准能有效降低开发者的理解成本。
所以,MCP他并不是什么具体的技术,他只是服务方和消费者之间的一种协议,这种协议是来标准化被服务方的协议,当服务方提供的服务是通过MCP协议提供的,那么消费者就必须要遵守MCP协议
举个简单的例子:
我们的“恋爱大师Agent”现在要添加一个根据地理位置推荐周边约会地点的工具,我们需要调用“高德地图”的服务,如果高德地图服务可以直接调用,那么我们在程序中直接调用他们家的服务即可,但是如果高德地图的服务是通过“MCP”提供的,那么我们就必须要遵守MCP协议才能调用
MCP 架构
1、宏观架构
MCP 的核心是 “客户端(client) - 服务器(sever)” 架构,其中 MCP 客户端主机可以连接到多个服务器。客户端主机是指希望访问 MCP 服务的程序,比如 Claude Desktop、IDE、AI 工具或部署在服务器上的项目。
2、SDK 3 层架构
如果我们要在程序中使用 MCP 或开发 MCP 服务,可以引入 MCP 官方的 SDK,比如 Java SDK。让我们先通过 MCP 官方文档了解 MCP SDK 的架构,主要分为 3 层:
分别来看每一层的作用:
- 客户端 / 服务器层:McpClient 处理客户端操作,而 McpServer 管理服务器端协议操作。两者都使用 McpSession 进行通信管理。
- 会话层(McpSession):通过 DefaultMcpSession 实现管理通信模式和状态。
- 传输层(McpTransport):处理 JSON-RPC 消息序列化和反序列化,支持多种传输实现,比如 Stdio 标准 IO 流传输和 HTTP SSE 远程传输。
客户端和服务端需要先经过下面的流程建立连接,之后才能正常交换消息:
流程解释如下:
1. MCP Client 连接 MCP Server
- stdio:启动本地 Server 子进程
- Streamable HTTP:访问远程 Server 的 HTTP 地址2. Client 发 initialize 初始化请求
- 告诉 Server:我支持的协议版本、我的能力、我的信息3. Server 返回 initialize 响应
- 告诉 Client:最终协议版本、Server 的能力、Server 的信息4. Client 发 initialized 通知
- 表示:初始化完成,可以开始正常工作了-------------------------------------------以下为“信息交换”内容------------------------------------------------
5. Client 按需发现服务能力
- tools/list
- resources/list
- prompts/list6. Client 调用具体能力
- tools/call
- resources/read
- prompts/get7. Server 返回结果
3、MCP 客户端(MCP Client)
简单来说MCP Client就是“使用、发现、调用”服务端能力的一方
MCP Client 是 MCP 架构中的关键组件,主要负责和 MCP 服务器建立连接并进行通信。它能自动匹配服务器的协议版本、确认可用功能、负责数据传输和 JSON-RPC 交互。此外,它还能发现和使用各种工具、管理资源、和提示词系统进行交互。
连接方式
MCP 客户端还支持一些额外特性,比如根管理、采样控制,以及同步或异步操作。为了适应不同场景,它提供了多种数据传输方式,包括:
- Stdio 标准输入 / 输出:适用于本地调用
- Streamable HTTP:远程网络服务通信(适用于远程调用)
- 基于 Java HttpClient 和 WebFlux 的 SSE 传输:适用于远程调用(注意:现在已经不再使用这种方式的传输,只有一些老的客户端还在使用)
请注意,这里提及的所谓“本地”“远程”只是他们的“连接形态”,并不是说调用的“工具位置”,例如使用http远程连接时,工具可以在别人的服务器上,也可以在我们自己电脑上,也可以是局域网中。他们之间的具体区别
- stdio更像“本机拉起一个工具进程”
- HTTP更像“访问一个已经在某处运行的服务”
客户端可以通过不同传输方式调用不同的 MCP 服务,可以是本地的、也可以是远程的。如图:
因此数据传输方式的结论可以这么记:
小型、本地、个人工具:stdio 远程部署、多人共享、服务化:Streamable HTTP4、MCP 服务端
MCP Server 也是整个 MCP 架构的关键组件,主要用来为客户端提供各种工具、资源和功能支持。它负责处理客户端的请求,包括解析协议、提供工具、管理资源以及处理各种交互信息。同时,它还能记录日志、发送通知,并且支持多个客户端同时连接,保证高效的通信和协作。
和客户端一样,它也可以通过多种方式进行数据传输,比如 Stdio 标准输入 / 输出、Streamable HTTP 传输,满足不同应用场景。
这种设计使得客户端和服务端完全解耦,任何语言开发的客户端都可以调用 MCP 服务。如图:
(这张图比较老了,其中的SSE可以替换成Streamable HTTP 传输)
MCP 核心概念
很多同学以为 MCP 协议就只能提供工具给别人调用,但实际上,MCP 协议的本领可大着呢!
按照官方的说法,总共有 6 大核心概念。大家简单了解一下即可,除了 Tools 工具之外的其他概念都不是很实用,如果要进一步学习可以阅读对应的官方文档。
- Resources 资源:让服务端向客户端提供各种数据,比如文本、文件、数据库记录、API 响应等,客户端可以决定什么时候使用这些资源。使 AI 能够访问最新信息和外部知识,为模型提供更丰富的上下文。
- Prompts 提示词:服务端可以定义可复用的提示词模板和工作流,供客户端和用户直接使用。它的作用是标准化常见的 AI 交互模式,比如能作为 UI 元素(如斜杠命令、快捷操作)呈现给用户,从而简化用户与 LLM 的交互过程。
- Tools 工具:MCP 中最实用的特性,服务端可以提供给客户端可调用的函数,使 AI 模型能够执行计算、查询信息或者和外部系统交互,极大扩展了 AI 的能力范围。
- Sampling 采样:允许服务端通过客户端向大模型发送生成内容的请求(反向请求)。使 MCP 服务能够实现复杂的智能代理行为,同时保持用户对整个过程的控制和数据隐私保护。
- Roots 根目录:MCP 协议的安全机制,定义了服务器可以访问的文件系统位置,限制访问范围,为 MCP 服务提供安全边界,防止恶意文件访问。
- Transports 传输:定义客户端和服务器间的通信方式,包括 Stdio(本地进程间通信)和 SSE(网络实时通信),确保不同环境下的可靠信息交换。
如果要开发 MCP 服务,我们主要关注前 3 个概念,当然,Tools 工具是重中之重!
二、使用 MCP
云平台使用 MCP
以阿里云百炼为例,参考 官方 MCP 文档,我们可以直接使用官方预置的 MCP 服务,或者部署自己的 MCP 服务到阿里云平台上。
我们本次就拿“按照位置推荐约会地点”这个功能来举例把,MCP服务选用前文提及的高德地图
我们自己在该平台上新建一个应用,让AI帮我们编写一下系统提示词,随后找到下方的MCP服务
随后,我们可以在用户对话窗口问他相关问题,看看它的MCP服务是怎么样被调用的
可以看到,该服务的调用其实本质上就是我们之前的工具调用,框架把用户请求和工具说明一同发给AI,AI更具用户请求和工具自主调用工具,工具执行完成后把结果返回给AI,AI整理结果发送给用户
软件客户端使用 MCP
以Workbuddy工具为例,来展示在常见的Agent软件中如何配置MCP
首先我们要在MCP市场中查询相关的MCP服务,看一下MCP服务的相关协议
The MCP Server Registry, Inspector & Gateway | Glama
在搜索框中搜索gaodemap(高德地图)查看相关协议与配置用法
配置相关MCP服务需要服务商提供的key,来到高德开放平台,创建应用来获取相对应的key
我的应用 | 高德控制台
最后来到workbuddy自主配置MCP,把在MCP市场的配置+获取到的key填写好
最后来测试mcp的调用情况:
程序中使用 MCP
接下来让我们在我们的项目中启用mcp服务,启用mcp服务我们需要用到SpringAI的官方依赖,所以首先我们先引入依赖
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> <version>1.1.2</version> </dependency>接下来我们在resource目录下新建mcp-servers.json配置,定义需要用到的 MCP 服务:
图中可以看一下这份json配置的实际意义(重点)
修改 Spring 配置文件,编写 MCP 客户端配置。由于是本地运行 MCP 服务,所以使用 stdio 模式,并且要指定 MCP 服务配置文件的位置。代码如下:
(注意:层级在“ai”这个层级之下)
配置文件都编写完成,接下来我们只需要在链路中使用配置好的工具(高德地图)即可
首先引入ToolCallbackProvider的对象,用于:
- 获取 MCP Client 发现到的工具
- 把 MCP 工具包装成 Spring AI 能识别的
ToolCallback(核心) - 让
ChatClient可以把这些工具交给大模型 - 当模型发起调用时,把调用转发给对应的 MCP Server
然后我们从上面的“工具调用+与AI交互”的方法稍微改一下(其实就是改了个工具参数)就大功告成了
让我们写一些相关的测试方法来验证一下:
返回结果如下:
点击链接,发现就是高德地图所提供的图片链接,证明我们的MCP配置与调用成功了
所以纵观下来,我们只是编写了几个配置文件,引入了一个toolCallbackProvider的对象,直接在调用链里面使用即可
真正的MCP服务(高德)内容就在于我们引入的js文件中,我们只需要调用即可
三、Spring AI MCP 开发模式
Spring AI 在 MCP 官方 Java SDK 的基础上额外封装了一层,提供了和 Spring Boot 整合的 SDK,支持客户端和服务端的普通调用和响应式调用。下面分别学习如何使用 Spring AI 开发 MCP 客户端和服务端。
其实我们要干的事情很简单,本质上就是:写配置+(调用/暴露)工具
客户端和服务端区别只是配置文件意义上的不同和后续对工具的处理上不同
在此之前我们再来回顾一下MCP的客户端和服务端到底是什么,知道了实质和本质才好开发
MCP实质:
所以我们可以看到,客户端就是“服务”的调用方,服务端就是服务的“提供方”,至于“服务”的形式,他可以是很多种,我们之前在程序中使用高德的服务,它的服务就是js文件,我们后面的实战会开发服务端,所提供的服务我们就选用jar的形式
MCP 客户端开发
客户端开发主要基于 Spring AI MCP Client Boot Starter,能够自动完成客户端的初始化、管理多个客户端实例、自动清理资源等。
1、引入依赖
spring-ai-starter-mcp-client:核心启动器,提供 STDIO 和基于 HTTPstreamable的支持
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency>2、配置连接
引入依赖后,需要配置与服务器的连接
Spring AI MCP Client 的连接配置通常有两种常见方式,二选一即可,不需要把两种都同时写上。
方式一:纯 YAML 配置。适合连接数量少、希望把所有内容统一放在application.yml里的项目。
spring: ai: mcp: client: stdio: connections: amap-maps: command: D:\潘\AI\node.exe args: - D:\潘\AI\mcp-servers\amap\build\index.js env: AMAP_MAPS_API_KEY: ${AMAP_MAPS_API_KEY}方式二:YAML + JSON 配置。YAML 只负责告诉 Spring AI 去读哪个 JSON 文件, JSON 负责描述一个或多个 stdio MCP Server。这个方式适合复用 Claude Desktop 风格的mcpServers配置。
spring: ai: mcp: client: stdio: servers-configuration: classpath:mcp-servers.json对应的 JSON 例如:
{ "mcpServers": { "amap-maps": { "command": "D:\\潘\\AI\\node.exe", "args": [ "D:\\潘\\AI\\mcp-servers\\amap\\build\\index.js" ], "env": { "AMAP_MAPS_API_KEY": "${AMAP_MAPS_API_KEY}" } } } }远程Streamable HTTP通常直接在 YAML 里配置地址和认证参数,不需要这个 stdio JSON 文件。
如果更偏向 WebFlux 体系,可以看spring-ai-starter-mcp-client-webflux。
客户端通用配置
spring: ai: mcp: client: enabled: true name: spring-ai-mcp-client version: 1.0.0 initialized: true request-timeout: 20s type: SYNC toolcallback: enabled: true如果使用stdio.servers-configuration,它代表 YAML + JSON 方式;
如果使用stdio.connections,它代表纯 YAML 方式。
两者不要给同一个 Server 同时配两份。、
3、使用服务
我们在client使用mcp提供的工具之前要先注入ToolCallbackProviderBean,从中获取到 ToolCallback 工具对象,这样才能利用 MCP 服务提供的工具来增强 AI 的能力
@Autowired private SyncMcpToolCallbackProvider toolCallbackProvider; ChatResponse response = chatClient .prompt() .user(message) .tools(toolCallbackProvider) .call() .chatResponse();MCP 服务端开发
服务端开发主要基于 Spring AI MCP Server Boot Starter,能够自动配置 MCP 服务端组件,使开发者能够轻松创建 MCP 服务,向 AI 客户端提供工具、资源和提示词模板,从而扩展 AI 模型的能力范围
1、引入依赖
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency>2、配置服务(配置传输模式)
不同的传输模式有着不同的配置方法,但是也就是在server层级下一句话的事情,stdio和streamable和stateless着三种的不同
spring: ai: mcp: server: stdio: true # 或 protocol: STREAMABLE # 或 protocol: STATELESS3、开发服务
无论采用哪种传输方式,开发 MCP 服务的过程都是类似的,跟开发工具调用一样,直接使用@Tool或者@McpTool(都是一样的)注解标记服务类中的方法。
例:使用@McpTool注解
@Component public class CalculatorTools { @McpTool(name = "add", description = "Add two numbers together") public int add( @McpToolParam(description = "First number", required = true) int a, @McpToolParam(description = "Second number", required = true) int b) { return a + b; } }
@McpTool:暴露工具。@McpResource:暴露资源。@McpPrompt:暴露提示词模板。@McpComplete:用于补全。
例:使用@Tool注解
@Service public class WeatherService { @Tool(description = "获取指定城市的天气信息") public String getWeather( @ToolParameter(description = "城市名称,如北京、上海") String cityName) { return "城市" + cityName + "的天气是晴天,温度22°C"; } }然后在 Spring Boot 项目启动时注册一个
ToolCallbackProviderBean 即可:@SpringBootApplication public class McpServerApplication { @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }
MCP 工具类
Spring AI 还提供了一系列 辅助 MCP 开发的工具类,用于 MCP 和 ToolCallback 之间的互相转换。
也就是说,开发者可以直接将之前开发的工具转换为 MCP 服务,极大提高了代码复用性:
所以说我们之前项目开发的几个工具理论上都可以通过这个工具类把他们转换为MCP服务,但是不建议这么做,工具和MCP混用是很不好的习惯
四、实战--开发MCP客户端服务端
服务端开发(图片搜索)
我们本质上就是开发一个“图片搜索”的工具,然后把装载这个工具的整一个模块打包即可
1.在我们项目的根目录下新建一个模块,用于单独创建服务端
注意,建议在新项目中单独打开该模块,不要直接在原项目的子文件夹中操作,否则可能出现路径上的问题。
2.引入相关依赖
3.编写yml配置文件
我们本次开发服务端选用StreamableHTTP的连接方式(想用stdio连接方式的自己配置即可)
4.工具开发
我们本次开发的工具的功能是“图片搜索”,因此我们依然可以选用外部服务,然后根据我们外部服务的官方文档,让AI助手帮我们完成相应的代码
(这也是我们上一章节《工具调用》开发工具的常用开发方式)关于“图片搜索”功能我们选用https://pixabay.com/这个网址
点击“API”字样选项跳转相关开发文档页面
该页面有你专属的APIkey和开发说明,我们直接复制这戏金额开发用的文档,让AI帮我们开发相关代码(注意你自己的key不要暴露)
开发代码如下:
/** * Pixabay 图片搜索 MCP 工具。 * * <p>工具本身只负责参数校验、调用 Pixabay API 和整理返回结果; * 主类中的 ToolCallbackProvider Bean 负责发现并注册这个带有 @Tool 的 Spring Bean。</p> */ @Service public class PixabayImageTool { private final RestClient restClient; private final ObjectMapper objectMapper; private final String apiKey; private final String baseUrl; public PixabayImageTool( RestClient.Builder restClientBuilder, ObjectMapper objectMapper, @Value("${pixabay.api-key}") String apiKey, @Value("${pixabay.base-url}") String baseUrl) { this.restClient = restClientBuilder.baseUrl(baseUrl).build(); this.objectMapper = objectMapper; this.apiKey = apiKey; this.baseUrl = baseUrl; } @Tool( name = "search_images", description = "Search Pixabay royalty-free images by keyword and return image URLs, authors, and source attribution." ) public String searchImages( @ToolParam(description = "Search keywords, for example: sunset beach or 东莞约会地点", required = true) String query, @ToolParam(description = "Language code, for example zh or en. Default: zh", required = false) String language, @ToolParam(description = "Image type: all, photo, illustration, or vector. Default: photo", required = false) String imageType, @ToolParam(description = "Orientation: all, horizontal, or vertical. Default: all", required = false) String orientation, @ToolParam(description = "Page number, starting from 1. Default: 1", required = false) Integer page, @ToolParam(description = "Number of results per page, from 3 to 200. Default: 10", required = false) Integer perPage) { String normalizedQuery = requireQuery(query); String normalizedLanguage = defaultValue(language, "zh"); String normalizedImageType = defaultValue(imageType, "photo"); String normalizedOrientation = defaultValue(orientation, "all"); int normalizedPage = page == null ? 1 : page; int normalizedPerPage = perPage == null ? 10 : perPage; validatePage(normalizedPage, normalizedPerPage); validateEnum("language", normalizedLanguage, "cs", "da", "de", "en", "es", "fr", "id", "it", "hu", "nl", "no", "pl", "pt", "ro", "sk", "fi", "sv", "tr", "vi", "th", "bg", "ru", "el", "ja", "ko", "zh"); validateEnum("imageType", normalizedImageType, "all", "photo", "illustration", "vector"); validateEnum("orientation", normalizedOrientation, "all", "horizontal", "vertical"); URI requestUri = UriComponentsBuilder.fromUriString(baseUrl) .queryParam("key", apiKey) .queryParam("q", normalizedQuery) .queryParam("lang", normalizedLanguage) .queryParam("image_type", normalizedImageType) .queryParam("orientation", normalizedOrientation) .queryParam("safesearch", true) .queryParam("page", normalizedPage) .queryParam("per_page", normalizedPerPage) .build() .encode() .toUri(); try { JsonNode response = restClient.get() .uri(requestUri) .retrieve() .body(JsonNode.class); return toToolResult(response, normalizedQuery, normalizedPage, normalizedPerPage); } catch (RestClientResponseException exception) { return "Pixabay 图片搜索失败,HTTP 状态码:" + exception.getStatusCode().value() + ",原因:" + exception.getResponseBodyAsString(); } catch (Exception exception) { return "Pixabay 图片搜索失败:" + exception.getMessage(); } } private String toToolResult( JsonNode response, String query, int page, int perPage) throws Exception { Map<String, Object> result = new LinkedHashMap<>(); result.put("source", "Pixabay"); result.put("source_notice", "搜索结果展示时请注明图片来源 Pixabay。图片 URL 仅适合临时展示,长期使用前请先下载到自己的服务器。"); result.put("query", query); result.put("page", page); result.put("per_page", perPage); result.put("total", response == null ? 0 : response.path("total").asInt(0)); result.put("total_hits", response == null ? 0 : response.path("totalHits").asInt(0)); var images = new java.util.ArrayList<Map<String, Object>>(); if (response != null && response.has("hits")) { for (JsonNode hit : response.path("hits")) { Map<String, Object> image = new LinkedHashMap<>(); image.put("id", hit.path("id").asLong()); image.put("type", hit.path("type").asText()); image.put("tags", hit.path("tags").asText()); image.put("preview_url", hit.path("previewURL").asText()); image.put("webformat_url", hit.path("webformatURL").asText()); image.put("large_image_url", hit.path("largeImageURL").asText()); image.put("page_url", hit.path("pageURL").asText()); image.put("user", hit.path("user").asText()); images.add(image); } } result.put("images", images); return objectMapper.writeValueAsString(result); } private String requireQuery(String query) { if (query == null || query.isBlank()) { throw new IllegalArgumentException("query 不能为空"); } if (query.length() > 100) { throw new IllegalArgumentException("query 不能超过 100 个字符"); } return query.trim(); } private String defaultValue(String value, String defaultValue) { return value == null || value.isBlank() ? defaultValue : value.trim(); } private void validatePage(int page, int perPage) { if (page < 1) { throw new IllegalArgumentException("page 必须大于等于 1"); } if (perPage < 3 || perPage > 200) { throw new IllegalArgumentException("perPage 必须在 3 到 200 之间"); } } private void validateEnum(String field, String value, String... allowedValues) { for (String allowedValue : allowedValues) { if (allowedValue.equals(value)) { return; } } throw new IllegalArgumentException(field + " 的值不合法:" + value); } }写好代码后我们再回到yml配置文件中配置好我们的服务和key(key推荐配置再环境变量中)
5.编写测试方法,查看工具是否可以成功使用,查看自己的配置有无问题
测试搜索图片关键词为“小蛋糕”,待会查看图片搜索结果是否含有蛋糕图片
测试返回结果如下,可以返回图片的url
访问图片url,发现图片是“小蛋糕”相关图片,测试成功
6.测试完成后,我们需要再主类中通过定义ToolCallbackProvider的Bean 来注册工具
7.注册完工具我们需要打包该模块,让该模块成为可以被调用的“服务”
打包完成后发现target目录下有相关可执行的jar包,到时候客户端使用服务的时候就会依赖这个包
这里需要注意一下:如果你刚刚配置APIkey是和我一样配置再环境变量中的,那么打包的时候需要把这个key同样配置再Windows的系统环境变量,否则打包会失败(读取不到key)
客户端开发
开发完服务端后我们接着开发客户端,其实我们之前在“程序中使用MCP”已经算是开发过客户端了,因为要使用高德的MCP服务那就必须要用客户端去调用他的服务,我们当时的连接方式是stdio,但是我们刚才开发的服务端是StreamableHTTP方式,因此我们需要修改一下我们的客户端
其实我们只需要改一下我们之前配置好的yml配置文件即可
修改完配置文件即可启用我们之前配置好的jar包服务
打开powershell,输入对应命令:
cd D:\AI_project\AI_agent_YU\Image_Mcp
java -jar target\Image_Mcp-0.0.1-SNAPSHOT.jar
这就是StreamableHTTP和stdin连接方式的区别,StreamableHTTP连接方式需要自己在服务器(可以是本机作为服务器,也可以是云服务器)上先启用服务,然后客户端再去连接这个服务再去调用,而不是和stdin连接一样自动拉起一个服务
启用服务完成后,编写测试方法查看该服务是否可以正确使用
返回结果如下:
点击返回的图片url,发现图片是正确的,服务调用成功