1. 项目概述:一个为AI应用构建的“标准插件库”

如果你最近在折腾AI应用开发,特别是想让你的AI助手(比如Claude、GPTs)能“联网”查询实时信息、读取本地文件或者操作你的数据库,那你大概率已经接触过“工具调用”(Tool Calling)这个概念。简单说,就是让AI模型在对话中,能按需调用外部函数来获取信息或执行操作。但这里有个痛点:每个开发者、每个项目都在重复造轮子。今天为Claude写个读取GitHub Issues的工具,明天为GPTs写个查询天气的接口,工具的定义、协议、实现方式五花八门,难以复用和共享。

这就是“InstaNode-dev/mcp”这个项目要解决的核心问题。它不是一个具体的工具,而是一个 协议标准 和一套 参考实现 。MCP,全称是 Model Context Protocol ,你可以把它理解为AI应用领域的“USB协议”。它定义了一套标准化的方式,让AI模型(客户端)能够发现、描述并安全地调用来自各种数据源和服务(服务器)提供的“工具”或“上下文”。

这个项目仓库,就是MCP协议在Node.js环境下的官方SDK和工具集。它提供了构建MCP服务器(提供工具和数据)和MCP客户端(调用工具)所需的一切核心库、类型定义和开发样板。对于开发者而言,它的价值在于: 只要你用Node.js按照MCP标准封装好你的数据源(比如公司内部CRM、你的Notion笔记、实时股票API),那么所有兼容MCP的AI应用(如Claude Desktop、Cursor等)都能立即、安全地使用它,无需为每个应用单独适配。

2. MCP协议的核心设计思想与优势拆解

在深入代码之前,理解MCP协议的设计哲学至关重要。这决定了我们为什么要用它,而不是自己随便写个HTTP接口。

2.1 从“一对一”适配到“一对多”标准

传统的AI工具集成是“烟囱式”的。假设你有一个“项目任务查询”接口。如果你想在Claude里用,需要按照Anthropic的特定格式(可能是特定的JSON Schema)封装一个工具;想在Cursor里用,又得按照Cursor的规则再写一遍。这种模式效率低下,且无法积累。

MCP的核心思想是引入一个 标准化中间层 。你只需要按照MCP协议实现一次“项目任务查询”服务器。这个服务器会以一种标准格式向外界宣告:“我这里有一个叫 list_project_tasks 的工具,这是它的输入参数描述,这是它能返回的数据结构。” 任何兼容MCP的客户端(无论是Claude Desktop,还是未来任何其他AI应用)都能理解这个标准描述,并自动生成对应的UI和调用逻辑。这就实现了“一次编写,处处可用”。

2.2 不仅仅是“工具调用”,更是“上下文提供”

MCP协议的能力分为两大类,这比单纯的函数调用更强大:

  1. 工具(Tools) :即我们常说的“函数调用”。AI可以主动调用,执行一个操作并返回结果。例如: search_web (搜索)、 execute_sql (查询数据库)。
  2. 资源(Resources) :这是MCP一个非常关键的特性。服务器可以声明一系列“资源”(如 file:///path/to/notes.md weather://beijing/today )。AI客户端可以“订阅”或“加载”这些资源。当资源内容发生变化时(例如文件被修改),服务器可以主动通知客户端,客户端再将最新的内容作为上下文注入给AI模型。这使得AI能持续获取动态信息,而不必每次都主动调用工具。

这种设计让AI不仅能“做事”,还能持续“感知”环境变化,为实现更智能、更贴身的AI助手奠定了基础。

2.3 传输层抽象与安全性

MCP协议本身是传输层无关的。它定义了客户端与服务器之间通信的消息格式(基于JSON-RPC),但具体是通过标准输入输出(stdio)、HTTP还是WebSocket进行传输,由实现决定。 instanode-dev/mcp 的Node.js SDK主要支持stdio方式,这也是Claude Desktop集成MCP服务器的方式,因为它简单、安全、无需处理网络权限。

安全性是内置设计。服务器运行在本地或受信环境中,通过stdio与客户端通信,避免了敏感数据暴露在公网。客户端只能调用服务器明确声明的工具和访问声明的资源,并且服务器实现中可以包含完整的授权和验证逻辑。

3. 使用 instanode-dev/mcp 快速构建你的第一个MCP服务器

理论讲完,我们动手实战。假设我们要构建一个“时间信息服务器”,为AI提供获取当前时间、计算日期差等工具。

3.1 环境准备与项目初始化

首先,确保你的环境已安装Node.js(建议18.x或更高版本)。然后创建一个新的项目目录并初始化。

mkdir mcp-time-server
cd mcp-time-server
npm init -y

接下来,安装MCP Node.js SDK的核心包。这里我们需要两个包: @modelcontextprotocol/sdk 包含了协议的核心类型和通用逻辑; @modelcontextprotocol/sdk-server 提供了构建服务器的便利类。

npm install @modelcontextprotocol/sdk @modelcontextprotocol/sdk-server

同时,我们安装TypeScript和相关的类型定义,以获得更好的开发体验。

npm install --save-dev typescript @types/node
npx tsc --init

在生成的 tsconfig.json 中,确保 target 设置为 "ES2022" 或更高, module 设置为 "NodeNext"

3.2 定义工具(Tools)与资源(Resources)

在项目根目录创建 src/index.ts 文件。我们首先导入必要的模块,并定义服务器提供的工具。

import { Server } from '@modelcontextprotocol/sdk-server/index.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod'; // 用于参数验证,需要安装:npm install zod

// 初始化服务器,给它起个名字
const server = new Server(
  {
    name: 'mcp-time-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {}, // 声明我们支持工具功能
      resources: {}, // 声明我们支持资源功能(稍后添加)
    },
  }
);

