1. 项目概述:一个为AI智能体构建“工具箱”的脚手架

如果你最近在折腾AI应用开发,特别是想让大语言模型(比如ChatGPT、Claude)能调用外部工具、读取本地文件或连接数据库,那你大概率听说过“模型上下文协议”(Model Context Protocol, 简称MCP)。简单来说,MCP就像给AI智能体定义了一套标准的“插口”和“电源协议”,让不同的AI模型可以安全、统一地接入各种工具和资源,比如文件系统、数据库、API,甚至是代码解释器。

swarmclawai/create-mcp-server 这个项目,就是一个专门用来快速创建这种“工具箱”(即MCP Server)的脚手架工具。想象一下,你有一个很棒的想法,想让AI帮你分析本地日志文件,或者自动查询数据库生成报表。按照MCP的规范从头手写一个Server,你需要处理协议通信、资源定义、工具注册等一系列繁琐且容易出错的底层细节。这个脚手架的价值就在于,它帮你把这些“脏活累活”都封装好了,你只需要像填空一样,专注于实现你核心的业务逻辑——也就是“我这个工具箱到底要提供什么工具?”。

它基于Node.js和TypeScript构建,这意味着它天然适合现代JavaScript/TypeScript开发者生态。通过一行简单的命令,你就能生成一个结构清晰、配置完备的MCP Server项目骨架,内置了类型安全、热重载、调试配置等开箱即用的能力。无论你是想为团队内部构建一个私有工具,还是计划发布一个公开的MCP工具供社区使用,这个脚手架都能让你从“构思”到“可运行的原型”的时间缩短到几分钟。接下来,我将带你深入拆解这个项目的设计思路、核心实现,并分享从零开始构建一个自定义MCP Server的完整实操流程与避坑经验。

2. 核心架构与设计哲学解析

2.1 为什么是MCP?解决AI应用开发的“工具碎片化”难题

在MCP出现之前,为AI模型集成外部能力是一个混乱的领域。每个AI平台(如OpenAI的GPTs、Claude的Claude Desktop)都可能定义自己的一套插件或工具调用规范。开发者如果想让自己开发的工具被多个平台使用,往往需要为每个平台单独适配一遍,造成了巨大的重复劳动和兼容性噩梦。

MCP的核心理念是 标准化 解耦 。它将AI模型(客户端)与工具提供方(服务器端)的交互抽象成一套与具体模型无关的协议。这样一来,工具开发者只需要按照MCP规范实现一个Server,这个Server就可以被任何兼容MCP的客户端(如Claude Desktop、第三方AI应用框架)所发现和使用。这极大地降低了工具开发的边际成本,并促进了工具生态的繁荣。

create-mcp-server 脚手架正是深刻理解了这一痛点后的产物。它的设计目标不是再造一个MCP协议轮子,而是 最大化降低开发者遵循MCP协议的门槛 。它把协议中复杂的部分,如传输层(Stdio/SSE)、请求/响应序列化、错误处理、生命周期管理等,全部封装在底层。开发者被引导去关注更高层次的抽象:定义 Resources (资源,如文件、数据库条目)和 Tools (工具,如执行命令、调用API)。

2.2 脚手架生成的项目结构剖析

运行 npx @modelcontextprotocol/create-mcp-server@latest init my-mcp-server 后,你会得到一个结构如下的项目:

my-mcp-server/
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts          # Server主入口,初始化与配置
│   ├── server.ts         # Server核心逻辑,定义资源与工具
│   └── types.ts          # 类型定义(如有需要)
├── .gitignore
├── .prettierrc
└── README.md

