恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
WebMCP:让网页成为AI可调用的服务端点
首页
资讯中心
/
WebMCP:让网页成为AI可调用的服务端点
WebMCP:让网页成为AI可调用的服务端点
发布时间:2026/9/8 23:22:53
前段时间在给一个内部知识库页面做 AI 改造时我遇到一个特别别扭的困境页面本身是有价值的但它只能给人看AI 代理进来之后只能对着渲染好的 HTML 瞎猜。于是我开始尝试一种新的思路——把网页本身变成一个能够主动向 AI 暴露能力的服务端点。这个思路我把它叫做 WebMCP。这篇文章会聊聊它解决的问题、核心设计思路以及我实际搭建最小可运行版本的完整过程过程和代码都可以直接复用。1. 网页的“可读性”和 AI 的“可用性”之间差着一整层协议先说说我为什么觉得传统网页在 AI 时代有根本性的短板。现在主流的网页形态本质上是为人眼的视觉扫描设计的。标题要够大按钮要够显眼信息层级要靠 CSS 的间距和字号来体现。这些东西让人类用户觉得“啊这个页面很清晰”但对 AI 解析器来说完全不是这么回事。我之前给一个 AI 爬虫类项目做过测试让大模型去读取一个典型的电商商品页。模型能拿到 HTML 源码里面塞满了导航、推荐位、埋点脚本、样式类名真正的商品名称、价格、库存状态这些关键数据混杂在几百上千行的 DOM 节点里。模型要么靠运气把这几个字段捞出来要么就产生幻觉把促销文案里的“立减 50”当成实际价格。这个问题的根源不在于模型笨而是网页没有给模型一条结构化的、可信的信息通道。后来我接触到 MCPModel Context Protocol模型上下文协议这个思路一下子觉得方向对了。MCP 的核心想法是与其让 AI 去猜工具怎么用不如把工具的能力、参数、返回结构都用一套标准化的方式暴露出来。AI 通过一个统一的接口发现“这里有一个工具它接受什么参数会返回什么”然后像人类看说明书一样去调用它。WebMCP 就是把这个思路从“单个工具”延伸到“整个网页”。一个网页不应该只是一个文档它应该是一组能力和数据的集合。比如一个订单管理页面它底层的能力是“查询订单列表”“查看订单详情”“更新订单状态”。如果这些能力能以一种机器可读的方式暴露出来AI 代理就可以直接调用它们而不是通过解析 HTML 去模拟点击。这时候你可能会问这不就是做个 API 吗如果已经有 API直接让 AI 调 API 不就行了这个问题问得特别好。现实世界里绝大部分网页是没有配套 API 的。一个运营后台、一个内部报表系统、一个政府办事门户它们往往只提供了网页交互压根没有对外开放 API 的预算和计划。WebMCP 的定位就是在不重新开发整套后端接口的前提下给这些存量网页加装一层“AI 友好”的能力层。在协议层面它遵循 MCP 的工具发现和调用规范在落地层面它可以运行在网页的服务端把已有的页面逻辑包装成结构化工具。所以WebMCP 不是要替代 API它是给“没有 API 的网页”补上 AI 时代的接口能力。它也不是要替换 MCP而是 MCP 在网页场景下的一种具体化组织方式让网页本身变成 AI 可以理解和操作的服务。2. WebMCP 的组件划分网页就是一组资源和工具的集合要在实际工程里把 WebMCP 落地得先把一个网页拆成 MCP 能理解的语言。MCP 协议里有两个最核心的抽象Resources资源和 Tools工具。WebMCP 完全可以套用这两个抽象并且还需要补充一个第三层——Permissions权限边界否则网页能力暴露给 AI 后很容易失控。2.1 Resources把网页数据变成可订阅的上下文Resources 解决的是“AI 怎么拿到需要的信息”这个问题。一个网页给 AI 提供的资源不应该是一段完整的 HTML而应该是序列化后的结构化数据。举个例子。一个文章详情页它可以暴露这样一个资源{ uri: webmcp://articles/1024, mimeType: application/json, name: article_detail, description: 获取文章标题、作者、正文、标签等元信息, content: { articleId: 1024, title: WebMCP 实践指南, author: 张三, tags: [AI, 协议, 网页], wordCount: 8600 } }这个资源和真正的正文内容可以分开暴露。AI 在决定要不要深入了解全文之前可以先看这个瘦身的元信息。这样能大幅减少 token 消耗也让 AI 的决策链路更清晰。相比给 AI 丢一段 5000 字的 HTML 让它自己归纳这种资源式的暴露方式要有效率得多。我还建议把 Resources 设计成支持订阅更新的。比如一个股票行情页面它的价格数据是实时变化的。如果 AI 代理需要持续监控价格WebMCP 的资源端点应该能支持轮询或者 WebSocket 推送而不是让 AI 每次都全量拉取页面再自己对比差异。2.2 Tools把网页操作变成可调用的函数Tools 解决的是“AI 怎么操作网页”这个问题。一个网页上能够执行的用户操作都应该被映射成工具函数。拿一个典型的后台审核页面举例。人工操作时审核员要点开待审列表、查看详情、点击通过或驳回按钮、填写审核意见。在 WebMCP 里这套流程会被描述成三个工具list_pending_reviews获取待审核列表支持分页和状态筛选返回包含 reviewId 的数组。get_review_detail传入 reviewId返回该条数据的详细内容和历史操作记录。submit_review传入 reviewId、审核结论approve/reject、审核意见完成审核操作。每个工具都有清晰的入参、出参和错误码。AI 代理通过模型上下文协议的标准格式调用这些工具完全模拟一个审核员的决策闭环。有一点设计上的细节特别重要每个 Tools 必须是幂等的。也就是说同样的参数调用两次结果应该一致或者在设计上要明确允许重复操作的后果。因为大模型在生成工具调用参数时偶尔会出现重复调用同一个工具的情况。如果这个工具是“给用户发送优惠券”重复调用就可能造成事故。所以我在设计 WebMCP 的工具层时会把这类有副作用操作设计成需要额外的requestId请求唯一标识由服务端做去重。2.3 Permissions网页能力暴露中最容易被忽视的一环Resources 和 Tools 定义了“网页能做什么”而 Permissions 定义的是“谁在什么条件下可以做什么”。这一步在概念上容易被跳过但在落地时恰恰是整个 WebMCP 架构能不能安全的基石。我建议给每个工具和资源都设置可见范围。有的资源对所有 AI 请求开放有的只对认证过的内部代理开放还有的是只允许在特定上下文里调用。这个权限配置类似于前端的路由守卫但在服务端必须强制执行不能依赖 AI 客户端自觉遵守。举个例子一个企业内部的员工信息查询页面如果把“查询员工手机号”这个工具暴露给 AI那必须同时限定只有 HR 体系的代理凭证才能调用。我在实践中的做法是给工具标注等级L1公开信息如文章标题、公司介绍文案。L2登录用户可见如个人订单、收藏记录。L3特定角色可见如审核权限、财务导出权限。AI 请求到达 WebMCP 网关时先解析凭证身份再匹配工具等级等级不足直接拒绝调用。这层逻辑非常简单但非常关键。后面我会专门讲一下这个权限层在生产里会遇到哪些坑。3. 从零搭建一个最小可运行的 WebMCP 服务理论说了一堆下面直接进入动手环节。我实际搭建了一个尽可能精简的 WebMCP 服务目标是把一个静态网页变成能响应 AI 工具调用的后端服务。这个项目我用了 Node.js TypeScript 技术栈核心依赖是 MCP 官方 SDK。3.1 初始化项目并安装依赖mkdir webmcp-demo cd webmcp-demo npm init -y npm install modelcontextprotocol/sdk zod npm install -D typescript types/node tsx这里解释一下选型逻辑。modelcontextprotocol/sdk是 MCP 协议的官方 TypeScript SDK它帮你封装了协议握手、请求路由、JSON-RPC 消息解析这些底层细节这样我可以专心写业务逻辑。zod用来做工具入参的声明式校验它可以直接从 schema 生成工具描述对 AI 理解参数非常有帮助。写一个 TypeScript 配置文件{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: ./dist, strict: true, esModuleInterop: true, skipLibCheck: true }, include: [src/**/*] }3.2 用 MCP SDK 定义网页能力和工具这个 demo 的场景我用一个简单的“网页状态查询”功能来演示。假设我有一个部署在服务器上的网站我想让 AI 能够查询它的运行状态并且能够触发一个缓存刷新操作。在src/tools.ts里定义工具import { z } from zod; export const websiteStatusSchema { url: z.string().url().describe(要查询的网页地址), }; export const refreshCacheSchema { url: z.string().url().describe(要刷新缓存的网页地址), force: z.boolean().optional().describe(是否强制刷新默认 false), }; export const websiteTools [ { name: query_website_status, description: 查询网页的反向代理、源站和证书状态, inputSchema: websiteStatusSchema, handler: async (args: { url: string }) { // 这里模拟一次状态采集真实场景可替换成 fetch 检测 const status { url: args.url, proxy: up, origin: up, sslDaysRemaining: 32, lastCheckedAt: new Date().toISOString(), }; return { content: [{ type: text, text: JSON.stringify(status) }] }; }, }, { name: refresh_website_cache, description: 刷新网页的 CDN 缓存force 为 true 时绕过缓存直接回源, inputSchema: refreshCacheSchema, handler: async (args: { url: string; force?: boolean }) { const result { url: args.url, cacheAction: args.force ? bypass : refresh, status: ok, refreshedAt: new Date().toISOString(), }; return { content: [{ type: text, text: JSON.stringify(result) }] }; }, }, ];这里有一个容易犯的错误工具描述一定要写上它返回的 JSON 结构。因为我测试过不同的模型它们对工具返回内容的解析策略不太一样有些模型会直接信任返回文本有些则会对内容做二次解释。如果你返回的是JSON.stringify的纯文本但描述里没说明这是 JSON模型有概率把这个字符串当作普通文字念出来导致下游解析失败。3.3 创建服务入口并注册工具接着写src/server.ts这是整个服务的核心入口import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { SSEServerTransport } from modelcontextprotocol/sdk/server/sse.js; import express from express; import { websiteTools } from ./tools.js; const server new McpServer({ name: webmcp-demo, version: 1.0.0, }); websiteTools.forEach((tool) { server.registerTool(tool.name, tool.description, tool.inputSchema, tool.handler); }); const app express(); app.use(express.json()); let transport: SSEServerTransport | null null; // MCP 客户端会先连这个端点完成 SSE 握手 app.get(/sse, async (req, res) { transport new SSEServerTransport(/messages, res); await server.connect(transport); }); // 客户端通过这个端点发送 JSON-RPC 消息 app.post(/messages, async (req, res) { if (transport) { await transport.handlePostMessage(req, res); } else { res.status(400).json({ error: No SSE connection established }); } }); const PORT process.env.PORT || 3001; app.listen(PORT, () { console.log(WebMCP 服务运行于 http://localhost:${PORT}/sse); });MCP 的握手流程是用 SSE服务端推送和 HTTP POST 联合完成的。客户端先发起/sse请求建立一个长连接服务端通过这个连接向客户端推送消息。客户端需要回复或者发送请求时就向/messages端点发 POST 请求。这个模式我第一次用的时候很不适应因为一般 HTTP 开发习惯是“请求-响应”一一对应但 MCP 的 SSE 传输里连接和消息是分离的。理解之后会发现这样设计的好处服务端可以主动推送操作结果比如一个长时间的异步任务完成后服务端不用等客户端来轮询直接通过 SSE 通道把结果推过去就行。这里用到了express但不要以为只有 Express 才能做。MCP SDK 提供了一个传输抽象层理论上你可以把它接到 Fastify、Koa甚至是 Cloudflare Worker 的边缘运行时上。我之前试过在 Worker 上跑需要注意的一点是平台要支持长连接的保持因为 SSE 连接在边缘环境中容易被网关切断需要配合心跳机制处理。3.4 配置权限中间件的雏形在注册工具之前我加了一个简单的权限检查中间件用 token 区分调用者const AUTHORIZED_TOKENS: Recordstring, string[] { token-a: [query_website_status], token-b: [query_website_status, refresh_website_cache], }; app.use(/messages, (req, res, next) { const token req.headers[x-access-token] as string || ; const allowedTools AUTHORIZED_TOKENS[token] || []; res.locals.allowedTools new Set(allowedTools); next(); });然后在注册工具时把工具权限和 header 令牌做关联校验。MCP 协议本身的认证机制还在演进所以我在实践上更建议直接在 WebMCP 服务层自己做一层简单的 token 校验不要等到协议层面的标准成熟后再补。前期宁可做笨一点也要把权限边界立起来。4. 对接 AI 客户端让大模型真正“驱动”你这个网页WebMCP 服务跑起来只是第一步更重要的是验证它能被 AI 客户端正常发现和调用。这一步我踩了不少坑下面记录一下最有价值的几个点。4.1 用 Claude Desktop 或通用 MCP 客户端连接如果你部署的是本地服务最快捷的验证方式是通过支持 MCP 的客户端。我测试时用的是 Claude Desktop在它的配置文件里加一段 MCP server 地址。MCP 客户端会把 WebMCP 暴露的工具自动注册成可用的工具集合。这时你在聊天框里输入“帮我查一下 example.com 的网站状态”模型就会自动发起工具调用请求完成查询后把结果组织成自然语言回答。如果没有桌面端客户端也可以用 MCP Inspector 这类纯命令行工具。它的好处是能直观看到每一次工具调用的请求和响应 JSON特别适合调试工具描述是否准确。4.2 手动构造 MCP 请求验证核心链路如果只是想确认服务端逻辑是否正确直接手动发请求也行。MCP 的消息格式是 JSON-RPC 2.0以下是一次 tools/call 的经典请求curl -N -X POST http://localhost:3001/messages \ -H Content-Type: application/json \ -H x-access-token: token-b \ -d { jsonrpc: 2.0, id: 1, method: tools/call, params: { name: query_website_status, arguments: { url: https://example.com } } }需要说明的是上面这个 curl 命令简化了 SSE 握手的步骤它假设你已经在另一个终端建立的 SSE 连接才会转发消息。实际调试时我更推荐直接用 MCP Inspector手工构造 JSON 容易在细节上出问题。不过这样手动请求有一个好处就是能准确理解 MCP 每个字段的含义比如jsonrpc固定为2.0、id必须是唯一递增的请求号、method区分是发现工具tools/list还是调用工具tools/call。4.3 工具描述词打磨模型是否调用你的工具一半看描述我测试时发现了一个特别微妙的现象两个功能完全相同的网页服务只是工具描述的词序不同大模型调用它们的概率就差很多。比如我一开始给refresh_website_cache的描述写的是“刷新网站缓存”后来改成“当用户报告页面内容不是最新时调用此工具来强制刷新 CDN 缓存确保后续访问获取到的是最新的源站内容”模型的调用准确率明显提升了。这件事的原理也不难理解。大模型在决定是否调用工具时会把用户的自然语言问题转化成向量然后去比对当前可用工具的 description 向量。描述越接近用户问题的表达方式被匹配到的概率就越高。所以写工具描述时不要端着一副“接口文档”的架子要模拟真实用户会怎么描述这个问题把触发场景写进描述里。我整理了一个工具描述模板供参考一句话说清工具的作用主语明确。列举两到三个典型的触发场景用自然语言描述。说明工具返回值的核心字段。加上一句“什么时候不应该用这个工具”的负面说明。比如{ name: refresh_website_cache, description: 当用户反馈页面内容未更新或要求强制刷新时调用本工具刷新CDN缓存。返回cacheAction、status、refreshedAt字段。若只是想确认网站是否正常运行请勿调用本工具改用query_website_status。, inputSchema: refreshCacheSchema, }这个负面说明特别管用。它帮大模型排除了一个容易被误匹配的工具减少了无意义的工具调用也间接降低了 token 消耗。5. 打通网页动态数据把真实页面逻辑接入 WebMCP前面 demo 里的工具返回的都是模拟数据真实业务中 WebMCP 的价值在于把现有网页的真实能力和数据暴露出去。这一节展开讲讲如何把一个真实网页的“内部操作”接进 WebMCP 的工具层。5.1 通用抓取类工具让 AI 读取渲染后的页面有些网页本身不提供结构化数据WebMCP 只是作为 AI 和网页中间的一个代理。这种情况下我用的方案是写一个通用抓取类工具内部用 Playwright 无头浏览器打开页面等页面渲染完成后把指定区块的文本和关键属性提取出来。示例实现思路如下import { chromium } from playwright; const tool { name: fetch_page_content, description: 打开指定的网页地址提取正文区域文本和标题信息, inputSchema: { url: z.string().url().describe(要访问的网页地址), selector: z.string().optional().describe(要提取的CSS选择器缺省时自动提取主要文本), }, handler: async (args: { url: string; selector?: string }) { const browser await chromium.launch({ headless: true }); const page await browser.newPage(); await page.goto(args.url, { waitUntil: networkidle, timeout: 30000 }); const content await page.evaluate((sel) { const root sel ? document.querySelector(sel) : document.body; return { title: document.title, text: root ? root.innerText.slice(0, 5000) : , }; }, args.selector); await browser.close(); return { content: [{ type: text, text: JSON.stringify(content) }] }; }, };这里最值得注意的一个参数是waitUntil: networkidle。很多网页的内容是通过异步请求动态加载出来的如果用默认的load事件经常出现 DOM 结构已经加载完但数据还没渲染出来的问题。networkidle会等到网络请求基本平静后再继续能有效避免抓到空白页面。但networkidle也有一些副作用。如果页面里有些轮询接口每 5 秒拉取一次networkidle可能会一直等不到完全安静的状态最终超时。这种情况下可以把waitUntil改成domcontentloaded等待时间调整为60000再配合固定等待page.waitForTimeout(2000)实测也能稳定拿到渲染后页面。5.2 动态表单提交类的封装很多网页交互的核心是表单。比如一个搜索页面用户输入关键词点击搜索结果通过接口回流并渲染在页面上。要把这类交互暴露给 AI把它封装成一个工具函数。实际操作中我会先手动抓包搞清楚表单提交的接口地址、参数名和鉴权方式然后再把它们封装成工具。比如某个内部系统的搜索接口其实是POST /api/search参数是keyword和pageNo返回格式是固定的 JSON。针对这种情况其实根本没有必要用无头浏览器模拟点击直接调用内部接口效率高得多。但接口可能有签名或者加密逻辑此时用无头浏览器处理反而更省事因为网页已经把加密和签名都做好了你只需要模拟提交表单。我最终采用的模式是混合模式优先封装内部接口遇到无法绕过的加密逻辑时回退到 Playwright 模拟操作。这两套路径都放在 WebMCP 工具内部AI 调用者感知不到后端的差异它们看到的就是一个稳定的工具函数。5.3 长耗时操作的异步任务处理网页里还有一种操作类型是耗时的比如导出几万条数据、批量生成报告。这类操作无法在一个 HTTP 请求周期内完成AI 代理如果一直挂着等结果体验很差。MCP 协议本身没有内置异步任务的标准但可以利用 SSE 长连接做一个民间方案。工具收到请求后先把任务提交到队列立即返回一个taskIdAI 代理拿着这个taskId轮询查询任务状态。同时在任务真正完成时服务端通过 SSE 通道主动推送一条通知AI 代理收到通知后再来取结果。我在两个工具之间用共享内存做了一个任务状态表const taskStore new Mapstring, { status: pending | running | done; result?: unknown }(); async function submitGenerateReport(args: { dateRange: string }) { const taskId task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}; taskStore.set(taskId, { status: pending }); // 模拟异步执行 setTimeout(() { taskStore.set(taskId, { status: done, result: { url: https://cdn.example/reports/${taskId}.pdf } }); }, 30000); return { content: [{ type: text, text: JSON.stringify({ taskId }) }] }; } async function queryTaskStatus(args: { taskId: string }) { const task taskStore.get(args.taskId); if (!task) { throw new Error(任务 ${args.taskId} 不存在); } return { content: [{ type: text, text: JSON.stringify(task) }] }; }生产环境肯定不能用setTimeout模拟内存态任务至少要换成 Redis 队列。但异步化的思路是一致的。这里提醒一点工具返回的立即结果里一定要包含后续查询所需的全部标识符。因为大模型的上下文是有限的它在发起一次工具调用之后只会记住工具返回文本里的内容。你如果返回值里漏了taskIdAI 就没有办法继续跟踪任务进度。6. 生产级 WebMCP 的边界底线排查、限流与安全实践做完最小 Demo 之后我把 WebMCP 接到了公司一个真实业务页面上跑了一段时间踩过几个实打实的坑。这一节的内容比前面的原理更值钱全是生产环境里的教训。6.1 日志里排查“AI 调用失败”的完整链路第一次上线时AI 代理频繁报工具调用失败但我在服务端日志里看不到任何异常。排查了很久最后发现问题出在 SSE 连接被网关闲置断开了。这个问题的表现特别隐蔽——不是请求报错而是连接静默断开后后续消息通过/messages发过来时服务端尝试向已断开的 SSE 连接写数据抛了一个底层网络异常但业务代码没有捕获这个异常导致调用看起来像“成功”了实际上结果没送到客户端。后来我把整个链路补上了日志。这里分享一个基本的 MCP 日志规范每次客户端对/sse的连接建立记录客户端 IP、建立时间、持有的 token 身份。每次/messages收到消息记录消息 ID、方法名、目标工具名、入参摘要。每次工具执行完毕记录返回的 JSON 大小、耗时。捕获所有写入 SSE 通道时发生的异常单独标记为connection-lost。有了这套日志我才能快速定位是连接问题、协议问题还是业务问题。如果第三次排查看到一堆connection-lost直接去刷新网关的 idle timeout 设置就行。我把 SSE 的空闲超时时间从默认的 60 秒调到了 300 秒同时服务端每 30 秒往连接上一次心跳包问题就解决了。6.2 看不清原委的限流策略大模型工具调用有一个特点AI 客户端内部的 agentic loop 会并发发起多个工具调用。比如 AI 准备生成一份汇总报告它可能同时调用“查询销售数据”“查询库存数据”“查询用户反馈”三个工具。这在人工 API 调用场景下不算高频但如果你把 WebMCP 暴露给多个 AI 客户端或者同一个客户端的多个任务瞬时 QPS 可能冲到很高。我给 WebMCP 服务加了两种限流基于调用方身份的令牌桶限流比如每个 token 每秒最多 5 个工具调用超出直接返回 429。基于工具类型的分级配额代价高的工具比如 L3 级别的批量导出可以并发数设低一点代价低的查询工具并发数放宽。限流触发的返回错误信息也要设计过。直接给模型返回“调用过于频繁”不会让它明白该怎么调整。更好的写法是告诉它“该工具配额已用尽建议同步执行其他任务或等待 30 秒后重试”。大模型读到这个提示真的会换个策略——拿我测试中的一个例子AI 在“用户推荐”任务里发现商品接口被限流它转而先调用“浏览历史”接口生成基础推荐拿到商品接口恢复后再来补充细节整个流程严丝合缝。6.3 权限校验必须放在协议适配层之外我前面提到过在/messages中间件做了 token 校验但实际中有一个认知陷阱MCP SDK 在连接建立时就会完成协议握手这之后它认为所有消息都是可信的。如果你只在校验http header里做 token 而连接建立时没做校验会有消息绕过你的权限中间件进入 SDK 内部直接执行工具。我把权限校验拆成了两层第一层在连接建立时校验只有携带合法 token 的 SSE 请求才允许建立连接。第二层在每次工具调用时校验从连接上下文里取出身份再匹配当前调用的工具是否在允许列表内。第一次只做连接校验是不够的因为 SSE 长连接共享一个身份如果这个连接被多个请求复用第二层校验就失效了。所以这两个层都要有缺一不可。更好的做法是每个工具执行前再校验一次调用方身份与工具的 L 等级是否匹配把权限边界真正收缩到最小粒度。6.4 工具调用的可观测性和审计日志网页向 AI 暴露能力等于把一个原本“人操作、人负责”的系统变成了“人操作 AI 操作”混合的系统。一旦 AI 因为理解偏差错误地调用了某个工具比如误把一个删除操作当成更新操作必须有事后追踪的线索。所以我在 WebMCP 的工具执行链路上埋了一条审计日志{ eventId: evt_xx, timestamp: 2025-06-18T10:23:11Z, callerIdentity: ai-agent-sales, toolName: submit_order_refund, input: { orderId: A10086, reason: ... }, output: { success: true, refundId: R7788 }, traceId: trace_xx }只有把审计日志做起来了才能在前置管控失效时知道到底发生了什么而不是对着代码和日志大海捞针。7. 关于这个模式我还想多说的几句私货实践 WebMCP 这一套下来我的核心认识是网页在 AI 时代的终点不应该是被“读懂”而是被“调用”。让 AI 直接读 HTML 是低效的因为那些信息是在为视觉通道编码而让 AI 去调工具、拿结构化结果才是和模型理解和推理方式最匹配的交互路径。如果你要开始在自己的项目里做这件事我建议从最小的场景入手找一个高频重复、逻辑清晰、副作用可控的页面操作把它包装成第一个 WebMCP 工具。比如你有一个内部的周报汇总页每次要人肉打开、筛选、复制、粘贴就让 AI 调用一个generate_weekly_report工具完成。跑通之后你会发现AI 在网页能力上的表现从“阅读理解”上升到了“执行任务”这个质变才是 WebMCP 真正想解决的问题。最后分享一个我做得比较顺手的技巧在设计 WebMCP 工具清单时不妨先拿一张纸把网页上所有的人类功能列出来然后用一句话概括每个功能“什么时候会被调用”。这个“什么时候”的表述基本可以直接拿来当工具描述的第一句。我几乎所有的工具描述都是先写用户场景再说参数结构这个顺序对模型极有帮助。以上就是我在 WebMCP 上从概念验证到小规模落地的一段完整经历。希望这些踩过的坑和沉淀下来的思路能帮你少绕一些弯路。