前言

最近做 AI 应用时,我越来越觉得模型本身只是其中一部分。真正让一个 AI 应用变得有用的,往往是它能不能读取项目文件、查询数据库、调用内部系统、按固定流程完成任务。以前这些能力基本靠每个应用自己写一套插件系统,写到最后就会出现大量重复的适配代码。

MCP,全称 Model Context Protocol,可以理解为给 AI 应用和外部系统之间加了一层标准协议。官方文档把它描述为一个开源标准,用来把 AI 应用连接到外部数据源、工具和工作流上。这个定义很朴素,但它解决的是 AI 应用里非常现实的问题:模型如何以统一方式拿到上下文,并安全地执行动作。

MCP 想解决什么问题

如果没有 MCP,一个编辑器、一个聊天机器人、一个自动化 Agent 想分别接入 Git、数据库、浏览器、工单系统,就需要各自维护一套接口。工具一多,就会变成 N 个客户端乘 M 个工具的组合复杂度。

MCP 的思路是把外部能力封装成 MCP Server,AI 应用侧通过 MCP Client 连接它。这样工具只要按协议暴露能力,支持 MCP 的客户端就都可以接入。类比一下,它不是让每个模型都懂所有系统,而是提供一个通用插座。

几个核心角色

Host 是真正承载用户交互的 AI 应用,比如桌面助手、IDE、聊天客户端。Client 在 Host 内部,负责和某个 MCP Server 建立连接。Server 则负责暴露外部能力,比如文件读取、数据库查询、搜索、内部 API、固定提示模板等。

从开发者视角看,Server 是最容易上手的入口。你可以先写一个只暴露 read_file、search_docs、query_issue 三个工具的小服务,让 AI 应用通过 MCP 调用它。以后换客户端时,只要协议支持,工具层不用跟着大改。

一个最小实践思路

我会先从“只读工具”开始做 MCP Server,例如只允许读取某个项目目录、查询只读数据库视图、搜索文档索引。等权限边界和日志都清楚了,再开放写操作。

实现时可以把每个工具当成普通后端接口:入参要有 schema,输出要结构化,错误也要明确。不要把“让模型自己猜”当成接口设计,工具越清晰,模型越不容易误用。

资源、提示词和工具怎么分

MCP 里经常会看到 Resources、Prompts 和 Tools 三类东西。我自己的理解是:Resources 是资料入口,Prompts 是工作方式,Tools 是动作按钮。比如博客文章、接口文档、数据库记录更像资源;“按我的博客风格扩写”更像提示词模板;“搜索文章”“读取文件”“查询 issue”才是工具。

这三类最好不要混在一起。资源应该尽量只读,提示词应该可复用,工具应该有清楚的入参和返回值。这样以后排查问题时会很直观:回答不准看资源召回,动作失败看工具日志,表达风格不统一看 prompt。

{
  "name": "search_posts",
  "description": "按关键词搜索博客文章,只返回标题、链接和摘要",
  "inputSchema": {
    "type": "object",
    "properties": {
      "keyword": { "type": "string" },
      "limit": { "type": "number", "default": 5 }
    },
    "required": ["keyword"]
  }
}

上面这个工具描述看起来很普通,但比一个模糊的 search 要可靠很多。模型知道它只能搜索博客文章,也知道必须传 keyword。工具越窄,越容易稳定;工具越泛,越需要额外权限和审计。

用 TypeScript 写一个最小工具

下面是一个偏伪代码的 TypeScript 示例,表达的是 MCP Server 暴露工具时的思路。真实项目可以直接使用官方 SDK,把传输、注册和参数校验交给框架处理。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

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

server.tool(
  "search_posts",
  {
    keyword: z.string().min(1),
    limit: z.number().int().min(1).max(10).default(5)
  },
  async ({ keyword, limit }) => {
    const rows = await searchLocalIndex(keyword, limit);
    return {
      content: [{
        type: "text",
        text: JSON.stringify({ status: "ok", data: rows })
      }]
    };
  }
);

这里最值得注意的是 limit 被限制在 1 到 10,keyword 不能为空。很多 Agent 不稳定,并不是模型突然变差,而是工具边界太松,导致它一次性请求太多内容,或者拿到一个格式很飘的返回值。

权限和日志要一开始就设计

MCP Server 不等于把整个系统权限交给模型。Server 应该像普通后端服务一样设计边界:文件工具限制目录,数据库工具只允许查询白名单视图,部署工具必须二次确认。能只读就先只读,等日志和权限都稳定了,再开放写操作。

const allowList = ["E:/blog/source/_posts"];

function assertReadable(path: string) {
  const normalized = normalizePath(path);
  const ok = allowList.some(root => normalized.startsWith(root));
  if (!ok) throw new Error("path is outside readable allow list");
}

日志也要从第一天开始收。至少记录工具名、入参摘要、耗时、结果状态和错误信息。不要把敏感数据完整写进日志,但要能在出问题时复盘:模型为什么调用这个工具,工具返回了什么,下一步又发生了什么。

小结

MCP 的价值不在于让模型变神奇,而在于让工具接入这件事更标准。对于独立开发者来说,它适合用来沉淀自己的常用能力:项目知识库、脚本工具、部署查询、数据库只读分析,都可以慢慢做成可复用的 MCP Server。

参考资料:Model Context Protocol 官方文档