MCP 解决了什么问题

大语言模型擅长推理,但不擅长“接系统”。过去每接一个数据源,就要写一套专属的 function calling 定义、鉴权逻辑和提示词,N 个模型 × M 个数据源 = N×M 份重复劳动。

MCP(Model Context Protocol) 把这件事标准化:工具提供方暴露一个 MCP Server,模型侧只需实现 MCP Client。协议基于 JSON-RPC 2.0,传输层支持 stdio(本地)与 Streamable HTTP(远程)。

一、协议三件套

MCP Server 可以暴露三类能力:

  • Tools:可被模型调用的函数(有副作用);
  • Resources:可读取的数据(类似文件,用 URI 标识);
  • Prompts:预置的提示词模板。

二、用 TypeScript 实现第一个 MCP Server

npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript tsx
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "ops-tools", version: "1.0.0" });

// Tool 1:查询订单
server.registerTool(
  "get_order",
  {
    title: "查询订单",
    description: "根据订单号查询订单状态与金额",
    inputSchema: { orderId: z.string().regex(/^SO\d{6}$/, "订单号格式如 SO123456") },
    outputSchema: { status: z.string(), amount: z.number() },
  },
  async ({ orderId }) => {
    const row = await db.orders.find(orderId);
    if (!row) {
      return {
        content: [{ type: "text", text: `未找到订单 ${orderId}` }],
        isError: true,
      };
    }
    return {
      content: [{ type: "text", text: JSON.stringify(row) }],
      structuredContent: { status: row.status, amount: row.amount },
    };
  },
);

// Tool 2:只读的报表查询(带注解,提示客户端这是只读操作)
server.registerTool(
  "daily_report",
  {
    title: "日报",
    description: "获取指定日期的销售日报",
    inputSchema: { date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/) },
    annotations: { readOnlyHint: true, openWorldHint: false },
  },
  async ({ date }) => {
    const r = await db.reports.daily(date);
    return { content: [{ type: "text", text: JSON.stringify(r) }] };
  },
);

// Resource:暴露文档
server.registerResource(
  "runbook",
  "docs://runbook/oncall",
  { title: "值班手册", mimeType: "text/markdown" },
  async () => ({
    contents: [{ uri: "docs://runbook/oncall", text: await fs.readFile("./runbook.md", "utf8") }],
  }),
);

const transport = new StdioServerTransport();
await server.connect(transport);

inputSchema 用 Zod 声明,SDK 会自动生成 JSON Schema 并在调用前做校验——模型传错参数不会打到你的业务代码。

三、Python 版:FastMCP

# server.py
from mcp.server.fastmcp import FastMCP
from pydantic import Field

mcp = FastMCP("data-tools")

@mcp.tool(description="执行只读 SQL 查询")
def query(sql: str = Field(description="仅支持 SELECT 语句")) -> list[dict]:
    if not sql.strip().lower().startswith("select"):
        raise ValueError("只允许 SELECT")
    return run_readonly(sql)

@mcp.resource("schema://tables")
def tables() -> str:
    return json.dumps(list_tables(), ensure_ascii=False)

if __name__ == "__main__":
    mcp.run(transport="stdio")

四、客户端接入(Host 侧)

// Claude Desktop / Cursor 的配置
{
  "mcpServers": {
    "ops-tools": {
      "command": "node",
      "args": ["/abs/path/server.js"],
      "env": { "DB_URL": "postgres://..." }
    },
    "remote": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer xxx" }
    }
  }
}

五、安全设计要点

  1. 最小权限:给 Server 专用账号,只读工具只给只读库连接。
  2. 参数白名单:禁止拼接原始 SQL/命令,用参数化查询或枚举约束。
  3. 审计日志:记录每次 tool 调用的入参与调用方,便于追溯。
  4. 危险操作二次确认:删除、转账类工具返回 elicitation,要求人工确认。
// 要求用户在客户端侧确认
const ok = await server.elicitInput({
  message: `确认删除订单 ${orderId}?`,
  requestedSchema: { type: "object", properties: { confirm: { type: "boolean" } } },
});
if (!ok?.confirm) return { content: [{ type: "text", text: "已取消" }] };

六、调试

# 官方 Inspector,可视化查看 tools/resources 并直接调用
npx @modelcontextprotocol/inspector node server.js

小结

MCP 把“给模型接工具”从定制开发变成了标准集成。写一个 Server 的成本不到一百行代码,却能让你的系统在所有支持 MCP 的客户端里被直接调用——这是当前 AI 工程里投入产出比最高的一块拼图。