引言
上一篇文章我们介绍了 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 这三个概念落地到可运行的代码中。
读完本文,你将能够:
- 自己搭建一个 MCP Server,提供真实项目操作能力
- 编写 MCP Client,让 Agent 发现并使用这些工具
- 理解 Tool Calling 的完整流程:LLM 决策 → Agent 执行 → 结果回传
- 把整个系统封装成 HTTP 服务,通过网页交互
- 理解 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:接收
projectPath和script参数,使用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_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 执行工具:
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包含以下脚本:
dev:vitebuild:vite 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 是整个系统的入口,它做的事情很简单:
- 启动 Express 服务,监听 3000 端口
- 接收
POST /chat请求 - 调用 Agent 的
chat函数,触发完整的 Tool Calling 流程 - 将 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,最终并不是只告诉你**「应该怎么做。」,而是可以进一步「让我帮你做。」**

4177

被折叠的 条评论
为什么被折叠?