这个结构看似简单,但每一部分都经过精心设计:

  • src/index.ts :这是应用的启动入口。它的核心工作是创建MCP Server实例,并调用 src/server.ts 中定义的函数来装配具体的资源和工具。它通常还负责处理命令行参数,例如指定服务器监听的端口或传输方式。
  • src/server.ts :这是 开发者需要投入最多精力的文件 。脚手架在这里为你预设了清晰的代码区域和示例,引导你如何:
    1. 初始化Server :调用 Server 构造函数。
    2. 定义资源(Resources) :通过 .setResourceTemplate() 方法,告诉客户端你有哪些“静态”或“动态”的资源可供读取。例如,一个“系统信息”资源,其内容可能是动态生成的。
    3. 定义工具(Tools) :通过 .setTool() 方法,注册你提供的可调用函数。每个工具都需要定义输入参数( inputSchema )和具体的执行函数( callback )。
  • 类型安全(TypeScript) :整个项目基于TypeScript,这意味着你在定义工具参数、资源内容时都能获得完善的类型提示和编译时检查,能有效避免运行时因数据类型错误导致的协议通信失败。

提示 :脚手架生成的示例代码中通常包含一个简单的“echo”工具和一个“get_time”资源。这些示例虽然简单,但完美演示了MCP核心概念的定义方法,是极佳的学习起点。建议在编写自己的逻辑前,先彻底理解这几个示例。

2.3 协议层封装与开箱即用的开发者体验

create-mcp-server 的强大之处在于其对底层协议的透明化封装。它通常依赖于官方的 @modelcontextprotocol/sdk 或类似的底层SDK。作为使用者,你几乎不需要直接处理原始的JSON-RPC消息。

脚手架为你预设了以下关键配置:

  1. 传输层配置 :默认支持标准输入输出(Stdio)和服务器发送事件(SSE)两种传输方式。Stdio模式适用于与本地AI桌面应用(如Claude Desktop)集成,而SSE模式则便于构建网络服务。
  2. 热重载与调试 :集成了 tsx nodemon 等工具,使得在开发过程中,修改代码后服务器可以自动重启。 package.json 中的 scripts 也配置了 dev 命令,方便快速启动开发服务器。
  3. 构建与发布 :提供了 build 命令,将TypeScript代码编译为JavaScript,便于在生产环境部署。清晰的入口文件指引也使得打包成可执行文件或Docker镜像变得 straightforward。

这种设计哲学体现了“约定大于配置”的思想。它通过提供一套合理的默认设置和最佳实践,让开发者能够快速启动,并在需要深度定制时,又有清晰的路径可以遵循(例如修改传输配置、添加中间件等)。

3. 从零构建一个自定义MCP Server:实战演练

理论说得再多,不如亲手构建一个。假设我们要创建一个“本地文件浏览器”MCP Server,它允许AI助手列出指定目录下的文件,并读取文本文件的内容。

3.1 环境准备与项目初始化

首先,确保你的系统已安装Node.js(建议18.x或更高版本)和npm。

打开终端,执行脚手架命令:

npx @modelcontextprotocol/create-mcp-server@latest init file-explorer-server
cd file-explorer-server
npm install

执行成功后,用你喜欢的代码编辑器(如VSCode)打开项目。运行 npm run dev ,如果看到服务器启动日志,说明基础环境一切正常。

3.2 核心工具一:实现“列出目录”工具

我们的第一个工具是 list_directory ,它接收一个 path 参数,返回该路径下的文件和文件夹列表。

打开 src/server.ts 文件。找到定义工具的部分(通常有一个示例的 echo 工具)。我们将其替换或添加为我们的新工具。

// 在 server.ts 的适当位置,例如在初始化server变量后
server.setTool(
  "list_directory",
  {
    description: "列出指定目录下的所有文件和文件夹",
    inputSchema: {
      type: "object",
      properties: {
        path: {
          type: "string",
          description: "要列出的目录绝对路径",
        },
      },
      required: ["path"],
    },
  },
  async (args: { path: string }, extra) => {
    // 注意:实际生产环境需要更严格的安全检查!
    const targetPath = args.path;
    try {
      const files = await fs.readdir(targetPath, { withFileTypes: true });
      const result = files.map((dirent) => ({
        name: dirent.name,
        type: dirent.isDirectory() ? "directory" : "file",
        // 可以添加更多信息,如大小、修改时间
      }));
      // 返回结构化的内容,AI更容易理解
      return {
        content: [
          {
            type: "text",
            text: `目录 "${targetPath}" 下的内容:\n` + 
                  result.map(item => `- [${item.type}] ${item.name}`).join('\n'),
          },
        ],
      };
    } catch (error: any) {
      // 友好的错误信息对于AI调试至关重要
      return {
        content: [
          {
            type: "text",
            text: `无法读取目录 "${targetPath}": ${error.message}`,
          },
        ],
        isError: true, // 标记为错误响应
      };
    }
  }
);

