从零手搓一个MCP服务器,我踩过的坑比写代码还多

2 阅读1分钟

从零手搓一个MCP服务器,我踩过的坑比写代码还多

上个月老板突然在群里丢了一句话:"我们的AI客服能不能直接查公司的数据库?"

我当时觉得这是个小事——写个function calling不就完了?结果越做越发现不对劲。先是Python写了一版,换到TypeScript项目又要重写一遍。然后产品那边说他们的AI助手也想用,好家伙,又得再来一次。

就在我准备提桶跑路的时候,同事甩了一个链接给我:"看看MCP?"

然后我就入坑了。

MCP到底是啥?一句话说清楚

MCP全称Model Context Protocol,是Anthropic在2024年11月搞出来的一个开放协议。

用大白话说:它是一个标准化的插头。

你想想USB-C——不管什么设备,只要接口是USB-C就能插。MCP就是这个思路:不管你用什么AI客户端(Claude Desktop、Cursor、VS Code Copilot),不管你后端接什么工具(数据库、GitHub、Slack),只要两边都支持MCP,就能直接连上。

以前每换一个AI客户端,就得重新写一遍工具集成。现在写一次MCP Server,所有客户端都能用。

这玩意的重要性在于——微软、OpenAI、Google全部表态支持了。这不是Anthropic一家在玩,这是整个AI行业的共识方向。

和Function Calling有啥区别?

我知道你肯定在想:这不就是Function Calling换了个皮吗?

还真不是。核心区别在这:

Function Calling是"一对一"的——你给OpenAI的API定义了一组tools,换了Claude的API就得重新定义。每个AI模型有自己的一套tool schema格式,互相不通。

MCP是"多对多"的——你写一个MCP Server,暴露一组标准化的工具接口。任何MCP Client(Claude、Cursor、Gemini CLI、自定义Agent)都能自动发现并调用这些工具。Server和Client完全解耦。

Function Calling:  AI模型 ←→ 你的代码(紧耦合)
MCP:              AI客户端 ←→ MCP协议 ←→ MCP服务器(松耦合)

打个比方:Function Calling是你自己焊了一根充电线,只能充你的手机。MCP是一个USB-C标准,任何USB-C设备都能用。

实际开发中,这两种方式并不冲突——你的私有小工具用Function Calling就够了,跨团队跨项目的共享工具才值得上MCP。

开干:6步搭建你的第一个MCP Server

我花了整整一个周末才把这个流程跑通。官方文档写得很"优雅"——优雅到看不懂。下面是我踩完坑之后的版本。

第1步:环境准备

# 确保Node.js >= 18
node --version

# 创建项目目录
mkdir my-first-mcp-server && cd my-first-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

# 初始化TypeScript
npx tsc --init

对了,官方也有Python SDK,但TypeScript生态更成熟,社区里的MCP Server大部分都是TS写的。Python党别急,后面我也会提。

第2步:写服务器骨架

创建 src/index.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: "my-first-server",
  version: "1.0.0",
});

// 注册一个工具
server.tool(
  "get_weather",                          // 工具名称
  "获取指定城市的天气信息",                  // 工具描述(AI靠这个理解工具用途)
  { city: z.string().describe("城市名") }, // 参数schema
  async ({ city }) => {
    // 这里写实际的业务逻辑
    const temp = Math.floor(Math.random() * 35 + 5);
    return {
      content: [{ type: "text", text: `${city}当前温度:${temp}°C` }],
    };
  }
);

// 启动服务器
const transport = new StdioServerTransport();
await server.connect(transport);

这就是一个完整的MCP Server了。核心就三样东西:

  1. server.tool() —— 注册工具
  2. z.object() —— 定义参数类型
  3. async callback —— 实现逻辑

第3步:配置编译

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "strict": true
  },
  "include": ["src/**/*"]
}
// package.json 加上
{
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  },
  "type": "module"
}
npm run build

第一次编译大概率会报错——别慌,检查一下moduleResolution是不是设的Node16。这个坑我踩了半小时。

第4步:用Inspector调试

这是最爽的一步。Anthropic提供了一个可视化的调试工具:

npx @modelcontextprotocol/inspector node build/index.js

它会打开一个网页,你可以在里面直接测试你的工具——填参数、看返回值、调试错误。

这一步千万别跳过。 我之前直接配到Claude Desktop里,结果报错了完全不知道哪里出问题。Inspector能让你在本地先把逻辑跑通。

第5步:接到Claude Desktop

打开Claude Desktop的配置文件:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "my-first-server": {
      "command": "node",
      "args": ["/绝对路径/my-first-mcp-server/build/index.js"]
    }
  }
}

重启Claude Desktop,打开对话,点击右下角的🔧图标——如果能看到你的服务器和工具列表,就说明连上了。

第6步:试试效果

在Claude Desktop里输入:"北京今天天气怎么样?"

Claude会自动调用你的get_weather工具,传入city: "北京",然后把返回结果组织成自然语言回复给你。

注意这里的魔法——你从来没有告诉Claude"遇到天气问题就调用这个工具"。它靠工具的namedescription自动判断什么时候该调用。所以工具的描述写得越清楚,AI调用就越准确。

进阶:做一个真正有用的Server

天气查询太玩具了。接下来我带你们做一个实际项目里能用的——GitHub代码库分析器

