1. 项目概述:一个为Claude和Cursor量身定制的MCP服务器开发脚手架
如果你正在为Claude、Cursor这类支持Model Context Protocol(MCP)的AI工具开发自定义服务器,并且厌倦了每次都要从零开始搭建项目结构、配置TypeScript、处理错误和日志,那么这个名为bsmi021/custom-mcp-template的模板项目,很可能就是你一直在找的“开箱即用”解决方案。
简单来说,这是一个高度结构化的MCP服务器开发脚手架。它不是一个功能完整的服务器,而是一个经过精心设计的项目骨架,里面已经预置了现代TypeScript项目所需的一切:从清晰的目录分层、统一的配置管理、标准的工具(Tool)与服务(Service)抽象,到完善的开发工具链(ESLint、Prettier、Husky)。它的核心价值在于,让你能跳过所有繁琐的初始化工作,直接聚焦于实现你的业务逻辑——也就是你真正想让AI去调用的那些工具。
我最初接触MCP开发时,花了不少时间在项目配置和架构设计上。后来发现,一个良好的起点能极大提升开发效率和代码质量。这个模板正是基于这样的实践总结,它遵循了MCP社区推荐的最佳实践,特别适合需要与Claude Desktop、Cursor等客户端稳定、高效通信的服务器场景。无论你是想开发一个连接内部数据库的查询工具,还是一个调用第三方API的天气服务,都可以基于这个模板快速启动。
2. 核心设计理念与项目结构深度解析
2.1 为什么需要这样一个模板?
在深入代码之前,我们先聊聊为什么“从模板开始”是一个明智的选择。MCP服务器的开发,虽然核心是定义工具(Tools)和处理请求,但其外围的工程化挑战一点也不少:
- 类型安全与开发体验:MCP涉及大量的数据交换,清晰的TypeScript接口和Zod模式定义能提前捕获许多运行时错误。
- 配置管理:服务器可能需要读取环境变量、配置文件,动态调整行为。一个集中的配置管理器至关重要。
- 代码组织:随着工具数量的增加,如何避免
server.ts变成一个上千行的“巨无霸”文件?业务逻辑、工具适配层、工具参数定义需要清晰分离。 - 错误处理与日志:统一的错误响应格式和清晰的日志输出,是调试和运维的基石。
- 开发效率:热重载、代码格式化、提交前检查,这些现代前端开发的标准配置,在服务器开发中同样能带来巨大便利。
这个模板的設計,正是为了系统性地解决上述问题。它不是简单地把文件堆在一起,而是体现了一种“关注点分离”和“约定优于配置”的架构思想。
2.2 项目骨架全景图与模块职责
让我们拆解模板的目录结构,理解每个部分的职责。这是你未来所有开发的“地图”。
custom-mcp-template/ ├── src/ │ ├── config/ │ │ └── ConfigurationManager.ts # 【核心】配置管理中心 │ ├── services/ │ │ ├── index.ts │ │ └── ExampleService.ts # 【核心】业务逻辑实现层 │ ├── tools/ │ │ ├── index.ts # 【核心】工具注册入口 │ │ ├── exampleTool.ts # 【核心】工具适配层 │ │ └── exampleToolParams.ts # 【核心】工具参数定义 │ ├── types/ │ │ ├── index.ts │ │ └── exampleServiceTypes.ts # 【核心】数据类型与Zod模式 │ ├── utils/ │ │ ├── index.ts │ │ ├── logger.ts # 统一日志工具 │ │ └── errors.ts # 统一错误类与处理 │ ├── initialize.ts # 服务器初始化与工具注册 │ └── server.ts # 应用主入口 ├── dist/ # TypeScript编译输出目录 ├── package.json # 项目元数据与脚本 ├── tsconfig.json # TypeScript编译器配置 ├── .eslintrc.json # 代码检查规则 ├── .prettierrc.json # 代码格式化规则 └── .gitignore # Git忽略规则各目录核心职责详解:
src/config/:这里是项目的“控制台”。ConfigurationManager类采用单例模式,负责统一管理所有配置。它通常会从环境变量、配置文件或默认值中读取配置,并提供类型安全的获取方法。这样做的好处是,你在代码的任何地方引入配置管理器,都能获得一致且最新的配置,避免了配置散落各处的问题。src/services/:这是你编写纯业务逻辑的地方。一个“服务”类应该只关注“做什么”,而不关心“谁在调用”或“数据如何进来/出去”。例如,一个WeatherService类可能只有一个getForecast(city: string)方法,里面封装了调用天气API、解析数据、计算指数等所有逻辑。这保证了业务逻辑的可测试性和可复用性。src/tools/:这是MCP协议层与业务逻辑层的适配器。每个工具文件(如exampleTool.ts)的职责是:- 定义工具在MCP中的名称和描述。
- 使用Zod严格校验从Claude/Cursor传来的输入参数。
- 调用对应的
Service来执行业务逻辑。 - 将
Service返回的业务数据,格式化成MCP协议要求的Content对象(通常是text或image类型)。 - 捕获并处理错误,将其转换为标准的
McpError。 这种分离使得工具层很薄,只做协议适配,而复杂的逻辑都在Service中。
src/types/:项目的“数据字典”。这里用TypeScript的interface或type定义数据结构,并用Zod创建对应的运行时验证模式(schema)。Zod模式不仅用于工具参数校验,也可以用于验证Service的输入输出,确保整个数据流都是类型安全的。src/utils/:共享的“工具箱”。logger提供了分级(如info, warn, error)且可配置的日志输出,是调试的利器。errors定义了自定义错误类(如ValidationError,ServiceError),并提供了将各种错误统一转换为MCP错误格式的工具函数。src/initialize.ts:这是连接一切的“接线板”。它创建服务器实例,并从tools/index.ts导入所有工具注册函数并依次调用,最终返回一个配置好的服务器实例给server.ts。src/server.ts:程序的“启动器”。它非常简洁,主要就是调用initialize.ts创建服务器,然后启动它监听某个端口(通常从配置中读取)。
注意:这种分层架构(工具层-服务层)是模板的精髓。它强制你进行良好的职责分离。我见过不少初学者把API调用、数据处理、错误处理全堆在工具函数里,代码很快变得难以维护。遵循这个模板的结构,能让你的项目从一开始就保持清晰。
3. 从零开始:使用模板创建并运行你的第一个MCP服务器
理论讲得再多,不如亲手跑一遍。下面我们一步步来,看看如何从这个模板孵化出一个属于你自己的、能实际运行的MCP服务器。
3.1 初始化你的项目
模板提供了一个非常方便的创建命令。假设你想创建一个管理个人书签的MCP服务器,项目名定为bookmark-mcp-server。
# 使用npx直接运行模板的创建命令 npx create-mcp-server bookmark-mcp-server执行这个命令后,会发生以下几件事:
- 创建项目目录:在当前路径下,一个名为
bookmark-mcp-server的新文件夹会被创建。 - 交互式配置:命令行会提示你输入项目的基本信息,例如:
Project name: (默认是bookmark-mcp-server,可直接回车)Description: 你可以输入A MCP server to manage my browser bookmarks.
- 复制模板文件:模板的所有源文件(
src/,docs/, 配置文件等)都会被复制到新目录中。 - 更新元数据:新项目里的
package.json中的name,description等字段会根据你的输入自动更新。
一个可能的坑点:如果create-mcp-server这个包还没有发布到npm,或者你是在本地开发这个模板本身,上述命令可能会失败。这时,你需要先在模板项目根目录下执行npm link,将其链接到全局,然后再在目标目录使用create-mcp-server命令。
初始化完成后,终端会给出后续指引:
cd bookmark-mcp-server npm install进入项目目录并安装依赖,这是标准操作。
3.2 理解并运行示例代码
安装完依赖后,先别急着写代码。让我们看看模板自带的示例,理解各个部分是如何协作的。
- 查看示例类型定义:打开
src/types/exampleServiceTypes.ts。这里定义了一个简单的ExampleData接口和一个对应的Zod模式exampleDataSchema。Zod的.describe()方法非常有用,它生成的描述可能会被MCP客户端用来生成更友好的提示。 - 查看示例服务:打开
src/services/ExampleService.ts。这是一个简单的服务类,有一个getData方法。在实际项目中,这里可能会是数据库查询、API调用等复杂逻辑。 - 查看工具参数定义:打开
src/tools/exampleToolParams.ts。这里定义了工具的名称、描述,以及最重要的输入参数模式。注意TOOL_PARAMS是一个Zod对象,它严格定义了客户端必须传入什么参数。 - 查看工具实现:打开
src/tools/exampleTool.ts。这是关键文件。registerExampleTool函数做了以下几件事:- 调用
server.tool(...)注册工具。 - 在异步处理函数中,首先用
TOOL_PARAMS.parse(args)校验输入,校验失败Zod会直接抛出错误。 - 实例化
ExampleService并调用其getData方法。 - 将返回的数据用
McpServer的createTextContent方法包装成MCP内容。 - 用
try-catch包裹,捕获错误并调用utils/errors中的函数将其转化为MCP错误。
- 调用
- 查看工具注册入口:打开
src/tools/index.ts。所有的工具注册函数都在这里导入,并在registerTools函数中被调用。当你添加新工具时,必须记得在这里“挂号”。 - 查看配置:打开
src/config/ConfigurationManager.ts。目前它可能只管理日志级别等基础配置。你需要根据自己服务器的需求,在这里添加新的配置项,比如数据库连接字符串、API密钥等。
现在,让我们运行这个示例服务器:
npm run dev这个命令启动了开发服务器,它使用了ts-node和nodemon。ts-node让你能直接运行TypeScript代码而无需先编译,nodemon则会监听文件变化并自动重启服务器。你会看到控制台输出服务器已启动,并监听着某个端口(例如3000)。
3.3 连接Claude Desktop进行测试
服务器跑起来了,但怎么知道它工作正常呢?我们需要一个MCP客户端来调用它。以Claude Desktop为例:
- 配置Claude Desktop:找到Claude Desktop的配置文件夹。
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
- macOS:
- 编辑配置文件:在
mcpServers对象中添加你的服务器配置。对于本地开发的服务器,通常使用stdio传输方式。
{ "mcpServers": { "my-bookmark-server": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/bookmark-mcp-server/dist/server.js" ], "env": { "NODE_ENV": "production" } } } }重要提示:在开发阶段,使用npm run dev启动的是ts-node,但这里配置的是生产构建后的JS文件路径。所以你需要先运行npm run build来编译项目,生成dist目录下的server.js。或者,你也可以配置为直接调用ts-node运行src/server.ts,但生产环境推荐使用编译后的JS文件。
- 重启Claude Desktop:保存配置文件并完全重启Claude Desktop应用。
- 进行测试:在Claude的对话窗口中,你现在可以尝试使用你的工具了。根据示例工具的定义,你可能会输入类似“调用exampleTool,参数是...”的指令。如果配置成功,Claude会识别到这个工具并调用它,你将看到服务器的日志输出和Claude返回的结果。
4. 实战:添加一个真实可用的新工具
让我们超越示例,添加一个稍微真实一点的工具:一个获取指定GitHub仓库最近Issue的工具。我们将它命名为get_recent_issues。
4.1 第一步:定义数据类型与模式
在src/types/下创建githubTypes.ts。
// src/types/githubTypes.ts import { z } from 'zod'; // TypeScript接口,用于内部代码提示 export interface GithubIssue { id: number; number: number; title: string; state: 'open' | 'closed'; html_url: string; created_at: string; user: { login: string; }; } export interface GithubServiceConfig { apiToken?: string; // GitHub API令牌,用于提高速率限制 baseUrl?: string; // 可选,用于企业GitHub } // Zod模式,用于运行时验证 export const githubIssueSchema = z.object({ id: z.number().describe('The unique ID of the issue'), number: z.number().describe('The issue number within the repository'), title: z.string().describe('The title of the issue'), state: z.enum(['open', 'closed']).describe('The state of the issue'), html_url: z.string().url().describe('The URL to view the issue on GitHub'), created_at: z.string().datetime().describe('The creation timestamp'), user: z.object({ login: z.string().describe('The username of the issue creator'), }), }); export const githubServiceConfigSchema = z.object({ apiToken: z.string().optional().describe('GitHub Personal Access Token (optional)'), baseUrl: z.string().url().optional().describe('Base URL for GitHub API (e.g., for GitHub Enterprise)'), });别忘了在src/types/index.ts中导出它们:
// src/types/index.ts export * from './exampleServiceTypes'; export * from './githubTypes'; // 新增4.2 第二步:实现核心业务逻辑服务
在src/services/下创建GithubService.ts。这个服务负责与GitHub API通信。
// src/services/GithubService.ts import { ConfigurationManager } from '../config/ConfigurationManager'; import { GithubIssue, GithubServiceConfig } from '../types/githubTypes'; import { ServiceError } from '../utils/errors'; import { logger } from '../utils/logger'; export class GithubService { private config: GithubServiceConfig; private baseApiUrl: string; constructor(config?: Partial<GithubServiceConfig>) { // 合并默认配置、全局配置和传入的配置 const globalConfig = ConfigurationManager.getInstance().getConfig(); this.config = { apiToken: globalConfig.github?.apiToken, baseUrl: globalConfig.github?.baseUrl || 'https://api.github.com', ...config, }; this.baseApiUrl = this.config.baseUrl!; } async getRecentIssues(owner: string, repo: string, count: number = 5): Promise<GithubIssue[]> { const url = `${this.baseApiUrl}/repos/${owner}/${repo}/issues`; logger.info(`Fetching recent issues from ${url}`); const headers: Record<string, string> = { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'My-MCP-Server', // GitHub API要求User-Agent }; if (this.config.apiToken) { headers['Authorization'] = `token ${this.config.apiToken}`; } const params = new URLSearchParams({ per_page: count.toString(), state: 'all', // 可以改为 'open' 或 'closed' sort: 'created', direction: 'desc', }); try { const response = await fetch(`${url}?${params}`, { headers }); if (!response.ok) { // 处理常见的API错误 if (response.status === 404) { throw new ServiceError(`Repository ${owner}/${repo} not found`, 'NOT_FOUND'); } if (response.status === 403) { throw new ServiceError('API rate limit exceeded or access forbidden', 'RATE_LIMIT'); } throw new ServiceError(`GitHub API error: ${response.statusText}`, 'API_ERROR'); } const issues: GithubIssue[] = await response.json(); // 过滤掉Pull Request(GitHub API的/issues端点也返回PR) const filteredIssues = issues.filter(issue => !('pull_request' in issue)); logger.debug(`Fetched ${filteredIssues.length} issues for ${owner}/${repo}`); return filteredIssues.slice(0, count); // 确保返回数量不超过请求数 } catch (error) { if (error instanceof ServiceError) { throw error; // 重新抛出我们已经处理过的业务错误 } // 处理网络错误等 logger.error(`Failed to fetch issues from GitHub: ${error}`); throw new ServiceError('Failed to communicate with GitHub API', 'NETWORK_ERROR'); } } }在src/services/index.ts中导出:
// src/services/index.ts export * from './ExampleService'; export * from './GithubService'; // 新增4.3 第三步:定义工具参数模式
在src/tools/下创建getRecentIssuesParams.ts。
// src/tools/getRecentIssuesParams.ts import { z } from 'zod'; export const TOOL_NAME = 'get_recent_issues'; export const TOOL_DESCRIPTION = 'Fetches the most recent issues from a specified GitHub repository.'; export const TOOL_PARAMS = z.object({ owner: z.string() .min(1) .describe('The owner (username or organization) of the GitHub repository. Example: "nodejs"'), repo: z.string() .min(1) .describe('The name of the GitHub repository. Example: "node"'), count: z.number() .int() .min(1) .max(30) .optional() .default(5) .describe('The number of recent issues to fetch (max 30, default 5).'), });4.4 第四步:实现工具适配器
在src/tools/下创建getRecentIssuesTool.ts。这是连接MCP协议和业务服务的桥梁。
// src/tools/getRecentIssuesTool.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { GithubService } from '../services/GithubService'; import { handleToolError, ValidationError } from '../utils/errors'; import { logger } from '../utils/logger'; import { TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMS } from './getRecentIssuesParams'; /** * 注册获取GitHub最近Issue的工具 */ export function registerGetRecentIssuesTool(server: McpServer) { server.tool( TOOL_NAME, TOOL_DESCRIPTION, // 这里需要将Zod模式转换为JSON Schema,MCP SDK内部会处理 TOOL_PARAMS as z.ZodTypeAny, async (args) => { try { logger.info(`Tool ${TOOL_NAME} called with args:`, args); // 1. 参数验证 (Zod会在server.tool内部处理,但这里再次明确解析以获取类型提示) const { owner, repo, count } = TOOL_PARAMS.parse(args); // 2. 实例化服务并执行业务逻辑 const githubService = new GithubService(); const issues = await githubService.getRecentIssues(owner, repo, count); // 3. 格式化输出为MCP内容 if (issues.length === 0) { return { content: [ { type: 'text', text: `No recent issues found in repository ${owner}/${repo}.`, }, ], }; } // 将Issue列表格式化为易读的文本 const issueListText = issues .map( (issue) => `#${issue.number} [${issue.state}] ${issue.title}\n` + ` Created by: ${issue.user.login} on ${new Date(issue.created_at).toLocaleDateString()}\n` + ` Link: ${issue.html_url}\n` ) .join('\n'); const summary = `Found ${issues.length} most recent issue(s) in **${owner}/${repo}**:\n\n`; return { content: [ { type: 'text', text: summary + issueListText, }, ], }; } catch (error) { // 4. 统一错误处理 logger.error(`Error in tool ${TOOL_NAME}:`, error); throw handleToolError(error, TOOL_NAME); } } ); logger.debug(`Tool ${TOOL_NAME} registered successfully.`); }4.5 第五步:注册新工具
打开src/tools/index.ts,导入并调用新工具的注册函数。
// src/tools/index.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { registerExampleTool } from './exampleTool'; import { registerGetRecentIssuesTool } from './getRecentIssuesTool'; // 新增导入 export function registerTools(server: McpServer) { registerExampleTool(server); registerGetRecentIssuesTool(server); // 新增调用 // ... 未来其他工具的注册也在这里添加 }4.6 第六步:添加服务配置(可选但推荐)
如果你的服务需要配置(如GitHub API Token),需要在配置管理器中添加。打开src/config/ConfigurationManager.ts。
// src/config/ConfigurationManager.ts (部分代码) import { githubServiceConfigSchema } from '../types/githubTypes'; // 导入模式 import { z } from 'zod'; // 1. 扩展全局配置模式 const serverConfigSchema = z.object({ logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), port: z.number().int().min(1).max(65535).default(3000), // 新增github配置节 github: githubServiceConfigSchema.optional().default({}), }); type ServerConfig = z.infer<typeof serverConfigSchema>; export class ConfigurationManager { private static instance: ConfigurationManager; private config: ServerConfig; private constructor() { // 从环境变量等来源加载配置 this.config = serverConfigSchema.parse({ logLevel: process.env.LOG_LEVEL as any, port: process.env.PORT ? parseInt(process.env.PORT) : undefined, // 从环境变量加载GitHub Token github: { apiToken: process.env.GITHUB_API_TOKEN, baseUrl: process.env.GITHUB_BASE_URL, }, }); } // 2. 添加一个获取GitHub配置的便捷方法 getGithubConfig() { return this.config.github || {}; } // ... 其他现有方法 }现在,你可以在服务中通过ConfigurationManager.getInstance().getGithubConfig()来获取配置了,正如我们在GithubService的构造函数中所做的那样。
4.7 第七步:测试你的新工具
构建并运行:
npm run build npm start或者继续使用
npm run dev进行开发。更新Claude Desktop配置:确保配置指向最新编译的服务器文件。
在Claude中测试:重启Claude Desktop后,尝试对Claude说:“使用get_recent_issues工具,查看nodejs组织下node仓库最近的3个issue。” Claude应该能识别这个工具,并返回格式化的issue列表。
通过以上七个步骤,我们完整地实现了一个具备实际功能、结构清晰、易于维护的新工具。这个流程是模板所倡导的标准开发模式,熟练掌握后,添加新工具将变得非常高效和规范。
5. 开发、构建与部署全流程指南
5.1 开发工作流与效率工具
模板内置的工具链旨在提升开发体验和代码质量。
- 开发模式:
npm run dev是你的主要开发命令。它利用nodemon监控src/目录下的文件变化,一旦你保存了.ts文件,服务器会自动重启。这避免了手动停止、重启的麻烦,实现了近乎实时的反馈。 - 代码质量:
npm run lint:运行ESLint检查代码风格和潜在问题。模板通常配置了较为严格的规则,帮助你养成良好习惯。npm run format:运行Prettier自动格式化代码,确保团队协作时代码风格统一。
- Git提交前检查:模板通过
husky和lint-staged配置了Git钩子。当你执行git commit时,会自动对暂存区的文件运行lint和format。这保证了提交到仓库的代码都是符合规范的。如果这是你第一次使用该项目,可能需要运行npx husky install来初始化钩子。
实操心得:我强烈建议在编辑器中集成ESLint和Prettier插件,并设置为保存时自动格式化。这样在开发时就能即时看到问题并修正,而不是等到提交时才被拦截。
5.2 生产环境构建与部署
开发完成后,你需要将TypeScript代码编译成JavaScript,以便在生产环境(如服务器、容器)中运行。
- 构建:运行
npm run build。这个命令执行tsc(TypeScript编译器),根据tsconfig.json中的配置,将src/下的所有.ts文件编译成.js文件,并输出到dist/目录。同时,它可能还会处理非.ts文件的复制(通过配置tsc或额外的脚本)。 - 运行生产版本:构建完成后,使用
npm start来启动生产服务器。这个命令通常直接运行node dist/server.js。生产环境应确保NODE_ENV环境变量设置为production,这可能会影响一些库的行为(如更少的调试日志)。 - 进程管理:在生产环境中,不建议直接通过
node命令运行。使用进程管理器如pm2或systemd可以保证应用在崩溃后自动重启,并方便日志收集和性能监控。# 使用pm2的例子 npm install -g pm2 pm2 start dist/server.js --name "my-mcp-server" pm2 save pm2 startup # 设置开机自启 - 环境变量:生产环境的配置(如端口、API密钥、数据库连接)务必通过环境变量或安全的配置文件来管理。绝对不要将敏感信息硬编码在代码中或提交到版本库。模板的
ConfigurationManager从process.env读取,你需要在部署环境中设置这些变量。
5.3 调试与日志查看
清晰的日志是排查问题的生命线。模板的utils/logger通常配置了不同级别(debug, info, warn, error)的日志。
- 开发时:将
LOG_LEVEL环境变量设为debug,可以看到最详细的日志,包括工具被调用、参数详情、服务内部步骤等。 - 生产环境:建议将
LOG_LEVEL设为info或warn,以减少日志量,同时又能记录关键事件和错误。 - 日志输出:日志默认输出到控制台。对于生产环境,你可能需要配置日志库(如Winston、Pino)将日志写入文件或发送到日志聚合服务(如ELK、Sentry)。你可以修改
utils/logger.ts来实现这些功能。
当工具调用失败时,首先查看服务器的错误日志(error级别)。模板的错误处理机制会将捕获的错误信息记录于此,这通常是定位问题的第一步。
6. 常见问题、故障排查与进阶技巧
6.1 工具注册失败或客户端无法识别
症状:服务器启动正常,但Claude Desktop中看不到新添加的工具。
排查步骤:
- 检查工具注册:确认你的工具注册函数(如
registerGetRecentIssuesTool)确实在src/tools/index.ts的registerTools函数中被调用。这是最容易被忽略的一步。 - 检查服务器重启:如果你在开发模式(
npm run dev)下,确保文件保存后服务器成功重启了。查看控制台是否有编译错误。有时TypeScript错误不会阻止nodemon重启,但会导致工具注册代码未执行。 - 检查MCP客户端配置:确认Claude Desktop的配置文件路径正确,且指向的是最新编译的服务器入口文件(
dist/server.js)。修改服务器代码后,需要重新运行npm run build,或者确保开发服务器的stdio路径配置正确。 - 重启MCP客户端:Claude Desktop有时会缓存服务器列表。修改服务器配置后,务必完全退出并重启Claude Desktop应用,而不仅仅是刷新对话窗口。
- 查看服务器启动日志:服务器启动时,
initialize.ts和各个工具注册函数中的logger.debug信息会输出。确认你新工具的成功注册日志出现了。
6.2 工具调用时报参数验证错误
症状:Claude能列出工具,但调用时服务器返回参数错误。
排查步骤:
- 仔细阅读错误信息:MCP SDK和Zod会返回具体的错误信息,比如“Expected string, received number”或缺少某个必需字段。根据错误信息修正你的请求参数。
- 检查Zod模式定义:确认
TOOL_PARAMS中的模式定义与你期望的参数完全匹配。特别注意.optional()和.default()的使用。一个常见的错误是,模式期望一个number,但客户端传入了字符串"5"。 - 在工具函数内部打印参数:在工具处理函数的开头添加
logger.debug('Raw args received:', args),查看实际收到的原始参数是什么,这有助于发现客户端传参和模式定义之间的差异。
6.3 服务逻辑中的网络或API错误
症状:工具调用触发了,但服务器日志显示业务服务(如GithubService)中发生了网络超时、API返回4xx/5xx错误等。
排查步骤:
- 检查网络连通性:确保运行服务器的机器可以访问目标API(如
api.github.com)。如果是公司内网,可能需要配置代理。 - 检查API认证与配额:如果使用需要令牌的API(如GitHub Token),确认令牌有效且具有所需权限,并且没有超过速率限制。在代码中妥善处理
403 Forbidden或429 Too Many Requests等错误。 - 实现重试与退避机制:对于不稳定的网络或API,考虑在服务层添加重试逻辑(例如,使用
p-retry库)。但要注意幂等性(即重试不会导致重复副作用)。 - 完善错误处理:确保服务类中的
try-catch块能捕获所有可能的异常,并将其转换为有意义的ServiceError,以便工具层能将其转化为用户友好的MCP错误信息。避免将底层库的原始错误堆栈直接暴露给客户端。
6.4 性能优化与最佳实践
当你的MCP服务器工具越来越多,逻辑越来越复杂时,以下几点可以帮助你保持项目的健壮性:
- 配置管理:将所有配置集中到
ConfigurationManager。对于敏感信息,使用dotenv从.env文件读取(但确保.env在.gitignore中),或使用Docker Secrets、云服务商提供的密钥管理服务。 - 依赖注入(可选):对于大型项目,可以考虑引入一个轻量级的依赖注入容器(如
tsyringe),来管理Service类的实例化,而不是在每个工具中直接new。这便于进行单元测试和模拟。 - 单元测试:为
services/目录下的核心业务逻辑编写单元测试(使用Jest、Vitest等)。由于服务层不依赖MCP SDK,测试起来相对容易。工具层(tools/)可以编写集成测试,验证参数验证和错误处理流程。 - 工具描述清晰化:在定义
TOOL_DESCRIPTION和参数的.describe()时,尽可能清晰、具体。好的描述能帮助Claude等AI更准确地理解工具的用途和使用方法,从而在合适的场景下自动调用它。 - 资源清理:如果服务中打开了数据库连接、文件句柄或网络连接池,确保在服务器关闭或长时间空闲时能正确释放。虽然Node.js服务器通常长时间运行,但良好的资源管理习惯很重要。
这个custom-mcp-template模板为你铺平了道路,但真正强大的MCP服务器源于你对业务需求的深刻理解和扎实的代码实践。从这个小而美的模板开始,逐步构建起能为你和你的AI助手赋能的高效工具集吧。如果在使用中遇到模板本身的问题,不妨回头仔细阅读其源码和文档,或者参与到项目的社区中,你的实践反馈也可能成为它未来改进的一部分。