如何编写自己的 MCP 服务器来抓取网站数据:面向初学者的分步指南
引言:读完这篇指南你能得到什么
想象一下,你打开和 AI 助手的聊天窗口,输入:「打开竞争对手的页面,把商品目录里所有商品的名称和价格收集起来,整理成一张表」。助手不会回答「我无法访问互联网」,而是真的打开页面、提取数据,然后把结果交给你。这正是你读完这篇指南后能亲手做出来的东西。连接语言模型和网页之间的桥梁,就是你自己用 Python 写的 MCP 服务器。
先声明一点:我们不会去讲现成的 Playwright MCP 或其他开箱即用的方案,博客里另有专门文章介绍它们。这里的任务不同:从零开始写一个服务器,让你看懂每一行代码,能够添加自己的工具、接入移动代理,并把逻辑改造成适合具体任务的样子。自己写的东西,永远比别人的更灵活。
这篇指南适合谁
- 营销人员和商家:需要快速收集价格、评价、商品描述和竞品内容,又不想专门找开发者定制爬虫。
- 投放优化师(Affiliate):需要监控 offer、落地页和素材,想把重复劳动交给 AI 智能体。
- 开发者:听说过 MCP 协议,但还没搭过自己的服务器,想要一份能直接跑的模板。
- 移动代理用户:希望智能体的请求走自己的代理,而不是从家庭 IP 直接发出去。
需要提前掌握什么
这篇指南面向初学者。不要求编程经验,但懂一点命令行、知道怎么用文本编辑器打开文件会很有帮助。所有代码都可以整段复制,每一部分都会用大白话解释。如果你已经会写 Python,文章后段有专门给进阶读者的高级功能章节。
需要多少时间
第一次完整走一遍,建议预留 2-3 小时。安装工具大约 30 分钟,一小时内能跑通最小的 MCP 服务器,剩下的时间用来添加数据提取工具、接入代理和测试。换一台电脑从零重做,20-30 分钟就能搞定。
前期准备:工具、权限和系统要求
动手写代码之前,先确认该有的东西都齐了。这一节半小时就能过完,却能帮你避开后面一半的坑。
系统要求
- 电脑系统为 Windows 10/11、macOS 12 及以上或 Linux(Ubuntu 22.04 及以上)。本文内容在所有系统上都能跑,区别只在于文件路径。
- 至少 4 GB 内存和 1 GB 可用磁盘空间。
- 稳定可用的网络连接。
需要安装什么
- Python 3.11 或更高版本。 2026 年主流版本是 3.12 和 3.13。去 Python 官网下载安装包。Windows 上在安装向导第一个界面一定要勾选 Add python.exe to PATH,否则终端里找不到 python 命令。macOS 上更推荐用 Homebrew 安装,命令是 brew install python。Ubuntu 上执行 sudo apt install python3 python3-venv python3-pip。
- 写代码用的文本编辑器。 推荐 Visual Studio Code:免费、语法高亮、能提示错误。其他编辑器也行,连记事本都能用,不过用 VS Code 会舒服很多。
- MCP 客户端,也就是带 AI 智能体、用来连接你服务器的应用。初学者最简单的选择是 Claude Desktop。此外 Cursor 编辑器、装了 GitHub Copilot 扩展的 VS Code 等工具也支持 MCP。开始之前至少装好其中一个。
- Node.js 20 或更高版本。 它不是给服务器本身用的,而是给调试工具 MCP Inspector 用的。去 Node.js 官网下载 LTS 版安装包,按默认设置装好即可。
需要的权限信息
讲代理的那一节需要你的移动代理数据:主机、端口、用户名和密码,以及更换 IP 的链接(如果你的套餐支持)。这些在服务商后台都能找到。如果暂时没有代理也没关系:服务器会直接访问,代理以后再补一行代码就行。
做好备份
我们后面要修改 MCP 客户端的配置文件。改之前先把它复制到一个安全的地方,比如桌面并标注「backup」。万一出问题,把副本放回去就行。服务器代码存到单独的文件夹里,每做完一步都保存一份文件副本,或者用 Git 提交一次(如果你会用的话)。
建议: 在磁盘上建一个路径短、不含空格和中文的文件夹,比如 Windows 上的 C:/mcp-collector 或 macOS、Linux 上的 ~/mcp-collector。路径里有空格和中文经常会导致服务器无法从配置启动,而你会为此浪费一小时排查原因。
基础概念:MCP 服务器的结构,以及它为什么对 AI 智能体有用
写第一行代码之前,先弄清楚术语。不搞明白这些,整篇教程就像一串魔法咒语;弄明白了,每一步操作都有它的道理。
什么是 MCP
MCP(Model Context Protocol,模型上下文协议) 是一个开放协议,规定语言模型如何与外部工具交互。在它出现之前,每个服务都想自己的办法「给 AI 一双手」。MCP 把这件事标准化了:只要你的服务器按协议实现,任何兼容的客户端都能识别它,无论是 Claude Desktop、Cursor 还是你自己写的智能体。可以把 MCP 比作 USB 接口:插U盘还是鼠标不重要,接口是同一个。
客户端和服务器
MCP 架构里有两个角色。客户端是带 AI 的应用,负责提问和调用工具。MCP 服务器是提供这些工具的程序。在我们的场景里,服务器提供「上网取数据」的能力,客户端就是你的 AI 助手。服务器跑在你本地电脑上,客户端直接和它通信。
工具、资源和提示词
MCP 服务器可以给客户端提供三类东西:
- 工具(tools):模型可以调用的函数,比如「下载页面」「提取所有链接」「切换代理 IP」。这是我们这篇指南的核心。
- 资源(resources):供读取的数据,例如配置文件内容或上一次采集的结果。
- 提示词(prompts):预制好的请求模板,用户用一条命令就能调用。
做数据采集,有工具就够了。资源和提示词我们放到进阶章节再说。
模型怎么知道该调用什么
这里有个重要的细节。客户端连接服务器时会请求工具列表,包括它们的名称、描述和参数。这些描述会进入模型的上下文。之后模型就依靠描述文本,自己决定调用哪个工具、传什么参数。所以代码里的函数描述不是走过场,而是给 AI 的说明书。你把工具干什么、什么时候用写得越清楚,智能体工作得越准。
传输方式:stdio 和 HTTP
服务器和客户端总得互通消息。协议规定了两种主要方式。stdio:客户端把脚本作为子进程启动,通过标准输入输出通信。这是本地运行最简单的方式,我们从它入手。Streamable HTTP:服务器作为 Web 服务运行,客户端通过地址连接。服务器部署在远程机器上,或多个客户端共用时,就需要这种方式。这部分放到进阶章节讲。
⚠️ 注意: 使用 stdio 传输时,进程的标准输出全被协议的通信消息占用了。如果你在代码里用普通的 print 调试,客户端收到的就是一堆乱码而不是正常响应,会直接断开连接。调试信息只能输出到错误流 stderr。记住这条规则,能帮你省下大量时间。
为什么通过 MCP 做数据采集很方便
传统爬虫是写死的:它只会在特定网站上抓特定字段。页面结构一变,爬虫就挂。而「AI 智能体 + MCP 服务器」的组合不一样:服务器提供通用工具(下载、提取文本、按选择器查找元素),模型自己看懂页面结构并总结结果。你得到了灵活性,不用为每个新数据源重写代码。
第 1 步:创建项目并安装依赖
本步目标: 准备好一个隔离的 Python 环境,装好 MCP 服务器需要的库。到这一步结束时,你会有一个带可用虚拟环境的项目文件夹。
为什么需要虚拟环境
虚拟环境是项目文件夹里的一份独立 Python 副本,带自己的库。它的作用是让我们的服务器不和电脑上其他 Python 程序冲突,也让 MCP 客户端明确知道该用哪个解释器启动。没有它,「终端里能跑、客户端里跑不起来」这类问题几乎必然发生。
分步操作
- 打开终端。Windows 上按 Win+R,输入 powershell,回车。macOS 上用 Spotlight 打开 Terminal(Cmd+空格,然后输入 Terminal)。Linux 上按 Ctrl+Alt+T。
- 创建项目文件夹并进入。Windows 上执行两条命令:mkdir C:/mcp-collector,然后 cd C:/mcp-collector。macOS 和 Linux 上:mkdir ~/mcp-collector,然后 cd ~/mcp-collector。
- 用 python --version 检查 Python 版本(macOS 和 Linux 上可能需要 python3 --version)。应该看到类似 Python 3.12.x 的输出。如果版本低于 3.11 或提示命令找不到,回到准备章节重新安装 Python。
- 用 python -m venv .venv 创建虚拟环境。项目文件夹里会出现一个隐藏的 .venv 文件夹,耗时 10-20 秒。
- 激活环境。Windows PowerShell 里执行:.venv/Scripts/Activate.ps1。如果 PowerShell 提示禁止运行脚本,先执行 Set-ExecutionPolicy -Scope CurrentUser RemoteSigned,按 Y 确认,再重新激活。macOS 和 Linux 上:source .venv/bin/activate。激活后终端行首会出现 (.venv) 标记。
- 更新包管理器:python -m pip install --upgrade pip。
- 一条命令装好所有库:pip install "mcp[cli]" httpx beautifulsoup4。其中 mcp 是协议的官方 Python SDK(2026 年主流是 1.x 分支),httpx 是支持代理的现代 HTTP 请求库,beautifulsoup4 是解析 HTML 的工具。安装需要 1-2 分钟。
- 在项目文件夹里新建一个空文件 server.py。在 VS Code 里:用 File、Open Folder 打开文件夹,然后点左侧面板的新建文件图标,输入文件名。
这些库分别是干什么的
- mcp 负责整个协议:注册工具、消息收发、参数描述。其中的 FastMCP 模块让你用一个装饰器就把普通函数声明成工具。
- httpx 负责下载页面。相比过时的 requests,它支持 HTTP/2、异步,配置代理也很方便。
- beautifulsoup4 把 HTML 变成一棵树,方便按标签和 CSS 选择器查找元素。
建议: 现在就记住虚拟环境里解释器的完整路径。Windows 上是 C:/mcp-collector/.venv/Scripts/python.exe,macOS 和 Linux 上是 /Users/用户名/mcp-collector/.venv/bin/python(Linux 上是 /home/用户名/...)。连接客户端时要用到它。环境激活状态下,用 where python(Windows)或 which python(macOS 和 Linux)可以查到准确路径。
✅ 检查: 执行 pip list,列表里应该有 mcp、httpx 和 beautifulsoup4。再执行 python -c "import mcp, httpx, bs4; print('ok')",应该输出 ok,没有任何报错。
可能遇到的问题
- 提示找不到 python 命令。 Windows 上重新安装 Python 并勾选 Add to PATH。macOS 上用 python3 代替 python。
- pip 提示权限不足。 多半是虚拟环境没激活,你把包装到了系统 Python 里。检查命令行开头的 (.venv) 标记。
- 安装时报编译错误。 先更新 pip 再试。如果还不行,确认 Python 版本不低于 3.11。
第 2 步:写一个最小的 MCP 服务器,带上第一个工具
本步目标: 写出一个能跑的 MCP 服务器,只有一个工具:按地址下载页面并返回 HTML。这是后面所有功能的地基。
服务器代码
打开 server.py,把下面的代码整段粘进去:
import sys
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP('web-collector')
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36',
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
}
def log(message: str) -> None:
print(message, file=sys.stderr)
@mcp.tool()
def fetch_page(url: str, max_chars: int = 20000) -> str:
'''Скачивает страницу по указанному URL и возвращает её HTML-код.
Используй, когда нужно посмотреть исходную разметку страницы.
Параметр max_chars ограничивает длину ответа, чтобы не переполнять контекст.'''
log(f'fetch_page: {url}')
with httpx.Client(headers=HEADERS, timeout=20.0, follow_redirects=True) as client:
response = client.get(url)
response.raise_for_status()
return response.text[:max_chars]
if __name__ == '__main__':
mcp.run()逐行解读代码
- FastMCP('web-collector') 创建一个名为 web-collector 的服务器对象。客户端会在已连接服务器列表里显示这个名字。
- HEADERS 是我们发给网站的请求头。很多网站如果收到没有常见浏览器 User-Agent 的请求,会返回不完整内容或直接报错。Accept-Language 头表示我们需要俄语版本的页面。
- log 函数把消息写到 stderr。之所以不用普通 print,是因为 stdout 被协议占用了。这些消息你能在客户端日志和 MCP Inspector 里看到。
- @mcp.tool() 是把普通函数变成 MCP 工具的装饰器。SDK 会自动读取函数名、参数类型和文档字符串,生成给模型看的描述。默认值 max_chars = 20000 表示这个参数可以省略。
- 文档字符串(三引号里那段)就是 AI 会读到的东西。这里我们解释工具做什么、什么时候用。这种描述要写详细,用你和智能体交流的语言来写。
- httpx.Client 带 follow_redirects=True 会自动跟随重定向,timeout=20.0 防止请求一直挂着。
- raise_for_status() 在网站返回 4xx 或 5xx 时抛错。SDK 会捕获它,把清楚的错误信息返回给客户端,而不是默不作声。
- mcp.run() 用默认的 stdio 传输启动服务器,等待客户端的命令。
用 MCP Inspector 做第一次验证
直接运行 server.py 没有意义:它会一直等客户端消息,什么都不显示。验证要用 MCP Inspector,这是一个模拟客户端、可以手动调用工具的网页界面。
- 确认虚拟环境已激活,且当前在项目文件夹里。
- 执行 mcp dev server.py。这个命令包含在装好的 mcp 包的 cli 扩展里。第一次运行时它会通过 npx 下载 Inspector,大约需要一分钟。
- 终端里会显示类似 http://localhost:6274 的地址,新版本里还会给出访问令牌。在浏览器里打开这个地址(通常会自己弹出来)。
- 在 Inspector 左侧面板确认传输方式选的是 STDIO,命令是 python,参数是 server.py。点 Connect 按钮。
- 状态指示变绿,显示 Connected。在顶部菜单切到 Tools 标签,点 List Tools。
- 列表里会出现 fetch_page 工具,带文档字符串里的描述和两个参数。点它。
- url 字段填 https://example.com,max_chars 留空或填 5000。点 Run Tool。
- 右侧显示结果:从 doctype 标签开头的页面 HTML。下方服务器日志标签里会看到 fetch_page: https://example.com 这行。
✅ 检查: Inspector 显示 Connected,Tools 列表里有 fetch_page,用 example.com 调用返回 HTML 且没有报错。都符合的话,你的第一个 MCP 服务器已经跑起来了。
可能遇到的问题
- mcp dev 提示找不到 npx。 说明没装 Node.js。装好并重启终端。
- Inspector 打开了,但 Connect 报错。 检查命令字段里填的 python 是不是来自已激活的环境。也可以填 .venv 里 python.exe 的完整路径。
- 连接时报 SyntaxError。 代码复制时丢了缩进。Python 里缩进是必须的:函数体要缩进四个空格。在编辑器里检查文件。
- 工具返回 403 错误。 网站没接受这个请求。example.com 不会这样,真实网站遇到这个问题我们会在代理那一步解决。
第 3 步:把 MCP 服务器接入 AI 客户端
本步目标: 在 AI 客户端的设置里注册服务器,让智能体能看到你的工具,并在普通聊天里调用它。我们讲最普及的 Claude Desktop,再简要提一下其他选择。
接入 Claude Desktop
- 打开 Claude Desktop。进设置:Windows 上点左上角菜单里的 Settings;macOS 上点菜单栏 Claude 里的 Settings。
- 切到 Developer 标签,点 Edit Config 按钮。会打开放着 claude_desktop_config.json 的文件夹。如果文件不存在,客户端会帮你创建。
- 把这个文件复制到桌面,做个备份。
- 用 VS Code 或其他编辑器打开。文件如果是空的,直接整段粘进去。如果里面已经有其他服务器,把自己的配置块加进 mcpServers 对象里,用逗号隔开。
{
"mcpServers": {
"web-collector": {
"command": "C:/mcp-collector/.venv/Scripts/python.exe",
"args": ["C:/mcp-collector/server.py"]
}
}
}macOS 和 Linux 上把路径换成自己的,例如 /Users/ivan/mcp-collector/.venv/bin/python 和 /Users/ivan/mcp-collector/server.py。注意:即使在 Windows 上,路径也写成正斜杠。这样更省事,因为 JSON 里的反斜杠要写成双份,而正斜杠 Windows 也认。
- 保存文件。确认最后一项后面没有多余逗号,所有括号都闭合。多一个逗号就使 JSON 无效,客户端会悄悄忽略整个配置。
- 彻底关闭 Claude Desktop 再重新打开。Windows 上光关窗口不够:右键任务栏托盘图标,选 Quit。客户端只在启动时读配置。
- 重启后新建一个对话。在输入框下方找到工具图标(滑块或插头样子的)。点开:列表里应该出现 web-collector 服务器,带一个 fetch_page 工具。
- 在聊天里写:「用 fetch_page 下载 https://example.com 这个页面,告诉我它的标题是什么」。客户端会请求调用工具的权限。点 Allow 或 Allow for this chat。
- 几秒后智能体就会回答页面标题是 Example Domain。它确实通过你的服务器发出了真实请求。
接入 Cursor 和 VS Code
在 Cursor 里打开 Settings,找到 MCP 分区,点 Add new global MCP server。会打开一个 mcp.json 文件,结构和 Claude Desktop 的完全一样。粘进同样的配置块保存即可。在装了 Copilot 的 VS Code 里,在工作文件夹根目录创建 .vscode/mcp.json,其中不用 mcpServers 键而是 servers 键,里面还是同样的 command 和 args。保存后服务器配置块上方会出现 Start 按钮。所有客户端原理都一样:指定启动解释器的命令和脚本路径。
建议: command 里要填虚拟环境里的 python,而不是光写 python。客户端启动进程时用的环境变量和你的终端不一样,系统的 python 命令可能是另一个版本、没装你需要的库。填完整路径一劳永逸。
✅ 检查: 客户端界面里能看到 web-collector 服务器,智能体收到请求后会调用 fetch_page,并正确复述 example.com 页面的内容。客户端日志里(Claude Desktop 是与配置同级的 logs 文件夹,文件是 mcp-server-web-collector.log)能看到 fetch_page: https://example.com 这行。
可能遇到的问题
- 列表里没有出现服务器。 检查 JSON 是否有效:把内容粘到任意在线 JSON 校验工具里,或者在 VS Code 里打开,它会标出错误。确认客户端是彻底重启过的。
- 服务器旁边有红色错误指示。 打开日志文件。最多见的是 ModuleNotFoundError:解释器指错了。检查 command 里的路径。
- 智能体说它无法访问互联网。 它没看到工具。确认工具面板里工具已启用,并明确要求:「用 fetch_page 工具」。
- 报 spawn ENOENT 错误。 python 或 server.py 的路径写错了。从文件管理器复制路径,把反斜杠改成正斜杠。
第 4 步:添加数据提取工具
本步目标: 让服务器不只返回原始 HTML,还能给出有用的数据:干净文本、链接列表和按 CSS 选择器找到的元素。这样智能体就能采集结构化信息,不用把上下文浪费在标签上。
为什么光有 fetch_page 不够
真实页面的 HTML 动辄几百 KB,其中大部分是脚本、样式和服务性标记。每次都把整页丢给模型,很快会撞上上下文上限,你还要为多余的 token 付费。正确的策略是:服务器先做粗清洗和结构化,模型只处理紧凑的数据。所以我们再加三个专用工具。
更新后的代码
把 server.py 的内容换成扩展版。fetch_page 还在,但公共的下载逻辑抽成了单独的函数 _get_html,供所有工具使用。
import sys
from urllib.parse import urljoin
import httpx
from bs4 import BeautifulSoup
from mcp.server.fastmcp import FastMCP
mcp = FastMCP('web-collector')
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36',
'Accept-Language': 'ru-RU,ru;q=0.9,en;q=0.8',
}
def log(message: str) -> None:
print(message, file=sys.stderr)
def _get_html(url: str) -> str:
log(f'GET {url}')
with httpx.Client(headers=HEADERS, timeout=20.0, follow_redirects=True) as client:
response = client.get(url)
response.raise_for_status()
return response.text
def _clean(text: str) -> str:
return ' '.join(text.split())
@mcp.tool()
def fetch_page(url: str, max_chars: int = 20000) -> str:
'''Возвращает сырой HTML страницы. Используй только когда нужна именно разметка,
например чтобы подобрать CSS-селектор. Для чтения содержимого используй extract_text.'''
return _get_html(url)[:max_chars]
@mcp.tool()
def extract_text(url: str, max_chars: int = 15000) -> str:
'''Возвращает чистый текст страницы без скриптов, стилей и разметки.
Лучший выбор, когда нужно прочитать статью, описание товара или отзывы.'''
soup = BeautifulSoup(_get_html(url), 'html.parser')
for tag in soup(['script', 'style', 'noscript', 'svg', 'header', 'footer', 'nav']):
tag.decompose()
title = _clean(soup.title.get_text()) if soup.title else ''
body = _clean(soup.get_text(' '))
return f'Заголовок: {title}. Текст: {body}'[:max_chars]
@mcp.tool()
def extract_links(url: str, limit: int = 100, contains: str = '') -> list[dict]:
'''Возвращает список ссылок со страницы: текст ссылки и полный адр��с.
Параметр contains фильтрует ссылки, в адресе которых есть указанная подстрока,
например /product/ или /catalog/.'''
soup = BeautifulSoup(_get_html(url), 'html.parser')
result = []
seen = set()
for a in soup.find_all('a', href=True):
full = urljoin(url, a['href'])
if full in seen or (contains and contains not in full):
continue
seen.add(full)
result.append({'text': _clean(a.get_text())[:120], 'url': full})
if len(result) >= limit:
break
return result
@mcp.tool()
def select_elements(url: str, css_selector: str, limit: int = 50) -> list[str]:
'''Находит на странице элементы по CSS-селектору и возвращает их текст.
Примеры селекторов: h2, .price, div.product-card, table tr.
Используй, когда нужны конкретные повторяющиеся блоки: цены, названия, строки таблицы.'''
soup = BeautifulSoup(_get_html(url), 'html.parser')
elements = soup.select(css_selector)[:limit]
return [_clean(el.get_text(' ')) for el in elements]
if __name__ == '__main__':
mcp.run()每个工具做什么
- extract_text 会把脚本、样式、页头、页脚和导航从文档里删掉,剩下的文本拼成一行,用单个空格分隔。_clean 函数通过 split 和 join 去掉多余换行和制表符。回答开头会加上页面标题,让智能体一眼知道自己在看什么。
- extract_links 收集所有 a 标签,用 urljoin 把相对地址转成绝对地址,用 seen 集合去重,还支持按子串过滤链接。这样智能体一次调用就能拿到比如目录里所有商品卡片的链接。
- select_elements 是最强大的工具。它接收 CSS 选择器,返回找到的元素文本。智能体可以先用 fetch_page 看一段 HTML,发现价格放在 price 类里,然后用选择器 .price 调用 select_elements。
注意这些文档字符串:我们明确提示模型在什么情况下选哪个工具。这会明显提升智能体的表现。
怎么验证
- 运行 mcp dev server.py 并在 Inspector 里连接。Tools 列表里现在有四个工具。
- 用任意新闻站或商品目录的地址调用 extract_links,contains 参数填地址里某一段。结果是一组带 text 和 url 字段的对象。
- 用同样的地址和选择器 h2 调用 select_elements。你会得到一组标题。
- 重启 Claude Desktop(配置不用改,只是代码变了),然后要求:「从某某网站首页收集所有 h2 标题,以及指向新闻栏目的链接,整理成表格」。
建议: 不知道用哪个选择器的话,在浏览器里打开页面,按 F12,选左上角带箭头的元素选取工具,点你想抓的区块。代码里就能看到它的类名。带点的类名选择器,比如 .product-title,通常都能用。更进一步,你甚至可以直接让智能体:「下载 HTML 自己给价格挑个选择器」。
✅ 检查: Inspector 和客户端里都能看到四个工具,extract_text 返回没有标签的可读文本,extract_links 返回带绝对地址的列表,select_elements 用 h2 选择器返回标题。
可能遇到的问题
- select_elements 返回空列表。 要么选择器不对,要么内容是在页面加载后由 JavaScript 渲染的。用 fetch_page 检查:如果 HTML 里没有需要的数据,说明网站是在客户端渲染的。这类网站需要浏览器引擎,那就是另一篇文章的话题了。
- extract_text 输出乱码。 网站用了非标准编码。在 response.raise_for_status() 后面加一行 response.encoding = response.charset_encoding or 'utf-8'。
- 回答被截断。 把调用时的 max_chars 调大,或者让智能体分几次、用不同的选择器分批取页面。
第 5 步:接入移动代理和 IP 轮换
本步目标: 让 MCP 服务器的所有请求都走移动代理,增加切换 IP 和查询当前地址的工具。之后智能体就会以移动运营商的身份工作,而不是用你的家庭或公司 IP。
采集数据为什么需要移动代理
当你用一个 IP 地址采集数据时,网站会看到几十个一模一样的连续请求,于是开始返回验证码、截断内容或 429「请求过多」错误。移动代理能同时解决几个问题。第一,地址属于真实的移动运营商,这类地址由成千上万的用户共享,网站对它们更宽容。第二,你可以通过链接或定时切换 IP,分散压力。第三,把智能体的工作活动和你的个人会话分开。对营销人员来说,这还能让他们看到特定地区移动用户看到的网站样子。
⚠️ 注意: 代理是让采集器稳定、正常工作的工具,不是用来违反规则的手段。只采集公开可访问的数据,遵守网站的使用条款和 robots.txt,不要制造过量负载,不要在没有合法依据的情况下收集个人数据。使用工具的责任由你自己承担。
分步操作
- 打开你的移动代理服务商后台,找到连接信息:主机、端口、用户名、密码。通常它们会被拼成 login:password@host:port 这样一行。顺便复制换 IP 的链接(如果有)。
- 在 server.py 里,其他 import 后面加上 import os。然后在 HEADERS 块下面加设置:
PROXY_URL = os.environ.get('MOBILE_PROXY_URL', '')
ROTATE_URL = os.environ.get('PROXY_ROTATE_URL', '')
def _client() -> httpx.Client:
kwargs = {'headers': HEADERS, 'timeout': 30.0, 'follow_redirects': True}
if PROXY_URL:
kwargs['proxy'] = PROXY_URL
return httpx.Client(**kwargs)- 把 _get_html 里创建 httpx.Client 的那行改成调用 _client()。现在它写成:with _client() as client。所有工具会自动走代理。
- 在 if __name__ 那行前面加两个新工具:
@mcp.tool()
def current_ip() -> str:
'''Показывает IP-адрес, с которого сервер сейчас выходит в интернет.
Используй, чтобы убедиться, что прокси подключён, или после смены IP.'''
with _client() as client:
return client.get('https://api.ipify.org').text.strip()
@mcp.tool()
def rotate_ip() -> str:
'''Запрашивает смену IP-адреса мобильного прокси через ссылку из личного кабинета.
Вызывай, если сайт начал отдавать ошибки 429 или капчу. После вызова подожди 5-10 секунд.'''
if not ROTATE_URL:
return 'Ссылка смены IP не настроена в переменной PROXY_ROTATE_URL'
response = httpx.get(ROTATE_URL, timeout=15.0)
log(f'rotate_ip: status {response.status_code}')
return f'Запрос смены IP отправлен, ответ прокси-сервиса: {response.status_code}'- 通过客户端配置里的环境变量传入代理数据。我们特意不把用户名密码写进代码,免得它随文件被误发出去。打开 claude_desktop_config.json,给服务器配置块加上 env 部分:
{
"mcpServers": {
"web-collector": {
"command": "C:/mcp-collector/.venv/Scripts/python.exe",
"args": ["C:/mcp-collector/server.py"],
"env": {
"MOBILE_PROXY_URL": "http://login:password@proxy-host:port",
"PROXY_ROTATE_URL": "https://ссылка-смены-ip-из-кабинета"
}
}
}
}- 把 login、password、proxy-host 和 port 换成真实值。如果服务商给的是 SOCKS5 代理,把 http:// 换成 socks5://,并额外安装 pip install httpx[socks]。
- 保存配置,彻底重启客户端。
- 对智能体说:「调用 current_ip 告诉我们现在的地址。然后调用 rotate_ip,等十秒再查一次 IP」。两个地址应该不一样。
在 Inspector 里带代理验证
Inspector 也能传环境变量。在左侧面板展开 Environment Variables,加上 MOBILE_PROXY_URL 和 PROXY_ROTATE_URL 并填写你的值,连接后调用 current_ip。返回的应该和服务商后台显示的 IP 一致。
建议: 不要每个请求前都调 rotate_ip。多数服务商换 IP 要花几秒,切换太频繁还可能撞上换 IP 的次数限制。合理的策略是每 30-100 个请求换一次,或者只在遇到 429、403 错误时换。这个逻辑也可以直接写进 _get_html,我们下一步就做。
✅ 检查: current_ip 返回的是代理地址,不是你的家庭 IP。rotate_ip 加等待后地址变了。extract_text 和 extract_links 继续正常工作,日志里能看到带页面地址的 GET 行。
可能遇到的问题
- 报 407 Proxy Authentication Required。 用户名或密码不对,或者里面有特殊字符。@、: 这类符号在密码里要编码:@ 换成 %40,: 换成 %3A。
- 报 ConnectTimeout。 主机或端口不对,或者你的 IP 没加到服务商后台的白名单里(如果套餐有这种绑定的话)。
- current_ip 显示你自己的地址。 环境变量没传到服务器。检查配置里 MOBILE_PROXY_URL 的拼写,确认客户端重启了。
- rotate_ip 返回 429 或提示已达上限。 你换 IP 的频率超过了套餐允许。把间隔调大。
第 6 步:让服务器更可靠:重试、延迟、缓存和限制
本步目标: 把教学示例变成不会因为一次网络错误就挂掉、不轰炸网站、也不撑爆模型上下文的工具。这是正式使用前的最后一个必做步骤。
要加什么,为什么
- 自动重试。 网络错误很常见。与其立刻把错误返回给智能体,不如带停顿再试两次。
- 被封时自动换 IP。 如果网站返回 429 或 403,而配置了轮换链接,服务器自己换地址再重试。
- 请求之间的延迟。 有礼貌的采集器不会一秒发几十个请求。一两秒的停顿能减轻网站压力,也降低被封的风险。
- 缓存。 智能体常用不同工具反复请求同一个页面。几分钟的内存缓存能省下重复下载。
- 大小限制。 不下载超过几兆的页面。
代码
在文件开头加上 import time,然后把 _get_html 换成这样:
CACHE: dict[str, tuple[float, str]] = {}
CACHE_TTL = 300
REQUEST_DELAY = 1.5
MAX_BYTES = 3_000_000
_last_request = 0.0
def _get_html(url: str) -> str:
global _last_request
now = time.time()
cached = CACHE.get(url)
if cached and now - cached[0] < CACHE_TTL:
log(f'cache hit: {url}')
return cached[1]
last_error = None
for attempt in range(3):
wait = REQUEST_DELAY - (time.time() - _last_request)
if wait > 0:
time.sleep(wait)
try:
with _client() as client:
_last_request = time.time()
response = client.get(url)
if response.status_code in (403, 429) and ROTATE_URL:
log(f'status {response.status_code}, rotating ip')
httpx.get(ROTATE_URL, timeout=15.0)
time.sleep(8)
continue
response.raise_for_status()
if len(response.content) > MAX_BYTES:
raise ValueError(f'Страница слишком большая: {len(response.content)} байт')
html = response.text
CACHE[url] = (time.time(), html)
return html
except httpx.HTTPError as error:
last_error = error
log(f'attempt {attempt + 1} failed: {error}')
time.sleep(2 * (attempt + 1))
raise RuntimeError(f'Не удалось загрузить {url} после 3 попыток: {last_error}')它是怎么工作的
- CACHE 字典为每个地址保存下载时间和 HTML。如果页面在五分钟内被请求过,直接返回保存的副本,不再发请求。
- 每次请求前计算距上次过了多久,必要时补足停顿到 REQUEST_DELAY 秒。
- 三轮尝试的循环。遇到 403 或 429 且配置了轮换时,服务器换 IP、等八秒再试。网络错误则分别等两秒、四秒、六秒再试。
- 页面超过三兆就算错误:这种文档本来也塞不进上下文。
- 三次都失败后,抛出带地址和原因的清楚错误。智能体拿到这段文本,可以转告你或换个思路。
另外建议加一个清缓存的工具,让智能体可以强制重新加载页面:
@mcp.tool()
def clear_cache() -> str:
'''Очищает кэш загруженных страниц. Вызывай, если нужно получить свежую версию страницы.'''
count = len(CACHE)
CACHE.clear()
return f'Кэш очищен, удалено записей: {count}'建议: REQUEST_DELAY 和 CACHE_TTL 最好也像代理那样抽成环境变量,不用改代码就能调。监控价格适合两三秒延迟加一分钟缓存,采集文章适合一秒延迟加一小时缓存。
✅ 检查: 对同一个页面连续调用两次 extract_text。第二次日志里会出现 cache hit,回答瞬间返回。填一个不存在的域名,几秒后智能体会收到「Не удалось загрузить... после 3 попыток」这样的消息,而不是卡住。
可能遇到的问题
- NameError: ROTATE_URL 未定义。 _get_html 声明在代理设置块之前。把 PROXY_URL 和 ROTATE_URL 的设置挪到文件更上面。
- 智能体抱怨速度慢。 这很正常:延迟和换 IP 都要花时间。着急的话把 REQUEST_DELAY 降到 0.5,但记住被封的风险。
- 内存不断增长。 缓存会保存整个会话期间的页面。长时间会话可以每次调用时顺手清掉超过 TTL 的记录,或者限制字典大小。
验收:成品 MCP 服务器的检查清单
对照清单逐项打勾。如果全部满足,你的数据采集 MCP 服务器就可以投入实战了。
应该能工作的部分
- mcp dev server.py 能无报错启动,Inspector 能连上并显示 Connected。
- 工具列表里有 fetch_page、extract_text、extract_links、select_elements、current_ip、rotate_ip 和 clear_cache。
- AI 客户端的工具面板里能看到 web-collector 服务器,没有错误指示。
- 智能体收到自由表述的请求后,能自己挑合适的工具并调用。
- current_ip 显示移动代理地址,rotate_ip 之后地址会变。
- 重复请求同一个页面会从缓存返回。
- 错误地址会导致清楚的错误消息,而不是卡死。
综合测试
- 选一个公开的、允许使用其数据的目录型网站或文章列表页。
- 对智能体说:「打开网站首页,找到商品目录栏目的链接,进入前五个卡片,收集名称和价格,整理成一张表,列名为 名称、价格、链接」。
- 观察调用链:智能体应该先用带过滤的 extract_links,然后多次调用 select_elements 或 extract_text,最后生成表格。
- 手动在浏览器里打开几个卡片,核对几行数据。应该一致。
成功标准
考虑到延迟,采集五张卡片用不了 30-40 秒。客户端日志里没有 traceback 级别的错误。智能体不会反问该用哪个工具,而是自己动手。如果都做到了,恭喜:你搭好了自己的 MCP 服务器,把 AI 智能体接到了互联网上。
创建 MCP 服务器时的常见错误及解决办法
这里汇总了几乎每个人第一次都会遇到的问题。格式是:问题、原因、解决办法。
1. 服务器在 Inspector 里能连,客户端里不行
原因: 客户端配置里指的是系统 python,没装需要的库,或者文件路径不对。解决办法: 填 .venv 里 python 的完整路径和 server.py 的完整路径,用正斜杠,彻底重启客户端。
2. 客户端启动后立刻断开连接
原因: 代码里残留了不带 file=sys.stderr 的普通 print,把协议用的 stdout 弄脏了。解决办法: 把所有 print 换成 log 函数。另外检查库有没有往 stdout 输出:比如某些进度条默认就会。
3. 智能体不调用工具,直接凭自己的知识回答
原因: 工具描述太短或太含糊,模型不知道什么时候用。解决办法: 把文档字符串写详细,加上「适用于……」这样的说明和例子。头几次提问时明确点名工具。
4. 加载真实网站时报 403
原因: 网站不接受没有浏览器请求头或来自可疑 IP 的请求。解决办法: 确认 HEADERS 传进去了,把 User-Agent 更新到当前浏览器版本,接入移动代理并确认轮换正常工作。
5. 选择器没错但 select_elements 返回空结果
原因: 数据在页面加载后由 JavaScript 渲染,原始 HTML 里没有。解决办法: 用 fetch_page 检查。如果确实没有,去浏览器 Network 标签找网站的内部 API:卡片数据常常以 JSON 形式从另一个地址返回,可以直接用同样的 extract_text 请求它。
6. 走代理时报 407 或 ConnectTimeout
原因: 凭据不对、密码里的特殊字符没编码,或端口错误。解决办法: 从后台重新复制一行连接信息,对特殊字符做编码,检查协议是 http 还是 socks5。
7. JSON 配置不生效
原因: 多了个逗号、少了引号,或路径里用了反斜杠。解决办法: 用校验工具检查文件,把反斜杠换成正斜杠,确认最后一项后面没有逗号。
8. 服务器能跑,但数据编码不对
原因: 网站没在响应头里声明编码。解决办法: 显式设置 response.encoding,或者用 response.content 手动 decode('utf-8', errors='ignore')。
进阶玩法:给进阶读者的章节
基础服务器已经完成。如果你能熟练写 Python,想要更多,下面几个方向每个都能用一个晚上做出来。
用 Streamable HTTP 做远程服务器
想让服务器跑在另一台机器上,或者让多个客户端共用,把最后一行改成 mcp.run(transport='streamable-http')。默认服务器会监听 8000 端口,连接地址是 http://机器地址:8000/mcp。客户端配置里不用 command 和 args,改用 url 键填这个地址。这种模式下可以往 stdout 写东西,但还是建议保持日志写 stderr 的习惯。如果服务器不只对本地网络开放,一定要把端口对外关闭,并在请求头里加令牌校验。
资源和提示词
一个提供当前设置的资源,能帮智能体理解工作上下文:
@mcp.resource('collector://settings')
def settings() -> str:
'''Текущие настройки сборщика.'''
return f'proxy: {"on" if PROXY_URL else "off"}, delay: {REQUEST_DELAY}, cache ttl: {CACHE_TTL}'提示词提供预制场景,用户一条命令就能调用:
@mcp.prompt()
def price_monitor(url: str) -> str:
'''Сценарий мониторинга цен в каталоге.'''
return f'Открой {url}, собери ссылки на карточки товаров, зайди в каждую, вытащи название и цену и составь таблицу. Если увидишь ошибку 429, вызови rotate_ip и продолжи.'把结果保存到文件
加一个 save_csv 工具,接收字典列表和文件路径,用 csv 模块写入数据。这样智能体不仅能采集,还能把结果存成表格,你用 Excel 打开就行。把保存路径限制在一个文件夹里,别让智能体在磁盘上到处写文件。
异步与并行采集
FastMCP 支持异步函数:用 async def 声明工具,用 httpx.AsyncClient。这样 fetch_many 工具就能通过 asyncio.gather 同时下载十个页面。别忘了加信号量限制并发数,也别忘了并行时请求间的延迟要换一种算法。
多代理与智能轮换
如果你有多个面向不同地区的移动代理,把它们以逗号分隔存进环境变量,给工具加一个 region 参数。服务器按地区选代理,智能体就能对比不同城市用户看到的网站价格。这是营销和投放人员最常见的需求之一。
用 Docker 打包
要在服务器上运行,就基于 python:3.12-slim 构建镜像,复制 server.py 和依赖文件,安装包,入口指定用 HTTP 传输。代理变量在启动容器时传入,不要写死在镜像里。
⚠️ 注意: 千万不要把带用户名、密码和轮换链接的代码发到公开仓库。只放在环境变量或 .env 文件里,并把 .env 加进 .gitignore。换 IP 链接泄露,别人就能控制你的代理。
常见问题:关于创建 MCP 服务器的高频疑问
MCP 服务器只能用 Python 写吗?
不是。官方 SDK 覆盖 TypeScript、Java、Kotlin、C# 等语言。原理都一样:声明带描述的工具,启动传输。本指南选 Python 是因为它简单,而且处理 HTML 的库很丰富。
用 MCP 需要付费版 AI 客户端吗?
Claude Desktop 免费版也支持本地 MCP 服务器,但对消息条数有限制。Cursor 和 VS Code 也支持接入服务器。具体条件下请以各客户端的当前说明为准。
一定要用代理吗?
不一定,服务器直连也能跑。当请求量比较明显、网站对频率敏感,或者你想看特定地区的移动端内容时,才需要代理。
怎么确认请求确实走了代理?
调用 current_ip 工具,把返回的地址和服务商后台显示的对比。也可以让智能体用 extract_text 加载一个查询 IP 的网站页面。
一个服务器能加多少工具?
技术上几乎没有限制,但每段描述都会占用模型上下文。实践表明,5-15 个描述清晰的工具,比 50 个碎工具更好用。把相近功能合并成带参数的工具。
怎么在不重启客户端的情况下更新服务器?
用 stdio 传输时,客户端在启动时拉起进程,所以代码改动只有重启客户端后才生效。开发时用 Inspector 验证改动更方便,客户端等做完再重启。
网站只在执行 JavaScript 后才返回数据怎么办?
我们的服务器处理的是原始 HTML,看不到这类数据。办法有两个:去浏览器 Network 标签找网站的内部 API,或者接入浏览器引擎。后一条路博客里另有文章介绍,这里我们有意不涉及。
怎么限制智能体,不让它访问不想让它访问的网站?
在 _get_html 里加一个域名白名单或黑名单检查(从环境变量读取),对禁止的地址返回清楚的错误。这比在聊天里写指令可靠得多。
同一个 MCP 服务器能同时被多个客户端使用吗?
用 stdio 时,每个客户端会启动自己的进程副本,这没问题:它们互不干扰,但缓存也是分开的。想要共享缓存和统一代理,就换成进阶章节里的 HTTP 传输。
结语:你做了什么,接下来往哪走
来总结一下。你准备好了 Python 环境,装好了协议官方 SDK。从零写出了 MCP 服务器,弄明白了模型如何通过描述理解工具。把服务器接进了 AI 客户端,看到智能体自己下载页面。添加了提取文本、链接和按选择器查找元素的工具。把流量导向带 IP 轮换的移动代理。最后让服务器变得稳健:重试、延迟、缓存和限制。这已经不是教学示例,而是能干日常活的工具。
接下来做什么?把服务器用到真实场景里:监控竞品价格、收集评价、检查落地页、分析细分领域的内容。过程中你会发现自己缺哪些工具,照着现有工具的样子加上去就行。每个新工具都是一个带清晰描述的函数,没有更复杂的东西。
下一个层次就是进阶章节:HTTP 远程服务器、并行采集、按地区使用多个代理,以及把结果保存成表格。等你撞上动态内容的网站时,去看看博客里浏览器自动化相关的文章。最重要的事你已经做完了:你的 AI 智能体通过自己的 MCP 服务器走向了互联网,而整个过程完全由你掌控。