☰
手写 MCP Server 和 MCP Client:Node.js 实现 AI Agent 自动调用本地工具(附完整源码)
2026/10/9 7:34:30 网站建设 项目流程

引言

上一篇文章我们介绍了 MCP(Model Context Protocol)的概念和 Tool Calling 的基本原理。但概念讲得再多,也不如动手写一遍来得实在。

这篇文章的目标是:从零开始,手写一个完整的 MCP + Agent 项目,让你亲眼看到:

  • MCP Server 如何提供工具
  • MCP Client 如何连接并发现工具
  • Agent 如何把 MCP Tool 交给 LLM
  • LLM 如何通过 Tool Calling 调用真实工具
  • 最终通过一个 Web 页面,让用户直接与 Agent 交互

本文目的

很多教程只讲概念或只贴零散代码,读者看完还是不知道「这些东西到底怎么串起来」。本文的核心目的只有一个:

用一条完整的链路,把 MCP、Tool Calling 和 Agent 这三个概念落地到可运行的代码中。

读完本文,你将能够:

  1. 自己搭建一个 MCP Server,提供真实项目操作能力
  2. 编写 MCP Client,让 Agent 发现并使用这些工具
  3. 理解 Tool Calling 的完整流程:LLM 决策 → Agent 执行 → 结果回传
  4. 把整个系统封装成 HTTP 服务,通过网页交互
  5. 理解 Agent Loop 的本质:不是特殊模型,而是「LLM + Tool Calling + 循环控制」

准备好了吗?我们开始写代码。

一、我们开始真正写代码

项目结构如下:

simple-mcp │ ├── mcp-server │ └── project-server.js │ ├── agent │ └── agent.js │ ├── frontend │ └── index.html │ ├── gateway.js │ ├── package.json │ └── .env

整个系统的最终架构如下:

浏览器 ↓ Gateway ↓ Agent ↓ LLM ↓ MCP Client ↓ MCP Server ↓ 项目文件系统

二、创建 MCP Server

// project-server.jsimport{McpServer}from'@modelcontextprotocol/sdk/server/mcp.js'import{StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js'import{z}from'zod'importfsfrom'fs'importpathfrom'path'import{spawn}from'child_process'constserver=newMcpServer({name:'project-manager',version:'1.0.0',})constprocesses={}constlogs={}// =================================// Tool 1:分析项目// =================================server.tool('analyze_project','分析 Node.js 项目,读取 package.json 中的 scripts',{projectPath:z.string(),},async({projectPath})=>{constpackageFile=path.join(projectPath,'package.json')// 检查 package.jsonif(!fs.existsSync(packageFile)){return{content:[{type:'text',text:'没有找到 package.json',},],}}// 读取 package.jsonconstpackageJson=JSON.parse(fs.readFileSync(packageFile,'utf-8'))// 返回项目 scriptsreturn{content:[{type:'text',text:JSON.stringify({projectPath,scripts:packageJson.scripts||{},}),},],}})// =================================// Tool 2:启动项目// =================================server.tool('start_project','启动 Node.js 项目中的指定 npm script',{projectPath:z.string(),script:z.string(),},async({projectPath,script})=>{constprocessId=Date.now().toString()constchild=spawn('npm',['run',script],{cwd:projectPath,shell:true})processes[processId]=child logs[processId]=''// 监听标准输出child.stdout.on('data',data=>{consttext=data.toString()logs[processId]+=text console.log(`[${processId}]`,text)})// 监听错误输出child.stderr.on('data',data=>{consttext=data.toString()logs[processId]+=text console.error(`[${processId}]`,text)})return{content:[{type:'text',text:JSON.stringify({status:'started',processId,script,})}]}})// =================================// 启动 MCP Server// =================================consttransport=newStdioServerTransport()awaitserver.connect(transport)

