本文将带你从零开始构建 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 服务主要由这三个核心组件构成:
- MCP 服务器:注册工具、处理请求并返回响应的核心
- 工具定义与实现:定义工具的名称、描述、参数和执行逻辑
- 传输层:与 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
文件实现了飞书客户端,主要包括以下功能:
- 身份验证:获取和刷新飞书访问令牌
- API 请求封装:处理 HTTP 请求、响应和错误
- 知识库操作:提供获取空间列表、文档列表等方法
客户端的核心代码结构如下:
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 工具开发是整个服务的核心,包含三个关键部分:
- 工具定义:名称、描述和参数类型
- 参数验证:使用 Zod 等库进行参数验证
- 执行逻辑:实际功能实现和标准化的返回格式
实现一个高质量的 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
数组的对象,数组中是带有type
和text
字段的内容项 - 失败时,返回一个带有
isError: true
标志和错误消息的对象
这种标准格式确保了 AI 能够正确解析工具的执行结果,无论成功还是失败。
实现传输层
传输层可以说是 MCP 服务中最容易被忽视却最容易出问题的部分。在实际项目中,大多数 MCP 服务故障都出在传输层,尤其是 SSE 连接相关的问题。
MCP 支持两种主要传输方式:
- HTTP SSE(Server-Sent Events):适用于 Web 应用集成
- 命令行 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();
这个入口文件完成了几个关键任务:
- 加载环境变量配置
- 验证必要的凭证是否存在
- 创建服务器实例
- 启动 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。集成步骤:
- 启动你的 MCP 服务
- 在 Cursor 设置中添加 MCP 服务 :
- 打开 Cursor 设置
- 找到 "LLM Tools" 部分
- 添加自定义 MCP 服务,URL 填写
http://localhost:7777/mcp
- 测试集成:在 Cursor 中向 AI 提问,要求它使用你的 MCP 工具
2. 在 Claude API 中使用
通过命令行模式可以将 MCP 服务集成到 Claude API 应用中:
- 启动 MCP 服务(命令行模式) :
node dist/index.js --stdio
- 在 API 请求中包含 MCP 工具描述
- 管道连接 MCP 服务
3. 作为独立服务运行
你也可以将 MCP 服务部署为独立的微服务,提供给多个 AI 应用使用:
- 部署 MCP 服务到服务器
- 通过反向代理提供安全访问
- 添加认证机制
结语
通过本文,我们详细探讨了 MCP 服务的构建过程,从飞书文档 API 客户端到 MCP 工具定义,再到传输层实现,每个环节都进行了深入讲解。MCP 为 AI 提供了一种安全、标准化的方式与外部世界交互,解决了知识截止和能力边界的问题。
希望这篇文章能帮助你开始构建自己的 MCP 服务。无论你是想增强现有 AI 应用,还是构建全新的智能系统,MCP 都提供了一个强大的框架,让 AI 的能力边界不再受限。