一文掌握 MCP 开发:从原理到实战(飞书客户端 MCP)

本文将带你从零开始构建 MCP 服务,打通 AI 调用外部系统的能力边界。无论你是想拓展 AI 模型能力,还是构建企业级 AI 工具链,这篇实战指南都能帮你入门 MCP 服务构建。

在使用 Claude、GPT 等 AI 模型时,你是否遇到过这些问题:

  • AI 无法访问最新数据,知识截止日期限制了应用场景
  • 需要 AI 操作外部系统(如数据库、API),但缺乏连接机制
  • 想让 AI 访问企业内部数据,但担心数据安全问题

MCP (Model Context Protocol) 协议正是为解决这些问题而生,它为 AI 提供了统一的工具调用规范和外部系统连接能力。本文将详细讲解如何构建飞书文档 MCP 服务,从理论到实战,帮助你快速掌握 MCP 开发技能。

项目代码仓库,可先行下载代码再往下学习,这个项目结构完全可以当作一个 MCP 脚手架,只需要修改你的业务逻辑就能完美适配,建议收藏 star。

MCP 服务构建:核心原理与实战

什么是 MCP?

MCP (Model Context Protocol) 是由 Anthropic 公司推出的标准化协议,允许 AI 模型通过统一接口与外部工具和服务进行通信。MCP 解决了大型语言模型无法直接访问外部最新数据和系统的限制,是一个"AI 能力扩展协议"。

我第一次接触 MCP 是在使用 Cursor 编辑器时,它能够通过 MCP 连接本地代码库和外部工具,极大提升了开发效率。MCP 的强大之处在于统一了工具调用格式,提供了请求/响应的规范定义,使得 AI 可以像调用函数一样使用外部工具。

MCP 的核心价值在于:

  • 提供统一的工具调用规范
  • 实现 AI 与外部系统的无缝连接
  • 支持实时数据获取与操作

MCP 服务核心组件

MCP 服务主要由这三个核心组件构成:

  1. MCP 服务器:注册工具、处理请求并返回响应的核心
  2. 工具定义与实现:定义工具的名称、描述、参数和执行逻辑
  3. 传输层:与 AI 客户端通信的机制,通常是 HTTP SSE 或命令行标准输入/输出

下面我们将以飞书文档 MCP 服务为例,详细讲解每个组件的实现。

技术选型与项目结构

在本项目中,我们采用以下技术栈实现飞书文档 MCP 服务:

  • Node.js + TypeScript:强类型支持,提高代码质量
  • Express.js:轻量且灵活,完美适配 HTTP SSE 传输需求
  • @modelcontextprotocol/sdk:Anthropic 官方 SDK
  • zod:参数验证库,确保 AI 传入的参数符合预期

项目结构:

bash 复制代码
.
├── src/
│   ├── api/                  # 外部服务的API客户端
│   │   └── feishu.ts         # 飞书API客户端
│   ├── server.ts             # MCP服务器实现
│   ├── index.ts              # 应用入口
│   └── mcp-types.d.ts        # 类型声明
├── .env                      # 环境变量配置
├── package.json              # 项目配置和依赖
└── tsconfig.json             # TypeScript配置

这种结构的好处是职责分明:

  • api 目录负责封装与飞书服务的交互
  • server.ts 负责 MCP 核心逻辑和工具注册
  • index.ts 处理服务启动和环境配置
  • mcp-types.d.ts 解决了 TypeScript 与 MCP SDK 的类型兼容问题

环境准备

开始前,先安装必要的依赖:

bash 复制代码
pnpm init -y
pnpm install @modelcontextprotocol/sdk express zod axios --save
pnpm install typescript tsx @types/node @types/express --save-dev

然后初始化 TypeScript 配置:

bash 复制代码
npx tsc --init

修改 tsconfig.json,主要调整这几项:

json 复制代码
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "outDir": "./dist",
    "strict": true
  }
}

创建 MCP 类型声明文件。因为官方 SDK 缺少完整的 TypeScript 类型声明,我们需要自己定义。创建 src/mcp-types.d.ts

typescript 复制代码
declare module '@modelcontextprotocol/sdk/server/mcp.js' {
  import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
  
  export class McpServer {
    constructor(options: any, capabilities: any);
    tool(name: string, description: string, params: any, handler: Function): void;
    connect(transport: Transport): Promise<void>;
    server: any;
  }
}

declare module '@modelcontextprotocol/sdk/server/sse.js' {
  import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
  
  export class SSEServerTransport implements Transport {
    constructor(path: string, res: any);
    onMessage: (callback: (message: any) => void) => void;
    send: (message: any) => void;
    handlePostMessage(req: any, res: any): Promise<void>;
  }
}

