跳过正文
  1. 文章/

MCP完全指南

·5954 字·12 分钟·
hujiacheng
作者
hujiacheng
Front-end Developer / Strive To Become Better
目录

目录
#

  1. 什么是 MCP
  2. 为什么需要 MCP
  3. 核心架构
  4. 三大核心能力
  5. 传输层详解
  6. 实战项目:文件系统 + 天气查询 MCP Server
  7. 进阶:在 Claude Desktop 中接入自定义 Server
  8. 常见问题 & 最佳实践

1. 什么是 MCP
#

MCP(Model Context Protocol) 是 Anthropic 于 2024 年 11 月开源的一套标准协议,旨在解决 AI 大模型与外部工具、数据源之间的集成碎片化问题。

你可以把它理解为:

AI 世界的 USB-C 接口

以前,每当你想让 AI 访问数据库、调用 API、读取文件,都需要为每个模型、每个平台单独写集成代码。MCP 定义了一套统一标准,让任意 MCP Client(Claude、Cursor、自定义 AI 应用)都能无缝接入任意 MCP Server(文件系统、数据库、GitHub、Slack……)。

┌─────────────────────────────────────────────────────────────┐
│                        MCP 生态全貌                          │
│                                                             │
│  ┌──────────────┐     MCP 协议      ┌──────────────────┐   │
│  │  MCP Client  │ ◄──────────────► │   MCP Server     │   │
│  │              │                  │                  │   │
│  │  - Claude    │                  │  - 文件系统       │   │
│  │  - Cursor    │                  │  - 数据库         │   │
│  │  - 你的 App  │                  │  - GitHub API    │   │
│  └──────────────┘                  │  - Slack         │   │
│                                    │  - 你的自定义服务 │   │
│                                    └──────────────────┘   │
└─────────────────────────────────────────────────────────────┘

2. 为什么需要 MCP
#

没有 MCP 之前的痛点
#

Claude ──► 自定义工具调用 ──► 碎片化集成代码(每次都要重写)
GPT-4 ──► Function Calling ──► 又一套不兼容的集成代码
Cursor ──► 专有扩展 API ──► 再一套……

问题:

  • 开发者要为不同 AI 框架重复造轮子
  • 工具提供商无法一次性支持所有 AI 平台
  • 没有统一的安全、权限、上下文管理标准

MCP 解决了什么
#

问题MCP 的解决方式
集成碎片化一套协议,所有 Client/Server 通用
工具发现困难Server 自动暴露能力,Client 动态发现
上下文丢失Resources 机制持久化上下文
安全边界不清明确的权限声明与授权流程
重复造轮子生态共享,npm 上直接安装 MCP Server

3. 核心架构
#

MCP 采用经典的 Client-Server 架构,通信基于 JSON-RPC 2.0

┌─────────────────────────────────────────────────────────────────┐
│                         完整架构图                               │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    Host Application                     │   │
│  │  (Claude Desktop / Cursor / 你的自定义 AI 应用)           │   │
│  │                                                         │   │
│  │   ┌───────────┐    ┌───────────┐    ┌───────────┐      │   │
│  │   │ MCP Client│    │ MCP Client│    │ MCP Client│      │   │
│  │   │     1     │    │     2     │    │     3     │      │   │
│  │   └─────┬─────┘    └─────┬─────┘    └─────┬─────┘      │   │
│  └─────────┼───────────────┼───────────────┼─────────────┘   │
│            │               │               │                  │
│    ┌───────▼───────┐ ┌─────▼───────┐ ┌────▼────────┐        │
│    │  MCP Server A │ │ MCP Server B│ │ MCP Server C│        │
│    │  (文件系统)    │ │  (数据库)   │ │  (GitHub)   │        │
│    └───────────────┘ └─────────────┘ └─────────────┘        │
│            │               │               │                  │
│    ┌───────▼───────┐ ┌─────▼───────┐ ┌────▼────────┐        │
│    │  本地文件系统  │ │  PostgreSQL │ │  GitHub API │        │
│    └───────────────┘ └─────────────┘ └─────────────┘        │
└─────────────────────────────────────────────────────────────────┘

关键角色说明
#