// 定义工具:获取当前时间
const getCurrentTimeTool: Tool = {
  name: 'get_current_time',
  description: '获取当前的系统日期和时间,以及对应的UTC时间和时间戳。',
  inputSchema: {
    type: 'object',
    properties: {
      timezone: {
        type: 'string',
        description: '可选的时区名称,如 Asia/Shanghai, America/New_York。默认为系统时区。',
      },
      format: {
        type: 'string',
        description: '输出时间格式。可选值:iso (ISO 8601), human (人类可读), timestamp (毫秒时间戳)。默认为 iso。',
        enum: ['iso', 'human', 'timestamp'],
      },
    },
  },
};

// 定义工具:计算两个日期的差值
const calculateDateDiffTool: Tool = {
  name: 'calculate_date_diff',
  description: '计算两个日期之间的差值,可以按天、周、月、年输出。',
  inputSchema: {
    type: 'object',
    properties: {
      startDate: {
        type: 'string',
        description: '开始日期,格式为 YYYY-MM-DD 或 ISO 8601。',
      },
      endDate: {
        type: 'string',
        description: '结束日期,格式为 YYYY-MM-DD 或 ISO 8601。默认为今天。',
      },
      unit: {
        type: 'string',
        description: '差值单位。可选值:days, weeks, months, years。默认为 days。',
        enum: ['days', 'weeks', 'months', 'years'],
      },
    },
    required: ['startDate'], // 标记 startDate 为必填参数
  },
};

注意 :工具定义中的 inputSchema 使用了 JSON Schema 格式。这是MCP协议的要求,AI客户端(如Claude)会解析这个schema来生成调用界面并验证用户输入。务必把 description 字段写清楚,这直接决定了AI模型是否能正确理解和使用你的工具。

3.3 实现工具处理逻辑

定义了工具之后,我们需要在服务器上注册处理这些工具请求的处理器。

// 处理工具列表请求:当客户端查询“你有什么工具”时,返回定义好的工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [getCurrentTimeTool, calculateDateDiffTool],
  };
});