declare module '@modelcontextprotocol/sdk/server/stdio.js' {
  import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
  
  export class StdioServerTransport implements Transport {
    constructor();
    onMessage: (callback: (message: any) => void) => void;
    send: (message: any) => void;
  }
}

declare module '@modelcontextprotocol/sdk/shared/transport.js' {
  export interface Transport {
    onMessage: (callback: (message: any) => void) => void;
    send: (message: any) => void;
  }
}

有了这些准备,我们就可以开始构建 MCP 服务的核心部分了!

实现飞书 API 客户端

实现飞书功能前提需要到飞书开放平台去创建应用,使用应用的 App ID、App Secret,还要创建群聊,添加应用权限才能访问飞书云文档,具体这里不详细讲述,自行在开放平台学习,本文章主要讲述 MCP 架构的实现。

首先,我们需要一个飞书 API 客户端来处理与飞书服务的通信。这是 MCP 服务的基础层,负责封装所有与飞书相关的 API 调用。

在我们的项目中,src/api/feishu.ts 文件实现了飞书客户端,主要包括以下功能:

  1. 身份验证:获取和刷新飞书访问令牌
  2. API 请求封装:处理 HTTP 请求、响应和错误
  3. 知识库操作:提供获取空间列表、文档列表等方法

客户端的核心代码结构如下:

typescript 复制代码
export class FeishuClient {
  private appId: string;
  private appSecret: string;
  private accessToken: string | null;
  private tokenExpireTime: number;
  private baseUrl: string;

  constructor(appId?: string, appSecret?: string) {
    this.appId = appId || process.env.FEISHU_APP_ID || '';
    this.appSecret = appSecret || process.env.FEISHU_APP_SECRET || '';
    this.accessToken = null;
    this.tokenExpireTime = 0;
    this.baseUrl = 'https://open.feishu.cn/open-apis';
  }

  // 获取访问令牌
  async getAccessToken(): Promise<string> {
    // 如果token还有效,直接返回
    if (this.accessToken && Date.now() < this.tokenExpireTime) {
      return this.accessToken;
    }

    // 获取新的访问令牌...
  }

  // 通用请求方法
  async request<T>(method: string, url: string, data: any = null, params: any = null): Promise<T> {
    // 处理HTTP请求和响应...
  }

  // 获取知识空间列表
  async getSpaces(pageSize = 50, pageToken = ''): Promise<SpaceResponse> {
    // 实现获取知识空间列表的逻辑...
  }

  // 获取知识空间内的节点列表
  async getNodes(spaceId: string, parentNodeToken = '', pageSize = 50, pageToken = ''): Promise<NodeResponse> {
    // 实现获取节点列表的逻辑...
  }
}

在我们的实现中,飞书客户端完全负责与飞书 API 的交互,使 MCP 服务层可以专注于工具逻辑实现。

构建 MCP 服务和工具注册

MCP 服务的核心部分是工具的定义和注册。在我们的项目中,src/server.ts 文件实现了一个专门的飞书 MCP 服务类:

typescript 复制代码
export class FeishuMcpServer {
  private server: McpServer;
  private feishuClient: FeishuClient;
  private transport: Transport | null = null;

  constructor(appId?: string, appSecret?: string) {
    // 创建飞书客户端
    this.feishuClient = new FeishuClient(appId, appSecret);

    // 创建MCP服务器
    this.server = new McpServer({
      name: "飞书文档MCP服务",
      version: "1.0.0",
    }, {
      capabilities: {
        logging: {},
        tools: {},
      },
    });

    // 注册工具
    this.registerTools();
  }

  // 工具注册方法
  private registerTools() {
    const { feishuClient } = this;

    // 注册工具...
  }
}

接下来,让我们详细看看工具注册的实现。我们将实现两个主要工具:列出知识空间和列出文档。

标准工具定义与实现模式

MCP 工具开发是整个服务的核心,包含三个关键部分:

  1. 工具定义:名称、描述和参数类型
  2. 参数验证:使用 Zod 等库进行参数验证
  3. 执行逻辑:实际功能实现和标准化的返回格式

实现一个高质量的 MCP 工具,需要遵循这些原则:

  • 工具名称要简洁明了(推荐使用连字符命名,如 list-spaces
  • 描述要清晰全面,这直接影响 AI 对工具用途的理解
  • 参数验证一定要严格,防止错误输入导致工具执行失败
  • 返回格式必须符合 MCP 规范,否则 AI 无法正确解析

在我们的实现中,有两种工具注册模式:无参数工具和带参数工具。

无参数工具示例:list-spaces
typescript 复制代码
// 列出文档空间
this.server.tool(
  "list-spaces",
  "列出所有可用的飞书文档空间",
  {},
  async () => {
    try {
      const response = await feishuClient.getSpaces();
      const spaces = response.items || [];
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              spaces: spaces.map(space => ({
                id: space.space_id, 
                name: space.name
              }))
            })
          }
        ]
      };
    } catch (error: any) {
      Logger.error(`获取空间列表失败:`, error);
      return {
        isError: true,
        content: [
          { 
            type: "text", 
            text: `获取空间列表失败: ${error.message || '未知错误'}` 
          }
        ]
      };
    }
  }
);