这段代码创建了一个名为project-manager的 MCP Server,通过 Stdio 协议通信,对外暴露了两个工具:

  • analyze_project:接收projectPath参数,读取指定路径下的package.json,返回其中的scripts配置;如果找不到package.json则返回提示信息。
  • start_project:接收projectPath和script参数,使用child_process.spawn启动npm run <script>,记录进程 ID 和运行日志,方便后续查看。最后通过StdioServerTransport启动服务,等待 MCP Client 连接并调用这些工具。

三、创建 MCP Client

首先连接 MCP Server:

// agent.jsimport{Client}from'@modelcontextprotocol/sdk/client/index.js'import{StdioClientTransport}from'@modelcontextprotocol/sdk/client/stdio.js'constmcp=newClient({name:'project-agent',version:'1.0.0',})consttransport=newStdioClientTransport({command:'node',args:['mcp-server/project-server.js',]})awaitmcp.connect(transport)

这里发生了什么?

Agent 启动流程如下:

agent.js ↓ 创建 MCP Client ↓ 启动 project-server.js ↓ 建立 MCP 连接

四、完整的 Tool Calling 流程

现在正式进入 Tool Calling 环节。整个过程分为三步:LLM 决策 → Agent 执行 → 结果回传。

第一步:把 MCP Tools 交给 LLM

首先,我们需要从 MCP Server 获取工具列表,然后将 MCP Tool 转换为大模型能够理解的格式。关于为什么要做这个转换,可以参考这篇文章。

// agent.js — 工具转换 + 第一次 LLM 调用exportasyncfunctionchat(message){// 获取工具consttoolList=awaitmcp.listTools()consttools=toolList.tools.map(t=>({type:'function',function:{name:t.name,description:t.description,parameters:t.inputSchema,},}))letmessages=[{role:'system',content:`你是一个项目管理AI助手。根据用户需求选择工具。不要自己编造结果。需要操作项目时调用工具。`,},{role:'user',content:message,},]// 第一次请求模型,让 LLM 决定是否调用工具letresponse=awaitopenai.chat.completions.create({model:process.env.MODEL,messages,tools,})letmsg=response.choices[0].message// 判断是否调用工具if(msg.tool_calls){// 模型选择了工具,进入下一步执行// 注意:模型此时并没有执行工具,它只是告诉 Agent:我想调用某个工具}returnmsg.content}

流程如下:

MCP Server ↓ listTools() ↓ analyze_project start_project ↓ 转换 ↓ LLM Tools

现在大模型知道它可以调用以下工具:analyze_project、start_project。
假设用户输入「帮我分析 D:\2025_pc 项目」,关键在于tools参数告诉模型有哪些工具可用。模型看到的信息如下:

用户:帮我分析 D:\2025_pc 项目 可用工具:analyze_project start_project

模型判断用户想分析项目,于是返回:

{"tool_calls":[{"function":{"name":"analyze_project","arguments":"{\"projectPath\":\"D:\\\\2025_pc\"}"}}]}

注意:模型此时并没有执行工具。它只是告诉 Agent:我想调用analyze_project。

第二步:Agent 执行 MCP Tool

Agent 收到 LLM 返回的tool_calls后,真正去调用 MCP Server 执行工具:

letmsg=response.choices[0].messagefor(constcallofmsg.tool_calls){constname=call.function.nameconstargs=JSON.parse(call.function.arguments)// 调 MCPconstresult=awaitmcp.callTool({name,arguments:args,})messages.push(msg)messages.push({role:'tool',tool_call_id:call.id,content:JSON.stringify(result),})}

整个调用链路如下:

LLM ↓ 返回 analyze_project ↓ Agent ↓ MCP Client ↓ MCP Server ↓ 执行 analyze_project

MCP Server 读取 D:\2025_pc\package.json,返回:

{"projectPath":"D:\\2025_pc","scripts":{"dev":"vite","build":"vite build"}}

第三步:把 Tool 结果再次交给 LLM

现在 Agent 已经拿到了 Tool 的执行结果,但用户还没有得到最终答案。因此,我们需要将工具返回的结果再次交给模型进行总结回答:

Tool执行结果 ↓ 再次发送给LLM ↓ LLM理解结果 ↓ 生成自然语言

代码实现如下:

// 第二次让模型总结response=awaitopenai.chat.completions.create({model:process.env.MODEL,messages,})returnresponse.choices[0].message.content

模型最终回答:

项目D:\2025_pc包含以下脚本:

  • dev:vite
  • build:vite build

如果你想启动开发环境,建议执行npm run dev。

五、创建 Gateway(HTTP 服务)

现在我们需要一个 HTTP 服务来连接前端和 Agent。gateway.js负责接收浏览器发来的请求,调用 Agent 的chat函数,并将结果返回给前端。

// gateway.jsimportexpressfrom'express'importcorsfrom'cors'import{chat}from'./agent/agent.js'constapp=express()app.use(cors())app.use(express.json())app.post('/chat',async(req,res)=>{try{const{message}=req.bodyconstanswer=awaitchat(message)res.json({answer})}catch(err){console.error(err)res.status(500).json({error:err.message})}})app.listen(3000,()=>{console.log('Gateway running on http://localhost:3000')})

Gateway 是整个系统的入口,它做的事情很简单:

  1. 启动 Express 服务,监听 3000 端口
  2. 接收POST /chat请求
  3. 调用 Agent 的chat函数,触发完整的 Tool Calling 流程
  4. 将 Agent 返回的结果以 JSON 格式返回给前端

这样,前端只需要发一个 HTTP 请求,就能与整个 MCP + Agent 系统交互。

六、前端页面

<!DOCTYPEhtml><html><head><metacharset="UTF-8"><title>AI 项目管理助手</title></head><body><h1>AI 项目管理助手</h1><textareaid="message"rows="5"cols="60"placeholder="请输入你的需求"></textarea><buttononclick="sendMessage()">发送</button><preid="result"></pre><script>asyncfunctionsendMessage(){constmessage=document.getElementById('message').valueconstresult=document.getElementById('result')result.innerText='AI 正在思考...'constresponse=awaitfetch('http://localhost:3000/chat',{method:'POST',headers:{'Content-Type':'application/json',},body:JSON.stringify({message}),})constdata=awaitresponse.json()result.innerText=data.answer}</script></body></html>

现在打开网页,输入:帮我分析 D:\2025_pc 项目

完整调用链如下:

浏览器 ↓ POST /chat ↓ Agent ↓ GLM-4.7-FP8 ↓ 模型选择 analyze_project ↓ MCP Client ↓ MCP Server ↓ 读取 D:\2025_pc/package.json ↓ 返回 scripts ↓ GLM ↓ 生成最终回答 ↓ 浏览器

七、最终总结:Tool Calling、Agent 和 MCP 到底是什么关系?

如果把整个系统比作一个人:

LLM ↓ 大脑 Tool Calling ↓ 大脑发出行动指令 Tool ↓ 手和脚 Agent ↓ 大脑不断思考 行动 观察 再思考 MCP ↓ 连接外部世界的标准接口

所以它们之间的关系是:

AI Agent │ │ ┌──────┴──────┐ │ │ LLM Tools │ │ 思考、推理、决策 │ │ │ └──────┬──────┘ │ Tool Calling │ │ MCP Client │ MCP Protocol │ MCP Server │ ┌──────────┼──────────┐ │ │ │ Files Git Docker │ │ │ └──────────┴──────────┘

最终:

Tool Calling 让 AI 能够提出"我要调用什么工具"。

Agent 让 AI 能够不断思考、调用工具并完成复杂任务。

MCP 让 AI 能够用统一的方式连接和使用各种外部工具。

这也是为什么现在 AI 正在从聊天机器人逐渐走向 AI Agent。

因为真正有价值的 AI,最终并不是只告诉你**「应该怎么做。」,而是可以进一步「让我帮你做。」**

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询