// 处理具体的工具调用请求
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    if (name === 'get_current_time') {
      const { timezone, format = 'iso' } = args as any;
      let now = new Date();

      // 处理时区(简化示例,生产环境应用用库如`luxon`)
      if (timezone) {
        // 这里只是示例,实际转换时区需要更复杂的逻辑
        console.log(`Requested timezone: ${timezone}`);
      }

      let result;
      switch (format) {
        case 'timestamp':
          result = { timestamp: now.getTime() };
          break;
        case 'human':
          result = { humanReadable: now.toLocaleString() };
          break;
        case 'iso':
        default:
          result = { isoString: now.toISOString(), localString: now.toLocaleString() };
      }

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };

    } else if (name === 'calculate_date_diff') {
      const { startDate, endDate = new Date().toISOString().split('T')[0], unit = 'days' } = args as any;
      const start = new Date(startDate);
      const end = new Date(endDate);

      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
        throw new Error('无效的日期格式。请使用 YYYY-MM-DD 或 ISO 8601 格式。');
      }

      const diffMs = end.getTime() - start.getTime();
      const diffDays = diffMs / (1000 * 60 * 60 * 24);

      let diffValue: number;
      let unitText: string;
      switch (unit) {
        case 'weeks':
          diffValue = diffDays / 7;
          unitText = '周';
          break;
        case 'months':
          diffValue = diffDays / 30.44; // 近似值
          unitText = '个月';
          break;
        case 'years':
          diffValue = diffDays / 365.25; // 近似值
          unitText = '年';
          break;
        case 'days':
        default:
          diffValue = diffDays;
          unitText = '天';
      }

      const result = {
        startDate: start.toISOString().split('T')[0],
        endDate: end.toISOString().split('T')[0],
        difference: {
          value: parseFloat(diffValue.toFixed(2)),
          unit: unit,
          unitText: unitText,
        },
        rawDiffDays: parseFloat(diffDays.toFixed(2)),
      };

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    } else {
      throw new Error(`未知的工具: ${name}`);
    }
  } catch (error: any) {
    // 将错误信息清晰地返回给客户端
    return {
      content: [
        {
          type: 'text',
          text: `工具调用失败: ${error.message}`,
        },
      ],
      isError: true,
    };
  }
});

3.4 添加资源(Resources)支持

为了让示例更完整,我们再添加一个“资源”:一个动态显示当前时间的资源。客户端可以加载它,并且当时间变化时(通过轮询),服务器会通知客户端更新上下文。

import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  Resource,
  ResourceTemplate,
} from '@modelcontextprotocol/sdk/types.js';

// 定义一个时间资源模板
const currentTimeResourceTemplate: ResourceTemplate = {
  uriTemplate: 'time://current/{format?}',
  name: '当前时间',
  description: '获取当前时间的动态资源。格式可以是 text 或 json。',
  mimeType: 'application/json',
};

// 定义资源列表
const resources: Resource[] = [
  {
    uri: 'time://current/json',
    name: '当前时间 (JSON)',
    description: '以JSON格式返回当前时间信息',
    mimeType: 'application/json',
  },
  {
    uri: 'time://current/text',
    name: '当前时间 (文本)',
    description: '以人类可读文本返回当前时间',
    mimeType: 'text/plain',
  },
];

// 处理资源列表请求
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: resources,
  };
});

// 处理读取资源请求
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;
  const now = new Date();

  if (uri === 'time://current/json') {
    return {
      contents: [
        {
          uri: uri,
          mimeType: 'application/json',
          text: JSON.stringify({
            iso: now.toISOString(),
            local: now.toLocaleString(),
            timestamp: now.getTime(),
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
          }, null, 2),
        },
      ],
    };
  } else if (uri === 'time://current/text') {
    return {
      contents: [
        {
          uri: uri,
          mimeType: 'text/plain',
          text: `当前时间是:${now.toLocaleString()} (${Intl.DateTimeFormat().resolvedOptions().timeZone})`,
        },
      ],
    };
  } else {
    throw new Error(`资源未找到: ${uri}`);
  }
});

3.5 启动服务器

最后,我们需要让服务器运行起来,并通过标准输入输出(stdio)与客户端通信。这是MCP服务器最常用的运行方式。

// 启动服务器,监听 stdio
async function runServer() {
  try {
    await server.connect(process.stdin, process.stdout);
    console.error('MCP时间服务器已启动,正在通过stdio通信...');
  } catch (error) {
    console.error('服务器启动失败:', error);
    process.exit(1);
  }
}

// 处理进程退出信号
process.on('SIGINT', () => {
  console.error('收到中断信号,关闭服务器...');
  server.close();
  process.exit(0);
});

runServer();

package.json 中添加启动脚本:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

现在,编译并运行服务器:

npx tsc
npm start

此时,服务器会在后台运行,等待通过stdio接收MCP客户端(如Claude Desktop)的连接。它自己不会输出任何东西到控制台(日志通过 console.error 输出到标准错误,不影响协议通信)。

4. 在Claude Desktop中集成与测试你的MCP服务器

构建好服务器后,最关键的一步是让它被AI客户端使用。我们以目前对MCP支持最完善的Claude Desktop为例。

4.1 配置Claude Desktop

