纯小白入门:手把手教你实现第一个MCP服务器
简单来说,它就像是AI模型与外部工具之间的“翻译官”,让AI能够更好地理解和利用各种外部资源和工具。本文面向毫无经验的纯小白,旨在通过详细的步骤和易懂的讲解,帮助你完成第一个MCP服务器的搭建和使用。MCP听起来可能很高深,但实际上,只要跟着本指南操作,你就能在短时间内理解其核心概念并实现实战应用。MCP采用Client/Server架构,支持上下文传递与SSE流式响应,大大提升了工具调用的效率与
问题背景
在开发一个TCP通信的DLL库时,遇到了一个诡异的运行时错误:Run-Time Check Failure #2 - Stack around the variable 'stData' was corrupted.
这个错误发生在数据接收处理环节,表现为结构体成员变量神秘"丢失"——在函数内部赋值正常,但函数返回后某些成员值就变成了0。
错误现象
BOOL CMideaBedCtrl::ExecuteCommandImpl(PipePackage& stPipePackage,
const char* commandPrefix,
const char* formattedCommand)
{
// ... 发送数据 ...
RecvData stData; // 栈上分配
stData.iSeqID = iSeqID;
if (!m_cTCP.RecvDataFromServer(stData)) {
AfxMessageBox("数据接收失败");
return FALSE;
}
// 调试发现这里 iHeaderSize, iDataSize 等成员变成了0!
// 但在 RecvDataFromServer 内部查询时值是正确的
}
排查过程
第一阶段:怀疑栈溢出
首先怀疑是缓冲区溢出,因为RecvData结构体很大:
#define MAX_RECV_DATA_LENGTH 64*1000 // 64,000字节
struct RecvData
{
int iSeqID;
BYTE btRecv[MAX_RECV_DATA_LENGTH]; // 64KB数组
int iHeaderSize;
int iDataSize;
int iDeviceId; // 新增的成员
RecvData() { /* 初始化 */ }
};
检查了所有memcpy操作,添加了长度校验:
// 添加安全检查
int iTotalCopySize = m_dequeRecvData[i].iDataSize +
m_dequeRecvData[i].iHeaderSize +
COMMHEADER;
if (iTotalCopySize > MAX_RECV_DATA_LENGTH) {
iTotalCopySize = MAX_RECV_DATA_LENGTH; // 截断
}
memcpy(stRecv.btRecv, m_dequeRecvData[i].btRecv, iTotalCopySize);
但问题依旧存在。
第二阶段:怀疑多线程同步问题
检查了临界区使用情况,确保线程安全:
BOOL CTCPClient::QuerySeqID(RecvData & stRecv)
{
EnterCriticalSection(&m_csDequeData);
// ... 查询和拷贝操作 ...
LeaveCriticalSection(&m_csDequeData);
}
甚至调整了锁的顺序,确保不会死锁:
// 先处理队列操作,再调用其他可能涉及锁的函数
m_dequeRecvData.erase(m_dequeRecvData.begin() + i);
LeaveCriticalSection(&m_csDequeData);
DeleteSeqID(stRecv.iSeqID); // 这个函数用不同的锁
第三阶段:发现根本原因
最终通过对比发现,DLL和调用方使用的头文件不一致:
调用方使用的旧头文件:
struct RecvData
{
int iSeqID;
int iHeaderSize;
int iDataSize;
BYTE btRecv[MAX_RECV_DATA_LENGTH];
// 缺少 iDeviceId 成员!
};
DLL使用的新头文件:
struct RecvData
{
int iSeqID;
int iHeaderSize;
int iDataSize;
int iDeviceId; // 新增成员
BYTE btRecv[MAX_RECV_DATA_LENGTH];
};
问题分析
内存布局差异
成员 | 旧结构体偏移 | 新结构体偏移 | 大小 |
---|---|---|---|
iSeqID | 0 | 0 | 4字节 |
iHeaderSize | 4 | 4 | 4字节 |
iDataSize | 8 | 8 | 4字节 |
iDeviceId | 不存在 | 12 | 4字节 |
btRecv | 12 | 16 | 64,000字节 |
栈损坏机制
- 调用方认为RecvData大小是:
12 + 64000 = 64012
字节 - DLL认为RecvData大小是:
16 + 64000 = 64016
字节 - 当DLL写入
iDeviceId
时,实际上写入了调用方栈帧的其他数据 - 当DLL读取
iDeviceId
时,实际上读取的是垃圾数据
解决方案
1. 统一头文件管理
创建公共头文件,确保DLL和所有调用方使用相同的定义:
// CommonProtocol.h
#pragma once
#define PROTOCOL_VERSION 2
#define MAX_RECV_DATA_LENGTH 64000
struct RecvData
{
int iSeqID;
int iHeaderSize;
int iDataSize;
#if PROTOCOL_VERSION >= 2
int iDeviceId; // V2新增功能
#endif
BYTE btRecv[MAX_RECV_DATA_LENGTH];
RecvData();
};
2. 添加编译时检查
在关键位置添加静态断言,确保结构体大小符合预期:
// 在DLL和调用方都添加检查
static_assert(sizeof(RecvData) == 64016,
"RecvData结构体大小不匹配,请检查头文件版本");
3. 版本兼容性检查
在DLL接口中添加版本验证:
// DLL导出函数
__declspec(dllexport) int GetProtocolVersion();
__declspec(dllexport) bool VerifyCompatibility(size_t expectedSize);
// 调用方初始化时检查
if (!VerifyCompatibility(sizeof(RecvData))) {
// 处理版本不兼容错误
}
经验教训
- 头文件是契约:修改DLL接口时,必须同步更新所有调用方的头文件
- 二进制兼容性:C++结构体的内存布局很脆弱,添加成员会破坏兼容性
- 防御性编程:添加编译时和运行时的版本检查
- 文档的重要性:接口变更需要有清晰的版本记录和变更说明
预防措施
- 建立统一的头文件管理机制
- 使用接口版本控制
- 添加自动化构建检查,确保头文件同步
- 考虑使用更安全的接口设计,如使用抽象接口而不是直接结构体传递
// 更安全的设计:使用接口类而不是裸结构体
class IRecvData {
public:
virtual int GetSeqID() = 0;
virtual int GetDataSize() = 0;
virtual const BYTE* GetData() = 0;
virtual ~IRecvData() {}
};
这个坑虽然踩得痛苦,但让我对C++的二进制兼容性和DLL接口设计有了更深的理解。希望这篇记录能帮助其他开发者避免类似问题!
无需AI背景,从零开始掌握模型上下文协议
本文面向毫无经验的纯小白,旨在通过详细的步骤和易懂的讲解,帮助你完成第一个MCP服务器的搭建和使用。MCP听起来可能很高深,但实际上,只要跟着本指南操作,你就能在短时间内理解其核心概念并实现实战应用。
一、MCP是什么?为什么它如此重要?
在开始实战前,我们先简单了解下MCP是什么。MCP全称Model Context Protocol,是一种开放协议,它定义了向大语言模型提供上下文的标准方式。简单来说,它就像是AI模型与外部工具之间的“翻译官”,让AI能够更好地理解和利用各种外部资源和工具。
MCP主要解决了以下痛点:
- 碎片化问题:不同AI模型有不同的工具调用方式,MCP提供统一标准
- 高耦合问题:工具逻辑与模型代码深度绑定,难以复用
- 上下文丢失:多轮调用时状态管理复杂
MCP采用Client/Server架构,支持上下文传递与SSE流式响应,大大提升了工具调用的效率与灵活性。这意味着你可以构建一次工具,然后在多种AI应用中使用它。
二、环境准备:搭建开发基础
1. 安装Python和必要工具
MCP支持多种编程语言,但Python拥有最完善的生态支持,对新手最为友好。
首先,访问Python官网下载并安装最新版本的Python(推荐3.9或以上版本)。安装时记得勾选“Add Python to PATH”选项。
安装完成后,打开终端(Windows用户使用Command Prompt或PowerShell,Mac用户使用Terminal),输入以下命令验证安装是否成功:
python --version
如果显示Python版本号,说明安装成功。
2. 创建项目目录和虚拟环境
虚拟环境是Python开发的最佳实践,它可以隔离项目依赖,避免版本冲突。
# 创建项目目录
mkdir my-first-mcp
cd my-first-mcp
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
# Windows用户
venv\Scripts\activate
# Mac/Linux用户
source venv/bin/activate
激活虚拟环境后,你的命令行提示符前会出现(venv)
标识。
3. 安装MCP核心库
在激活的虚拟环境中,安装必要的依赖包:
pip install mcp aiohttp anyio
这些库中,mcp
是核心库,aiohttp
用于处理HTTP请求,anyio
用于管理异步并发。
三、创建第一个MCP服务器:天气预报查询
让我们从一个实用的例子开始——创建一个可以查询天气信息的MCP服务器。
1. 创建项目文件
在项目目录中创建server.py
文件,这将是我们的MCP服务器主文件。
2. 搭建服务器骨架
首先,我们导入必要的库并搭建一个基本的MCP服务器框架:
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
# 创建Server实例,名字叫"weather-server"
app = Server("weather-server")
# 此处将注册我们的工具和资源
async def main():
# 使用stdio传输层,这是与Claude等客户端通信的标准方式
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
以上代码创建了一个最简MCP服务器,虽然它还不能做任何有用的事情,但骨架已经搭建完成。
3. 添加天气查询功能
现在我们来为服务器添加实际功能。我们将使用OpenWeatherMap的API作为数据源(你需要先免费注册获取API key)。
在app = Server("weather-server")
后添加以下代码:
import aiohttp
from mcp.server.models import ToolResult
# 你的OpenWeatherMap API Key,建议从环境变量读取
API_KEY = "your_api_key_here"
@app.tool()
async def get_weather(city: str) -> Tool:
"""根据城市名称查询当前天气情况
Args:
city: 城市名称,例如"Beijing"或"上海"
Returns:
返回该城市的当前天气信息,包括温度和天气状况。
"""
# 构建请求URL
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_KEY}&units=metric"
# 错误处理与超时控制
try:
async with aiohttp.ClientSession() as session:
# 设置10秒超时
async with asyncio.timeout(10):
async with session.get(url) as response:
# 检查HTTP状态码是否成功
response.raise_for_status()
data = await response.json()
# 解析返回的JSON数据
temperature = data['main']['temp']
description = data['weather'][0]['description']
result_text = f"{city}的当前天气:温度{temperature}℃,{description}"
return ToolResult(content=result_text)
except asyncio.TimeoutError:
return ToolResult(content="天气请求超时,请稍后重试", isError=True)
except aiohttp.ClientResponseError as e:
return ToolResult(content=f"HTTP错误: {e.status} - {e.message}", isError=True)
except Exception as e:
return ToolResult(content=f"获取天气信息时发生错误: {str(e)}", isError=True)
4. 代码解析与最佳实践
- 工具声明:
@app.tool()
装饰器将函数注册为MCP工具。函数的参数和文档字符串会自动成为工具Schema的一部分,帮助AI模型理解如何调用它。 - 错误处理:我们添加了多层错误处理,包括超时控制、HTTP状态码检查和通用异常捕获。这是生产级代码的基本要求。
- 返回值:函数返回
ToolResult
对象,其中content
是给AI模型看的结果,isError=True
表示此次调用失败。
四、测试MCP服务器
1. 使用MCP CLI测试
首先安装MCP CLI工具:
pip install mcp-cli
然后运行你的服务器:
python server.py
在另一个终端中,使用MCP CLI连接测试:
mcp-tool --tool get_weather --parameters '{"city": "Beijing"}'
如果一切正常,你将看到北京的天气信息。
2. 集成到Claude Desktop应用中
MCP服务器的真正价值在于可以被AI应用使用,如Claude Desktop。以下是配置步骤:
-
找到Claude Desktop的配置文件位置:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
- Windows:
-
编辑配置文件,添加你的MCP服务器:
{
"mcpServers": {
"weather-server": {
"command": "/path/to/your/venv/bin/python",
"args": ["/path/to/your/project/server.py"]
}
}
}
- 重启Claude Desktop应用,现在你可以直接让Claude查询天气了,例如:“请问北京天气怎么样?”
五、实战进阶:封装高德地图API
除了天气查询,我们还可以尝试更实用的功能。以下是一个集成高德地图地理编码API的示例:
@app.tool()
async def geocode_address(address: str) -> Tool:
"""根据中文地址获取其经纬度坐标
Args:
address: 详细的中文地址,例如:北京市朝阳区阜通东大街6号
Returns:
返回该地址的经纬度信息
"""
# 你的高德API Key
AMAP_API_KEY = "your_amap_api_key_here"
url = f"https://restapi.amap.com/v3/geocode/geo?key={AMAP_API_KEY}&address={address}"
try:
async with aiohttp.ClientSession() as session:
async with asyncio.timeout(10):
async with session.get(url) as response:
response.raise_for_status()
data = await response.json()
if data.get('status') != '1':
error_info = data.get('info', 'Unknown error')
return ToolResult(
content=f"地理编码API错误: {error_info}",
isError=True
)
geocodes = data.get('geocodes', [])
if not geocodes:
return ToolResult(content="未找到指定地址的结果")
location = geocodes[0].get('location')
formatted_address = geocodes[0].get('formatted_address')
result_text = f"地址'{formatted_address}'的坐标是:{location}"
return ToolResult(content=result_text)
except asyncio.TimeoutError:
return ToolResult(content="请求超时", isError=True)
except Exception as e:
return ToolResult(content=f"发生错误: {str(e)}", isError=True)
这个工具可以让AI模型将中文地址转换为经纬度坐标,在实际应用中非常实用。
六、常见问题与解决方案
1. 服务器启动失败
- 问题:Python路径错误或依赖缺失
- 解决:确认虚拟环境已激活,所有依赖已安装
pip install mcp aiohttp anyio
2. Claude无法识别MCP服务器
- 问题:配置文件路径或格式错误
- 解决:检查配置文件路径是否正确,JSON格式是否合法
3. 工具调用返回错误
- 问题:API密钥无效或网络连接问题
- 解决:检查API密钥是否正确,网络是否通畅
七、学习资源与下一步
完成以上步骤后,你已经成功创建了第一个MCP服务器!为了进一步学习,你可以:
- 探索官方文档和示例
- 学习更复杂的MCP功能,如资源管理和提示词模板
- 尝试将MCP服务器集成到更多AI应用中,如Cursor、AG2等
MCP技术正在快速发展,2025年以后将看到更多创新应用。作为开发者,现在掌握MCP技术将为你打开AI应用开发的新世界大门。
注意:本文中的API密钥仅为例示,实际使用时请替换为自己的有效密钥,并遵循相关API的使用条款。本文示例代码基于MIT许可证,可自由使用和修改。
更多推荐
所有评论(0)