关键点解析:

  • 输入模式( inputSchema :我们定义了一个名为 path 的必需字符串参数。清晰的 description 能帮助AI模型更好地理解如何使用这个工具。
  • 异步回调函数 :工具执行是异步的。我们使用Node.js的 fs.readdir API来读取目录。
  • 返回格式 :MCP工具要求返回特定格式。 content 数组中的 text 字段是AI主要读取的信息。我们将其格式化为清晰的列表。
  • 错误处理 :必须用 try...catch 包裹可能失败的操作,并通过返回 isError: true 来明确告知客户端发生了错误。 永远不要抛出未捕获的异常 ,这会导致Server崩溃或协议通信中断。

3.3 核心工具二:实现“读取文件”工具

接下来,实现 read_file 工具,用于读取文本文件内容。

server.setTool(
  "read_file",
  {
    description: "读取指定文本文件的内容",
    inputSchema: {
      type: "object",
      properties: {
        path: {
          type: "string",
          description: "要读取的文件的绝对路径",
        },
        // 可选:限制读取行数,防止大文件拖垮上下文
        maxLines: {
          type: "number",
          description: "最大读取行数(可选)",
        },
      },
      required: ["path"],
    },
  },
  async (args: { path: string; maxLines?: number }, extra) => {
    const filePath = args.path;
    const maxLines = args.maxLines;
    try {
      // 安全检查:确保是文件且路径安全(此处简化,生产环境需加强)
      const stat = await fs.stat(filePath);
      if (!stat.isFile()) {
        return {
          content: [{ type: "text", text: `路径 "${filePath}" 不是一个文件。` }],
          isError: true,
        };
      }

      // 使用流式读取或分批读取来处理大文件是更优方案,此处为演示使用简单读取
      let content = await fs.readFile(filePath, 'utf-8');
      
      if (maxLines && maxLines > 0) {
        const lines = content.split('\n');
        content = lines.slice(0, maxLines).join('\n');
        if (lines.length > maxLines) {
          content += `\n\n(文件过长,已截断前${maxLines}行,共${lines.length}行)`;
        }
      }

      return {
        content: [
          {
            type: "text",
            // 可以添加文件信息作为前缀
            text: `文件 "${path.basename(filePath)}" 的内容:\n---\n${content}\n---`,
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [{ type: "text", text: `读取文件 "${filePath}" 失败: ${error.message}` }],
        isError: true,
      };
    }
  }
);

实操心得:文件读取的边界处理

  • 大小限制 :AI模型的上下文长度有限。无节制地读取大文件(如日志、数据库dump)会立刻耗尽上下文。因此, maxLines 这样的参数非常必要。更高级的实现可以采用流式读取或只读取文件头部/尾部。
  • 二进制文件 :这个工具目前只处理文本文件(UTF-8)。如果尝试读取二进制文件(如图片、PDF),会得到乱码。一个健壮的Server应该通过文件扩展名或魔数检查来拒绝或特殊处理二进制文件,或者提供另一个专门处理二进制文件(如返回Base64编码)的工具。
  • 路径安全 :这是 安全的重中之重 。示例代码直接使用了用户输入的路径,这存在目录遍历攻击的风险(如用户传入 ../../../etc/passwd )。在生产环境中,必须将操作限制在某个沙箱目录内,或对输入路径进行规范化并检查是否越界。

3.4 定义资源:让AI主动“看到”文件信息

工具是“被动”的,需要AI主动调用。资源则是“主动”提供的,AI客户端可以在初始化时或按需列出和读取资源。我们可以定义一个资源,让AI知道当前“工作目录”是什么。

// 定义一个动态资源:当前工作目录
server.setResourceTemplate(
  "current_dir",
  {
    description: "当前服务器进程的工作目录",
    // 这个资源没有URI参数,所以uriTemplate就是固定的
    uri: "file:///current_working_directory",
  },
  async (uri: string) => {
    // 当客户端请求这个资源时,动态返回当前目录
    const cwd = process.cwd();
    return {
      contents: [
        {
          uri: uri,
          mimeType: "text/plain",
          text: `当前工作目录:${cwd}\n\n你可以使用 list_directory 工具来浏览它。`,
        },
      ],
    };
  }
);

现在,当兼容MCP的客户端(如Claude Desktop)连接上你的Server时,它不仅能调用 list_directory read_file 工具,还能在它的资源面板里看到一个名为“当前工作目录”的资源,点击即可查看内容。这为AI提供了更丰富的上下文信息。

4. 调试、集成与发布全流程指南

4.1 本地调试与测试

  1. 使用内置开发服务器 npm run dev 会启动一个监听Stdio的服务器。要测试它,你需要一个MCP客户端。
  2. 使用MCP Inspector进行测试 :最方便的调试工具是官方或社区的MCP Inspector(一个简单的命令行工具或Web界面)。你可以通过它手动发送工具调用请求并查看响应,无需启动完整的AI客户端。
    # 假设你全局安装了某个mcp-inspector工具
    mcp-inspect --transport stdio npm run dev
    
    在Inspector界面中,你可以看到Server公告的工具和资源列表,并可以手动填写参数进行调用,直观地验证逻辑和返回格式是否正确。
  3. 集成到Claude Desktop :这是最常见的用例。编辑Claude Desktop的配置文件(通常在 ~/Library/Application Support/Claude/claude_desktop_config.json 或类似位置)。
    {
      "mcpServers": {
        "my-file-explorer": {
          "command": "node",
          "args": [
            "/ABSOLUTE/PATH/TO/YOUR/file-explorer-server/build/index.js"
          ],
          "env": {
            // 可选环境变量
          }
        }
      }
    }
    
    重启Claude Desktop后,在聊天界面你应该能看到一个新的“工具”图标,点击即可发现你的 list_directory read_file 工具。你可以直接对Claude说:“请用 my-file-explorer 工具列出我的桌面目录”,它就会自动调用对应的工具。

4.2 常见问题与排查技巧实录

在开发和集成过程中,你几乎一定会遇到以下问题:

问题现象 可能原因 排查步骤与解决方案
Claude Desktop 中看不到工具 1. 配置文件路径或格式错误。
2. Server启动失败。
3. 命令路径不是绝对路径。
1. 检查Claude Desktop日志(应用内设置或系统日志)。
2. 在终端独立运行Server命令,看是否有报错。
3. 确保 args 中的路径是绝对路径 ,使用 pwd 命令获取。
调用工具时报“协议错误”或超时 1. Server代码抛出未捕获的异常。
2. 工具回调函数没有正确返回Promise或返回值格式不对。
3. Stdio传输被阻塞。
1. 在 server.ts 中每个工具的回调函数内部添加最外层的 try-catch ,并打印错误到 console.error
2. 使用MCP Inspector测试,它能给出更详细的错误信息。
3. 检查是否有同步的无限循环或长时间阻塞操作。
工具调用成功,但AI不理解返回内容 返回的 text 字段内容过于杂乱或非结构化。 优化返回文本的格式。使用清晰的标题、列表、分隔符。对于复杂数据,可以考虑返回简化的JSON或Markdown表格格式,AI的解析能力很强。例如,列出文件时附带大小和修改时间。
Server启动后立即退出 1. 依赖未安装。
2. index.ts server.ts 中有语法错误。
3. 传输配置错误。
1. 运行 npm install
2. 运行 npx tsc --noEmit 检查TypeScript错误。
3. 检查 index.ts 中是否正确调用了 server.connect() await 了其Promise。
资源在客户端不显示 1. 资源URI模板定义错误。
2. 资源初始化函数有错误。
3. 客户端不支持资源列表。
1. 使用MCP Inspector检查Server初始化的公告信息,看是否包含资源列表。
2. 确保资源回调函数也做了错误处理。
3. 查阅客户端文档,确认其对资源的支持情况。

独家避坑技巧:

  • 日志是你的眼睛 :在开发初期,在 server.ts 的关键位置(如工具回调开始、结束、错误时)添加 console.log console.error 。这些日志会输出到Server的Stdio,在Claude Desktop的日志或独立终端中都能看到。
  • 从简单到复杂 :先实现一个像 echo 一样绝对正确的工具,确保整个通信链路畅通。然后再逐步添加业务逻辑。
  • 善用TypeScript类型 :定义工具 inputSchema 时,尽量使用 as const 和精确的类型定义。这不仅能获得更好的IDE提示,还能在编译阶段发现许多潜在的类型不匹配问题。
  • 环境变量管理 :对于需要配置的项(如允许访问的根目录),不要硬编码在代码里。使用 dotenv 等库从 .env 文件读取,并在 package.json 的脚本和Claude Desktop配置中传递。

4.3 构建与发布

当你完成开发并通过测试后,就可以准备发布了。

  1. 构建生产版本

    npm run build
    

    这会在 build/ 目录下生成编译后的JavaScript文件。确保 build/index.js 可以独立运行。

  2. 发布到NPM(可选) :如果你想将你的MCP Server作为公共工具分享:

    • 更新 package.json 中的 name , version , description , bin 字段。
    • README.md 中详细说明工具的功能、配置方法和使用示例。
    • 运行 npm publish 发布到NPM仓库。其他用户就可以通过 npx your-package-name 来安装运行你的Server,或者像你配置Claude Desktop一样来配置它。
  3. 打包为独立可执行文件(进阶) :使用 pkg nexe 等工具,可以将Node.js项目打包成针对不同操作系统(Windows/macOS/Linux)的单个可执行文件,免除用户安装Node.js环境的麻烦,体验更佳。

5. 安全考量与最佳实践总结

构建一个MCP Server,尤其是涉及文件系统、网络或系统命令的Server,安全是生命线。以下是我在实际项目中总结出的几条铁律:

  1. 输入验证与沙箱化 永远不要信任客户端传入的任何参数 。对于文件路径,必须解析为绝对路径后,检查其是否被限制在预先设定的安全目录(沙箱)内。可以使用 path.resolve path.relative 进行检查。

    const safeRoot = '/Users/me/safe_dir';
    const requestedPath = path.resolve(args.path);
    if (!requestedPath.startsWith(safeRoot)) {
        throw new Error('访问路径越界!');
    }
    
  2. 最小权限原则 :Server进程本身不应该以高权限(如root)运行。考虑是否需要访问网络或特定端口。在Docker或沙箱环境中运行是更安全的选择。

  3. 操作限制 :对于可能消耗大量资源或产生副作用的操作(如删除文件、执行命令),应该提供显式的确认机制,或者通过参数进行严格限制(如上述的 maxLines )。更好的设计是,只提供“读”操作,将“写”或“执行”操作留给更受控的环境。

  4. 错误信息脱敏 :返回给客户端的错误信息应足够友好以指导AI,但 绝不能泄露服务器内部敏感信息 (如堆栈跟踪、内部文件路径、系统用户名等)。在错误处理中,应返回通用错误信息,而将详细日志记录在服务器端。

  5. 依赖安全 :定期使用 npm audit 检查并更新项目依赖,避免使用含有已知漏洞的第三方包。

swarmclawai/create-mcp-server 这个脚手架,为你铺平了进入MCP世界的第一公里。它抽象了协议的复杂性,让你能聚焦于创造有价值的工具本身。从简单的文件操作,到连接数据库、调用第三方API、甚至控制智能家居,MCP为AI的能力扩展提供了无限可能。我个人的体会是,成功的MCP工具不在于技术多复杂,而在于对AI使用场景的深刻理解,以及稳定、安全、可靠的实现。当你看到AI助手通过你编写的工具,流畅地完成一项原本不可能的任务时,那种成就感正是驱动我们不断探索的动力。

更多推荐