Claude Desktop允许通过一个简单的JSON配置文件来添加本地MCP服务器。配置文件的位置因操作系统而异:

  • macOS : ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows : %APPDATA%\Claude\claude_desktop_config.json
  • Linux : ~/.config/Claude/claude_desktop_config.json

如果文件或目录不存在,请手动创建。配置文件的基本结构如下:

{
  "mcpServers": {
    "time-server": {
      "command": "node",
      "args": [
        "/ABSOLUTE/PATH/TO/YOUR/mcp-time-server/dist/index.js"
      ]
    }
  }
}

重要提示

  1. command 必须是能在系统PATH中找到的命令,这里是 node
  2. args 中的路径 必须是绝对路径 。相对路径会导致启动失败。
  3. 确保你的 index.js 文件已经通过 tsc 编译好。

4.2 测试与验证

  1. 保存配置并重启Claude Desktop :修改配置文件后,完全退出并重新启动Claude Desktop应用。
  2. 观察连接 :重新启动你的MCP服务器( npm start )。如果配置正确,Claude Desktop启动时会自动执行你配置的命令,与你的服务器建立stdio连接。你可以在服务器日志中看到连接成功的消息(如果你在代码中添加了连接日志)。
  3. 在Claude中调用工具 :在Claude Desktop的聊天框中,你可以直接尝试使用工具。例如,输入:“请帮我获取现在的北京时间。” Claude会识别出可用的 get_current_time 工具,并可能询问你是否要调用它,或者直接调用并返回结果。你也可以问:“从2023-01-01到今天有多少天了?” 这会触发 calculate_date_diff 工具。
  4. 使用资源 :你可以尝试让Claude“加载”或“关注”某个资源,例如:“请加载 time://current/json 这个资源作为上下文。” 如果服务器支持资源推送,最新的时间信息会被注入到对话上下文中。

4.3 配置技巧与常见问题

路径问题 :这是最常见的错误。在Windows上,路径分隔符是反斜杠,需要转义或使用正斜杠。

// Windows示例
"args": [
  "C:\\Users\\YourName\\Projects\\mcp-time-server\\dist\\index.js"
]
// 或使用正斜杠(Node.js支持)
"args": [
  "C:/Users/YourName/Projects/mcp-time-server/dist/index.js"
]

环境变量 :如果你的脚本依赖特定环境变量,可以在配置中指定 env 字段。

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/path/to/server.js"],
      "env": {
        "MY_API_KEY": "your_secret_key_here",
        "NODE_ENV": "production"
      }
    }
  }
}

调试 :如果工具没有出现,首先检查Claude Desktop的配置界面(某些版本在设置中有MCP服务器状态显示)。最有效的调试方法是查看服务器进程的标准错误输出。确保你的服务器代码在启动和收到请求时有清晰的 console.error 日志输出。你也可以在配置中暂时将命令改为 node + -e 执行一段打印日志的代码,来测试命令是否被正确执行。

5. 进阶开发:构建生产级MCP服务器的关键考量

一个能在团队或生产环境中使用的MCP服务器,需要考虑更多因素。 instanode-dev/mcp SDK提供的基础能力之上,我们需要自己构建健壮性。

5.1 错误处理与健壮性

上面的示例包含了基础的try-catch,但生产环境需要更精细的错误处理。

  • 输入验证 :使用像 zod 这样的库严格验证工具参数。MCP协议只要求提供JSON Schema,但服务器端必须自己验证。

    import { z } from 'zod';
    const GetCurrentTimeArgsSchema = z.object({
      timezone: z.string().optional(),
      format: z.enum(['iso', 'human', 'timestamp']).default('iso'),
    }).strict(); // .strict() 确保没有多余字段
    
    // 在处理函数中
    const validatedArgs = GetCurrentTimeArgsSchema.parse(args);
    
  • 资源加载失败 :对于资源(如读取文件、访问网络API),要有重试机制和友好的错误信息返回,避免服务器进程崩溃。

  • 连接保活与重连 :虽然stdio连接通常稳定,但需要考虑客户端意外退出的情况。服务器应优雅地处理EOF(标准输入结束),并可以安全地关闭自身。