角色职责
Host宿主应用,如 Claude Desktop。管理多个 MCP Client 实例
MCP Client嵌入在 Host 中,与单个 MCP Server 保持 1:1 连接
MCP Server轻量级服务,暴露工具/资源/提示词给 Client

通信流程(握手 → 调用)
#

Client                          Server
  │                               │
  │──── initialize ─────────────►│  发送客户端能力 & 版本
  │◄─── initialize result ───────│  返回服务端能力 & 版本
  │                               │
  │──── initialized (通知) ──────►│  确认握手完成
  │                               │
  │──── tools/list ─────────────►│  列出所有工具
  │◄─── tools/list result ───────│  返回工具描述
  │                               │
  │──── tools/call ─────────────►│  调用具体工具
  │◄─── tools/call result ───────│  返回执行结果
  │                               │
  │──── resources/list ─────────►│  列出资源
  │◄─── resources/read result ───│  返回资源内容

4. 三大核心能力
#

MCP Server 可以向 Client 暴露三种能力:

4.1 Tools(工具)—— 模型发起的操作
#

Tools 是 MCP 最核心的能力,允许 AI 模型执行操作,类似 OpenAI 的 Function Calling。

特点:
- 由 AI 模型决定何时调用
- 有明确的输入 Schema(JSON Schema)
- 返回结构化或文本结果
- 典型场景:查询数据库、调用 API、执行命令

示例工具定义:

{
  "name": "read_file",
  "description": "读取指定路径的文件内容",
  "inputSchema": {
    "type": "object",
    "properties": {
      "path": {
        "type": "string",
        "description": "文件的绝对路径"
      }
    },
    "required": ["path"]
  }
}

4.2 Resources(资源)—— 结构化上下文
#

Resources 允许 Server 暴露数据/内容给 Client,用于丰富 AI 的上下文。

特点:
- 由应用层(Host)控制何时读取,而非 AI 自主决策
- 通过 URI 标识(如 file:///path/to/file)
- 支持文本和二进制内容
- 典型场景:项目文件、数据库记录、配置信息

Resource URI 示例:

file:///home/user/project/README.md
postgres://localhost/mydb/users/schema
github://repos/anthropics/mcp/README

4.3 Prompts(提示词模板)—— 可复用的交互模式
#

Prompts 允许 Server 定义可复用的提示词模板,供用户选择触发。

特点:
- 由用户主动选择(而非 AI 自主触发)
- 可接受参数,动态生成提示词
- 支持多轮对话模板
- 典型场景:代码审查模板、翻译模板、报告生成模板

三者对比
#

能力控制方典型用途类比
ToolsAI 模型执行操作、获取实时数据函数调用
Resources应用/用户注入背景上下文文件附件
Prompts用户触发预设工作流斜杠命令

5. 传输层详解
#

MCP 支持两种传输机制:

5.1 Stdio(标准输入输出)
#

适用场景:本地运行的 MCP Server(最常用)

Host Process
    ├── 启动子进程(MCP Server)
    │       stdin  ◄─── JSON-RPC 请求
    │       stdout ───► JSON-RPC 响应
    │       stderr ───► 日志(不参与协议)

特点:

  • 简单,无需网络配置
  • 安全(进程隔离)
  • Claude Desktop 默认使用此方式

5.2 SSE(Server-Sent Events)
#

适用场景:远程 MCP Server、多 Client 共享

Client ──── HTTP POST /messages ────► Server(发送请求)
Client ◄─── GET /sse (流式推送)──── Server(接收响应)

特点:

  • 支持远程部署
  • 多个 Client 可连接同一 Server
  • 适合团队共享工具服务

6. 实战项目
#

项目目标
#

构建一个 MCP Server,包含以下功能:

  • Tool 1:read_file - 读取本地文件
  • Tool 2:list_directory - 列出目录结构
  • Tool 3:get_weather - 模拟天气查询
  • Resource:file:// - 动态暴露文件资源

6.1 项目初始化
#

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

package.json 关键字段:

{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

6.2 核心代码
#

src/index.ts

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";

// ============================================================
// 1. 创建 Server 实例
// ============================================================
const server = new Server(
  {
    name: "my-mcp-server",       // Server 名称
    version: "1.0.0",            // Server 版本
  },
  {
    capabilities: {
      tools: {},        // 声明支持 Tools
      resources: {},    // 声明支持 Resources
    },
  }
);

// ============================================================
// 2. 注册 Tools 列表处理器
//    Client 调用 tools/list 时触发
// ============================================================
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "read_file",
        description: "读取指定路径的文件内容,支持文本文件",
        inputSchema: {
          type: "object",
          properties: {
            path: {
              type: "string",
              description: "文件的绝对路径或相对路径",
            },
          },
          required: ["path"],
        },
      },
      {
        name: "list_directory",
        description: "列出指定目录下的文件和文件夹",
        inputSchema: {
          type: "object",
          properties: {
            path: {
              type: "string",
              description: "目录路径,默认为当前目录",
            },
            show_hidden: {
              type: "boolean",
              description: "是否显示隐藏文件(以.开头)",
            },
          },
          required: ["path"],
        },
      },
      {
        name: "get_weather",
        description: "获取指定城市的当前天气信息(模拟数据)",
        inputSchema: {
          type: "object",
          properties: {
            city: {
              type: "string",
              description: "城市名称,如:北京、上海、深圳",
            },
            unit: {
              type: "string",
              enum: ["celsius", "fahrenheit"],
              description: "温度单位,默认摄氏度",
            },
          },
          required: ["city"],
        },
      },
    ],
  };
});

