Node写MCP入门教程

引言:用 Node + TypeScript 实现一个基于 stdio 的 MCP(Model Context Protocol)服务------mcp-file-counter,并通过 @modelcontextprotocol/inspector 进行交互式调试。文中提供完整代码、命令与调试步骤,按步骤即可搭建运行。

一、MCP 简介与目标

  • MCP 是一种让模型与外部工具安全、结构化交互的协议,常用于将代码、数据库、Web API 等能力接入到模型对话中。
  • 本文目标:实现一个名为 mcp-file-counter 的 MCP 服务,通过 count_files 工具统计指定目录的文件数量(支持忽略目录),并用 Inspector 可视化调用与调试。

二、创建项目

环境准备

  • Node 版本:>= 18
  • 包管理:推荐 pnpm
  • 辅助工具:ni/nr(可选,统一安装/运行体验)

检查与安装:

bash 复制代码
node -v
pnpm -v

# 若未安装 pnpm
npm i -g pnpm

# 可选:安装 ni/nr(Antfu 的统一包管理器命令)
npm i -g @antfu/ni

初始项目和安装依赖

在空目录中初始化并安装依赖:

bash 复制代码
# 初始化(可选)
pnpm init -y

# 运行时依赖
ni @modelcontextprotocol/sdk

# 开发依赖
ni -D typescript tsx @types/node @modelcontextprotocol/inspector

若未安装 ni,可使用 pnpm add 等价替代:

bash 复制代码
pnpm add @modelcontextprotocol/sdk
pnpm add -D typescript tsx @types/node @modelcontextprotocol/inspector

项目结构

建议目录结构如下:

text 复制代码
package.json
tsconfig.json
src/
  server.ts
  tools/
    fileCount.ts

TypeScript 配置

在项目根目录创建 tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

三、实现文件计数工具

新建 src/tools/fileCount.ts

功能与实现简述:

  • 功能:遍历指定根目录,统计普通文件总数,并支持忽略目录或相对路径。
  • 实现:以深度优先递归遍历目录,使用 ignore 集合过滤,visited 集合避免重复计数与符号链接循环。
  • 关键点:withFileTypes: true 获取类型;目录继续递归,文件累加;path.relative 帮助按根相对路径做忽略匹配。
ts 复制代码
import fs from "node:fs";
import path from "node:path";

export async function countFiles(root: string, ignore: string[] = []): Promise<number> {
  const visited = new Set<string>();
  const ig = new Set(ignore.map(s => s.trim()).filter(Boolean));

  async function walk(dir: string): Promise<number> {
    const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
    let count = 0;
    for (const d of dirents) {
      const full = path.join(dir, d.name);
      const rel = path.relative(root, full);
      if (ig.has(d.name) || ig.has(rel)) continue;
      if (d.isDirectory()) {
        count += await walk(full);
      } else if (d.isFile()) {
        if (!visited.has(full)) {
          visited.add(full);
          count += 1;
        }
      }
    }
    return count;
  }

  return walk(root);
}

四、实现 MCP Server(Stdio)

传输协议概览:

  • stdio:通过进程的标准输入/输出传输结构化消息,适合本地或被其他进程托管的场景,零网络依赖。
  • SSE:基于 HTTP 的 text/event-stream 单向服务器推送通道,客户端建立长连接,服务端持续按事件流推送消息,适合跨网络的实时输出与状态变更。
  • Streamable HTTP:用标准 HTTP 请求/响应承载 MCP 消息,响应体支持分块/增量输出(非 SSE 帧),保持 REST 语义与中间件兼容,适合网关与服务化部署。

StdioServerTransport 能力:

  • 封装 process.stdin/process.stdout 为消息通道,负责读取、解析与写回协议消息。
  • 管理连接的生命周期与错误处理,确保消息循环稳定运行。
  • Server.connect(transport) 协作完成握手、工具注册与调用路由。对业务代码暴露的主要用法是构造并传入 connect,其余由 SDK 管理。

