从零搭建自定义 MCP Server:打造你的专属工具服务

🎯 本文目标

读完本文,你将学会:

  • MCP 协议的核心概念和工作原理
  • 如何开发自己的 MCP Server(工具 + 资源)
  • 如何用 LangChain 调用自定义 MCP Server
  • 跨进程通信的实现原理

一、为什么需要 MCP?

1.1 传统工具调用的问题

在学习 AI Agent 的过程中,我们通常会直接在代码中定义工具:

javascript 复制代码
// 传统方式:工具和代码耦合
async function queryUser(userId) {
  const users = {
    "001": { name: "张三", email: "zh@qq.com" },
    "002": { name: "李四", email: "ls@qq.com" },
  };
  return users[userId];
}

这种方式有两个问题:

问题 说明
耦合度高 工具只能在当前项目中使用,无法复用
语言限制 如果工具是 Java/Python 写的,Node.js 项目无法调用

1.2 MCP 的解决方案

MCP(Model Context Protocol) 提供了标准化的跨进程通信方式:

scss 复制代码
┌─────────────────┐          MCP协议          ┌─────────────────┐
│   AI Agent      │ ────────────────────────→ │   MCP Server    │
│   (客户端)       │ ←─────────────────────── │   (服务端)       │
└─────────────────┘     stdio / HTTP          └─────────────────┘
     Node.js                                           Python
     LangChain                                          Java
                                                       Go

核心优势

  • 跨进程:工具运行在独立进程中,不影响主进程
  • 跨语言:MCP Server 可以用任何语言开发
  • 即插即用:任何 MCP Server 都可以被任何 Agent 调用

1.3 MCP 的两种通信方式

方式 适用场景 通信原理
stdio 本地工具 通过标准输入输出流通信
HTTP 远程工具 通过网络请求通信

二、准备工作

2.1 创建项目

powershell 复制代码
mkdir mcp-server-demo
cd mcp-server-demo
npm init -y

2.2 安装依赖

powershell 复制代码
pnpm i @modelcontextprotocol/sdk @langchain/core @langchain/openai @langchain/mcp-adapters chalk zod dotenv

依赖说明

依赖 作用
@modelcontextprotocol/sdk MCP 协议核心库
@langchain/core LangChain 核心库
@langchain/openai OpenAI 模型集成
@langchain/mcp-adapters LangChain MCP 适配器
chalk 命令行彩色输出
zod 参数校验
dotenv 加载环境变量

2.3 配置环境变量

创建 .env 文件:

env 复制代码
DEEPSEEK_API_KEY=your_deepseek_api_key

三、开发自定义 MCP Server

3.1 创建 Server 文件

创建 src/my-mcp-server.mjs

javascript 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

代码解释

javascript 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

导入 MCP Server 类,用来创建工具和资源服务。

javascript 复制代码
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

导入 stdio 传输类,用来实现跨进程通信。

javascript 复制代码
import { z } from "zod";

导入 zod,用来定义参数校验规则。

3.2 定义数据库

javascript 复制代码
const database = {
  users: {
    "001": { id: "001", name: "祖豪", email: "zh@qq.com", role: "admin" },
    "002": { id: "002", name: "光光", email: "gg@qq.com", role: "user" },
    "003": { id: "003", name: "小红", email: "xh@qq.com", role: "user" },
  },
};

这是一个模拟数据库,存储用户信息。实际项目中可以替换为真实数据库。

3.3 创建 MCP Server

javascript 复制代码
const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
});

创建一个 MCP Server 实例,指定名称和版本号。

3.4 注册工具

