AI 每日一课 #008MCP 不是“给 AI 装插件”这么简单。它更像一层标准化上下文和工具协议,把 Agent、工具、数据源之间的边界说清楚。
引言
MCP 这两年很火。
我见过一种很危险的 MCP Server。
run_sql(query: string)模型想查什么,就让它拼 SQL。
Demo 很爽。
接到业务系统就很吓人。
它绕过了太多边界:权限、字段脱敏、查询范围、审计、限流、误操作保护。你以为自己给 Agent 接了一个工具,实际上开了一个数据库后门。
这也是我不太喜欢把 MCP 简单讲成“AI 插件”的原因。
MCP 规范把能力拆成 tools、resources、prompts;Anthropic 发布 MCP 时强调的是连接 AI assistant 和数据源的开放标准;MCP security best practices 也专门讲 consent、access control、tool safety 和数据保护。看完这些材料再回头看 run_sql,问题就很明显:它把边界拆掉了。
业务里接 MCP,我不太关心一口气能接多少 Server。我会先看这层协议边界有没有设计清楚。
具体就是这些问题:
• Host 怎么发现 Server 提供了哪些能力; • Server 怎么声明 tools、resources、prompts; • Client 怎么把模型请求转成协议调用; • 工具参数和返回值怎么结构化; • 数据源怎么暴露给模型而不是直接塞进 prompt; • 权限和执行边界应该放在哪里。
订单查询适合拆 MCP:模型不能直接连数据库,业务系统也不应该把整张订单表塞给模型。中间这层边界,才是 MCP 最容易被写歪的地方。
先把角色分清楚
MCP 里有几个角色,容易混。
可以先按这个图理解:

