引言:读完这篇指南你能得到什么

想象一下,你打开和 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 可用磁盘空间。
  • 稳定可用的网络连接。

需要安装什么

  1. 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。
  2. 写代码用的文本编辑器。 推荐 Visual Studio Code:免费、语法高亮、能提示错误。其他编辑器也行,连记事本都能用,不过用 VS Code 会舒服很多。
  3. MCP 客户端,也就是带 AI 智能体、用来连接你服务器的应用。初学者最简单的选择是 Claude Desktop。此外 Cursor 编辑器、装了 GitHub Copilot 扩展的 VS Code 等工具也支持 MCP。开始之前至少装好其中一个。
  4. 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 客户端明确知道该用哪个解释器启动。没有它,「终端里能跑、客户端里跑不起来」这类问题几乎必然发生。

分步操作

  1. 打开终端。Windows 上按 Win+R,输入 powershell,回车。macOS 上用 Spotlight 打开 Terminal(Cmd+空格,然后输入 Terminal)。Linux 上按 Ctrl+Alt+T。
  2. 创建项目文件夹并进入。Windows 上执行两条命令:mkdir C:/mcp-collector,然后 cd C:/mcp-collector。macOS 和 Linux 上:mkdir ~/mcp-collector,然后 cd ~/mcp-collector。
  3. 用 python --version 检查 Python 版本(macOS 和 Linux 上可能需要 python3 --version)。应该看到类似 Python 3.12.x 的输出。如果版本低于 3.11 或提示命令找不到,回到准备章节重新安装 Python。
  4. 用 python -m venv .venv 创建虚拟环境。项目文件夹里会出现一个隐藏的 .venv 文件夹,耗时 10-20 秒。
  5. 激活环境。Windows PowerShell 里执行:.venv/Scripts/Activate.ps1。如果 PowerShell 提示禁止运行脚本,先执行 Set-ExecutionPolicy -Scope CurrentUser RemoteSigned,按 Y 确认,再重新激活。macOS 和 Linux 上:source .venv/bin/activate。激活后终端行首会出现 (.venv) 标记。
  6. 更新包管理器:python -m pip install --upgrade pip。
  7. 一条命令装好所有库:pip install "mcp[cli]" httpx beautifulsoup4。其中 mcp 是协议的官方 Python SDK(2026 年主流是 1.x 分支),httpx 是支持代理的现代 HTTP 请求库,beautifulsoup4 是解析 HTML 的工具。安装需要 1-2 分钟。
  8. 在项目文件夹里新建一个空文件 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()

逐行解读代码

  1. FastMCP('web-collector') 创建一个名为 web-collector 的服务器对象。客户端会在已连接服务器列表里显示这个名字。
  2. HEADERS 是我们发给网站的请求头。很多网站如果收到没有常见浏览器 User-Agent 的请求,会返回不完整内容或直接报错。Accept-Language 头表示我们需要俄语版本的页面。
  3. log 函数把消息写到 stderr。之所以不用普通 print,是因为 stdout 被协议占用了。这些消息你能在客户端日志和 MCP Inspector 里看到。
  4. @mcp.tool() 是把普通函数变成 MCP 工具的装饰器。SDK 会自动读取函数名、参数类型和文档字符串,生成给模型看的描述。默认值 max_chars = 20000 表示这个参数可以省略。
  5. 文档字符串(三引号里那段)就是 AI 会读到的东西。这里我们解释工具做什么、什么时候用。这种描述要写详细,用你和智能体交流的语言来写。
  6. httpx.Client 带 follow_redirects=True 会自动跟随重定向,timeout=20.0 防止请求一直挂着。
  7. raise_for_status() 在网站返回 4xx 或 5xx 时抛错。SDK 会捕获它,把清楚的错误信息返回给客户端,而不是默不作声。
  8. mcp.run() 用默认的 stdio 传输启动服务器,等待客户端的命令。

用 MCP Inspector 做第一次验证