javascript 复制代码
server.registerTool(
  "query_user",
  {
    description: `查询数据库中的用户信息。输入用户ID, 
  返回该用户的详细信息(姓名、邮箱、角色)`,
    inputSchema: {
      userId: z.string().describe("用户ID, 例如:001, 002, 003"),
    },
  },
  async ({ userId }) => {
    const user = database.users[userId];
    if (!user) {
      return {
        content: [
          {
            type: "text",
            text: `用户 ID ${userId} 不存在。可用的ID: 001, 002, 003`,
          },
        ],
      };
    }
    return {
      content: [
        {
          type: "text",
          text: `用户 ${user.id} 的信息是:姓名: ${user.name}, 邮箱: ${user.email}, 角色: ${user.role}`,
        },
      ],
    };
  },
);

代码逐行解释

javascript 复制代码
server.registerTool(name, definition, handler);

注册一个工具,接收三个参数:

  • name:工具名称
  • definition:工具定义(描述和参数)
  • handler:工具执行函数
javascript 复制代码
description: `查询数据库中的用户信息...`,

工具的描述信息,AI 会根据这个描述来判断是否调用该工具。

javascript 复制代码
inputSchema: {
  userId: z.string().describe("用户ID, 例如:001, 002, 003"),
},

参数校验规则:

  • userId:字符串类型
  • describe:参数说明,帮助 AI 理解如何传参
javascript 复制代码
async ({ userId }) => { ... }

工具的执行函数,接收参数并返回结果。

javascript 复制代码
return {
  content: [
    {
      type: "text",
      text: `用户信息...`,
    },
  ],
};

返回结果格式:

  • content:内容数组
  • type:内容类型(text 表示纯文本)
  • text:返回的文本内容

3.5 注册资源

javascript 复制代码
server.registerResource(
  "使用指南",
  "docs://guide",
  {
    description: "MCP Server使用指南",
    mimeType: "text/plain",
  },
  async () => {
    return {
      contents: [
        {
          uri: "docs://guide",
          mimeType: "text/plain",
          text: `MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 MCP Client 中通过自然语言对话,会自动调用相应工具。`,
        },
      ],
    };
  },
);

代码解释

javascript 复制代码
server.registerResource(name, uri, definition, handler);

注册一个资源,资源可以作为 AI 的上下文信息:

  • name:资源名称
  • uri:资源访问路径(类似 URL)
  • definition:资源定义
  • handler:资源内容生成函数

资源 vs 工具

类型 作用 示例
Tool AI 主动调用执行操作 查询用户、搜索酒店
Resource 被动提供上下文信息 使用指南、文档说明

3.6 启动 Server

javascript 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

代码解释

javascript 复制代码
const transport = new StdioServerTransport();

创建 stdio 传输实例,用于进程间通信。

javascript 复制代码
await server.connect(transport);

启动 Server,开始监听 stdio 输入。


四、开发 MCP Client

4.1 创建 Client 文件

创建 src/langchain-mcp-test.mjs

javascript 复制代码
import "dotenv/config";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { ChatOpenAI } from "@langchain/openai";
import chalk from "chalk";
import { HumanMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";

4.2 配置 MCP Client

javascript 复制代码
const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    "my-mcp-server": {
      command: "node",
      args: ["src/my-mcp-server.mjs"],
    },
  },
});

代码解释

javascript 复制代码
const mcpClient = new MultiServerMCPClient({ ... });

创建多服务器 MCP 客户端。

javascript 复制代码
"my-mcp-server": {
  command: "node",
  args: ["src/my-mcp-server.mjs"],
},

配置要连接的 MCP Server:

  • command:启动命令(node)
  • args:命令参数(要执行的文件路径)

当 Client 启动时,会自动通过 node src/my-mcp-server.mjs 启动一个子进程。

4.3 连接模型

javascript 复制代码
const model = new ChatOpenAI({
  modelName: "deepseek-v4-flash",
  apiKey: process.env.DEEPSEEK_API_KEY,
  configuration: {
    baseURL: "https://api.deepseek.com/v1",
  },
});

4.4 获取工具和资源

javascript 复制代码
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
let resourceContent = "";
for (const [serverName, resources] of Object.entries(res)) {
  for(const resource of resources){
    const content = await mcpClient.readResource(serverName, resource.uri);
    resourceContent += content[0].text;
  }
}

