从零手写 MCP + Agent:让 AI 真正「动手干活」的完整项目实战

AI 时代程序员必备技能

Claude Code 完整实战,MCP 与子代理工程化落地,让 AI 接管脏活累活

引言

上一篇文章我们介绍了 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.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import fs from 'fs'
import path from 'path'
import { spawn } from 'child_process'

const server = new McpServer({
  name: 'project-manager',
  version: '1.0.0',
})

const processes = {}
const logs = {}

// =================================
// Tool 1:分析项目
// =================================
server.tool(
  'analyze_project',
  '分析 Node.js 项目,读取 package.json 中的 scripts',
  {
    projectPath: z.string(),
  },
  async ({ projectPath }) => {
    const packageFile = path.join(projectPath, 'package.json')
    // 检查 package.json
    if (!fs.existsSync(packageFile)) {
      return {
        content: [
          {
            type: 'text',
            text: '没有找到 package.json',
          },
        ],
      }
    }

    // 读取 package.json
    const packageJson = JSON.parse(fs.readFileSync(packageFile,'utf-8'))
    // 返回项目 scripts
    return {
      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 }) => {
    const processId = Date.now().toString()
    const child = spawn('npm', ['run',script ], { cwd: projectPath, shell: true })
    processes[processId] = child
    logs[processId] = ''
    // 监听标准输出
    child.stdout.on('data',data => {
        const text = data.toString()
        logs[processId] += text
        console.log(`[${processId}]`, text)
      }
    )
    // 监听错误输出
    child.stderr.on('data', data => {
        const text = data.toString()
        logs[processId] += text
        console.error(`[${processId}]`,text)
      }
    )

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify({
            status: 'started',
            processId,
            script,
          })
        }
      ]
    }
  }
)


// =================================
// 启动 MCP Server
// =================================
const transport = new StdioServerTransport()
await server.connect(transport)

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

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

三、创建 MCP Client

首先连接 MCP Server:

// agent.js
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
const mcp = new Client({
  name: 'project-agent',
  version: '1.0.0',
})

const transport = new StdioClientTransport({
    command: 'node',
    args: [
      'mcp-server/project-server.js',
    ]
  })

await mcp.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 调用
export async function chat(message) {
  // 获取工具
  const toolList = await mcp.listTools()
  const tools = toolList.tools.map(t => ({
    type: 'function',
    function: {
      name: t.name,
      description: t.description,
      parameters: t.inputSchema,
    },
  }))

  let messages = [
    {
      role: 'system',
      content: `你是一个项目管理AI助手。根据用户需求选择工具。不要自己编造结果。需要操作项目时调用工具。`,
    },
    {
      role: 'user',
      content: message,
    },
  ]

  // 第一次请求模型,让 LLM 决定是否调用工具
  let response = await openai.chat.completions.create({
    model: process.env.MODEL,
    messages,
    tools,
  })

  let msg = response.choices[0].message

  // 判断是否调用工具
  if (msg.tool_calls) {
    // 模型选择了工具,进入下一步执行
    // 注意:模型此时并没有执行工具,它只是告诉 Agent:我想调用某个工具
  }

  return msg.content
}

流程如下:

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

现在大模型知道它可以调用以下工具:analyze_projectstart_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 执行工具:

let msg = response.choices[0].message
for (const call of msg.tool_calls) {
   const name = call.function.name
   const args = JSON.parse(call.function.arguments)

   // 调 MCP
   const result = await mcp.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 = await openai.chat.completions.create({
  model: process.env.MODEL,
  messages,
})

return response.choices[0].message.content

模型最终回答:

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

  • devvite
  • buildvite build

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

五、创建 Gateway(HTTP 服务)

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

// gateway.js
import express from 'express'
import cors from 'cors'
import { chat } from './agent/agent.js'

const app = express()
app.use(cors())
app.use(express.json())

app.post('/chat', async (req, res) => {
  try {
    const { message } = req.body
    const answer = await chat(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 系统交互。

六、前端页面

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>AI 项目管理助手</title>
</head>
<body>
<h1>AI 项目管理助手</h1>
<textarea id="message" rows="5" cols="60" placeholder="请输入你的需求"></textarea>
<button onclick="sendMessage()">发送</button>
<pre id="result"></pre>

<script>
async function sendMessage() {
  const message = document.getElementById('message').value
  const result = document.getElementById('result')
  result.innerText = 'AI 正在思考...'
  const response = await fetch('http://localhost:3000/chat',
      {
        method: 'POST',
        headers: {
          'Content-Type':'application/json',
        },
        body: JSON.stringify({ message }),
      }
    )


  const data = await response.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,最终并不是只告诉你**「应该怎么做。」,而是可以进一步「让我帮你做。」**

AI 时代程序员必备技能

Claude Code 完整实战,MCP 与子代理工程化落地,让 AI 接管脏活累活

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值