这个工具没有参数,只需提供一个空对象 {} 作为参数定义。它直接调用飞书客户端获取空间列表,并以标准 MCP 格式返回结果。

带参数工具示例:list-documents
typescript 复制代码
// 列出文档
this.server.tool(
  "list-documents",
  "获取指定空间或所有空间的文档列表",
  {
    spaceId: z.string().optional().describe("空间ID,不提供则返回所有空间的文档")
  },
  async ({ spaceId }: { spaceId?: string }) => {
    try {
      let allDocuments: any[] = [];

      if (spaceId) {
        // 获取指定空间的文档
        try {
          const response = await feishuClient.getNodes(spaceId);
          allDocuments = (response.items || []).map(node => ({
            id: node.node_token,
            name: node.title || 'Untitled',
            type: node.obj_type || 'Unknown',
            spaceId: spaceId
          }));
        } catch (error: any) {
          if (error.message?.includes('权限')) {
            return {
              isError: true,
              content: [{ type: "text", text: `无权访问空间(${spaceId})的文档` }]
            };
          }
          throw error;
        }
      } else {
        // 获取所有空间的文档...
      }
      
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({ documents: allDocuments })
          }
        ]
      };
    } catch (error: any) {
      Logger.error(`获取文档列表失败:`, error);
      return {
        isError: true,
        content: [
          { 
            type: "text", 
            text: `获取文档列表失败: ${error.message || '未知错误'}` 
          }
        ]
      };
    }
  }
);

这个工具使用 Zod 定义了一个可选的字符串参数 spaceId。根据是否提供 spaceId,工具会采取不同的处理逻辑:获取指定空间的文档或获取所有空间的文档。

标准化错误处理与返回格式

在上面的两个例子中,我们都采用了一致的错误处理模式和返回格式:

  • 成功时,返回一个包含 content 数组的对象,数组中是带有 typetext 字段的内容项
  • 失败时,返回一个带有 isError: true 标志和错误消息的对象

这种标准格式确保了 AI 能够正确解析工具的执行结果,无论成功还是失败。

实现传输层

传输层可以说是 MCP 服务中最容易被忽视却最容易出问题的部分。在实际项目中,大多数 MCP 服务故障都出在传输层,尤其是 SSE 连接相关的问题。

MCP 支持两种主要传输方式:

  1. HTTP SSE(Server-Sent Events):适用于 Web 应用集成
  2. 命令行 STDIO:适用于命令行环境和 API 集成

在我们的项目中,两种传输方式都已实现:

typescript 复制代码
/**
 * 启动标准输入输出模式
 */
async startStdio() {
  const transport = new StdioServerTransport();
  this.transport = transport;
  await this.connect(transport);
  return this;
}

/**
 * 启动HTTP服务器
 * @param port 端口号
 */
async startHttp(port: number = 7777) {
  const app = express();
  
  // 配置路由...
  
  // SSE 路由
  app.get('/mcp', async (req: Request, res: Response) => {
    console.log("新的MCP SSE连接已建立");
    const transport = new SSEServerTransport(
      "/mcp-messages",
      res as unknown as ServerResponse<IncomingMessage>,
    );
    this.transport = transport;
    await this.connect(transport);
  });

  app.post('/mcp-messages', async (req: Request, res: Response) => {
    if (!this.transport) {
      res.status(400).send("SSE连接尚未建立");
      return;
    }
    
    const sseTransport = this.transport as SSEServerTransport;
    await sseTransport.handlePostMessage(
      req as unknown as IncomingMessage,
      res as unknown as ServerResponse<IncomingMessage>,
    );
  });

  // 启动HTTP服务器...
}

传输层的连接管理

无论使用哪种传输方式,我们都需要一个统一的连接方法来处理连接建立后的逻辑:

typescript 复制代码
/**
 * 连接到传输层
 */
private async connect(transport: Transport): Promise<void> {
  await this.server.connect(transport);

  // 配置日志...

  Logger.log("服务器已连接,可以处理请求");
}

这种设计确保了无论使用哪种传输方式,服务器都能正确处理连接和日志记录。

入口文件实现

入口文件是整个服务的启动点,需要处理命令行参数、环境变量以及不同启动模式的选择。在我们的项目中,src/index.ts 实现了服务的启动逻辑:

typescript 复制代码
#!/usr/bin/env node
import { FeishuMcpServer } from "./server.js";
import { resolve } from "path";
import { config } from "dotenv";
import 'dotenv/config';

// 加载.env文件
config({ path: resolve(process.cwd(), ".env") });

// 检查必要的环境变量
if (!process.env.FEISHU_APP_ID || !process.env.FEISHU_APP_SECRET) {
  console.error("错误: 请设置环境变量 FEISHU_APP_ID 和 FEISHU_APP_SECRET");
  console.error("您可以在 .env 文件中配置这些变量,例如:");
  console.error("FEISHU_APP_ID=your_app_id");
  console.error("FEISHU_APP_SECRET=your_app_secret");
  process.exit(1);
}

export async function startServer(): Promise<void> {
  const port = parseInt(process.env.PORT || '7777', 10);
  
  // 创建服务器实例
  const server = new FeishuMcpServer(
    process.env.FEISHU_APP_ID as string,
    process.env.FEISHU_APP_SECRET as string
  );
  
  try {
    // 启动HTTP服务器
    console.log("启动飞书MCP服务器...");
    await server.startHttp(port);
  } catch (error) {
    console.error("服务器启动失败:", error);
    process.exit(1);
  }
}


// 启动服务器
startServer();

这个入口文件完成了几个关键任务:

  1. 加载环境变量配置
  2. 验证必要的凭证是否存在
  3. 创建服务器实例
  4. 启动 HTTP 服务

项目执行与测试

运行项目

sql 复制代码
pnpm start

在 cursor 中使用

在 Cursor Settings 中添加 MCP,把下面代码写进去即可,亮绿灯了则表示连接成功

json 复制代码
{
  "mcpServers": {
    "feishu-docs": {
      "url": "http://127.0.0.1:7777/mcp",
      "enabled": true
    }
  }
}

测试

至此,项目已经成功搭建~

与 AI 工具集成

创建完 MCP 服务后,主要有以下几种 AI 工具集成方式:

1. 在 Cursor 中使用

Cursor 是一款专为 AI 开发优化的编辑器,原生支持 MCP。集成步骤:

  1. 启动你的 MCP 服务
  2. 在 Cursor 设置中添加 MCP 服务
    • 打开 Cursor 设置
    • 找到 "LLM Tools" 部分
    • 添加自定义 MCP 服务,URL 填写 http://localhost:7777/mcp
  3. 测试集成:在 Cursor 中向 AI 提问,要求它使用你的 MCP 工具

2. 在 Claude API 中使用

通过命令行模式可以将 MCP 服务集成到 Claude API 应用中:

  1. 启动 MCP 服务(命令行模式)node dist/index.js --stdio
  2. 在 API 请求中包含 MCP 工具描述
  3. 管道连接 MCP 服务

3. 作为独立服务运行

你也可以将 MCP 服务部署为独立的微服务,提供给多个 AI 应用使用:

  1. 部署 MCP 服务到服务器
  2. 通过反向代理提供安全访问
  3. 添加认证机制

结语

通过本文,我们详细探讨了 MCP 服务的构建过程,从飞书文档 API 客户端到 MCP 工具定义,再到传输层实现,每个环节都进行了深入讲解。MCP 为 AI 提供了一种安全、标准化的方式与外部世界交互,解决了知识截止和能力边界的问题。

希望这篇文章能帮助你开始构建自己的 MCP 服务。无论你是想增强现有 AI 应用,还是构建全新的智能系统,MCP 都提供了一个强大的框架,让 AI 的能力边界不再受限。

相关推荐
DoraBigHead9 小时前
从LLM到MCP:AI的进化三部曲
人工智能·mcp
大熊猫侯佩10 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(下)
llm·ai编程·mcp
大熊猫侯佩10 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(上)
llm·ai编程·mcp
下位子11 小时前
『AI 编程』用 Claude Code 从零到一开发全栈减脂追踪应用
前端·ai编程·claude
子昕11 小时前
Claude Code插件系统上线!AI编程的“App Store”时代来了
ai编程
Java中文社群11 小时前
n8n和在线免费体验蚂蚁万亿开源大模型Ling-1T!
aigc·ai编程
yaocheng的ai分身13 小时前
氛围编码革命进入下一阶段: Bolt v2
ai编程
大熊猫侯佩14 小时前
AI 开发回魂夜:捉鬼大师阿星的 Foundation Models 流式秘籍
llm·ai编程·swift
用户3071409584814 小时前
深入剖析Dify Web前端聊天模块:从架构设计到核心实现
ai编程
tangdou36909865516 小时前
LibreChat-图文并茂手把手教你界面配置 | Adorable LibreChat Interface Configuration Guide
aigc·openai·ai编程