代码逐行解释

javascript 复制代码
const tools = await mcpClient.getTools();

获取所有 MCP Server 提供的工具列表。

javascript 复制代码
const res = await mcpClient.listResources();

获取所有 MCP Server 提供的资源列表。

javascript 复制代码
for (const [serverName, resources] of Object.entries(res)) {
  for(const resource of resources){
    const content = await mcpClient.readResource(serverName, resource.uri);
    resourceContent += content[0].text;
  }
}

遍历所有资源,读取资源内容,拼接成字符串。这个内容会作为 SystemMessage 传给 AI。

4.5 绑定工具到模型

javascript 复制代码
const modelWithTools = model.bindTools(tools);

代码解释

model.bindTools(tools) 把工具绑定到模型上,这样模型就知道有哪些工具可用,会自动决定是否调用工具。

4.6 AI 思考循环

javascript 复制代码
async function runAgentWithTools(query, maxIterations = 30) {
  const messages = [
    new SystemMessage(resourceContent),
    new HumanMessage(query)
  ];

  for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待AI思考, 第 ${i + 1} 轮....`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    if (!response.tool_calls || response.tool_calls.length === 0) {
      console.log(`\n AI 最终回复: ${response.content}`);
      return response.content;
    }

    console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length}个工具调用`));
    console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map((t) => t.name).join(", ")}`));

    for (const toolcall of response.tool_calls) {
      const foundTool = tools.find((t) => t.name === toolcall.name);
      if (foundTool) {
        const toolResult = await foundTool.invoke(toolcall.args);
        messages.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolcall.id,
        }));
      }
    }
  }
  return messages[messages.length - 1].content;
}

代码解释

  1. 初始化消息:SystemMessage(资源内容)+ HumanMessage(用户问题)
  2. AI 思考:调用模型,获取响应
  3. 判断是否调用工具
    • 没有工具调用 → 返回最终答案
    • 有工具调用 → 执行工具调用
  4. 执行工具调用
    • 通过工具名称查找工具
    • 调用工具并获取结果
    • 把结果作为 ToolMessage 添加到消息历史

4.7 启动程序

javascript 复制代码
await runAgentWithTools("查一下用户001的信息");
// await runAgentWithTools("MCP Server的使用指南是?");

await mcpClient.close();

代码解释

javascript 复制代码
await mcpClient.close();

关闭 MCP Client,同时会关闭所有子进程(包括 MCP Server),释放资源。


五、运行程序

5.1 启动 Server(测试)

powershell 复制代码
node src/my-mcp-server.mjs

Server 启动后会等待 stdio 输入,不会有任何输出(这是正常的)。

5.2 启动 Client

powershell 复制代码
node src/langchain-mcp-test.mjs

5.3 执行日志

场景1:查询用户信息

erlang 复制代码
正在等待AI思考, 第 1 轮....
检测到 1个工具调用
工具调用: query_user

正在等待AI思考, 第 2 轮....
 AI 最终回复: 用户 001 的信息是:姓名:祖豪, 邮箱:zh@qq.com, 角色:admin

场景2:查询使用指南

arduino 复制代码
正在等待AI思考, 第 1 轮....
 AI 最终回复: MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 MCP Client 中通过自然语言对话,会自动调用相应工具。

六、MCP 工作原理详解

6.1 通信流程

arduino 复制代码
┌─────────────────────────────────────────────────────────────────┐
│  1. Client 启动子进程: node src/my-mcp-server.mjs                │
│     └─→ 启动 my-mcp-server 进程                                  │
├─────────────────────────────────────────────────────────────────┤
│  2. Client 通过 stdio 发送初始化请求                             │
│     └─→ {"jsonrpc":"2.0","id":1,"method":"initialize",...}      │
├─────────────────────────────────────────────────────────────────┤
│  3. Server 响应初始化请求                                        │
│     └─→ {"jsonrpc":"2.0","id":1,"result":{"capabilities":...}}  │
├─────────────────────────────────────────────────────────────────┤
│  4. Client 请求工具列表                                          │
│     └─→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}          │
├─────────────────────────────────────────────────────────────────┤
│  5. Server 返回工具列表                                          │
│     └─→ {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}       │
├─────────────────────────────────────────────────────────────────┤
│  6. AI 决定调用工具,Client 发送工具调用请求                      │
│     └─→ {"jsonrpc":"2.0","id":3,"method":"tools/call",...}      │
├─────────────────────────────────────────────────────────────────┤
│  7. Server 执行工具,返回结果                                    │
│     └─→ {"jsonrpc":"2.0","id":3,"result":{"content":[...]}}     │
└─────────────────────────────────────────────────────────────────┘

6.2 stdio 通信原理

stdio(Standard Input/Output)是进程间通信的标准方式:

arduino 复制代码
┌─────────────────┐     标准输入      ┌─────────────────┐
│    主进程        │ ───────────────→ │    子进程       │
│  (Client)       │                  │  (Server)       │
│                 │ ←─────────────── │                 │
│                 │     标准输出      │                │
└─────────────────┘                  └─────────────────┘
  • 标准输入 (stdin):主进程向子进程发送数据
  • 标准输出 (stdout):子进程向主进程返回数据

这种方式不需要网络,速度快,适合本地工具调用。


七、常见问题与解决方案

问题1:Module not found

报错信息

arduino 复制代码
Error: Cannot find module '@modelcontextprotocol/sdk/server/stdio'

解决方案

检查导入路径是否正确,确保安装了 @modelcontextprotocol/sdk

powershell 复制代码
pnpm add @modelcontextprotocol/sdk

正确的导入路径:

javascript 复制代码
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

问题2:Server 无法连接

报错信息

vbnet 复制代码
MCPClientError: Failed to connect to stdio server "my-mcp-server": ...

解决方案

  1. 检查文件路径是否正确
  2. 确保 Server 文件能正常运行
  3. 检查端口是否被占用

问题3:工具调用失败

报错信息

lua 复制代码
ToolInputParsingException: Received tool input did not match expected schema

解决方案

检查工具的 inputSchema 定义是否正确,确保 AI 传的参数类型符合要求。

问题4:进程无法退出

问题描述:程序执行完后进程一直挂着不退出

解决方案

在程序末尾调用 mcpClient.close() 关闭所有子进程:

javascript 复制代码
await mcpClient.close();

八、完整代码

8.1 MCP Server

javascript 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const database = {
  users: {
    "001": { id: "001", name: "祖豪", email: "zh@qq.com", role: "admin" },
    "002": { id: "002", name: "光光", email: "gg@qq.com", role: "user" },
    "003": { id: "003", name: "小红", email: "xh@qq.com", role: "user" },
  },
};

const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
});

server.registerTool(
  "query_user",
  {
    description: `查询数据库中的用户信息。输入用户ID, 返回该用户的详细信息(姓名、邮箱、角色)`,
    inputSchema: {
      userId: z.string().describe("用户ID, 例如:001, 002, 003"),
    },
  },
  async ({ userId }) => {
    const user = database.users[userId];
    if (!user) {
      return {
        content: [
          {
            type: "text",
            text: `用户 ID ${userId} 不存在。可用的ID: 001, 002, 003`,
          },
        ],
      };
    }
    return {
      content: [
        {
          type: "text",
          text: `用户 ${user.id} 的信息是:姓名: ${user.name}, 邮箱: ${user.email}, 角色: ${user.role}`,
        },
      ],
    };
  },
);

server.registerResource(
  "使用指南",
  "docs://guide",
  {
    description: "MCP Server使用指南",
    mimeType: "text/plain",
  },
  async () => {
    return {
      contents: [
        {
          uri: "docs://guide",
          mimeType: "text/plain",
          text: `MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 MCP Client 中通过自然语言对话,会自动调用相应工具。`,
        },
      ],
    };
  },
);

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

8.2 MCP Client

javascript 复制代码
import "dotenv/config";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { ChatOpenAI } from "@langchain/openai";
import chalk from "chalk";
import { HumanMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";

const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    "my-mcp-server": {
      command: "node",
      args: ["src/my-mcp-server.mjs"],
    },
  },
});

const model = new ChatOpenAI({
  modelName: "deepseek-v4-flash",
  apiKey: process.env.DEEPSEEK_API_KEY,
  configuration: {
    baseURL: "https://api.deepseek.com/v1",
  },
});

const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
let resourceContent = "";
for (const [serverName, resources] of Object.entries(res)) {
  for(const resource of resources){
    const content = await mcpClient.readResource(serverName, resource.uri);
    resourceContent += content[0].text;
  }
}

const modelWithTools = model.bindTools(tools);

async function runAgentWithTools(query, maxIterations = 30) {
  const messages = [
    new SystemMessage(resourceContent),
    new HumanMessage(query)
  ];

  for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待AI思考, 第 ${i + 1} 轮....`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    if (!response.tool_calls || response.tool_calls.length === 0) {
      console.log(`\n AI 最终回复: ${response.content}`);
      return response.content;
    }

    console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length}个工具调用`));
    console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map((t) => t.name).join(", ")}`));

    for (const toolcall of response.tool_calls) {
      const foundTool = tools.find((t) => t.name === toolcall.name);
      if (foundTool) {
        const toolResult = await foundTool.invoke(toolcall.args);
        messages.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolcall.id,
        }));
      }
    }
  }
  return messages[messages.length - 1].content;
}