5.2 性能与资源管理

  • 工具耗时 :如果某个工具执行时间很长(如大数据查询),应考虑实现异步操作并支持可能的“取消请求”功能(如果MCP客户端支持)。同时,在工具描述中注明可能耗时。
  • 资源订阅 :对于支持“订阅”并主动推送更新的资源,要管理好订阅列表,避免内存泄漏。在客户端断开连接时,清理对应的订阅。
  • 限制与配额 :对于可能被频繁调用的工具(如搜索),考虑实现简单的速率限制或调用次数配额,防止滥用。

5.3 安全性增强

  • 敏感工具 :对于执行删除、修改、支付等敏感操作的工具, 必须在服务器端实现二次确认或权限检查 。不能仅仅依赖AI客户端的调用。可以在工具实现中加入人工确认环节(例如,生成一个需要用户输入的验证码),或者与系统的身份认证绑定。
  • 参数净化 :特别是涉及文件路径、系统命令、数据库查询的工具,必须对输入参数进行严格的净化和转义,防止注入攻击。
  • 本地运行 :坚持MCP服务器在本地或受信网络环境运行的原则,这是其安全模型的基础。避免将能执行高危操作的MCP服务器暴露给不可信的客户端。

5.4 测试与调试策略

  • 单元测试 :为每个工具的处理函数编写单元测试,模拟不同的输入参数和边界情况。
  • 集成测试 :可以编写一个简单的MCP客户端测试脚本,模拟Claude Desktop的行为,连接你的服务器并发送标准的JSON-RPC请求,验证返回结果。
    // 一个简单的测试客户端示例
    import { Client } from '@modelcontextprotocol/sdk/client/index.js';
    import { StdioClientTransport } from '@modelcontextprotocol/sdk/stdio.js';
    import { spawn } from 'child_process';
    
    const serverProcess = spawn('node', ['dist/index.js']);
    const transport = new StdioClientTransport(serverProcess);
    const client = new Client({ name: 'test-client' }, { capabilities: {} });
    await client.connect(transport);
    const tools = await client.listTools();
    console.log('Available tools:', tools);
    
  • 日志记录 :使用结构化的日志库(如 pino winston ),记录工具调用、参数、执行时间、错误信息等,便于问题追踪和审计。

6. 生态展望与项目实践建议

MCP协议和 instanode-dev/mcp 项目代表了一种重要的趋势:将AI的能力从封闭的模型内部,扩展到开放、可组合的外部系统。对于开发者而言,现在投入学习并构建MCP服务器,是在为未来的AI原生应用生态积累资产。

实践建议

  1. 从封装内部工具开始 :最好的起点是你日常工作中高频使用的内部系统。比如,封装一个查询Jira任务、获取GitLab合并请求状态、查看内部仪表盘数据的MCP服务器。这能立即提升你的工作效率。
  2. 设计良好的工具语义 :工具的名称和描述要清晰、无歧义。遵循“动词+名词”的命名惯例(如 create_issue , search_documents )。详细的参数描述能极大提升AI模型调用的准确性。
  3. 模块化设计 :一个MCP服务器可以提供多个相关工具。但如果你有截然不同的数据源(如天气API和数据库操作),考虑拆分成多个独立的、职责单一的服务器。这样更易于维护和更新。
  4. 关注社区 :关注MCP协议的官方动态和社区。已经有许多优秀的开源MCP服务器出现,例如用于文件系统操作、Git仓库查询、网络搜索等。参考这些项目的代码是快速学习的最佳途径。

踩坑心得

  • 版本兼容性 :MCP协议和SDK可能还在快速迭代中。注意你使用的 @modelcontextprotocol/sdk 版本与目标AI客户端(如Claude Desktop)的兼容性。有时需要根据客户端版本调整SDK版本。
  • Claude Desktop缓存 :Claude Desktop可能会缓存工具列表。当你更新了服务器工具定义后,如果Claude里没出现新工具,尝试完全退出Claude Desktop并重启,或者清除其缓存(具体位置参考其文档)。
  • 错误信息反馈 :工具调用失败时,返回给AI的 content 中的错误信息要尽可能对用户友好。AI可能会直接将这个信息读给用户。避免输出堆栈跟踪等技术细节。

构建MCP服务器的过程,本质上是在为AI定义一套与你的数字世界交互的“标准操作接口”。随着兼容MCP的客户端越来越多,你今天编写的这个时间服务器,也许明天就能在另一个全新的AI办公套件中被调用。这种“一次编写,随处可用”的能力,正是标准化协议带来的最大红利。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