大模型开发 - Spring AI 之 @McpTool、@McpPrompt、@McpResource
文章目录

本文深入讲解 Model Context Protocol (MCP) 的三大核心能力——Tool(工具)、Prompt(提示词模板)和 Resource(资源)。通过 Spring AI 的 McpSyncClient,我们可以在客户端优雅地调用 MCP 服务器暴露的这些能力,实现更灵活的 AI 应用架构。
引言
在前一篇文章中,我们学习了如何使用 @McpTool 注解在 MCP 服务器中定义工具,并通过 Spring AI 的工具调用机制进行远程工具调用。但这仅仅是 MCP 协议的冰山一角。
MCP 是一个更广泛的协议生态,不仅支持工具调用,还支持:
- Tool — 可调用的函数或方法,执行具体业务逻辑
- Prompt — 预定义的提示词模板,支持参数化,用于快速生成高质量的初始 prompt
- Resource — MCP 服务器管理的资源(配置、数据、文件等),客户端可以读取
本文的主人公是第 23 次提交,它在 McpController 中新增了两个端点 /mcpPrompt 和 /mcpResource,演示了如何在客户端直接调用 MCP 服务器的 Prompt 和 Resource 能力,而不仅仅依赖于自动的工具调用。
MCP 协议的三大能力
让我们先建立一个整体认识:
能力对比表
| 能力 | 用途 | 定义位置 | 调用方式 | 典型场景 |
|---|---|---|---|---|
| Tool | 执行具体操作或计算 | MCP 服务器(@McpTool) | 自动工具调用或 callTool() | 获取天气、搜索资讯、文件操作 |
| Prompt | 提供预制的 prompt 模板 | MCP 服务器(@McpPrompt) | listPrompts() 列表,getPrompt() 获取 | 欢迎语、工作流模板、角色 prompt |
| Resource | 访问服务器管理的资源 | MCP 服务器(@McpResource) | readResource() 读取 | 配置参数、知识库、系统状态 |
三者的关系与数据流
┌─────────────────────────────────────────────┐ │ Spring AI 客户端应用 │ │ (McpController 使用 McpSyncClient) │ └───────────────┬─────────────────────────────┘ │ │ MCP 协议 │ (JSON-RPC over stdio/TCP) ▼ ┌─────────────────────────────────────────────┐ │ MCP 服务器 (WeatherService) │ ├─────────────────────────────────────────────┤ │ @McpTool: getWeather(cityName) │ │ @McpPrompt: greeting(name) │ │ @McpResource: getConfig(key) │ └─────────────────────────────────────────────┘ - Tool 在大模型决策时被主动调用,参与推理过程
- Prompt 是静态或半静态的文本模板库,供客户端查询与使用
- Resource 是服务器数据的接口,客户端可按需读取
Prompt 能力深解
什么是 Prompt?
在 MCP 中,Prompt 是一种可参数化的文本模板。不同于硬编码的系统 prompt,MCP Prompt 允许:
- 动态参数化 — 通过参数传入动态内容(如用户名、日期、数据)
- 版本管理 — 服务器可维护多个版本的 prompt 模板
- 跨应用共享 — 多个客户端应用可复用同一套 prompt 模板库
服务器端:@McpPrompt 定义
在 spring-ai-mcp-server-demo 中,我们在 WeatherService 定义了一个问候语 prompt:
@McpPrompt(name ="greeting", description ="欢迎语")publicMcpSchema.GetPromptResultgreeting(@McpArg(name ="name")String name){String message ="你好, "+ name +"! 有什么可以帮您?";returnnewMcpSchema.GetPromptResult("Greeting",List.of(newMcpSchema.PromptMessage(McpSchema.Role.ASSISTANT,newMcpSchema.TextContent(message))));}关键点:
@McpPrompt— 标记该方法作为 MCP prompt 源name = "greeting"— prompt 的唯一标识符description = "欢迎语"— 对 prompt 用途的简短说明@McpArg(name = "name")— 定义可参数化的参数- 返回值
GetPromptResult包含一条或多条PromptMessage,每条消息有角色(ASSISTANT/USER)和内容
客户端:通过 McpSyncClient 调用 Prompt
在客户端,我们通过 McpSyncClient 来列举和获取 prompt:
第一步:列举所有 Prompt
@GetMapping("/mcpPrompt")publicStringmcpPrompt(String question){McpSyncClient mcpSyncClient = mcpSyncClients.get(0);// 列举 MCP 服务器提供的所有 promptMcpSchema.ListPromptsResult listPromptsResult = mcpSyncClient.listPrompts();List<McpSchema.Prompt> prompts = listPromptsResult.prompts();// 获取第一个 prompt 的元数据McpSchema.Prompt prompt = prompts.get(0);return prompt.toString();}此时得到的是 prompt 的元数据,而非其具体内容:
{"name":"greeting","description":"欢迎语","arguments":{"type":"object","properties":{"name":{"type":"string","description":"用户名"}}}}第二步:获取具体的 Prompt 内容(含参数)
注释中展示的实现:
McpSchema.GetPromptRequest getPromptRequest =newMcpSchema.GetPromptRequest("greeting",Map.of("name","周瑜"),null);McpSchema.GetPromptResult result = mcpSyncClient.getPrompt(getPromptRequest);// 提取文本内容String promptText =((McpSchema.TextContent)result.messages().get(0).content()).text();// 输出:"你好, 周瑜! 有什么可以帮您?"调用流程:
- 构造
GetPromptRequest,包含 prompt 名称(“greeting”)和参数映射({“name”: “周瑜”}) - 调用
mcpSyncClient.getPrompt(),MCP 协议将请求发往服务器 - 服务器的
greeting()方法被执行,参数注入到方法 - 返回
GetPromptResult,包含消息列表 - 客户端通过类型转换提取文本内容
Prompt 的应用场景
场景 1:构建系统 Prompt 库
某个 AI 应用需要支持多个角色(产品经理、工程师、设计师),每个角色有不同的 system prompt。这些 prompt 不需要在代码中硬编码,而是由 MCP 服务器统一维护:
// 客户端String rolePrompt =getPromptFromMcp("role_"+ role,Map.of("domain", domain));ChatMessage systemMessage =newChatMessage(MessageType.SYSTEM, rolePrompt); chatClient.prompt().system(systemMessage).user(question).call().content();场景 2:多语言 Prompt 管理
服务器维护英文、中文、日文等多语言的 prompt 版本,客户端根据 locale 参数动态获取:
String promptName ="greeting_"+ locale;// "greeting_zh", "greeting_en", etc.McpSchema.GetPromptResult result = mcpSyncClient.getPrompt(newMcpSchema.GetPromptRequest(promptName,Map.of("userName", user),null));场景 3:工作流模板
复杂的多步骤 AI 工作流(如需求分析 → 方案设计 → 代码生成 → 测试)的每一步都有标准 prompt,服务器维护这些 prompt 的版本演进。
Resource 能力深解
什么是 Resource?
MCP Resource 是一个统一的资源访问接口。通过 URI 式的寻址,客户端可以读取服务器管理的任意资源:
- 配置参数 — 应用配置、API 密钥、特性开关
- 知识库 — 文档、FAQ、业务规则
- 运行时数据 — 系统状态、用户信息、动态数据
- 文件资源 — 模板文件、预训练模型、数据文件
服务器端:@McpResource 定义
在 WeatherService 中定义一个配置资源:
@McpResource(uri ="config://{key}", name ="configuration")publicStringgetConfig(String key){return environment.getProperty(key,"123");}关键点:
@McpResource— 标记该方法暴露资源接口uri = "config://{key}"— 资源的 URI 模式,其中{key}是通配符,会被注入到方法参数name = "configuration"— 资源类型或分组的名称- 方法参数
key来自 URI 中的{key}占位符
支持的资源类型
MCP 规范定义了多种资源内容类型:
// 文本资源McpSchema.TextResourceContents textResource =newMcpSchema.TextResourceContents(text);// 二进制资源McpSchema.BlobResourceContents blobResource =newMcpSchema.BlobResourceContents(base64EncodedData, mimeType);客户端:通过 McpSyncClient 读取 Resource
完整的资源读取流程:
@GetMapping("/mcpResource")publicStringmcpResource(String question){McpSyncClient mcpSyncClient = mcpSyncClients.get(0);// 1. 构造资源读取请求// URI "config://username" 中,{key} 被替换为 "username"McpSchema.ReadResourceRequest readResourceRequest =newMcpSchema.ReadResourceRequest("config://username");// 2. 调用 mcpSyncClient 读取资源McpSchema.ReadResourceResult readResourceResult = mcpSyncClient.readResource(readResourceRequest);// 3. 从结果中提取内容// contents 是一个列表,通常只有一个元素McpSchema.ResourceContents resourceContents = readResourceResult.contents().get(0);// 4. 类型转换为 TextResourceContents,提取文本String configValue =((McpSchema.TextResourceContents)resourceContents).text();return configValue;// 若找不到,返回默认值 "123"}执行流程分解:
客户端构造请求 ↓ 读取 URI "config://username" ↓ MCP 协议转发到服务器 ↓ 服务器的 getConfig("username") 被调用 ↓ 方法返回配置值(或默认值) ↓ 包装成 TextResourceContents ↓ 客户端接收并类型转换提取文本 Resource 的应用场景
场景 1:集中配置管理
所有应用从 MCP 服务器读取配置,实现配置的动态修改而无需重启应用:
String apiKey =readResource("config://openai_api_key");String maxTokens =readResource("config://max_tokens");String enableCache =readResource("config://enable_cache");场景 2:知识库快速查询
一个 FAQ 知识库由 MCP 服务器维护,不同的应用可以快速查询:
String faqContent =readResource("faq://how_to_register");String troubleshootingGuide =readResource("faq://common_issues");场景 3:用户权限与配置
读取当前用户的权限配置、个性化设置等:
String userPermissions =readResource("user://current/permissions");String userTheme =readResource("user://current/preferences/theme");场景 4:业务规则引擎
维护业务规则(定价规则、风控规则、营销规则等),应用通过 Resource 动态获取最新规则:
String pricingRule =readResource("rules://pricing/v2024Q1");String riskControlPolicy =readResource("rules://risk_control/current");代码实现详解
完整的 McpController
packagecom.Artisan;importio.modelcontextprotocol.client.McpSyncClient;importio.modelcontextprotocol.spec.McpSchema;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.tool.ToolCallbackProvider;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.List;importjava.util.Map;@RestControllerpublicclassMcpController{@AutowiredprivateChatClient chatClient;@AutowiredprivateToolCallbackProvider toolCallbackProvider;@AutowiredprivateList<McpSyncClient> mcpSyncClients;/** * 使用自动工具调用机制 */@GetMapping("/mcp")publicStringmcp(String message){return chatClient .prompt().user(message).toolCallbacks(toolCallbackProvider.getToolCallbacks()).call().content();}/** * 手动调用 Prompt 能力 * 列举所有可用的 prompt,获取第一个 prompt 的元数据 */@GetMapping("/mcpPrompt")publicStringmcpPrompt(String question){McpSyncClient mcpSyncClient = mcpSyncClients.get(0);// 列举所有 promptMcpSchema.ListPromptsResult listPromptsResult = mcpSyncClient.listPrompts();List<McpSchema.Prompt> prompts = listPromptsResult.prompts();// 获取第一个 prompt 的元数据McpSchema.Prompt prompt = prompts.get(0);return prompt.toString();// 也可以获取具体 prompt 内容(含参数):// McpSchema.GetPromptRequest getPromptRequest =// new McpSchema.GetPromptRequest("greeting", Map.of("name", "周瑜"), null);// McpSchema.GetPromptResult promptResult = mcpSyncClient.getPrompt(getPromptRequest);// String promptText = ((McpSchema.TextContent)promptResult.messages().get(0).content()).text();// return promptText;}/** * 手动调用 Resource 能力 * 读取 MCP 服务器管理的配置资源 */@GetMapping("/mcpResource")publicStringmcpResource(String question){McpSyncClient mcpSyncClient = mcpSyncClients.get(0);// 构造资源读取请求McpSchema.ReadResourceRequest readResourceRequest =newMcpSchema.ReadResourceRequest("config://username");// 读取资源McpSchema.ReadResourceResult readResourceResult = mcpSyncClient.readResource(readResourceRequest);// 提取第一个资源内容McpSchema.ResourceContents resourceContents = readResourceResult.contents().get(0);// 类型转换为文本资源并提取文本return((McpSchema.TextResourceContents)resourceContents).text();}}完整的 WeatherService(MCP 服务器端)
packagecom.Artisan;importio.modelcontextprotocol.spec.McpSchema;importlombok.extern.log4j.Log4j2;importorg.springaicommunity.mcp.annotation.McpArg;importorg.springaicommunity.mcp.annotation.McpPrompt;importorg.springaicommunity.mcp.annotation.McpResource;importorg.springaicommunity.mcp.annotation.McpTool;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.core.env.Environment;importorg.springframework.stereotype.Service;importjava.util.List;@Service@Log4j2publicclassWeatherService{@AutowiredprivateEnvironment environment;/** * Tool 能力:获取天气 */@McpTool(description ="获取指定城市的天气")publicStringgetWeather(String cityName){ log.info("正在获取天气信息...");if(cityName.equals("上海")){return"天晴";}elseif(cityName.equals("北京")){return"下雨";}return"不知道";}/** * Prompt 能力:欢迎语模板(支持参数化) */@McpPrompt(name ="greeting", description ="欢迎语")publicMcpSchema.GetPromptResultgreeting(@McpArg(name ="name")String name){String message ="你好, "+ name +"! 有什么可以帮您?";returnnewMcpSchema.GetPromptResult("Greeting",List.of(newMcpSchema.PromptMessage(McpSchema.Role.ASSISTANT,newMcpSchema.TextContent(message))));}/** * Resource 能力:读取配置资源 * URI 模式:config://{key} * 例如:config://username 会调用 getConfig("username") */@McpResource(uri ="config://{key}", name ="configuration")publicStringgetConfig(String key){return environment.getProperty(key,"123");}}类型转换的细节
在客户端接收到 Resource 后,需要进行显式的类型转换。这是因为 ResourceContents 是一个接口,具体实现可能是:
// 文本资源if(resourceContents instanceofMcpSchema.TextResourceContents){String text =((McpSchema.TextResourceContents)resourceContents).text();}// 二进制资源if(resourceContents instanceofMcpSchema.BlobResourceContents){String mimeType =((McpSchema.BlobResourceContents)resourceContents).mimeType();String data =((McpSchema.BlobResourceContents)resourceContents).data();// Base64}实际项目中,可以写一个工具方法来简化:
privateStringextractTextResource(McpSchema.ResourceContents resourceContents){if(resourceContents instanceofMcpSchema.TextResourceContents){return((McpSchema.TextResourceContents)resourceContents).text();}thrownewIllegalArgumentException("Expected TextResourceContents");}- 使用
@McpTool定义工具 - 通过
ToolCallbackProvider获取所有工具的 callback - 通过
chatClient.toolCallbacks()让大模型自动调用工具
这是 Tool 能力的完整体现 —— 大模型在推理过程中自主决策是否需要调用工具,框架自动处理调用和结果注入。
这里引入了两个新的 MCP 能力:
- Prompt 能力 — 不再仅依靠硬编码 system prompt,而是从 MCP 服务器动态获取参数化的 prompt 模板
- Resource 能力 — 超越工具调用的业务逻辑,直接访问服务器管理的资源和配置
这三个能力形成了 MCP 生态的完整三角:
┌─────────────────────────────────────────┐ │ MCP 生态的三大支柱 │ ├─────────────────────────────────────────┤ │ Tool → 动态业务逻辑执行 │ │ Prompt → 动态文本模板获取 │ │ Resource → 动态资源与配置读取 │ └─────────────────────────────────────────┘ 最佳实践与常见陷阱
最佳实践
1. 合理划分 Tool、Prompt、Resource 的职责
| 需求 | 选择 | 原因 |
|---|---|---|
| 执行计算或操作 | Tool | Tool 专门用于执行业务逻辑 |
| 提供文本模板 | Prompt | Prompt 为文本生成优化设计 |
| 读取静态或配置数据 | Resource | Resource 用于数据和配置访问 |
| 修改数据或状态 | Tool | Resource 只读,修改需要 Tool |
2. Prompt 参数化设计
不要硬编码参数值,始终使用参数化:
// 不推荐@McpPrompt(name ="greeting_john")publicMcpSchema.GetPromptResultgreetingJohn(){...}// 推荐@McpPrompt(name ="greeting")publicMcpSchema.GetPromptResultgreeting(@McpArg(name ="name")String name){...}3. Resource URI 的一致性
定义明确的 URI 规范,保证客户端和服务器的一致性:
// 约定:config://部分/键名@McpResource(uri ="config://{module}/{key}")publicStringgetConfig(Stringmodule,String key){...}// 客户端调用readResource("config://database/host");readResource("config://cache/ttl");4. 错误处理
MCP 调用可能失败,需要适当的异常处理:
try{McpSchema.ReadResourceResult result = mcpSyncClient.readResource(request);if(result.contents().isEmpty()){ logger.warn("Resource not found: {}", request.uri());return defaultValue;}return((McpSchema.TextResourceContents)result.contents().get(0)).text();}catch(Exception e){ logger.error("Failed to read resource", e);return defaultValue;}常见陷阱
陷阱 1:忘记类型转换
// 错误:直接将 ResourceContents 当作 TextResourceContentsString text = resourceContents.text();// ❌ ResourceContents 无此方法// 正确:先进行类型转换String text =((McpSchema.TextResourceContents)resourceContents).text();// ✓陷阱 2:忘记处理空列表
// 错误:假设 contents 总是非空McpSchema.ResourceContents contents = result.contents().get(0);// 正确:检查列表是否为空if(result.contents().isEmpty()){return defaultValue;}McpSchema.ResourceContents contents = result.contents().get(0);陷阱 3:混淆 Prompt 名称与内容
// 错误:listPrompts() 返回的是元数据(名称、描述、参数定义),不是实际内容McpSchema.ListPromptsResult result = mcpSyncClient.listPrompts();String content = result.prompts().get(0).toString();// 这是元数据,不是实际 prompt 内容// 正确:使用 getPrompt() 获取实际内容McpSchema.GetPromptRequest request =newMcpSchema.GetPromptRequest("greeting", params,null);McpSchema.GetPromptResult result = mcpSyncClient.getPrompt(request);String content =((McpSchema.TextContent)result.messages().get(0).content()).text();总结
本文深入讲解了 MCP 协议的三大能力在 Spring AI 中的实现:
核心要点
- Tool 能力 — 执行业务逻辑,通过
@McpTool定义,大模型自动调用 - Prompt 能力 — 提供参数化的文本模板,通过
@McpPrompt定义,客户端动态查询与填充 - Resource 能力 — 提供资源与配置的统一访问界面,通过
@McpResource定义,客户端按 URI 读取
客户端调用模式
// Tool:自动调用(第 22 讲) chatClient.toolCallbacks(callbacks).call().content();// Prompt:手动列举与获取 listPromptsResult = mcpSyncClient.listPrompts(); getPromptResult = mcpSyncClient.getPrompt(request);// Resource:手动读取 readResourceResult = mcpSyncClient.readResource(request);设计思想
MCP 的设计体现了关注点分离的原则:
- Tool 关注 能力 —— 服务器提供什么操作
- Prompt 关注 模板 —— 服务器维护什么知识
- Resource 关注 数据 —— 服务器管理什么资源
通过 MCP 这个统一协议,我们可以构建更灵活、更可扩展的 AI 应用架构 —— 业务逻辑、提示词知识和配置数据可以独立演进,通过协议契约进行集成。
这正是 Spring AI 对"企业级 AI 应用"的理解:不是将一切硬编码到代码中,而是通过清晰的协议和抽象,实现服务与客户端的解耦,让不同团队可以独立迭代。