await runAgentWithTools("查一下用户001的信息");

await mcpClient.close();

九、总结

你学到了什么?

  1. MCP 协议:标准化的跨进程通信协议,让 AI 能调用外部工具
  2. 开发 MCP Server:注册工具和资源,通过 stdio 提供服务
  3. 开发 MCP Client:配置 Server,获取工具,调用 AI 思考循环
  4. 通信原理:通过 stdio(标准输入输出)实现进程间通信

核心概念回顾

概念 解释
MCP Server 工具服务端,注册工具和资源
MCP Client 工具客户端,调用工具
stdio 进程间通信方式(标准输入输出)
Tool AI 主动调用的工具
Resource AI 被动获取的上下文信息

下一步可以做什么?

  • 添加更多工具(如数据库查询、API 调用)
  • 使用 HTTP 方式连接远程 MCP Server
  • 用 Python/Java 开发跨语言 MCP Server

现在动手试试吧!打造你的专属工具服务 🚀

相关推荐
黄敬峰1 小时前
远程 MCP 实战:LangChain 同时接入高德地图、Chrome、文件系统,让 AI Agent 操控真实世界
面试
KaifuZeng1 小时前
通信与接口协议面试问题汇总三
单片机·嵌入式硬件·面试·通信与接口协议
胡萝卜术2 小时前
从聊天模型到本地执行助手:Remote MCP 多工具 Agent 实战
面试·架构·github
ShineWinsu4 小时前
对于Linux:基于UDP实现简单聊天室功能
linux·c++·面试·udp·笔试·进程·简单聊天室
YHHLAI15 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试
兰令水20 小时前
leecodecode【面试150】【2026.7.9打卡-java版本】
java·数据结构·leetcode·面试·职场和发展
用户938515635071 天前
Node.js 路径与文件系统:从基础到异步进化之路
javascript·后端·面试
wear工程师1 天前
MySQL 索引失效别再背八股:看懂 EXPLAIN,面试官追问也不慌
mysql·面试
uhakadotcom1 天前
Scrapy-Redis 里面提供哪些即开即用的功能能力模块
前端·面试·github