// ============================================================
// 3. 注册 Tools 调用处理器
//    Client 调用 tools/call 时触发
// ============================================================
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    // ---------- Tool: read_file ----------
    case "read_file": {
      const { path: filePath } = z
        .object({ path: z.string() })
        .parse(args);

      try {
        const absolutePath = path.resolve(filePath);
        const content = await fs.readFile(absolutePath, "utf-8");
        const stats = await fs.stat(absolutePath);

        return {
          content: [
            {
              type: "text",
              text: [
                `📄 文件:${absolutePath}`,
                `📦 大小:${stats.size} 字节`,
                `🕒 修改时间:${stats.mtime.toLocaleString("zh-CN")}`,
                `\n${"─".repeat(50)}\n`,
                content,
              ].join("\n"),
            },
          ],
        };
      } catch (error) {
        throw new McpError(
          ErrorCode.InternalError,
          `读取文件失败:${(error as Error).message}`
        );
      }
    }

    // ---------- Tool: list_directory ----------
    case "list_directory": {
      const { path: dirPath, show_hidden = false } = z
        .object({
          path: z.string(),
          show_hidden: z.boolean().optional(),
        })
        .parse(args);

      try {
        const absolutePath = path.resolve(dirPath);
        const entries = await fs.readdir(absolutePath, { withFileTypes: true });

        const filtered = entries.filter(
          (e) => show_hidden || !e.name.startsWith(".")
        );

        // 目录在前,文件在后
        const dirs = filtered.filter((e) => e.isDirectory());
        const files = filtered.filter((e) => !e.isDirectory());

        const formatEntry = async (entry: typeof entries[0]) => {
          const entryPath = path.join(absolutePath, entry.name);
          if (entry.isDirectory()) {
            return `📁 ${entry.name}/`;
          } else {
            const stats = await fs.stat(entryPath);
            const size = formatBytes(stats.size);
            return `📄 ${entry.name} (${size})`;
          }
        };

        const lines = [
          `📂 目录:${absolutePath}`,
          `共 ${dirs.length} 个文件夹,${files.length} 个文件`,
          "─".repeat(50),
          ...(await Promise.all([...dirs, ...files].map(formatEntry))),
        ];

        return {
          content: [{ type: "text", text: lines.join("\n") }],
        };
      } catch (error) {
        throw new McpError(
          ErrorCode.InternalError,
          `读取目录失败:${(error as Error).message}`
        );
      }
    }

    // ---------- Tool: get_weather ----------
    case "get_weather": {
      const { city, unit = "celsius" } = z
        .object({
          city: z.string(),
          unit: z.enum(["celsius", "fahrenheit"]).optional(),
        })
        .parse(args);

      // 模拟天气数据(实际项目中替换为真实 API 调用)
      const weatherData = simulateWeather(city, unit);

      return {
        content: [
          {
            type: "text",
            text: [
              `🌤️ ${city} 天气报告`,
              "─".repeat(30),
              `🌡️ 温度:${weatherData.temp}°${unit === "celsius" ? "C" : "F"}`,
              `💧 湿度:${weatherData.humidity}%`,
              `🌬️ 风速:${weatherData.windSpeed} km/h`,
              `☁️ 天气:${weatherData.condition}`,
              `👁️ 能见度:${weatherData.visibility} km`,
              `🕒 更新时间:${new Date().toLocaleString("zh-CN")}`,
            ].join("\n"),
          },
        ],
      };
    }

    default:
      throw new McpError(
        ErrorCode.MethodNotFound,
        `未知工具:${name}`
      );
  }
});

