构建MCP Server:让LLM实时搜索与抓取网页 LLM 最尴尬的时刻就是你问它一件上周刚发生的事它却一本正经地对你说“我的知识有截止日期”。模型再聪明本质上也只是一本训练期定稿的“百科全书”它永远不知道训练集之外的世界发生了什么。这几年大家一直在喊“给 AI 装上眼睛和手”而 MCP 谷歌搜索与网页爬虫 Server就是让模型第一时间获得最新全球资讯的最快路径之一。这篇文章我不打算念概念直接带你从零造两个 MCP Server一个负责实时谷歌搜索一个负责把任意网页抓成干净的正文文本。两个服务搭完后接入支持 MCP 的客户端你就能亲眼看到原本“活在过去”的模型开始主动上网查资料、翻新闻、读文章然后给你交出一份带时效性的结论。适合正在做 LLM 应用、AI 智能体或者单纯想让个人助手不再“断网”的朋友。1. MCP 到底是什么为什么模型需要“感官”而不只是“记忆”1.1 一句话理解 Model Context ProtocolMCPModel Context Protocol是近年由头部 AI 研究机构牵头推动的一种开放协议说穿了就一句话给模型规定了一套标准方法让它能调用外部工具和数据源。在 MCP 之前如果你想让自己的模型会搜索只能自己写代码而且每接一个能力就要定制一套接口尤其在客户端、服务端、模型三方之间来回传数据效率很低结构也乱。有了 MCP 之后架构就变成了标准的三件套宿主客户端比如你正在用的 AI 助手界面、MCP Server实现具体能力比如搜索、爬虫、读写数据库、以及模型本身。Server 把能力通过统一的工具接口暴露出来模型在需要时会自动“申请调用”这些工具拿到结果后再继续推理。我把这个关系类比成一个侦探团队模型是那个很聪明但一直坐在小黑屋里的侦探MCP Server 是他伸到外面的手和眼。侦探本人不需要知道世界怎么运转他只需要说出“我需要今天的新闻”旁边有人帮他拿来报纸。这个“送报纸”的动作就是 MCP Server 干的事。1.2 为什么第一批要做的 Server 通常是搜索和爬虫看过很多团队的实践第一批接入的 MCP Server 往往不是花哨的数据库连接器而是最朴素的两样搜索和网页抓取。原因很现实——模型日常遇到的大部分“知识盲区”问题本质都是“信息过时”或“信息缺失”这两个工具正好能对症下药。比如你让模型帮你调研某款新上市的硬件产品它的脑子只知道旧型号但只要给它一次搜索能力它就能先找到目标商品的评测文章链接再调用爬虫 Server 把正文全文抓回来最后完成总结。整个链路很丝滑因为搜索解决“哪里有信息”的问题爬虫解决“如何把信息完整取回来”的问题模型则负责把信息变成答案。1.3 “接了搜索就不幻觉了”这话只说对了一半很多朋友以为只要给模型接上搜索幻觉就自动清零。实际用过你就会发现搜索返回的摘要本身就可能是旧闻抓回来的网页也可能被广告和导航栏淹没真正重要的内容也许被埋在最底下。MCP Server 不是魔法它只是把工具递到模型手里用得好不好很大程度取决于你的工具设计、描述质量以及返回内容的清洗程度。这也是为什么我坚持要亲手写 Server而不是直接找一个别人封装好的包就完事。自己的 Server你可以精确控制返回格式、内容长度、过滤规则也更容易排查“模型为什么没调用”这类气死人的问题。2. 开工前的准备技术栈选型与方案权衡2.1 Python 还是 TypeScript我建议这么选MCP 官方支持 Python 和 TypeScript 两套 SDK功能上基本对等。选 Python 的理由很直接数据获取与清洗生态实在太成熟了requests发请求、BeautifulSoup解析 HTML、lxml做底层解析这些都是别人踩过无数坑以后留下的轮子。如果你的团队本来就以 JavaScript 为主那也可以选 TypeScript写起来同样顺手npm 上也有对应的modelcontextprotocol/sdk。我自己开发调试这类 Server 时习惯用 Python主要是快。你想想Server 本身就是一个独立进程跟模型跑在哪里没有关系用什么语言完全看你自己的维护能力。项目越小、越追求快速出活选你最有把握的语言就好。环境准备只需要两件事Python 3.10 以上版本然后用下面的命令把依赖装齐。python -m venv mcp_env source mcp_env/bin/activate pip install mcp[cli] requests beautifulsoup4 lxml解释一下每个包的作用mcp[cli]是官方 SDK 的完整安装包带着命令行工具方便做配置和调试requests用于发 HTTP 请求是搜索和爬虫两个 Server 都在用的核心依赖beautifulsoup4和lxml一对搭档一个负责解析 HTML 标签一个负责提供高速解析引擎。老实说能把这几行依赖搞清楚项目就已经成了一半。2.2 搜索数据源官方 API 和“硬爬”的取舍既然标题写的是“谷歌搜索”那你首先要正视一个现实问题生产环境里千万别直接去抓搜索引擎的结果页。我见过有些同学图省事拿requests把搜索结果页的 HTML 拉回来再用正则抠链接当时是能用但搜索引擎的反爬机制分分钟教你做人而且这种抓取方式在服务条款上也不规范量一大就会触发验证码得不偿失。谷歌官方有 Custom Search JSON API虽然免费额度有限每日有一定次数限制但胜在稳定、返回结构干净、不会被反爬机制误伤。实现思路也很简单你去申请一个 API Key再创建一个自定义搜索资源搜索引擎 ID然后通过一个 GET 请求就能拿到结构化的搜索结果。做技术方案选型时我习惯列一张表把不同路线的优缺点摆出来不然很容易被“免费”两个字带偏方案优点缺点适合场景谷歌官方 Custom Search API稳定合规、结果结构清晰、带有摘要需申请 Key、默认每日免费配额有限生产环境、个人/团队正式项目直接抓取搜索结果页零成本、无需申请 Key反爬风险高、页面结构随时会变、条款不合规只建议在本地做极速技术验证不部署三方搜索聚合接口或开源搜索库成本低、无需申请稳定性参差不齐、部分也有请求限制个人学习、非关键用途你可能会问为什么我不直接写“用 API”就完事因为市场上很多教程喜欢教人去爬取搜索页看起来好像不用申请很省事但实际掉进坑里的人是你自己。这个项目如果只是玩票怎么都行一旦你想长期稳定地给模型提供信息那就得选正规军哪怕花两分钟去申请一个 Key 也是值得的。2.3 抓取方案静态解析还是无头浏览器网页爬虫 Server 的技术路线取决于你到底要抓什么类型的网页。如果目标页面是纯服务端渲染的博客、新闻、文档站那用requests拿 HTML再用 BeautifulSoup 提取正文又快又省资源绝大多数场景都够用。但有些站点是典型的 SPA 单页应用内容全靠 JavaScript 动态加载你直接发一个requests请求拿回来的往往是一大堆空壳 HTML里面什么都没有。这时候就需要上无头浏览器比如 Playwright让工具先模拟打开浏览器、等待页面渲染完再抓取最终内容。虽然代价是内存占用更高、启动速度更慢但面对“不渲染不出内容”的站点这是唯一靠谱的方案。我做 Server 的时候会默认先提供静态解析方案因为 80% 的需求它能扛住同时把无头浏览器的代码单独写成一个可选函数真有需要再切换而不是一上来就杀鸡用牛刀。3. 第一刀撸一个谷歌搜索 MCP Server3.1 申请密钥和创建搜索引擎动手写代码之前先去谷歌云控制台开通 Custom Search JSON API拿到 API Key。然后不要忘了第二步创建你自己的自定义搜索引擎Programmable Search Engine并把它的唯一标识cx 参数通常是一长串字符记下来。很多初学者只拿到 Key结果发请求的时候缺了 cx就会收到“缺少搜索资源编号”之类的报错卡在原地半天。有了这两项信息后建议用环境变量来管理而不是直接把密钥写死在代码里。潜在风险不用我多提你只要想象一下自己把密码写进代码再推到仓库里的后果就行。3.2 完整代码结构化、带错误返回的搜索工具先看我的第一个 Server 代码。开业第一步把 SDK 里的 FastMCP 类引入进来然后定义一个工具函数最后跑起来监听客户端的调用。# google_search_server.py import os import requests from mcp.server.fastmcp import FastMCP mcp FastMCP(google-search) API_KEY os.environ.get(GOOGLE_API_KEY, ) ENGINE_ID os.environ.get(GOOGLE_SEARCH_ENGINE_ID, ) SEARCH_URL https://www.googleapis.com/customsearch/v1 mcp.tool() def search_google(query: str, num: int 5, lang: str zh-CN) - str: 在谷歌上进行一次实时搜索返回结果的标题、链接和摘要。 当你需要获取最近发生的事件、产品信息、技术方案 或者需要验证模型内置知识是否过时时优先使用这个工具。 query搜索关键词num期望返回的结果数量默认 5最大 10lang结果语言。 if not API_KEY or not ENGINE_ID: return 搜索服务未配置 API Key 或搜索引擎 ID请检查环境变量 params { q: query, key: API_KEY, cx: ENGINE_ID, num: max(1, min(int(num), 10)), hl: lang, } try: resp requests.get(SEARCH_URL, paramsparams, timeout15) resp.raise_for_status() data resp.json() except requests.exceptions.HTTPError as e: return f搜索请求失败状态码 {resp.status_code}可能配额已用完 except requests.exceptions.RequestException as e: return f搜索请求异常{e} items data.get(items, []) if not items: return 未找到相关结果建议换个关键词再试 lines [] for idx, item in enumerate(items, 1): title item.get(title, ) link item.get(link, ) snippet item.get(snippet, ) lines.append(f{idx}. {title}\n链接{link}\n摘要{snippet}) return \n\n.join(lines) if __name__ __main__: mcp.run()这段代码看着简单里面其实埋了几个考虑。第一个是num参数的约束官方 API 单次返回最多 10 条所以代码里用min卡了一下避免模型乱传参数导致接口报错默认设成 5是因为经验告诉我返回太多结果模型反而容易迷失重点5 条已经足够让它交叉验证和判断。第二个是所有异常都返回成字符串而不是直接抛出原因是 MCP 工具运行在独立进程里抛异常只会让模型收到一个冷冰冰的错误码它会不知所措给一句人能读懂的提示模型就知道下一步该怎么办。3.3 别小看工具描述它也是一种“提示词”你可能已经注意到我给search_google写的 docstring 比较长这也是踩过坑后的体会。MCP 工具的描述对模型来说就是一份使用说明书描述写得太敷衍比如“搜索网页”模型就不知道什么时候该用它、能解决什么问题于是会很自然地调用它那些“凭空推理”能力。描述里建议写清楚三件事这个工具适合处理什么场景、参数分别是什么意思、以及有怎样的默认行为。你可以把描述当成在教一个聪明的实习生用工具说明越具体他越不会乱来。模型选择了工具之后这些描述也会影响它如何填写参数——你多写一句“默认 5”模型就不会非要传一个 10 进来。这一步做完后可以直接在命令行里跑一下python google_search_server.py正常情况下程序会启动并等待输入说明 Server 已经在监听客户端的调用了。4. 第二刀网页爬虫 Server让模型能读全文4.1 搜索只给线索爬虫负责读全文搜索 Server 有一个天然短板Google 返回的消息摘要通常只有一两句话信息密度不够。模型要是只依赖摘要回答你很大程度上等于拿标题写论文。这时候就需要爬虫 Server 出场——把搜索结果里的链接交给它它能抓取整个页面正文再转成干净文本后喂给模型让模型基于全文内容做总结、提取或问答。爬虫工具的职责可以拆成四层检查 URL 合法性和 robots 协议、发出网络请求、清洗 HTML 提取正文、控制返回长度。每一层都有讲究尤其最后那层直接决定模型上下文窗口会不会被撑爆。4.2 核心实现清洗、解码、截断一个不能少下面是我在实际项目中沉淀下来的完整实现适合绝大多数服务端渲染页面。# web_fetcher_server.py from urllib.parse import urlparse from urllib.robotparser import RobotFileParser import requests from bs4 import BeautifulSoup from mcp.server.fastmcp import FastMCP mcp FastMCP(web-fetcher) HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) } def check_robots(url: str) - bool: rp RobotFileParser() parsed urlparse(url) robots_url f{parsed.scheme}://{parsed.netloc}/robots.txt rp.set_url(robots_url) try: rp.read() return rp.can_fetch(*, url) except Exception: return True mcp.tool() def fetch_webpage(url: str, max_length: int 6000) - str: 抓取一个网址的正文内容并转换成适合模型阅读的纯文本。 当搜索结果中的链接需要进一步读取全文或者模型需要基于某个页面 做总结、问答、信息抽取时使用这个工具。 url完整的 http/https 地址max_length返回文本的最大字符数默认 6000。 if not url.startswith((http://, https://)): return 仅支持 http/https 协议请检查 URL if not check_robots(url): return 该站点 robots.txt 明确不允许抓取已跳过 try: resp requests.get(url, headersHEADERS, timeout(10, 20)) resp.raise_for_status() except requests.exceptions.RequestException as e: return f页面请求失败{e} resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, lxml) title soup.title.string.strip() if soup.title and soup.title.string else url for tag in soup([script, style, noscript, iframe, svg]): tag.decompose() lines [ line.strip() for line in soup.get_text(\n, stripTrue).split(\n) if line.strip() ] text \n.join(lines) if len(text) max_length: text text[:max_length] \n\n[内容过长已截断建议缩小目标范围或分批抓取] return f页面标题{title}\n页面地址{url}\n\n{text} if __name__ __main__: mcp.run()先说编码问题很多爬虫拿回来乱码十有八九是没用对字符编码。代码里resp.encoding resp.apparent_encoding这行起到的作用就是从 HTML 内容和 HTTP 头里自动推断编码而不是想当然地用默认编码去解码。这一行能帮你省掉大量“为什么中文是乱码”的排查时间。再说清洗逻辑。for tag in soup([...]): tag.decompose()这句会把脚本、样式、隐藏框、广告 iframe 等对正文毫无贡献的标签直接移除剩下的才是真正值得读的内容。然后我用get_text把标签全部剥掉只留下带换行的纯文本。最后说截断。很多人不设长度上限结果一个两百 KB 的周报页面直接把模型的上下文窗口塞满后续对话直接废掉。默认给 6000 字符既保证能读完一篇普通文章的骨架又不会让上下文压力太大。模型如果真想知道后续内容它自己会想办法换参数重新调用比我一次性塞给它一堆吞不下的大杂烩强得多。4.3 动态页面的兜底方案用 Playwright 等渲染上面这套静态抓取方案有个边界遇到纯 JavaScript 渲染的页面就会抓空。你requests.get之后拿到的 HTML 只有根节点和一段导流脚本那怎么办兜底方案是引入 Playwright 的无头浏览器让页面先完整跑一遍 JS再去取渲染完成后的内容。需要时可以加装依赖并运行pip install playwright playwright install chromium然后这样写一个备用抓取函数from playwright.sync_api import sync_playwright def fetch_dynamic_html(url: str) - str: with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, timeout30_000) page.wait_for_load_state(networkidle) html page.content() browser.close() return html一定要记住无头浏览器是“重武器”每个页面到渲染都要跑所有前端资源速度慢、吃内存。我的处理方式是把动态渲染单独拆成一个路径只有静态方式返回内容太少时才让 Server 提示模型考虑使用动态抓取不默认启用。这也呼应了前面说的“先想清楚再动代码”的理念不是越激进的方案越高级适合场景的方案才是好方案。5. 把两个 Server 接入模型让 AI 真正开始“上网冲浪”5.1 注册服务客户端的配置格式MCP Server 代码本身写完只是完成了第 50%另 50% 是把它们登记到客户端让模型能看见这些工具。绝大多数支持 MCP 的客户端都会提供一个配置文件通常叫mcp_servers.json或者类似的名字。你把自己的 Server 当成一个个子进程挂上去模型启动时就能自动列出它们的工具列表。下面是一个通用的注册示例请把路径替换成你自己机器上两个脚本的绝对路径并填好环境变量{ mcpServers: { google-search: { command: python, args: [/绝对路径/google_search_server.py], env: { GOOGLE_API_KEY: 你申请的 API Key, GOOGLE_SEARCH_ENGINE_ID: 你申请的自定义搜索引擎 ID } }, web-fetcher: { command: python, args: [/绝对路径/web_fetcher_server.py] } } }配置里有两个细节值得说。一是路径尽量写绝对路径因为客户端进程的工作目录不一定等于你的项目目录填相对路径容易“找不到模块”。二是环境变量直接写在这个配置块里而不是依赖系统全局变量这样这份配置可以跟随项目一起分发别人接管时不会因为少配了一个环境变量而崩溃。5.2 实操观察模型会自己组合工具接入完成后新开一个对话窗口试着输入一段“测试气味”很浓的问题比如帮我搜索最近一周里某个领域的重要新闻选择其中最有代表性的一篇读取全文然后提炼出三个核心要点。你会在聊天界面的后台记录里看到这样的调用链路模型先决定调用search_google拿到搜索结果列表然后它在一众标题里判断哪一篇最值得精读再调用fetch_webpage抓取那篇链接最后基于抓回的全文内容总结出三个要点。注意这个过程不需要你为每种网站单独写解析逻辑它全靠搜索工具返回的链接和描述来决策。这就是 MCP 最有魅力的一点工具本身不知道模型在想什么模型也会都临时发挥但只要接口清晰它们就可以像两个默契的上下级一样自动协作。5.3 场景延伸懂的都懂这只是一切的开始接完搜索和爬虫后你会发现模型的应用场景一下子就被打开了一个量级不再局限于训练集那点“库存”。比如每天让模型盯几个固定行业词替你做一次资讯整理或者让它对比多个网页来源交叉判断一件事是否属实再或者让它抓某个产品的官方文档基于最新版内容回答使用方法。这些场景的共同点都是“外部信息源模型推理”而 MCP 把两者低成本地焊接了起来。还有人会在这个基础上继续加更多 Server比如 RSS 定时抓取、数据库查询、任务管理接口模型就能从“会聊天”升级成“会干活”。搜索和爬虫只是你工具箱里最基础的两个螺丝刀但有了它们你才有底气继续往楼下接更多能力。6. 踩坑记录常见问题与排查速查表6.1 一张表格解决 80% 的现场故障用这套方案半年多我把遇到的问题整理成一张速查表后续只要照着查基本都能解决。现象常见原因解决思路模型始终不使用搜索工具仍用自己的知识回答工具描述太抽象或者未注册成功检查配置里能否看到工具把描述改得更具体写明适合场景搜索返回“请求失败状态码 403/429”API Key 有误或免费配额耗尽去控制台确认配额和余额或换个 Key免费额度有限注意请求频率模型拿到搜索摘要后不再往下读全文摘要已经让它觉得够了在问题里明确要求“点击链接阅读全文再做总结”抓取结果乱码编码识别失败或被压缩传输确认站点字符集必要时显式处理 gzip/br 解压长文被截断模型总结遗漏后半部分默认 max_length 太小或页面太长把 max_length 调到 8000甚至分两次抓不同分段某些站返回“页面请求失败”目标站有反爬或需要登录换用 Playwright 无头浏览器或增加 Cookie/登录态支持服务器运行时报模块找不到没有在正确的虚拟环境里跑脚本source mcp_env/bin/activate后再启动服务6.2 三件我在代码里没写、但强烈建议你做的“防御武器”第一件给 Server 加一个简单缓存。模型发起的搜索请求经常高度重复比如同一个问题它可能会反复确认你可以在搜索函数外面包一层小字典key是“query num”如果命中了就直接返回缓存结果别每次都打 API。省配额也省延迟。第二件永远让 Server 返回可读的错误信息而不是原始异常。你自己调试时看原始异常没问题但模型只拿到异常堆栈时是猜不出意图的你要用一句话告诉它“这次失败可能是配额问题请稍后再试”。工具是给模型用的错误信息也是给模型用的不是给工程师的。第三件在生产环境务必给模型提示引用出处。我建议代码里不要把链接和摘要丢弃而是保留固定格式输出这样模型在回答时能顺手带上链接读者一旦发现答案可疑可以点击来源去验证整体可信度也会高很多。不要小看这个细节它决定了你的 AI 助手是“盲信输出机”还是“可溯源的助手”。6.3 调试时的一个高效小习惯两个 Server 都在独立进程里跑调试时如果每次都要靠客户端界面触发效率太低。我习惯先在普通 Python 脚本里单独调用服务里的核心函数等稳定后再交给 MCP 框架包装。比如写一个简单的test.py直接调search_google和fetch_webpage先把逻辑调好再开 MCP 接入——这样定位问题的速度能快上一倍。尤其是爬虫环节HTML 清洗逻辑完全可以在本地反复验证不需要每次重启客户端。我现在个人的习惯是把 Server 里的“核心逻辑”和“工具声明”尽量拆开核心逻辑写成普通函数工具声明只是薄薄一层壳。这样既方便本地调试后续如果换了别的协议框架也只需要替换外壳核心功能代码可以直接复用。回头再看这套东西我最大的感受是模型接上搜索和爬虫之后它的形态真的会发生变化不再是你问一句它答一句的记忆复读机而是会自己判断“这个问题我知识不够需要查一下”然后有步骤地完成检索、阅读和归纳。这种从“背教材”到“出门跑腿”的转变才是 LLM 应用这几年最值得投入的方向。别嫌这两个 Server 简单它们是绝大多数智能体的地基地基打牢了后面接任何能力都会顺畅得多。