server.tool(
  "analyze_repo",
  "分析GitHub仓库的代码统计信息",
  {
    owner: z.string().describe("仓库所有者"),
    repo: z.string().describe("仓库名称"),
  },
  async ({ owner, repo }) => {
    try {
      const response = await fetch(
        `https://api.github.com/repos/${owner}/${repo}`
      );
      if (!response.ok) {
        return {
          content: [{ type: "text", text: `❌ 仓库不存在或无法访问: ${response.status}` }],
          isError: true,
        };
      }
      const data = await response.json();
      
      const summary = [
        `📦 ${data.full_name}`,
        `⭐ Stars: ${data.stargazers_count}`,
        `🍴 Forks: ${data.forks_count}`,
        `📝 语言: ${data.language || "未指定"}`,
        `📋 License: ${data.license?.name || "无"}`,
        `📊 Open Issues: ${data.open_issues_count}`,
        `📅 最后更新: ${new Date(data.updated_at).toLocaleDateString()}`,
        `📖 ${data.description || "无描述"}`,
      ].join("\n");

      return {
        content: [{ type: "text", text: summary }],
      };
    } catch (error) {
      return {
        content: [{ type: "text", text: `请求失败: ${error}` }],
        isError: true,
      };
    }
  }
);

这个工具不依赖任何数据库,直接调GitHub的公开API。在Claude Desktop里你可以问:"帮我分析一下facebook/react这个仓库",它就会调用这个工具帮你查。

MCP的三大核心概念

搞明白这三个概念,你就算真正理解MCP了:

Tools(工具)

就是上面演示的——让AI能执行动作。查询数据库、调API、读写文件,都算工具。

Resources(资源)

给AI提供数据源,但不执行动作。比如:

  • 一个数据库表的schema
  • 一份API文档
  • 项目配置文件
server.resource(
  "config",
  "config://app",
  async () => ({
    contents: [{
      uri: "config://app",
      text: JSON.stringify({ version: "2.1", env: "production" }),
    }],
  })
);

Prompts(提示模板)

预定义的提示词模板,带参数:

server.prompt(
  "code-review",
  "代码审查模板",
  { code: z.string().describe("待审查的代码") },
  async ({ code }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `请审查以下代码,关注安全性、性能和可维护性:\n\n${code}`,
      },
    }],
  })
);

我踩过的5个大坑

坑1:ESM vs CJS,配置地狱

MCP SDK v1.x只支持ESM(ECMAScript Modules)。如果你的package.json里没有"type": "module",或者tsconfig.jsonmodule设错了,编译能过但运行时报错。

解决: package.json必须加"type": "module"tsconfig.json"module": "Node16"

坑2:绝对路径和相对路径

Claude Desktop配置里args要用绝对路径。相对路径虽然不报错,但MCP Server会静默失败——就是连不上,也不告诉你为什么。

这个坑我卡了整整两小时。最后是在Claude Desktop的日志里(~/Library/Logs/Claude/)才发现的错误信息。

坑3:stderr是日志通道,别乱print

MCP协议通过stdout传输JSON-RPC消息。如果你在代码里用了console.log()调试,这些输出会被Claude Desktop当作协议消息解析,然后整个连接就炸了。

调试输出要用console.error()——stderr不会被MCP协议读取。

// ❌ 会破坏MCP通信
console.log("调试信息");

// ✅ 正确的调试方式
console.error("调试信息");

坑4:超时问题

MCP工具默认有30秒超时。如果你调的外部API比较慢(比如分析一个大的GitHub仓库),可能会被截断。

解决: 把耗时操作拆成异步的——先返回一个"正在处理",然后通过Resource或另一个Tool查结果。或者加缓存。

坑5:Schema不匹配

Zod schema定义和实际函数参数不一致的时候,MCP不会报编译错误——它会运行时静默失败。AI客户端调用你的工具,拿回一个莫名其妙的错误。

建议: 一定要在Inspector里手动测每一个工具,确认参数和返回值都对。

谁在用MCP?生态现状

截至2026年4月,MCP的生态已经相当丰富了:

官方/大厂出品:

  • Docker MCP Toolkit —— 一键管理和运行MCP Server
  • GitHub MCP Server —— 操作仓库、PR、Issue
  • Microsoft Learn Docs —— 查Azure文档
  • Playwright MCP —— AI操作浏览器做UI测试
  • PostgreSQL MCP —— 直接查数据库

社区热门:

  • Context7 —— 获取最新版本的库文档
  • DuckDuckGo Search —— AI搜索
  • Filesystem —— 安全的文件系统操作
  • Notion MCP —— 读写Notion页面

有意思的是,连OpenAI都支持MCP了。他们虽然在2025年推了自己的tool calling格式,但在2026年也加上了MCP兼容层。毕竟,谁会跟行业标准对着干呢?

Python开发者怎么办?

如果你不想用TypeScript,Anthropic也提供了Python SDK:

pip install mcp
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-python-server")

@mcp.tool()
def get_weather(city: str) -> str:
    """获取指定城市的天气信息"""
    temp = 15  # 假装在查API
    return f"{city}当前温度:{temp}°C"

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

比TypeScript版本简洁不少。但功能上两者完全对等,选哪个看你团队的栈。

最后说两句

MCP这个东西,目前还处在"早期基础设施"阶段。就像2010年你问"REST API有啥用",很多人觉得没必要——直接RPC不就完了?但现在REST已经是行业标准了。

MCP的定位是一样的:它是AI时代的标准接口协议。 现在可能觉得多此一举,等你的AI工具从1个变成10个,你就会感谢这个标准化。

对了,Anthropic官方有一个免费的MCP入门课程(在Skilljar上),涵盖Tools、Resources、Prompts三个核心概念,还有实操练习。比看文档强十倍,建议去看看。

你们有没有在项目里用过MCP?遇到什么问题?评论区聊聊,我最近在研究MCP + CI/CD的玩法,想看看有没有同好。