ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于MCP协议的多商户付费内容系统开发实战

2026/8/31 17:24:41 拓冰建站 浏览量
基于MCP协议的多商户付费内容系统开发实战 在实际的网站开发中内容系统往往不只是一个 CRUD 后台。只要加入付费内容、多商户和 AI 交互这三个约束系统就必须把“谁能发布、谁能阅读、什么时候允许看见完整正文”这些边界变成明确的业务规则。MCP内容系统开发是指基于 MCP 协议把内容平台的核心能力暴露给大模型和 Agent搜索内容库、判断用户是否已购买、获取付费正文、查询商户订单都通过标准工具调用完成。这样 AI 不需要直接连接数据库也不需要猜测表名和字段而是按协议调用 MCP Server 提供的工具拿到的结果天然受业务权限约束。这篇文章会沿着一条可以落地的技术主线展开先讲清楚 MCP 与内容系统如何配合再给出多商户付费内容系统的领域模型和表结构然后用 Java 和 Spring Boot 实现一个 MCP Server接入内容搜索、付费校验、订单查询几个真实场景最后讲客户端如何验证、常见问题怎么排查以及生产环境需要补齐哪些能力。代码用于说明思路落地时务必结合自己的包名、版本号和数据结构调整。1. MCP 在内容系统开发中解决什么问题1.1 用一句话理解 MCP 协议MCP 是 Model Context Protocol 的缩写中文通常叫模型上下文协议。它解决的问题是把“大模型调用外部能力”的方式标准化。类比 USB 接口以前不同外设各有各的接口现在外设只要实现统一协议就能被支持该协议的设备使用。MCP 中的 Server 可以暴露工具、资源和提示词AI 应用通过 MCP Client 连接 Server按协议发现工具、读取资源、执行调用。内容系统引入 MCP 之后AI 不再是问一句“你的数据库有哪些字段”而是通过工具完成明确动作。例如searchContent按关键词搜索可售内容。getContentAccess判断某个用户是否有权查看某篇付费内容。listPaidOrders查询用户在某个商户下的已支付订单。这些工具由后端开发人员定义内部实现可以查 MySQL、调 Redis、校验订单状态但对 AI 来说只看到一个稳定的方法签名。1.2 MCP 与 API、SDK、Agent Skill 的边界很多团队刚接触 MCP 时会混淆这几层概念。API 是一般的接口服务可以是 REST、RPC、GraphQL。MCP 不是替代 API而是在 AI 场景下统一“工具发现、参数声明、调用返回”的标准。普通 API 需要客户端自己读文档、构造请求MCP Client 可以直接获取工具列表和参数 Schema再按 Schema 传参。SDK 是某个语言内的开发工具包MCP Java SDK 的作用是帮助开发人员少写协议层代码。SDK 解决了“怎么实现 MCP Server/Client”MCP 本身解决“两边按什么协议对话”。Agent Skill 经常被进一步混在一起。Skill 通常是 Agent 框架里的能力封装包含任务拆解、提示词模板、工具调用顺序MCP 则是能支撑 Skill 正常执行的工具供给层。可以这样理解Skill 是剧本MCP 是舞台上的插座和开关。剧本写“先查订单状态再决定是否展示正文”插座负责让“查订单”这个动作真正执行。1.3 内容系统接入 MCP 的典型收益内容系统接入 MCP不等于要立刻上一个大模型。实际收益是分层的统一 AI 访问入口。以后聊天机器人、智能客服、编辑器里的 AI 助手都连同一个 MCP Server。权限逻辑可以下沉。工具内部先做商户隔离和付费校验AI 拿到的数据就是被裁剪过的。避免全库塞进上下文。不需要把付费内容全部喂给大模型而是让模型在执行任务时按需调用工具由后端决定返回哪些字段。这里有一条明确边界MCP 是工具层AI 仍是使用者业务规则必须永远保留在服务端。不要把权限判断交给提示词提示词可以被绕过。2. 面向付费内容系统的 MCP 架构设计2.1 核心业务对象与权限语义付费内容系统至少要有四个核心对象。商户对应内容提供方一个商户下有多篇内容。多商户场景下内容、订单和订阅都归属于某个商户。内容是最核心的商品包含标题、摘要、付费正文、价格和上下架状态。订单记录用户购买单篇内容的结果。订阅记录用户是否拥有在某商户下按月或按年阅读的权限。这几个对象直接决定阅读权限对象核心字段权限作用商户id、code、name、status数据隔离维度内容id、merchant_id、title、summary、body、price、status决定能展示哪些字段订单id、order_no、user_id、content_id、status、paid_at单篇内容是否可看订阅id、user_id、merchant_id、start_at、end_at、status某个商户下是否可看2.2 MCP Server 在整个系统中的位置把 MCP Server 放进内容系统后调用链路是AI Agent / Chat Client | MCP Client | 统一协议 | MCP Server | Service 层 / Repository 层 | MySQL / Redis / 对象存储MCP Server 可以内嵌在自己的 Web 应用里也可以独立部署。学习环境里内嵌最方便少一套部署。生产环境如果工具量大、并发高或者要单独扩容建议独立进程部署通过 memfree 发现、连接配置维护在客户端侧。MCP Server 不直接操作数据库表。它调用已有的 Service 层保证业务逻辑只有一套实现。比如订单支付回调后刷新权限MCP Server 能查到最新状态而不是缓存一份过期视图。2.3 多商户与付费边界的难点多商户内容系统接入 AI 后最难的不是“AI 能不能搜到内容”而是“AI 有没有把不该给的内容给了不该给的人”。典型难点有三个数据隔离。工具执行时必须绑定 merchantId否则 AI 可能跨商户搜索。付费边界。未购买用户只能看到摘要已购买用户才能看到正文或正文地址。参数不信任。AI 可能在你设计的参数里传任何值比如 userId1、contentId999服务端必须重新鉴权。这些难点决定了 MCP 工具不是简单查询方法而是带权限语义的业务入口。3. 环境准备与项目骨架3.1 技术选型与版本说明Java 生态下开发 MCP Server通常选 Java 17 或 21Spring Boot 3.x搭配 MCP Java SDK。数据库用 MySQL 8.x缓存可以用 Redis。如果原始项目版本不确定落地前先确认 SDK 对 Spring Boot 版本的兼容性避免 starter 自动配置冲突。学习环境可以在一个 Spring Boot 工程里同时跑 Web 服务和 MCP Server先用内存数据跑通再换成 MySQL。生产环境则需要独立配置数据源、连接池、日志和监控。组件建议选型用途JDK17 或 21Java 语言基础Web 框架Spring Boot 3.x承载 HTTP 接口MCP SDKMCP Java SDK 或 Spring Boot Starter协议层实现数据库MySQL 8.x内容、订单、订阅存储缓存Redis高频权限校验缓存构建工具Maven 或 Gradle依赖管理3.2 项目结构一个最小可运行的项目目录可以这样组织content-mcp-server/ src/main/java/com/example/contentmcp/ ContentMcpApplication.java mcp/ ContentAgentTools.java McpServerConfig.java service/ ContentAgentService.java OrderAccessService.java domain/ model/ Merchant.java ContentItem.java ContentOrder.java Subscription.java repository/ ContentItemRepository.java ContentOrderRepository.java src/main/resources/ application.yaml pom.xml这个结构把mcp包独立出来。业务逻辑放在service包MCP 工具类只做参数校验和结果转换这样后续把 MCP 换成普通 REST 接口也不影响核心逻辑。3.3 核心表结构先建商户表。商户是后续所有查询的隔离维度。CREATE TABLE merchant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(64) NOT NULL UNIQUE, name VARCHAR(128) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-启用 0-停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );内容表保存标题、摘要、正文和价格。正文字段建议使用LONGTEXT但实际生产可以考虑把正文拆到独立表或对象存储减少列表查询开销。CREATE TABLE content_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, merchant_id BIGINT NOT NULL, title VARCHAR(255) NOT NULL, summary VARCHAR(1024), body LONGTEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-上架 2-下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_merchant_status (merchant_id, status) );订单表记录单篇购买。用户购买某篇内容后订单状态为已支付才拥有阅读权限。CREATE TABLE content_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL UNIQUE, user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, content_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已退款, paid_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_merchant (merchant_id) );订阅表用于订阅制场景。订阅生效时用户在看某商户下所有内容时拥有额外权限。CREATE TABLE subscription ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, start_at DATETIME NOT NULL, end_at DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-未生效 1-生效中 2-已过期, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_merchant (user_id, merchant_id) );注意不要在content_order上建UNIQUE(user_id, content_id)的唯一约束除非产品明确要求同一用户对同一内容只能购买一次。实际运营中同一个用户可能购买后又退款再购买留多条记录更能保留审计轨迹。4. 用 Java 实现一个 MCP Server4.1 引入 MCP 依赖在pom.xml中加入 MCP Java SDK 相关依赖。不同版本提供的 starter 名称会有差异落地前先到 Maven 仓库确认。dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp-spring-boot-starter/artifactId version按项目实际使用的版本填写/version /dependency添加依赖后Spring Boot 启动时只要存在传输配置和工具定义就会自动注册 MCP Server。若不想依赖 starter也可以普通引入 SDK自己创建 Transport 和 ServerMcpServer。4.2 定义内容工具类工具类可以是一个 Spring Bean。以下代码演示了三个核心工具Component public class ContentAgentTools { private final ContentAgentService contentAgentService; public ContentAgentTools(ContentAgentService contentAgentService) { this.contentAgentService contentAgentService; } Tool(description 搜索指定商户下已上架内容返回标题、摘要、价格不返回正文) public ListContentSummary searchContent( ToolParam(description 商户ID) Long merchantId, ToolParam(description 搜索关键词) String keyword) { return contentAgentService.search(merchantId, keyword); } Tool(description 判断用户是否有权查看指定内容返回可访问状态、摘要和正文地址) public ContentAccessResult getContentAccess( ToolParam(description 用户ID) Long userId, ToolParam(description 内容ID) Long contentId) { return contentAgentService.buildAccessResult(userId, contentId); } Tool(description 查询用户在某商户下的已支付订单列表) public ListOrderInfo listPaidOrders( ToolParam(description 用户ID) Long userId, ToolParam(description 商户ID) Long merchantId) { return contentAgentService.listPaidOrders(userId, merchantId); } }如果项目的 MCP 接入方式不是注解式也可以手动注册 ToolCallback。思路相同把工具名称、描述、入参 Schema、处理方法组装好注册到 McpServer 实例。注解方式更适合代码维护但两者没有本质区别。4.3 配置传输方式和 ServerMCP Server 支持 stdio 和 HTTP/SSE 两种传输方式。学习环境用 stdio 最方便AI 客户端直接拉起一个 Java 进程。Web 系统通常用 HTTP/SSE客户端通过 URL 连接。这里给出一个配置示意Configuration public class McpServerConfig { Bean public McpServer mcpServer(ContentAgentTools tools) { // 根据你选择的 SDK 版本通过 Transport 创建 Server // 下面代码是核心逻辑示意传输部分以实际版本 API 为准 McpServer server McpServer.using(httpTransport()) .serverInfo(content-mcp-server, 1.0.0) .tools(tools) .register(); return server; } }这里最重要的一点是tools(tools)这类注册步骤不能漏。无论用注解扫描还是手动注册工具没有注册成功客户端能力列表里就是空的。4.4 工具入参与返回对象的设计MCP 工具的入参尽量使用简单类型。复杂对象会增加生成无关参数的概率。比如搜索工具只传merchantId和keyword两个参数AI 更容易理解。不要把整个查询对象丢进去会让模型猜字段意思。返回对象要区分权限等级。搜索接口返回摘要不返回正文访问判断接口返回是否可访问订单接口返回订单号、金额和状态。用 DTO 而不是直接返回数据库实体能避免把body字段意外带出去。public record ContentSummary( Long contentId, String title, String summary, BigDecimal price ) {}public record ContentAccessResult( boolean accessible, String title, String summary, String bodyUrl, String reason ) {}5. 接入多商户和付费内容控制5.1 商户上下文怎么传多商户系统里最忌讳的做法是让 AI 自由指定任意 merchantId 并直接信任。正确做法是工具调用时传入的身份参数要与登录态绑定。如果是内部 Agent通常由上层系统把“当前登录用户”和“当前商户”放进 MCP 请求上下文。如果 MCP Client 不支持透传用户上下文就把userId作为显式参数但服务端必须校验这个用户确实属于该商户或者用户确实拥有独立身份。public ContentAccessResult buildAccessResult(Long userId, Long contentId) { ContentItem content contentItemRepository.findById(contentId) .orElseThrow(() - new IllegalArgumentException(内容不存在)); boolean hasPaidOrder orderAccessService.hasPaidOrder(userId, contentId); boolean hasActiveSubscription orderAccessService.hasActiveSubscription(userId, content.getMerchantId()); if (hasPaidOrder || hasActiveSubscription) { return new ContentAccessResult(true, content.getTitle(), content.getSummary(), buildBodyUrl(content.getId()), 可以访问); } return new ContentAccessResult(false, content.getTitle(), content.getSummary(), null, 未购买或订阅已过期); }5.2 内容查询工具中的权限裁剪搜索接口只返回已上架内容不返回正文。无论 AI 怎么拼接提示词后端返回的对象里没有body字段模型就拿不到完整内容。public ListContentSummary search(Long merchantId, String keyword) { ListContentItem items contentItemRepository.searchMerchantItems(merchantId, keyword); return items.stream() .filter(item - item.getStatus() 1) .map(item - new ContentSummary( item.getId(), item.getTitle(), item.getSummary(), item.getPrice())) .toList(); }如果后续要接向量检索建议只把标题、摘要和已授权片段放进向量库。付费正文不要进 embedding否则一旦检索逻辑出现漏洞全文片段会被拼进上下文返回给未授权用户。5.3 订阅状态校验工具订阅状态会过期所以工具内部每次都要校验当前时间。public boolean hasActiveSubscription(Long userId, Long merchantId) { return subscriptionRepository .findByUserIdAndMerchantId(userId, merchantId) .map(sub - sub.getStatus() 1 sub.getStartAt().isBefore(LocalDateTime.now()) sub.getEndAt().isAfter(LocalDateTime.now())) .orElse(false); }生产环境里订阅到期时间往往由定时任务变更状态。但工具层不能只依赖状态字段还要比较时间范围避免定时任务延迟导致权限被错误开放。5.4 服务端校验必须在 MCP 工具边界重复做AI 客户端可能做了很完整的提示词约束比如“只允许查询自己的订单”。但提示词约束不是安全边界。MCP 工具必须假设所有参数都不可信并在工具方法内重新鉴权。常见错误是把校验放在 MCP Server 之前的拦截器工具内部不再校验。这样一旦某个工具被直接调用或拦截器配置漏掉某个端点就可能绕过校验。建议的校验顺序是校验用户身份是否有效。校验商户是否存在且启用。校验内容是否属于该商户。校验用户是否有权访问目标内容。只返回允许看见的字段。6. 在 AI 客户端消费 MCP 服务6.1 通过客户端配置连接如果你的 AI 客户端支持 MCP需要在配置中声明 Server。stdio 方式通常指定启动命令{ mcpServers: { content: { command: java, args: [-jar, content-mcp-server.jar], env: { DB_URL: jdbc:mysql://127.0.0.1:3306/content_platform } } } }HTTP/SSE 方式直接给出 URL{ mcpServers: { content-http: { url: http://127.0.0.1:8081/mcp } } }配置完成后客户端会走“识别工具列表、按 Schema 生成参数、调用工具、返回结果”这条链路。如果配置没有生效大多数时候是命令或端口不对而不是协议出错。6.2 用 LangChain 和 RAG 场景封装调用LangChain 生态里已经出现对应 MCP 的适配层可以把 MCP Server 中的工具作为 Agent 工具暴露给模型。整套链路是Agent / Chain - MCP Client Adapter - MCP Server - ContentAgentService - MySQL对内容平台来说常见的场景是检索增强生成。不要让模型一次性读取全部内容而是定义searchContent工具让模型根据用户问题决定是否搜索。搜索返回摘要和价格模型再判断是否进一步询问或展示购买链接。这里有一个关键点RAG 和 MCP 不是二选一。MCP 负责工具调用和权限控制RAG 负责把相关内容片段嵌入上下文。两者可以组合但权限边界必须放在 MCP Server 这一层。6.3 MCP 与 Agent Skill 如何配合Agent Skill 更关注“任务怎么编排”。比如一个“付费内容导购” Skill 可能是询问用户想找什么主题。调用searchContent搜索内容。调用getContentAccess判断用户是否购买过。未购买则展示摘要和价格引导下单。每一步需要的能力都由 MCP 提供。所以排错时要先分清是哪一层出问题。如果工具能调用成功但回答逻辑不对多半是 Skill 编排问题如果工具列表为空或调用报 method not found多半是 MCP 协议层或工具注册问题。6.4 验证 MCP 工具是否注册成功连接前最直接的方法是查看客户端是否有“MCP 工具列表”窗口。若工具列表为空依次检查MCP Server 是否正常启动。工具类是否被 Spring 扫描。注册代码是否执行。传输方式是否与客户端配置匹配。有条件的话用 MCP Inspector 这类调试工具连接 Server查看它获取到的 tools 列表。列表中有searchContent、getContentAccess、listPaidOrders才说明注册成功。7. 常见异常排查7.1 工具注册不上或调用报 method not found现象是客户端能力列表为空或调用时报Method not found。可能原因包括工具类没有被 Spring 容器管理、注册代码没执行、传输方式不匹配。排查顺序是查看 Spring Boot 启动日志确认是否加载了 MCP 配置类。在工具类构造函数里加一行日志确认 Bean 是否创建。用 MCP Inspector 连接查看实际返回的 tools 列表。对比客户端配置中的 URL、端口、协议是否一致。查看是否重复启动多个 Server端口被占用。预防建议把工具注册步骤做成独立配置类每次加新工具后都先用 Inspector 验证再接入 Agent 逻辑。7.2 MCP Server 连接超时或进程被杀死stdio 方式下AI 客户端会启动 Java 进程。如果环境变量缺失、数据库连接超时、进程内存不足可能直接启动失败或中途退出。排查时先手动执行配置中的命令java -jar content-mcp-server.jar观察是否能正常启动。再检查数据库连接地址、账号权限和环境变量。生产环境建议用 HTTP/SSE 方式独立部署客户端和 Server 进程分离避免客户端频繁启动进程导致资源耗尽。7.3 未购买用户拿到了正文这是付费内容系统最严重的问题。现象是 AI 回答里包含完整付费正文。可能原因有三个MCP 工具返回对象直接使用了数据库实体把body字段带出去了。权限判断只校验了订单状态没有校验内容归属。向量库存储了付费全文检索结果被拼入上下文。排查方式先查工具返回的 JSON确认有没有body字段。再查调用日志确认调用者传的 userId 是否被正确使用有没有出现越权拼接。最后看向量库数据来源是否包含未脱敏正文。解决方案是返回对象使用 DTO不返回实体权限校验必须基于 userId 和 contentId 联合查询向量库只保留摘要和已授权片段。7.4 工具返回 JSON 序列化失败Java 对象常用BigDecimal、LocalDateTime如果 MCP Server 的 JSON 序列化配置不完整可能在返回订单金额或者时间时出错。排查日志里看字段类型确认 DTO 中金额字段是BigDecimal时间字段是标准格式。必要的时候可以统一把时间转成字符串把金额转成字符串或数字减少客户端解析歧义。public record OrderInfo( String orderNo, BigDecimal amount, String statusText, String paidAt ) {}7.5 多商户数据串了现象是用户查询 A 商户内容时看到了 B 商户的数据。常见原因是 SQL 没有带merchant_id条件或者 MCP 工具接收的merchantId参数没有被用于查询。排查时先看 SQL 日志确认 where 条件是否完整。再看客户端调用时有没有传 merchantId。生产环境建议在 Service 层增加断言强制要求商户维度必须存在if (merchantId null) { throw new IllegalArgumentException(merchantId 不能为空); }同时为每个多商户查询方法增加测试用例分别传不同 merchantId确认返回结果互不包含。7.6 排错清单现象常见原因检查方式处理建议工具列表为空工具类未注册MCP Inspector 查看 tools检查注册代码和组件扫描调用超时数据库慢查询或无连接数据库慢日志、连接池状态优化 SQL、增加连接池返回正文泄露返回了数据库实体检查工具返回 DTO使用权限裁剪后的 DTO数据跨商户SQL 缺少商户条件查看 SQL 日志强制带 merchantId 条件金额或时间序列化失败类型未处理查看 JSON 输出转换时间、金额格式进程反复被杀内存不足或环境变量错误手动启动观察日志独立部署增加资源限制8. 多商户付费内容系统的生产化建议8.1 安全基线不要相信 MCP 调用里用户自述的 userId必须由认证层解析并注入。付费内容不要在日志里打印完整正文只记录内容 ID 和访问结果。所有正文地址使用带签名的临时 URL并设置过期时间。工具返回对象单独封装不要直接复用数据库实体。内容下架后MCP Server 的搜索和访问接口都要同步过滤不能让已下架内容继续通过 AI 访问。8.2 日志、审计与监控多商户付费系统接入 AI 后日志不只是给开发人员看还要能回答“哪个用户、在哪次对话、通过哪个工具、访问了哪篇内容”这类审计问题。在 MCP 工具入口记录结构化日志{ event: mcp_tool_call, tool: getContentAccess, userId: 1001, merchantId: 20, contentId: 3001, accessible: true, timestamp: 2025-01-01T10:00:00Z }再结合调用链 ID可以快速定位一次 Agent 对话中所有工具调用。8.3 扩展方向第一补充 MCP 的资源和提示词能力。除了工具内容系统还可以把“商户信息”“内容详情模板”作为资源暴露减少 AI 在对话中猜测信息口径。第二把 MCP Server 与工作流编排结合起来。比如订单支付成功后通过 Webhook 通知 MCP Server 或相关 Agent让 AI 能感知用户权益变化而不是依赖客户端重新查询。第三结合 RAG 构建更智能的内容问答。前提是向量库数据经过脱敏检索结果再做一次权限过滤。8.4 上线前检查清单检查项说明商户隔离所有多商户查询都带 merchantId 条件权限校验正文访问接口必须校验订单或订阅字段裁剪返回 DTO 不含正文字段工具注册用 Inspector 验证工具列表日志审计记录调用者、工具名、目标内容、结果敏感数据日志和向量库不存全文连接配置生产环境使用 HTTP/SSE 独立部署监控告警工具调用失败率、超时率有告警回归测试每个工具都有权限正反例测试多商户付费内容系统接入 MCP最重要的判断是MCP 只负责把能力标准化地暴露给 AI权限和业务规则必须留在服务端。工具接口定义得越干净返回字段裁剪得越严格后续接入更多 Agent 场景时就越安全。对新手来说先用一个最小项目把searchContent跑通再逐步加入订单校验和商户隔离比一开始设计一个大而全的工具集更容易踩稳。