// ============================================================
// 4. 注册 Resources 列表处理器
// ============================================================
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  // 动态列出当前目录的文件作为资源
  try {
    const entries = await fs.readdir(process.cwd(), { withFileTypes: true });
    const files = entries.filter(
      (e) => !e.isDirectory() && !e.name.startsWith(".")
    );

    return {
      resources: files.map((file) => ({
        uri: `file://${path.join(process.cwd(), file.name)}`,
        name: file.name,
        description: `工作目录中的文件:${file.name}`,
        mimeType: getMimeType(file.name),
      })),
    };
  } catch {
    return { resources: [] };
  }
});

// ============================================================
// 5. 注册 Resources 读取处理器
// ============================================================
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (!uri.startsWith("file://")) {
    throw new McpError(ErrorCode.InvalidRequest, "仅支持 file:// URI");
  }

  const filePath = uri.replace("file://", "");

  try {
    const content = await fs.readFile(filePath, "utf-8");
    return {
      contents: [
        {
          uri,
          mimeType: getMimeType(filePath),
          text: content,
        },
      ],
    };
  } catch (error) {
    throw new McpError(
      ErrorCode.InternalError,
      `读取资源失败:${(error as Error).message}`
    );
  }
});

// ============================================================
// 工具函数
// ============================================================

function formatBytes(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

function getMimeType(filename: string): string {
  const ext = path.extname(filename).toLowerCase();
  const mimeMap: Record<string, string> = {
    ".ts": "text/plain",
    ".js": "text/javascript",
    ".json": "application/json",
    ".md": "text/markdown",
    ".txt": "text/plain",
    ".html": "text/html",
    ".css": "text/css",
  };
  return mimeMap[ext] ?? "text/plain";
}

function simulateWeather(city: string, unit: string) {
  // 基于城市名生成确定性的模拟数据
  const hash = city.split("").reduce((a, c) => a + c.charCodeAt(0), 0);
  const conditions = ["晴天 ☀️", "多云 ⛅", "阴天 ☁️", "小雨 🌧️", "大风 💨"];

  let tempC = 15 + (hash % 20); // 15~34°C
  const temp =
    unit === "fahrenheit" ? Math.round(tempC * 1.8 + 32) : tempC;

  return {
    temp,
    humidity: 40 + (hash % 50),        // 40%~89%
    windSpeed: 5 + (hash % 30),         // 5~34 km/h
    condition: conditions[hash % conditions.length],
    visibility: 5 + (hash % 20),        // 5~24 km
  };
}

// ============================================================
// 6. 启动 Server(Stdio 传输)
// ============================================================
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // 注意:不要向 stdout 写日志(会污染 MCP 协议流)
  // 所有日志统一写到 stderr
  console.error("✅ MCP Server 已启动,等待连接...");
}

main().catch((error) => {
  console.error("❌ 启动失败:", error);
  process.exit(1);
});

6.3 代码结构说明
#

my-mcp-server/
├── src/
│   └── index.ts       # 主文件(所有逻辑)
├── package.json
├── tsconfig.json
└── dist/              # 编译输出(运行 npm run build 后生成)

6.4 本地测试
#

MCP 提供了官方调试工具 MCP Inspector

# 安装并启动 Inspector
npx @modelcontextprotocol/inspector tsx src/index.ts

浏览器打开 http://localhost:5173,你将看到一个可视化界面:

