做了内容平台之后,你迟早会遇到这样的诉求:想接入 AI 能力,让模型能查资料、能写内容、能帮用户找文章。最开始大家本能地写一个 REST API,然后把 API 文档扔给模型,希望模型自己学会调用。结果模型要么找不到入口,要么把付费内容当成免费资源直接搬运,要么在一个多商户系统里串了数据。
这不是开发能力的问题,而是内容系统正在面对一个过去没有的结构性变化:你的平台不再只服务“人”,还要服务“AI 代理”。人可以通过网页操作,AI 代理却需要一套它能理解、能安全调用的工具接口。MCP(Model Context Protocol)就是这个环节里目前最值得采用的标准化方案。
这篇文章想聊的不是“MCP 是什么”这种扫盲,而是把“MCP 内容系统开发”当做一个真实的工程项目来分析:它的架构应该怎么设计,多商户和付费内容怎么落在 MCP 工具层,写代码时有哪些容易踩的坑,以及怎么在生产环境里保证数据不越权、内容不泄漏。读完你会得到一套可以落地的设计思路和最小示例。
1. 为什么要关注 MCP 内容系统
过去做内容系统,核心用户是浏览器和 App。你提供页面、API、权限体系,人在界面上完成阅读、购买和管理。这套模型很成熟,REST API、前后端分离、微服务,任何一个做了三五年开发的工程师都能顺手搭出来。
但现在不一样了。越来越多用户通过 AI 助手访问内容:让助手总结某篇付费文章,让助手在平台内搜索指定领域的资料,甚至让助手在内容平台上创建文案。这些场景里,AI 代理不是一个“浏览器模拟器”,它需要按协议调用工具。你原来的 REST API 当然也能被模型调用,但问题是模型并不知道你的接口该怎么拼、参数是什么、什么时候能用、什么时候不能用。
MCP 解决的是这个“接口发现与标准化”的问题。它像一个插头标准,把内容系统包装成 AI 代理可以动态发现的工具集。只要客户端支持 MCP,模型就能读取你提供的工具清单,按 Schema 调用,拿到结构化结果。
如果只看到这一层,很多人会觉得 MCP 不过是“给 AI 用的 API 网关”。真实情况比这复杂。内容系统一旦接入 MCP,至少有三个问题会被瞬间放大:
- 数据越权:MCP 工具一旦返回了全部内容,就可能被 AI 代理透传出来,付费墙形同虚设。
- 多商户隔离:一个内容平台往往有多个商户,商户 A 的内容不能让商户 B 的 AI 代理读到,更不能让平台运营方之外的人随意搜索到。
- 状态管理:普通的 REST 调用有身份认证,有会话,有审计;MCP 工具调用如果设计不好,就会变成无状态、无身份、无日志的“三无接口”。
所以我的判断是:MCP 内容系统开发的重点,并不是“把一个 API 改写成 MCP”,而是你在协议层重新思考内容授权、租户边界和支付边界。工具只是外壳,隔离和权限才是内核。
2. MCP 核心概念与内容系统的关系
先花一小节把基础概念对齐,后面所有代码和架构讨论都依赖这里。
2.1 MCP 的三个关键概念
MCP 由三个角色组成:
- MCP Client:发起工具调用的 AI 应用,比如 Claude Desktop、Cursor、Codex,以及你自研的 AI Agent。
- MCP Server:提供工具、资源、提示词的本地服务或远程服务,你的内容系统需要暴露能力时,就是实现一个 MCP Server。
- 传输层:MCP 支持 stdio 和 Streamable HTTP 两种主流传输方式。本地客户端常用 stdio,生产环境多采用 HTTP 方式远程部署。
在 MCP 协议里,内容系统最常用到三类能力:
| 能力 | 说明 | 内容系统里的典型场景 |
|---|---|---|
| Tool(工具) | 可被模型调用的函数,带参数和返回值 | 搜索内容、获取文章详情、提交内容、管理订单 |
| Resource(资源) | 暴露结构化数据,供模型读取上下文 | 商户配置、系统提示词、内容目录 |
| Prompt(提示) | 预定义提示模板 | 内容总结、SEO 文案生成、付费推荐话术 |
对“MCP 内容系统”而言,工具是最核心的部分。因为付费内容、多商户权限、内容管理,本质上都是一个个“可授权操作”。
2.2 MCP 与 REST API 有什么区别
很多人问:我都有 REST API 了,为什么还要 MCP?直接让 AI 调用 REST API 不行吗?
可以,但代价高。REST API 是给开发者看的,开发者会看文档、按规范调、处理错误码。AI 模型没有这个“阅读文档并严格按文档调用”的稳定性。MCP 的意义在于让工具 Schema 成为模型上下文的一部分,模型通过工具描述就知道该传什么参数、不该传什么参数。
口径上可以这样理解:
- REST API 适合“人写代码去调用”,MCP 适合“AI 动态理解和调用”。
- REST API 是接口标准,MCP 是工具发现与调用协议。
- 内容系统可以两者共存:REST 服务业务前台,MCP 服务 AI 代理,底层复用同一套领域逻辑。
2.3 MCP 与 Agent Skill 的区别
近半年社区里讨论很多的一个问题是“Agent Skill 和 MCP 有什么区别”。如果只记一句话:MCP 是协议层,Skill 是能力层。
MCP 规定的是“工具怎么被客户端发现、怎么调用、怎么返回结果”,它不关心你把这些工具组合成一个多大的“技能包”。Agent Skill 则更像一种指令集和行为编排,比如“内容编辑技能”可以包含:调用搜索工具、调用文章生成工具、调用审核工具,并且定义了这些工具的执行顺序和策略。
在实际工程里,两者是配合关系。你可以在 Agent 系统里定义多个 Skill,每个 Skill 内部调用不同的 MCP 工具;也可以只把 MCP 工具暴露给通用 Agent,让 Agent 自己决定调用顺序。本书的例子以后者为主,这样更接近 MCP 的原生使用方式。
3. 整体架构与技术选型
3.1 系统分层
一个可扩展的 MCP 内容系统,我建议至少分四层:
[人工客户端 Web/App] [AI 客户端 Claude/Cursor/自研 Agent] | | | REST API | MCP 协议 v v [接入网关层] [MCP Server 层] | | +-----------+--------------+ v [内容引擎层:内容、订单、订阅、支付] | v [数据层:PostgreSQL、Redis、对象存储]关键点在于:MCP Server 不应直接操作数据库,它应该调用内容引擎的领域服务。这样同一个业务逻辑既能服务 REST API,也能服务 MCP 工具,不会出现两套规则。
3.2 技术选型
- 后端语言:我基于 TypeScript/Node.js 写示例,因为 MCP 的 TypeScript SDK 非常成熟,社区样例多。项目生产环境也可以选 Python,原理一致。
- Web 框架:Express 或 Fastify,用于对外 REST API 和 Streamable HTTP 传输。
- 数据库:PostgreSQL,多商户隔离和事务处理能力强。
- 缓存与限流:Redis,用于接口限流和热点内容缓存。
- 对象存储:S3 兼容存储,用于保存富媒体内容。
- MCP SDK:
@modelcontextprotocol/sdk,目前版本迭代较快,建议按官方文档锁定版本。
关于“mcp服务java”这类搜索词也说明一点:MCP 不是某种语言的专属,官方 SDK 覆盖 TypeScript、Python、Java、Kotlin 等。如果你的技术栈是 Java,完全可以用 Spring Boot 实现 MCP Server,架构思路不变。
4. 环境准备与基础配置
先跑通一个最小环境,后面所有代码都基于这个环境。版本号以你当前工具链为准,本文不锁死具体版本。
4.1 安装基础环境
# 需要 Node.js 18+ node -v # 初始化项目 mkdir mcp-content-system cd mcp-content-system npm init -y # 安装核心依赖 npm install express pg redis @modelcontextprotocol/sdk zod dotenv # 开发依赖 npm install -D typescript tsx @types/express @types/node4.2 创建基础目录结构
mcp-content-system/ ├── src/ │ ├── mcp/ │ │ └── server.ts # MCP Server 定义 │ ├── routes/ │ │ └── content.ts # REST API 路由 │ ├── services/ │ │ └── contentService.ts # 内容领域服务 │ ├── db/ │ │ └── pool.ts # 数据库连接池 │ └── config/ │ └── index.ts # 环境配置 ├── .env ├── tsconfig.json └── package.json4.3 配置环境变量
在.env文件中准备数据库和对象存储配置:
DATABASE_URL=postgres://user:password@localhost:5432/content_db REDIS_URL=redis://localhost:6379 OBJECT_STORAGE_ENDPOINT=http://localhost:9000 OBJECT_STORAGE_BUCKET=content-bucket MCP_AUTH_TOKEN=sk-local-dev-token PORT=8080环境变量统一集中管理,不要在代码里硬编码。
5. 内容引擎数据模型与多商户设计
内容系统的第一步,是把数据模型设计好。尤其要多商户,这一步做错了后面全是坑。
5.1 商户表
CREATE TABLE merchants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );5.2 内容表
CREATE TABLE contents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), merchant_id UUID NOT NULL REFERENCES merchants(id), title VARCHAR(255) NOT NULL, summary TEXT NOT NULL DEFAULT '', body TEXT NOT NULL, status SMALLINT NOT NULL DEFAULT 0, access_type VARCHAR(20) NOT NULL DEFAULT 'free', price DECIMAL(10,2) NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_contents_merchant_status ON contents(merchant_id, status);access_type只有两种值:free和paid。status为 1 表示已发布,0 表示草稿。
多商户的核心设计原则是:所有内容表必须有merchant_id,并且所有查询强制带merchant_id条件。不要靠应用层记住“这个用户属于哪个商户”,要在数据库层就隔离。
5.3 付费访问表
用户是否买过某个内容、订阅是否还在有效期内,单独建表:
CREATE TABLE content_access ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_id UUID NOT NULL REFERENCES contents(id), user_id UUID NOT NULL, merchant_id UUID NOT NULL, access_type VARCHAR(20) NOT NULL DEFAULT 'single', expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(content_id, user_id) ); CREATE INDEX idx_content_access_user ON content_access(user_id, expires_at);这个表的用途是:只要这里有一条有效记录,用户对这篇内容就是已授权状态。MCP 工具在返回付费正文前,必须先查这张表。
6. 核心业务实现:付费内容 API
在写 MCP 之前,先把普通 API 写出来。因为 MCP 工具最终会复用这里的领域逻辑,而不是另起炉灶。
先创建基础服务文件src/services/contentService.ts:
// 文件路径:src/services/contentService.ts import { pool } from '../db/pool'; export interface ContentDetail { id: string; merchantId: string; title: string; summary: string; body: string; accessType: string; price: number; } const PUBLIC_FIELDS = ` id, merchant_id, title, summary, body, access_type, price `; export async function findContentById(id: string): Promise<ContentDetail | null> { const result = await pool.query( `SELECT ${PUBLIC_FIELDS} FROM contents WHERE id = $1 AND status = 1`, [id] ); if (result.rowCount === 0) return null; const row = result.rows[0]; return { id: row.id, merchantId: row.merchant_id, title: row.title, summary: row.summary, body: row.body, accessType: row.access_type, price: row.price, }; } export async function checkUserContentAccess( contentId: string, userId: string ): Promise<boolean> { const result = await pool.query( `SELECT 1 FROM content_access WHERE content_id = $1 AND user_id = $2 AND (expires_at IS NULL OR expires_at > now())`, [contentId, userId] ); return (result.rowCount ?? 0) > 0; }然后写 REST 路由src/routes/content.ts:
// 文件路径:src/routes/content.ts import { Router } from 'express'; import { findContentById, checkUserContentAccess } from '../services/contentService'; export const contentRouter = Router(); contentRouter.get('/api/v1/contents/:id', async (req, res) => { const { id } = req.params; const userId = req.headers['x-user-id'] as string | undefined; const content = await findContentById(id); if (!content) { return res.status(404).json({ error: 'content_not_found' }); } if (content.accessType === 'free') { return res.json(content); } if (!userId || !(await checkUserContentAccess(content.id, userId))) { return res.status(403).json({ error: 'paid_content_requires_access', preview: content.summary, price: content.price, }); } return res.json(content); });这段逻辑是整篇文章的关键。后续 MCP Server 里的“读付费内容”工具,会调用同一个findContentById和checkUserContentAccess,只是入参来源从 HTTP Header 换成 MCP 上下文。
如果你在真实项目中已经有一套成熟的用户体系,可以把userId换成你自己的认证信息,比如 JWT 解析结果。务必要做到:MCP 拿到的用户身份不是客户端自己报出来的,而是认证网关解析并注入的。
7. MCP Server 实现:把内容系统暴露给 AI
现在进入正题。用 MCP SDK 创建一个内容平台 MCP Server。
7.1 创建设置基本框架
// 文件路径:src/mcp/server.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { findContentById, checkUserContentAccess } from '../services/contentService'; const server = new McpServer({ name: 'content-hub-mcp', version: '1.0.0', });7.2 定义第一个工具:搜索内容
AI 代理最常见的需求是搜索内容。注意,搜索工具只返回标题和摘要,不返回正文。这是付费内容安全的第一道防线。
// 搜索内容,只返回元信息 server.tool( 'search_contents', '在内容平台搜索已发布内容,返回标题、摘要、价格和访问类型,不返回正文', { keyword: z.string().describe('搜索关键词'), merchantId: z.string().uuid().optional().describe('商户ID,可选'), }, async ({ keyword, merchantId }) => { const result = await pool.query( `SELECT id, merchant_id, title, summary, price, access_type FROM contents WHERE status = 1 AND ($1 = '' OR title ILIKE '%' || $1 || '%') AND ($2::uuid IS NULL OR merchant_id = $2) ORDER BY created_at DESC LIMIT 20`, [keyword, merchantId ?? null] ); return { content: [{ type: 'text', text: JSON.stringify(result.rows), }], }; } );7.3 定义第二个工具:读取免费内容
免费内容可以直接返回正文,但应在工具描述里明确规定“仅限免费内容”。
server.tool( 'read_free_content', '读取免费内容的正文,仅适用于 access_type 为 free 的内容,付费内容请调用 check_paid_content_access', { contentId: z.string().uuid(), }, async ({ contentId }) => { const content = await findContentById(contentId); if (!content) { return { content: [{ type: 'text', text: 'content_not_found' }], isError: true }; } if (content.accessType !== 'free') { return { content: [{ type: 'text', text: 'this_content_is_paid_access_denied' }], isError: true, }; } return { content: [{ type: 'text', text: content.body }] }; } );7.4 定义第三个工具:读取付费内容
付费内容的权限校验必须放在 MCP Server 内,不能依赖外部模型自觉。这里我们从 MCP 调用上下文里取userId。实际项目中,这个userId可以由 MCP Client 通过 OAuth 或令牌注入,也可以在远程 HTTP 部署时由网关解析。
server.tool( 'read_paid_content', '读取付费内容全文。只有已购买或已订阅的用户才可以读取。若未授权,返回错误提示。', { contentId: z.string().uuid(), }, async ({ contentId }, extra) => { const userId = (extra as any).auth?.userId; // 真实项目中由认证网关注入 if (!userId) { return { content: [{ type: 'text', text: 'unauthorized_missing_user' }], isError: true, }; } const content = await findContentById(contentId); if (!content) { return { content: [{ type: 'text', text: 'content_not_found' }], isError: true }; } const hasAccess = await checkUserContentAccess(content.id, userId); if (!hasAccess) { return { content: [{ type: 'text', text: 'paid_content_requires_access' }], isError: true, }; } return { content: [{ type: 'text', text: content.body }] }; } );7.5 启动 MCP Server
MCP 最常用的本地传输方式是 stdio。把服务挂到标准输入输出上:
const transport = new StdioServerTransport(); await server.connect(transport);用 tsx 直接运行:
npx tsx src/mcp/server.ts你也可以用 MCP Inspector 可视化调试:
npx @modelcontextprotocol/inspector node dist/mcp/server.jsInspector 会打开一个本地调试面板,列出所有工具,手动传参调用,非常适合验证工具是否注册成功。
8. 多商户隔离与安全边界
前面几次提到多商户隔离,这一节专门把它讲透。
8.1 多商户可能出现的三个问题
- 数据越权:商户 A 的 AI 代理请求了商户 B 的内容。
- 搜索越权:全平台搜索返回了其他商户的私密内容。
- 管理越权:商户 A 的运营人员通过 MCP 工具修改了商户 B 的内容。
8.2 制定隔离策略
在多商户系统里,所有 MCP 工具应该遵循同一套租户上下文。什么是租户上下文?就是一次 MCP 调用里,服务端能够识别这是哪个商户发起的请求。
推荐做法:
- 每个商户一个独立 API Key / Token。AI 客户端配置 MCP Server 时,通过环境变量注入该商户的 Token。
- MCP Server 启动时读取 Token,把它绑定到当前会话的
merchantId。 - 所有数据查询都强制加上
merchant_id条件。
对应到 MCP 工具里,可以在工具外层包一个公共上下文:
// 文件路径:src/mcp/context.ts export interface McpContext { merchantId: string; userId?: string; }然后在读取内容时,不仅校验contentId,还校验这个内容的merchant_id是否与当前上下文一致。修改read_free_content为例:
server.tool( 'read_free_content', '读取免费内容的正文,仅限当前商户可见内容', { contentId: z.string().uuid(), }, async ({ contentId }, extra) => { const context = getMcpContext(extra); const content = await findContentById(contentId); if (!content) { return { content: [{ type: 'text', text: 'content_not_found' }], isError: true }; } if (content.merchantId !== context.merchantId) { return { content: [{ type: 'text', text: 'cross_merchant_access_denied' }], isError: true }; } if (content.accessType !== 'free') { return { content: [{ type: 'text', text: 'this_content_is_paid_access_denied' }], isError: true }; } return { content: [{ type: 'text', text: content.body }] }; } );还可以定义全站工具查重。下面的check_merchant_content_count工具返回某个商家库存的已发布内容总数,供运营 Agent 辅助统计:
server.tool( 'count_merchant_contents', '统计当前商户内容总数,按访问类型分组', {}, async (_args, extra) => { const context = getMcpContext(extra); const result = await pool.query( `SELECT access_type, count(*)::int AS cnt FROM contents WHERE merchant_id = $1 AND status = 1 GROUP BY access_type`, [context.merchantId] ); return { content: [{ type: 'text', text: JSON.stringify(result.rows) }] }; } );在数据库层还可以加行级安全策略(RLS),这样即使应用层代码漏写了merchant_id过滤,数据库也会拒绝跨商户查询。这一步强烈建议在生产开启:
ALTER TABLE contents ENABLE ROW LEVEL SECURITY; CREATE POLICY content_merchant_isolation ON contents USING (merchant_id = current_setting('app.merchant_id')::uuid);RLS 是双保险,不是替代品。它不能解决所有 MCP 上下文传递问题,但能挡住最危险的一类 SQL 漏洞。
9. 接入 AI 客户端与效果验证
写完 MCP Server,需要在真实的 AI 客户端里验证工具能不能被正确调用。
9.1 配置 MCP Server
一般支持 MCP 的客户端都使用一个 JSON 配置。下面以命令行工具为例:
mcp add content-hub \ -- command node \ -- args dist/mcp/server.js \ -- env CONTENT_API_BASE=http://localhost:8080 \ -- env MCP_AUTH_TOKEN=sk-merchant-a-token也可以手写 JSON 配置文件:
{ "mcpServers": { "content-hub": { "command": "node", "args": ["dist/mcp/server.js"], "env": { "CONTENT_API_BASE": "http://localhost:8080", "MCP_AUTH_TOKEN": "sk-merchant-a-token" } } } }注意:MCP Server 通过 stdio 进程启动,环境变量里的MCP_AUTH_TOKEN就是当前商户的标识。生产环境建议使用 HTTP 传输,把 Token 放到 Authorization Header,而不是进程环境变量。
9.2 手动验证工具注册
在支持 MCP 的客户端里,输入:
列出当前可用的 MCP 工具如果配置正确,应该能看到search_contents、read_free_content、read_paid_content、count_merchant_contents这些工具名。
然后可以下两句自然语言指令:
搜索关键词“MCP”的内容,返回前三条的标题和摘要。此时客户端会调用search_contents,返回结果应该是元信息 JSON,而不是正文。
再下一条:
读取这篇内容的全文:<contentId>如果内容为付费且未授权,工具返回paid_content_requires_access;如果授权了,才返回正文。这一步能直观验证付费边界是否生效。
9.3 观察 MCP 调用日志
MCP Server 启动后,控制台不会被 AI 客户端的输出打断,因为 stdio 是走标准输入输出传输,日志应该写到 stderr 或独立日志文件。开发时可以在服务里加一个简单日志:
console.error(`[MCP] tool=read_paid_content contentId=${contentId} user=${userId}`);在生产环境,建议把 MCP 调用日志结构化输出到日志系统,包含:客户端标识、商户 ID、用户 ID、工具名、请求参数摘要、返回状态、耗时。这套日志是未来审计和安全分析的基础。
10. 常见问题与排查思路
下面这些问题,是内容系统接入 MCP 后被问得最多的情况。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| MCP 客户端提示工具注册不上 | MCP Server 启动失败,或 stdout 被业务日志污染 | 在客户端外部直接运行node dist/mcp/server.js,检查 stderr 输出 | 日志输出到 stderr,确保 stdout 只留给 MCP 协议 |
| 工具存在但模型不调用 | 工具描述不清晰,或参数 Schema 太复杂 | 查看客户端调试界面,看模型是否已经读到工具列表 | 缩短工具描述,给出典型参数示例,同类工具合并 |
| 搜索返回了别的商户内容 | SQL 查询没有强制带merchant_id条件 | 查看 SQL 日志,检查查询参数 | 所有查询加租户条件,开启 RLS 做数据库兜底 |
| 付费内容被模型以“总结”的形式全部透传 | MCP 工具提供了正文,AI 把它复述出来 | 检查 Agent 提示词里是否允许全文引用 | 对 AI 客户端只提供摘要,正文只通过受控渲染读取 |
| 付费内容校验失败 | content_access表查不到记录,或过期时间判断错误 | 手动执行 SQL 查询该用户访问记录 | 检查时间比较逻辑,确保使用数据库时间而非应用服务器时间 |
| 工具调用超时 | 每次调用都全量读数据库,没有缓存 | 查看慢查询日志 | 对高频、低实时性需求增加 Redis 缓存;付费校验仍需实时 |
| 本地跑得通,远程连不上 | 使用了 stdio 传输,但远程环境不支持 | 检查远程进程是否把 stdout 作为日志输出 | 生产改用 Streamable HTTP 传输,不依赖 stdio |
关于“figma mcp 在 codex 中总是工具注册不上”这类类似工具注册问题,本质上也多是配置路径、stdout 污染或 SDK 版本不一致导致。调通一个 MCP Server 后,把同样的排查顺序应用到其他工具,往往能快速定位。
11. 生产环境最佳实践与安全边界
11.1 认证与最小权限
绝不能把管理员 Token 直接配置到 MCP Server 里。应该按商户维度签发独立的 Token,并且每个 Token 只具备该商户操作范围内的权限。可以理解为“商户级最小权限”。
同时要明确:MCP 工具列表里不要暴露不该暴露的工具。例如“删除商户”这样的管理工具,不应该出现在 AI 客户端的工具清单中。工具暴露范围要单独治理。
11.2 付费墙的保护措施
内容系统接 MCP 后,付费墙比普通网页更难保护。网页可以“一次加载全文,前端控制展示”,但 AI 客户端拿到全文后不会受前端控制。所以:
- 核心原则:正文只对已授权用户可见,摘要和元信息才是公开层。
- AI 代理需要“总结一篇付费文章”时,不应直接给正文,而是给摘要,或者由内容平台自己提供一个“生成授权摘要”工具。
- 不建议在一次工具调用里把所有正文塞进返回值,更不要同时返回多篇付费正文,避免上下文被批量带出。
11.3 限流与数据配额
AI 代理调用内容系统的频率可能远高于人工用户。一个会话内,模型可能反复调用搜索、反复读取。生产环境必须有配额和限流:
- 按商户和用户维度限制每分钟工具调用次数。
- 对
read_paid_content这类高价值工具单独设每日调用上限。 - 设置单次返回内容最大字节数,避免超大正文拖垮模型上下文。
11.4 审计日志
MCP 调用日志至少要包含这些字段:
{ "event": "mcp_tool_call", "timestamp": "2025-07-01T10:00:00Z", "merchantId": "merchant-a", "userId": "user-123", "toolName": "read_paid_content", "contentId": "content-456", "authorized": true, "durationMs": 120, "resultSize": 1024 }有了这些日志,当出现多商户数据越权、付费内容泄漏、配额超限时,你才有可能回溯责任链路。
11.5 部署形态建议
- 本地/开发环境:stdio 传输,进程由客户端拉起。
- 生产环境:用 Streamable HTTP 部署 MCP Server,统一走网关做认证、限流、审计。这样能避免 stdio 进程管理导致的不稳定。
- MCP Server 是无状态最佳。所有状态(用户身份、商户 ID、访问权限)从请求上下文解析,不放进 MCP Server 进程里。
11.6 与 RAG 管线的配合
如果你的系统已经做了 RAG 内容检索,MCP 能承担“更上层的工具调用”职责。RAG 负责向量化召回,MCP 负责向模型暴露可操作的工具。两者不是同一个层次,也不要互相替代。
例如:RAG 可以根据用户问题检索到“哪些内容相关”,然后把候选内容 ID 交给工具,由 MCP 的read_paid_content去校验权限并返回正文。这样的链路比把所有正文灌进向量库更安全,也更接近工程实践。
12. 总结与后续学习方向
MCP 内容系统开发,不是一个“做一个新网站”的活儿,而是给原有内容系统增加一层 AI 协议的接入能力。在这个过程中,最值得投入精力的不是 MCP 协议本身,而是内容授权、多商户隔离、付费边界这三件事。技术协议会随着版本迭代,但这三个工程问题会长期存在。
如果你接下来要实操,建议按这个顺序走:
- 先把内容表、访问表、商户表设计好,确保数据层有隔离基础。
- 实现一个最小 REST API,验证付费逻辑是否成立。
- 实现 MCP Server,把 REST 里已经写好的 area service 方法暴露成工具。
- 在 MCP Inspector 里验证工具注册和调用。
- 接入 AI 客户端,重点测试付费内容和跨商户访问。
- 最后再补生产环境的认证、限流、审计和远程部署。
后续值得深入的方向包括:MCP 的 Streamable HTTP 传输与网关集成、基于 OAuth 的 MCP 授权流程、以及多商户场景下的 RLS 策略细化。要是你在接入 MCP 时遇到工具注册、权限隔离或付费墙绕过的问题,欢迎在评论区把现象和日志贴出来一起讨论。