代码说明:

  • 定义 Server 元信息与能力声明,用于握手阶段告知客户端可用能力。
  • 通过 server.addTool 注册工具,包括名字、描述和输入参数描述,以及实现 execute
  • 创建 StdioServerTransportserver.connect(transport),启动基于 stdio 的消息循环。

新建 src/server.ts

ts 复制代码
import path from "node:path";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { countFiles } from "./tools/fileCount.js";

const server = new Server(
  { name: "mcp-file-counter", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.addTool({
  name: "count_files",
  description: "Count files under a directory",
  inputSchema: {
    type: "object",
    properties: {
      path: { type: "string", default: "." },
      ignore: {
        type: "array",
        items: { type: "string" },
        default: ["node_modules", ".git", "dist"]
      }
    }
  },
  execute: async (args) => {
    const rootArg = typeof args.path === "string" ? args.path : ".";
    const ignoreArg = Array.isArray(args.ignore) ? args.ignore : ["node_modules", ".git", "dist"];
    const root = path.resolve(process.cwd(), rootArg);
    const total = await countFiles(root, ignoreArg);
    return { content: [{ type: "text", text: String(total) }] };
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);

这个execute 可以理解为最终重新启动了个进程,提供服务,比如

ini 复制代码
node server.js --path=/my/path

这样,execute函数内部就能获取到命令行参数path(即args.path

MCP是提供给Agent使用的,Agent会根据名称和描述去调用这个工具(命令),并传入合适的参数。

五、配置脚本和使用inspector调试MCP

package.json 中加入关键配置与脚本:

json 复制代码
{
  "type": "module",
  "engines": { "node": ">=18" },
  "packageManager": "pnpm@9",
  "scripts": {
    "dev": "tsx src/server.ts",
    "build": "tsc -p tsconfig.json",
    "start": "node dist/server.js",
    "inspect:dev": "mcp-inspector tsx \"tsx src/server.ts\"",
    "inspect": "mcp-inspector node dist/server.js"
  }
}

Inspector 原理:

  • mcp-inspector 充当 MCP 客户端与可视化 UI,会启动你提供的命令(如 tsx src/server.ts),并通过 stdio 与服务端交换协议消息。
  • 启动后完成握手(识别服务名与能力),拉取工具列表,渲染交互界面;当你在 UI 中调用某个工具时,它将请求参数序列化为协议消息发送给服务,随后展示返回的内容。
  • 这种方式不依赖网络,便于本地开发与调试;若换成 SSEStreamable HTTP 等传输,Inspector 则通过对应通道进行连接。

运行示例:

bash 复制代码
# 开发态直接运行(Stdio)
nr dev

# 构建并启动(Node 执行 dist)
nr build
nr start

# 使用 Inspector 调试(开发态)
nr inspect:dev

# 使用 Inspector 调试(构建后)
nr build && nr inspect
相关推荐
Yanni4Night26 分钟前
LogTape:零依赖的现代JavaScript日志解决方案
前端·javascript
重铸码农荣光26 分钟前
一文吃透 ES6 Symbol:JavaScript 里的「独一无二」标识符
前端·javascript
申阳27 分钟前
Day 15:01. 基于 Tauri 2.0 开发后台管理系统-Tauri 2.0 初探
前端·后端·程序员
想吃电饭锅27 分钟前
前端大厦建立在并不牢固的地基,浅谈JavaScript未来
前端
重铸码农荣光28 分钟前
一文吃透 JS 事件机制:从监听原理到实战技巧
前端
2503_9284115634 分钟前
11.25 Vue内置组件
前端·javascript·vue.js
后端小张1 小时前
【AI 学习】从0到1深入理解Agent AI智能体:理论与实践融合指南
人工智能·学习·搜索引擎·ai·agent·agi·ai agent
我有一个object1 小时前
uniapp上传文件报错:targetSdkVersion设置>=29后在Android10+系统设备不支持当前路径。请更改为应用运行路径!
前端·javascript·vue.js·uniapp
北极糊的狐1 小时前
关于jQuery 事件绑定,记录常用事件类型及核心注意事项
前端·javascript·jquery