┌──────────────────────────────────────────────────┐
│              MCP Inspector                        │
│                                                  │
│  📦 Server Info                                  │
│  Name: my-mcp-server  Version: 1.0.0             │
│                                                  │
│  🔧 Tools (3)                                    │
│  ├── read_file                   [Test ▶]        │
│  ├── list_directory              [Test ▶]        │
│  └── get_weather                 [Test ▶]        │
│                                                  │
│  📚 Resources (动态列出文件)                       │
│  ├── package.json                                │
│  └── tsconfig.json                              │
└──────────────────────────────────────────────────┘

手动测试 get_weather:

// 输入
{ "city": "深圳", "unit": "celsius" }

// 输出
🌤️ 深圳 天气报告
──────────────────────────────
🌡️ 温度:28°C
💧 湿度:73%
🌬️ 风速:18 km/h
☁️ 天气:晴天 ☀️
👁️ 能见度:16 km
🕒 更新时间:2025/4/6 10:30:00

7. 接入 Claude Desktop
#

7.1 编译项目
#

npm run build
# 生成 dist/index.js

7.2 修改 Claude Desktop 配置
#

找到配置文件:

系统路径
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/绝对路径/my-mcp-server/dist/index.js"],
      "env": {
        "NODE_ENV": "production"
      }
    }
  }
}

7.3 重启 Claude Desktop
#

重启后,你可以直接在对话中说:

你好,请帮我读取 /Users/me/project/README.md 的内容

Claude 会自动调用 read_file 工具,无需任何额外配置。


8. 最佳实践
#

8.1 错误处理
#

始终使用 McpError 而非普通 Error,让 Client 能正确解析错误类型:

import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";

// ✅ 正确
throw new McpError(ErrorCode.InvalidRequest, "路径不能为空");

// ❌ 避免
throw new Error("路径不能为空");

ErrorCode 速查:

ErrorCode含义使用场景
InvalidRequest请求参数有误参数校验失败
MethodNotFound方法不存在未知 tool name
InternalError内部错误文件读取失败等
InvalidParams参数类型错误Schema 不匹配

8.2 日志规范
#

// ✅ 正确:日志写到 stderr,不污染协议流
console.error("[INFO] 工具被调用:", toolName);

// ❌ 错误:不要写到 stdout,会破坏 JSON-RPC 通信
console.log("工具调用");

8.3 工具描述要写好
#

AI 模型根据 description 决定何时调用哪个工具:

// ❌ 模糊的描述
{
  name: "process",
  description: "处理数据"
}

// ✅ 清晰的描述
{
  name: "search_products",
  description: "在商品数据库中搜索产品。支持按名称、品类、价格区间过滤。返回匹配产品的列表,包含ID、名称、价格和库存状态。"
}

8.4 输入验证用 Zod
#

import { z } from "zod";

const InputSchema = z.object({
  path: z.string().min(1, "路径不能为空"),
  encoding: z.enum(["utf-8", "base64"]).default("utf-8"),
  max_size: z.number().int().positive().max(10 * 1024 * 1024).optional(),
});

// 在 handler 中使用
const validated = InputSchema.parse(args);  // 自动抛出类型安全的错误

8.5 安全注意事项
#

// ✅ 路径遍历防护
const safePath = path.resolve(BASE_DIR, userInput);
if (!safePath.startsWith(BASE_DIR)) {
  throw new McpError(ErrorCode.InvalidRequest, "不允许访问该路径");
}

// ✅ 文件大小限制
const stats = await fs.stat(filePath);
if (stats.size > MAX_FILE_SIZE) {
  throw new McpError(ErrorCode.InvalidRequest, "文件过大");
}

总结
#

阶段核心概念你需要做的
理解协议JSON-RPC 2.0、Client-Server了解握手流程
暴露工具tools/list + tools/call实现 setRequestHandler
丰富上下文resources/list + resources/read暴露文件/数据源
接入宿主Stdio 传输 + 配置文件修改 claude_desktop_config.json

MCP 的价值在于生态:你写的 Server,不仅 Claude 能用,未来所有支持 MCP 的 AI 工具都能无缝接入。现在正是参与这个生态早期建设的最好时机。


参考资料:MCP 官方文档 · MCP SDK on npm

相关文章