跳转到内容

MCP 能力暴露

前面的章节一直在讲 HTTP:Controller 接收请求,调用业务代码,再返回文本或 JSON。MCP 让同一类业务能力多了一个入口:模型客户端可以发现它、理解它的参数,并把它当成工具、提示模板或资源来调用。

Feat Cloud 的优势仍然来自编译期生成。你在 Controller 上声明 @Tool@Prompt@Resource@McpEndpoint,编译期会把这些声明转成 MCP Server 的注册代码。运行时要做的事情很少:加载生成类,把能力挂到对应端点上。 简笔手绘卡通风格,一个模型客户端通过 MCP 线缆连接到 Feat Cloud Controller,Controller 旁边分出 Tool、Prompt、Resource 三个小卡片,表现业务能力被模型发现和调用,现代技术插画,900x383

如果只是把少量业务方法开放给 MCP 客户端,不需要一开始就设计完整端点。给方法加 @Tool,给参数补上 @Param 元信息,就能先跑通最小链路:

DefaultMcpController.java
import tech.smartboot.feat.cloud.annotation.Controller;
import tech.smartboot.feat.cloud.annotation.mcp.Tool;
import tech.smartboot.feat.cloud.annotation.mcp.Param;
@Controller
public class DefaultMcpController {
@Tool(description = "简单的问候工具")
public String greeting(@Param(required = true, description = "用户姓名") String name) {
return "你好," + name + "";
}
}

这里的 @Param 不是 HTTP 查询参数,而是 MCP 参数描述。它告诉客户端这个工具需要什么输入、参数是否必填、参数含义是什么。对模型客户端来说,这些描述会直接影响工具能不能被正确选择和调用。

Feat Cloud 提供两种 MCP 暴露方式:默认 MCP 服务和显式 @McpEndpoint

默认 MCP 服务适合早期接入:项目里出现 @Tool@Prompt@Resource 后,Feat Cloud 会生成默认 MCP Server,把这些能力挂进去。它的价值是低成本验证:先确认客户端能发现工具、能发起调用、能拿到结果。

当 MCP 已经成为应用边界的一部分,就应该使用 @McpEndpoint 显式定义端点。它让服务名、SSE 路径、消息路径和 Streamable HTTP 路径都写在代码里,部署、网关和客户端配置也更容易保持稳定。

McpDemoController.java
import tech.smartboot.feat.ai.mcp.enums.PromptType;
import tech.smartboot.feat.ai.mcp.model.ToolResult;
import tech.smartboot.feat.cloud.annotation.Controller;
import tech.smartboot.feat.cloud.annotation.mcp.McpEndpoint;
import tech.smartboot.feat.cloud.annotation.mcp.Param;
import tech.smartboot.feat.cloud.annotation.mcp.Prompt;
import tech.smartboot.feat.cloud.annotation.mcp.Resource;
import tech.smartboot.feat.cloud.annotation.mcp.Tool;
import java.util.HashMap;
import java.util.Map;
@Controller
@McpEndpoint(
name = "demo-mcp-service",
title = "Demo MCP Service",
sseEndpoint = "/mcp/demo/sse",
sseMessageEndpoint = "/mcp/demo/sse/message",
streamableEndpoint = "/mcp/demo/stream"
)
public class McpDemoController {
@Tool(name = "getUserInfo", description = "根据用户ID获取用户信息")
public Map<String, Object> getUserInfo(
@Param(required = true, description = "用户ID") Long userId,
@Param(required = false, description = "是否包含详细信息") Boolean detailed
) {
Map<String, Object> user = new HashMap<>();
user.put("id", userId);
user.put("name", "User " + userId);
user.put("email", userId + "@example.com");
user.put("detailed", Boolean.TRUE.equals(detailed));
return user;
}
@Prompt(
name = "codeReviewPrompt",
description = "代码审查提示词",
type = PromptType.TEXT
)
public String codeReviewPrompt(
@Param(required = true, description = "编程语言") String language,
@Param(required = true, description = "代码片段") String codeSnippet
) {
return String.format("请审查以下%s代码并提供改进建议:%s", language, codeSnippet);
}
@Resource(
uri = "/resources/coding-standards.md",
name = "编码规范",
description = "团队编码规范文档",
mimeType = "text/markdown",
isText = true
)
public String codingStandards() {
return "# 编码规范";
}
@Tool(description = "获取文本信息")
public ToolResult.TextContent getTextInfo() {
return ToolResult.ofText("这是文本内容");
}
}

这段代码里有三类能力:

  • @Tool 暴露可调用能力
  • @Prompt 暴露提示模板
  • @Resource 暴露文本或二进制资源

@McpEndpoint 则负责把这组能力放进一个明确的 MCP 服务边界里。到了这个阶段,你应该像设计 HTTP API 一样设计 MCP 端点:路径要稳定,名称要能表达服务意图,参数描述要足够让调用方理解。

@Tool 适合执行动作或查询数据。如果你的目标是“让模型调用这段业务逻辑”,从 Tool 开始最自然,例如查用户、算价格、生成报表摘要。

@Prompt 适合暴露提示模板。它不是普通 Controller 里直接返回一段文本,而是把一套可复用的提示结构交给 MCP 客户端,让客户端在合适时填入参数并使用。

@Resource 适合暴露稳定材料,例如团队规范、产品说明、接口文档、图片或二进制资源。资源一般不表达“做一件事”,而是让客户端读取一份内容。

MCP 能力应该围绕调用方的任务组织,而不是围绕注解类型堆叠。一个面向代码审查的 MCP 服务,可以包含代码审查 Prompt、读取编码规范的 Resource、查询项目元数据的 Tool;另一个面向运维的 MCP 服务,则应该独立成另一组 Controller 和端点。

如果你是第一次接入 MCP,比较稳的顺序是:

  1. 先暴露一个最小 Tool,确认客户端能调用
  2. 再补充必要的参数描述,观察模型是否能正确选择工具
  3. 需要复用提示结构时再加入 Prompt
  4. 需要稳定材料时再加入 Resource
  5. 服务边界稳定后,再迁移到显式 @McpEndpoint

注解方式适合大多数静态能力:工具、提示和资源在编译期就能确定,Feat Cloud 可以直接生成注册代码。

如果你的工具集合需要在运行时动态变化,例如从数据库、插件系统或外部配置加载工具,就可以注入 McpServer 并在代码里管理。这个方式更灵活,也意味着你要自己维护更多边界:何时注册、何时下线、如何避免重复、如何处理并发变化。

所以默认选择很简单:能用注解清楚表达的能力,优先交给编译期生成;只有当能力本身就是动态的,再直接操作 McpServer