直接运行 server.py 没有意义:它会一直等客户端消息,什么都不显示。验证要用 MCP Inspector,这是一个模拟客户端、可以手动调用工具的网页界面。

  1. 确认虚拟环境已激活,且当前在项目文件夹里。
  2. 执行 mcp dev server.py。这个命令包含在装好的 mcp 包的 cli 扩展里。第一次运行时它会通过 npx 下载 Inspector,大约需要一分钟。
  3. 终端里会显示类似 http://localhost:6274 的地址,新版本里还会给出访问令牌。在浏览器里打开这个地址(通常会自己弹出来)。
  4. 在 Inspector 左侧面板确认传输方式选的是 STDIO,命令是 python,参数是 server.py。点 Connect 按钮。
  5. 状态指示变绿,显示 Connected。在顶部菜单切到 Tools 标签,点 List Tools。
  6. 列表里会出现 fetch_page 工具,带文档字符串里的描述和两个参数。点它。
  7. url 字段填 https://example.com,max_chars 留空或填 5000。点 Run Tool。
  8. 右侧显示结果:从 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

  1. 打开 Claude Desktop。进设置:Windows 上点左上角菜单里的 Settings;macOS 上点菜单栏 Claude 里的 Settings。
  2. 切到 Developer 标签,点 Edit Config 按钮。会打开放着 claude_desktop_config.json 的文件夹。如果文件不存在,客户端会帮你创建。
  3. 把这个文件复制到桌面,做个备份。
  4. 用 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 也认。

  1. 保存文件。确认最后一项后面没有多余逗号,所有括号都闭合。多一个逗号就使 JSON 无效,客户端会悄悄忽略整个配置。
  2. 彻底关闭 Claude Desktop 再重新打开。Windows 上光关窗口不够:右键任务栏托盘图标,选 Quit。客户端只在启动时读配置。
  3. 重启后新建一个对话。在输入框下方找到工具图标(滑块或插头样子的)。点开:列表里应该出现 web-collector 服务器,带一个 fetch_page 工具。
  4. 在聊天里写:「用 fetch_page 下载 https://example.com 这个页面,告诉我它的标题是什么」。客户端会请求调用工具的权限。点 Allow 或 Allow for this chat。
  5. 几秒后智能体就会回答页面标题是 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()

每个工具做什么

  1. extract_text 会把脚本、样式、页头、页脚和导航从文档里删掉,剩下的文本拼成一行,用单个空格分隔。_clean 函数通过 split 和 join 去掉多余换行和制表符。回答开头会加上页面标题,让智能体一眼知道自己在看什么。
  2. extract_links 收集所有 a 标签,用 urljoin 把相对地址转成绝对地址,用 seen 集合去重,还支持按子串过滤链接。这样智能体一次调用就能拿到比如目录里所有商品卡片的链接。
  3. select_elements 是最强大的工具。它接收 CSS 选择器,返回找到的元素文本。智能体可以先用 fetch_page 看一段 HTML,发现价格放在 price 类里,然后用选择器 .price 调用 select_elements。

注意这些文档字符串:我们明确提示模型在什么情况下选哪个工具。这会明显提升智能体的表现。

怎么验证

  1. 运行 mcp dev server.py 并在 Inspector 里连接。Tools 列表里现在有四个工具。
  2. 用任意新闻站或商品目录的地址调用 extract_links,contains 参数填地址里某一段。结果是一组带 text 和 url 字段的对象。
  3. 用同样的地址和选择器 h2 调用 select_elements。你会得到一组标题。
  4. 重启 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,不要制造过量负载,不要在没有合法依据的情况下收集个人数据。使用工具的责任由你自己承担。

分步操作

  1. 打开你的移动代理服务商后台,找到连接信息:主机、端口、用户名、密码。通常它们会被拼成 login:password@host:port 这样一行。顺便复制换 IP 的链接(如果有)。
  2. 在 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)
  1. 把 _get_html 里创建 httpx.Client 的那行改成调用 _client()。现在它写成:with _client() as client。所有工具会自动走代理。
  2. 在 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}'
  1. 通过客户端配置里的环境变量传入代理数据。我们特意不把用户名密码写进代码,免得它随文件被误发出去。打开 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-из-кабинета"
}
}
}
}
  1. 把 login、password、proxy-host 和 port 换成真实值。如果服务商给的是 SOCKS5 代理,把 http:// 换成 socks5://,并额外安装 pip install httpx[socks]。
  2. 保存配置,彻底重启客户端。
  3. 对智能体说:「调用 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}')

它是怎么工作的

  1. CACHE 字典为每个地址保存下载时间和 HTML。如果页面在五分钟内被请求过,直接返回保存的副本,不再发请求。
  2. 每次请求前计算距上次过了多久,必要时补足停顿到 REQUEST_DELAY 秒。
  3. 三轮尝试的循环。遇到 403 或 429 且配置了轮换时,服务器换 IP、等八秒再试。网络错误则分别等两秒、四秒、六秒再试。
  4. 页面超过三兆就算错误:这种文档本来也塞不进上下文。
  5. 三次都失败后,抛出带地址和原因的清楚错误。智能体拿到这段文本,可以转告你或换个思路。

另外建议加一个清缓存的工具,让智能体可以强制重新加载页面:

@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 之后地址会变。
  • 重复请求同一个页面会从缓存返回。
  • 错误地址会导致清楚的错误消息,而不是卡死。

综合测试

  1. 选一个公开的、允许使用其数据的目录型网站或文章列表页。
  2. 对智能体说:「打开网站首页,找到商品目录栏目的链接,进入前五个卡片,收集名称和价格,整理成一张表,列名为 名称、价格、链接」。
  3. 观察调用链:智能体应该先用带过滤的 extract_links,然后多次调用 select_elements 或 extract_text,最后生成表格。
  4. 手动在浏览器里打开几个卡片,核对几行数据。应该一致。

成功标准

考虑到延迟,采集五张卡片用不了 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 服务器走向了互联网,而整个过程完全由你掌控。