在这个结构里:
“模型调用 MCP”这句话容易让人误解。真实链路更接近:
模型表达意图 -> Host 决定是否调用工具 -> MCP Client 调 MCP Server -> Server 调业务系统 -> 结果回到 Host -> 再给模型模型不应该直接连数据库。
MCP Server 也不应该把自己做成一个没有边界的万能后门。
Tools、Resources、Prompts 不是一回事
MCP Server 可以暴露多种能力。
最常见的是三类:
tools 动作:查订单、创建工单、触发搜索resources 资料:订单详情、文档片段、文件内容prompts 模板:按某种格式生成查询、审查、总结任务这三类不能混用。
比如订单系统里:
如果把所有东西都做成 tool,模型会更容易乱调。
如果把所有东西都塞成 resource,模型又缺少结构化动作。
MCP 的价值就在这里:把“能读什么”和“能做什么”拆开。
一个最小订单查询 MCP Server
这里用 TypeScript 写一个简化版。
这段代码不追求覆盖所有 SDK 细节,重点是看清 MCP Server 应该怎么组织边界。
先定义订单查询输入:
import { z } from "zod";const GetOrderInput = z.object({ orderId: z.string().min(1), requesterId: z.string().min(1),});type Order = { orderId: string; userId: string; status: "created" | "paid" | "shipped" | "cancelled"; amount: number; address: string;};然后写一个脱敏函数。
MCP Server 返回给模型的数据,最好不要直接等于数据库原始数据:
function maskAddress(address: string) { return address.replace(/(.{6}).+(.{4})/, "$1****$2");}function toModelSafeOrder(order: Order) { return { orderId: order.orderId, status: order.status, amount: order.amount, address: maskAddress(order.address), };}再写 tool handler:
async function getOrderForModel(input: unknown) { const parsed = GetOrderInput.parse(input); const order = await orderService.findById(parsed.orderId); if (!order) { return { ok: false, reason: "order_not_found", }; } if (order.userId !== parsed.requesterId) { return { ok: false, reason: "owner_mismatch", }; } return { ok: true, order: toModelSafeOrder(order), };}这里先别急着看 SDK 写法,几个边界更重要。
参数要校验,不要让模型传什么都进业务系统。
权限要在 Server 侧再校验,不要因为 Host 传了 requesterId,就默认它一定可信。
返回值要脱敏。模型很多时候只需要知道“订单已发货”,不需要知道完整地址、手机号、身份证号。
MCP Server 不是越全越好
很多团队一接 MCP,就想把系统能力全暴露出去:
get_ordersearch_ordersupdate_orderrefund_orderdelete_ordersend_smsexport_user_data这很危险。MCP Server 的工具设计,我会先按一个原则收住:
给模型完成任务需要的最小动作,不给它业务系统的完整后台权限。
比如客服场景里,第一版可以只开放只读工具:
type ToolRisk = "read" | "write" | "money" | "message";type ToolDefinition = { name: string; risk: ToolRisk; description: string; requiresApproval: boolean;};const tools: ToolDefinition[] = [ { name: "get_order", risk: "read", description: "查询当前用户订单详情", requiresApproval: false, }, { name: "search_policy", risk: "read", description: "查询售后政策文档", requiresApproval: false, }, { name: "create_ticket", risk: "write", description: "创建人工客服工单", requiresApproval: true, },];像 refund_order、delete_order、send_sms 这类工具,不应该第一天就开放。
即使开放,也必须有审批、幂等、审计、速率限制和策略拦截。
MCP 只是协议。
协议不会自动替你做好业务安全。
Resources 更适合做上下文,不适合做动作
假设模型需要查看订单政策文档。
你可以把政策文档做成 resource,而不是 tool。
type Resource = { uri: string; title: string; mimeType: string;};const resources: Resource[] = [ { uri: "policy://after-sale/refund", title: "售后退款政策", mimeType: "text/markdown", }, { uri: "policy://shipping/address-change", title: "收货地址修改政策", mimeType: "text/markdown", },];resource 的好处是,它天然表达“这是资料”。
模型可以读取它、引用它、基于它回答,但它不应该产生副作用。
如果一个东西只是文档、配置、知识库片段、文件内容,不要硬做成 tool。
这会让 Agent 更容易区分:
我是在读资料,还是在执行动作?这个边界直接关系到安全。
别把 MCP Server 写成万能后门
最容易踩的坑,是把 MCP Server 写成数据库万能代理。
比如给模型一个工具:
run_sql(query: string)这种接口表面上很灵活。
实际很危险。
模型可能会生成:
SELECT * FROM orders WHERE user_id = 'u_1';也可能生成:
DELETE FROM orders;就算你限制只读,也可能查出太多敏感字段。
更好的方式是把业务动作封成窄工具:
const allowedOrderFields = ["orderId", "status", "amount", "createdAt"] as const;type OrderSummary = Pick<Order, (typeof allowedOrderFields)[number]>;async function listUserOrderSummaries(input: { requesterId: string; limit: number;}): Promise<OrderSummary[]> { const limit = Math.min(input.limit, 20); const orders = await orderService.listByUser(input.requesterId, { limit }); return orders.map((order) => ({ orderId: order.orderId, status: order.status, amount: order.amount, createdAt: order.createdAt, }));}这段代码牺牲了一点灵活性,换来的是可控:
• 只能查当前用户; • 最多返回 20 条; • 只返回允许字段; • 不暴露 SQL; • 不允许模型临时发明查询。
业务系统接 MCP,优先要这种窄接口。不是越通用越好。
我会怎么检查一个 MCP Server
如果我要 review 一个业务 MCP Server,我会先看这些问题。
• 工具是不是太宽? run_sql、call_api、execute_action这类万能工具要非常谨慎。• 输入有没有 schema? 每个 tool 都应该有明确输入结构,不能把
any直接丢给业务系统。• 权限在哪里校验? 不能只相信模型或 Host 传来的上下文,Server 侧也要校验业务权限。
• 返回值有没有脱敏? 模型需要的是完成任务的信息,不是数据库原始行。
• 写操作有没有审批和幂等? 修改地址、退款、发短信、删数据,都不能只靠模型判断。
• Resources 和 Tools 有没有分开? 文档和知识库用 resource,动作才用 tool。
• 有没有审计日志? 谁通过哪个 Host 调了哪个 tool,参数是什么,结果是什么,要能追。
这些问题比“接了几个 MCP Server”更值得先看。
MCP 接得多,不代表系统更强。边界不清,反而会让 Agent 拥有一堆不该拥有的能力。
结尾
我现在看一个业务 MCP Server,会先问两句话:
这个能力到底是资料、动作,还是模板?权限、脱敏、审计、审批和幂等写在哪里?这两个问题答不上来,就先别急着接模型。
最小目录可以这样放:
mcp-server/ tools/ get-order.ts # 窄工具,带输入校验和权限校验 create-ticket.ts # 写工具,带审批和审计 resources/ refund-policy.ts # 文档和政策上下文 prompts/ customer-summary.ts # 可复用任务模板 auth.ts # requester / tenant / permission 校验 audit-log.ts # 每次调用可追踪做到这一步,MCP 才不是“装了个插件”,而是把 Agent 能读什么、能做什么、怎么受控地做写清楚。
资料来源
1. Model Context Protocol 文档:Introduction 2. Model Context Protocol 规范:Basic protocol 3. Model Context Protocol 规范:Tools 4. Model Context Protocol 规范:Resources 5. Model Context Protocol 规范:Prompts 6. Model Context Protocol 规范:Security best practices 7. Model Context Protocol TypeScript SDK:modelcontextprotocol/typescript-sdk 8. Anthropic 新闻稿:Introducing the Model Context Protocol
夜雨聆风