WebMCP:让网页直接变成AI Agent的可调用工具接口 最近在折腾 AI Agent 工具接入的时候我越来越觉得 WebMCP 这个方向值得认真聊一聊。简单说WebMCP 就是让网页不再只是给人看的界面而是把自身能力拆成一个一个具体的“工具”直接交给 AI Agent 调用。你不需要给 Agent 装一堆浏览器插件也不用教它读懂 HTML 结构只要网页暴露一个标准的工具入口Agent 就能像调用 API 一样使用网页上的功能。这篇文章我会从协议思路、核心设计、最小实现到实际踩坑经验完整讲一遍。如果你正在做 AI Agent 开发或者想让自己的网页具备“被 AI 直接用”的能力这篇内容应该对你有参考意义。1. 网页为什么需要把“工具”直接交给 Agent1.1 一个类比从“遥控器”到“USB-C 接口”先打个比方。过去我们想让 AI Agent 操作一个网页就像给一个机器人发了一个遥控器让它对着屏幕找按钮、按按钮。截图识别、DOM 分析、JavaScript 注入本质上都是在干这件事。问题是遥控器是给人设计的按钮藏在菜单里、样式变化、弹窗遮挡Agent 每一步都在“猜”猜错了就重来稳定性和效率都很难看。WebMCP 的思路完全不同。它更像是给网页开了一个 USB-C 接口Agent 不需要盯着屏幕按键而是直接插上接口告诉网页“我要查询天气”“我要创建工单”网页把结果直接返回来。这个“接口”和页面长什么样完全无关只关心能力本身。这样一来AI Agent 从“操作界面”变成“调用服务”可靠性高一个数量级消耗也小很多。MCPModel Context Protocol是 Anthropic 提出的模型上下文协议解决的就是让 AI 模型安全地调用外部工具和数据源。WebMCP 可以理解为 MCP 思路在网页场景下的延伸网页不再只是一个信息展示层而是作为工具提供方向 Agent 暴露一份“可机读”的工具清单。换句话说网页的每一个核心功能都可以被声明成一个带有参数、返回值和描述的工具Agent 直接调用。1.2 现有网页接入方式的痛点在做 WebMCP 之前我尝试过几种让 Agent 使用网页能力的方案各有各的问题。截图 视觉识别。把页面截图丢给多模态模型让它识别元素位置并点击。这种做法在演示场景很酷一旦页面稍微复杂一点按钮和卡片密密麻麻模型会开始“幻觉”经常点到错误的地方。而且每点一步都要传一张图Token 消耗非常可观。DOM 分析 JavaScript 注入。通过 Playwright 或 Puppeteer 读取 DOM 树再用选择器定位元素、执行点击和输入。听起来比截图靠谱但实际上 DOM 结构一改所有选择器失效维护成本极高。还有一类页面是 Canvas 渲染的DOM 里根本没东西可拿。后端 API 封装。功能最稳定但问题是网页本身往往没有现成 API或者有 API 却和历史遗留页面不同步。你需要单独写一套接口、鉴权、错误码等于是为网页再造一个后端开发和维护周期都很长。我把这三种方式的特点整理了一下方案稳定性开发成本维护成本适用场景截图 视觉识别低易受页面变化影响较低高需要反复调 prompt快速演示、交互简单的页面DOM 分析 注入中页面改版就挂中高选择器依赖太重页面结构稳定的内部系统后端 API 封装高高需要专门开发中有完整后端资源、不追求快速接入WebMCP 声明 调用高工具语义稳定中一次声明长期有效低只维护工具描述大多数网页服务化场景WebMCP 的核心价值在于它把网页功能从“表现层”中抽离出来用一套协议描述能力让 Agent 不再依赖页面细节。改动页面样式不影响工具调用替换后端实现也不影响调用方只要工具名称和参数语义保持一致。1.3 哪些网页真正适合接 WebMCP不是所有网页都需要 WebMCP。像博客、资讯站用户就是来看内容的Agent 只需要抓取阅读没必要把“评论”“点赞”暴露成工具。适合做 WebMCP 的网页通常有明确的业务动作比如内部管理系统创建工单、审批流程、查询订单状态在线工具天气查询、汇率换算、图片压缩、内容生成电商和表单页面加购、下单、填写报名信息数据看板把某个查询能力开放给 Agent让 AI 自动拉取数据并分析办公协作平台日程查询、会议预订、部门通讯录检索。这些场景的共同点是功能边界清晰参数可枚举结果可结构化。我在一个内部工单系统上实验过把“创建工单”“查询工单状态”“指派处理人”三个功能声明成工具Agent 当天就能稳定完成整个提交流程比之前教它点击表格按钮可靠很多。2. WebMCP 的核心设计网页怎么把工具“递”出来2.1 先想清楚协议边界WebMCP 目前还没有像 MCP 那样统一的官方标准我在实践中使用的是一套“兼容 MCP 子集”的做法网页后端暴露一个 HTTP 端点端点支持tools/list列出工具和tools/call调用工具请求和响应使用 JSON-RPC 2.0 风格的格式。这样现有的 MCP 客户端稍作适配就能调用不需要为每种 Agent 单独写插件。另一种思路是在前端浏览器里通过 Service Worker 拦截请求把网页内的 JavaScript 能力暴露给 Agent。这种方式对静态站很友好但安全边界和跨域问题比较难处理我建议大部分项目优先做服务端 WebMCP。原因很简单服务端能控制鉴权、限流、审计而且 Agent 调用不依赖浏览器上下文稳定性好很多。WebMCP 的协议边界可以拆成三层发现层让 Agent 知道“这个网页有哪些工具”。描述层用 JSON Schema 说明每个工具的参数、返回值和语义。执行层接收调用请求执行业务逻辑返回结构化结果。下面逐一展开。2.2 工具描述让 AI 能理解“这个按钮是干嘛的”Agent 不是人它不会通过按钮的样式和位置来猜测功能。它依赖的是一份明确的工具描述。我在设计工具描述时最看重三个字段name、description、inputSchema。name要短且唯一建议用动词加名词比如create_ticket、query_weather不要用handle_data这种含糊的名字。description要写清楚这个工具的用途、使用条件、返回内容甚至可以写“当用户问天气时使用”。inputSchema则使用 JSON Schema这是大模型最熟悉的格式也好做参数校验。一个典型工具描述长这样{ name: create_ticket, description: 创建一条新的客户反馈工单成功返回工单编号。适用于客户投诉、售后问题、功能建议等场景。, inputSchema: { type: object, properties: { customer: { type: string, description: 客户名称或客户邮箱例如 zhangsanexample.com }, priority: { type: string, enum: [low, medium, high], default: medium, description: 处理优先级紧急故障选 high一般咨询选 low }, content: { type: string, description: 工单内容需要描述问题现象和期望结果 } }, required: [customer, content] } }这段描述看起来简单但信息密度很高。description里我加了“适用于什么场景”这能帮助 Agent 在意图识别阶段更准确地决定要不要调这个工具。priority字段给了枚举和默认值Agent 就不会随便传一个 “urgent” 进来而会老老实实用high。2.3 调用接口一次可靠的 Agent 调用是什么样的工具描述解决“能不能找到”调用接口解决“能不能跑通”。WebMCP 的调用接口我建议使用 POST JSON-RPC 风格因为现有 AI Agent 生态已经熟悉这种格式。约定如下请求{ method: tools/call, params: { name: create_ticket, arguments: { customer: 张三, priority: high, content: 登录页面在点击验证码后无响应 } } }成功响应{ result: { content: [ { type: text, text: 工单已创建编号 T-202606-001 } ] } }失败响应一定要带机器可读的错误码和人类可读的消息因为 Agent 会根据错误消息决定是重试还是换一个参数。如果只是返回一个 HTTP 500 和一个空页面Agent 完全不知道发生了什么。我习惯统一返回{ error: { code: INVALID_ARGUMENT, message: 字段 content 不能为空, data: { field: content } } }这里code是给 Agent 看的message既可给 Agent 也可给用户看data放额外的调试信息。有了这个结构Agent 可以识别是参数问题还是服务问题从而决定是否换个思路重试。2.4 安全与权限不能把整个网页裸奔给 Agent把工具交给 Agent 是一件危险的事尤其是网页背后有数据库、有支付、有敏感操作。我调整过几次架构后总结了三条安全原则。第一认证和授权分离。WebMCP 接口本身要有认证推荐用 Bearer Token 或 OAuth 2.0不要依赖 Cookie。因为 Agent 很可能在服务端直接发起请求不经过浏览器Cookie 在这种场景下很别扭。Token 的粒度要细到工具级别比如某个 Token 只能调query_weather不能调create_ticket。第二调用前必须校验参数。Agent 生成参数有概率产生“幻觉”明明只定义了low/medium/high它可能给你传一个very_high。所以服务端要严格校验 JSON Schema不符合直接返回INVALID_ARGUMENT。不要指望 Agent 自觉。第三限流和审计不能省。给每个 Token 设置调用次数上限比如每分钟 30 次。同时记录每次调用的工具名、参数、调用者 ID、耗时时长。一旦出现异常批量操作审计日志能帮你快速定位。我曾经犯过一个错误为了方便调试把一个测试接口接进了内部系统没有做鉴权。结果一个自动化脚本误跑了大量“创建工单”操作把生产环境刷了几百条测试数据。后来我加了一层工具级 Token并把所有“写操作”都设置为需要二次确认这类问题才基本杜绝。3. 从零实现一个最小 WebMCP 服务3.1 环境准备与项目结构我选择 Node.js Express 做演示因为 Express 中间件生态成熟写 JSON API 很方便。你如果熟悉 Python FastAPI思路完全一样协议部分不受语言限制。mkdir webmcp-demo cd webmcp-demo npm init -y npm install express cors项目结构很简单webmcp-demo/ ├── server.js ├── package.json └── public/ └── index.htmlpublic/index.html是一个普通网页用来模拟“一个真实业务页面”。server.js既是网页后端也承担 WebMCP 端点。实际生产环境可以把 WebMCP 单独拆成一个服务但最小 Demo 放一起更直观。3.2 编写工具描述文件我们做一个“模拟天气查询”和“模拟工单创建”两个工具。先定义工具列表后续所有逻辑都围绕这份配置展开const tools [ { name: query_weather, description: 查询指定城市的当前天气和温度适合用户询问天气时调用。, inputSchema: { type: object, properties: { city: { type: string, description: 城市中文名例如北京、上海、广州 } }, required: [city] } }, { name: create_ticket, description: 创建一条客户反馈工单成功返回工单编号。, inputSchema: { type: object, properties: { customer: { type: string, description: 客户名称或邮箱例如 zhangsanexample.com }, content: { type: string, description: 问题描述需要写清楚现象和期望结果 } }, required: [customer, content] } } ];tools数组就是 Agent 看到的全部能力。后续新增功能只需要向数组里加一个对象然后在执行函数里加一个分支即可不需要改协议层。3.3 实现调用接口接下来是核心的server.js。我实现两个端点GET /webmcp/manifest.json返回工具清单POST /webmcp/rpc处理tools/list和tools/call。const express require(express); const cors require(cors); const app express(); app.use(cors()); app.use(express.json()); // 上面定义的 tools 数组省略实际代码中在此处粘贴 async function callTool(name, args) { switch (name) { case query_weather: { const data await fakeWeatherApi(args.city); return { content: [ { type: text, text: ${args.city} 当前天气${data.weather}温度${data.temp}°C } ] }; } case create_ticket: { const ticketId T- Date.now(); // 实际项目里这里会写入数据库 return { content: [ { type: text, text: 工单已创建编号 ${ticketId} } ] }; } default: throw new Error(unknown tool: ${name}); } } function fakeWeatherApi(city) { const table { 北京: { weather: 多云, temp: 22 }, 上海: { weather: 小雨, temp: 26 }, 广州: { weather: 晴, temp: 30 } }; return Promise.resolve( table[city] || { weather: 未知, temp: 20 } ); } app.get(/webmcp/manifest.json, (req, res) { res.json({ tools }); }); app.post(/webmcp/rpc, async (req, res) { const { method, params } req.body; if (method tools/list) { return res.json({ result: { tools } }); } if (method tools/call) { try { const result await callTool(params.name, params.arguments || {}); return res.json({ result }); } catch (e) { return res.status(400).json({ error: { code: TOOL_EXECUTION_FAILED, message: e.message } }); } } return res.status(400).json({ error: { code: UNSUPPORTED_METHOD, message: method ${method} not supported } }); }); app.listen(3000, () { console.log(WebMCP demo running at http://localhost:3000); });这个实现有几个细节值得注意。callTool是 async 函数这样内部可以接数据库、redis、外部 API。返回内容我用了 MCP 标准的content数组结构未来如果接标准 MCP 客户端字段可以直接复用。错误处理统一放在catch里避免工具内部抛异常时返回给 Agent 一个空白响应。3.4 接入 Agent 端网页端的 WebMCP 服务跑起来后怎么让 Agent 用起来我写一个简单的 Python 客户端模拟 Agent 调用import requests rpc_url http://localhost:3000/webmcp/rpc def list_tools(): resp requests.post(rpc_url, json{method: tools/list}) return resp.json()[result][tools] def call_tool(name, arguments): resp requests.post(rpc_url, json{ method: tools/call, params: {name: name, arguments: arguments} }) if error in resp.json(): raise Exception(resp.json()[error][message]) return resp.json()[result][content][0][text] if __name__ __main__: print(list_tools()) print(call_tool(query_weather, {city: 上海}))这个脚本就是 Agent 与 WebMCP 交互的最小闭环。把它接入 LangChain、Semantic Kernel、或者其他 Agent 框架时你的工作就是写一个适配器把list_tools翻译成框架的Function定义把call_tool翻译成调用函数。如果你用的是支持 MCP 的客户端甚至可以省掉翻译层直接指向这个 HTTP 端点。3.5 把 WebMCP 挂到网页上服务端接口已经就绪但还有一个问题Agent 怎么知道这个网页支持 WebMCP总不能每次手动把 URL 告诉它吧。我建议在网页里加一个自动发现入口。在public/index.html的head中加入link relwebmcp href/webmcp/manifest.json /同时在站点根目录放一个/.well-known/webmcp.json{ name: 示例业务系统, description: 提供天气查询和工单创建能力, manifest: /webmcp/manifest.json, endpoint: /webmcp/rpc }这样当 Agent 需要处理一个网站时它可以尝试访问/.well-known/webmcp.json如果文件存在就加载工具清单开始调用。这种发现机制和 PWA 的 manifest 设计思路类似成本很低但对自动化发现非常有帮助。4. 常见问题与实测排查4.1 跨域问题Agent 在浏览器里调用被拦截第一次接入时我踩了个很典型的坑Agent 运行在浏览器插件内插件页面发请求到http://localhost:3000/webmcp/rpc结果被浏览器 CORS 拦截。原因很简单Node 服务没有配置允许跨域响应头。解决方法有几种。第一个是在服务端加cors中间件我上面的代码已经用了app.use(cors())本地调试就够用。生产环境建议把来源限制到具体域名const allowedOrigins [https://agent.example.com]; app.use(cors({ origin: function (origin, callback) { if (!origin || allowedOrigins.includes(origin)) { return callback(null, true); } return callback(new Error(Not allowed by CORS)); } }));第二种方法更彻底如果 Agent 是在后端调用就不存在浏览器 CORS 限制直接把 WebMCP 的内网地址配给 Agent 即可。我建议生产环境优先走后端调用避免把接口直接暴露在公网也方便做内网访问控制。4.2 工具描述太“薄”导致 Agent 乱调工具描述不完整是 WebMCP 接入后最常见的问题。一开始我给query_weather的参数只写了city是“城市名”Agent 调用时传了拼音shanghai我的模拟接口查不到返回了“未知”。虽然没出事故但体验很差。后来我把每个字段的描述都补全了规范是把单位写清楚、把枚举值写全、把默认值标出来、把典型示例写进去。改进后的参数示例字段描述优化前描述优化后city城市名城市中文名例如北京、上海、广州接口目前仅支持这三个城市priority优先级处理优先级可选值 low/medium/high默认 medium紧急故障选 highcustomer客户客户名称或客户邮箱例如 zhangsanexample.com这看起来只是在描述里多写了几句话但对 Agent 的意图识别准确度提升非常明显。因为大模型在做参数填充时主要依赖语义匹配描述越具体它会越倾向于“按示例填写”而不是凭自己的理解编造。4.3 长任务执行与超时问题有些工具不是一下子就能返回结果的比如“生成一份销售报表”可能需要几十秒。如果 WebMCP 一直同步等待Agent 端的 HTTP 请求很容易超时而且用户体验很差。我在实践中采用的方案是异步任务模式。调用工具时如果任务是长任务先返回一个jobId和status: running同时后台执行任务Agent 拿到jobId后可以轮询查询结果。const jobs {}; async function runReportTask(args, jobId) { // 假设这里执行报表生成 await new Promise(resolve setTimeout(resolve, 20000)); jobs[jobId] { status: done, result: { reportUrl: https://example.com/reports/123 } }; } // 在 tools/call 分支中判断 if (params.name generate_report) { const jobId job_ Date.now(); jobs[jobId] { status: running }; runReportTask(params.arguments, jobId); return res.json({ result: { jobId, status: running, hint: 请调用 query_job_status 查询任务结果 } }); }对于 Agent 来说这个异步模式更自然它调用一次拿到jobId过一段时间再调用查询接口。关键在于超时和错误都要在状态接口里显式返回否则 Agent 会卡在等待里。4.4 页面身份与工具调用的联动网页通常有自己的登录状态而 Agent 调用 WebMCP 接口时它没有浏览器的 Cookie身份怎么带过去我的做法是给每个会话单独发一个短期访问令牌。具体场景是用户已经在网页里登录网页向用户浏览器返回一个 JWT。用户告诉 Agent “帮我创建工单”时Agent 拿到这个 JWT在 WebMCP 请求的Authorization头里带上。服务端解析 JWT 后就知道当前是哪个用户然后把用户信息自动注入到工具参数里。这样做的另一个好处是工具本身不需要接收current_user这种东西作为参数因为身份是协议层的而不是业务参数。否则 Agent 很容易把用户 A 的身份错填到用户 B 的参数里造成越权。我在项目里被这个问题坑过一个 Agent 在毫不知情的情况下用管理员身份调用了删除接口幸好当时接口没有真正执行删除否则损失会很大。5. 从 WebMCP 想到的更多玩法跑通最小实现只是开始我在实际项目中还做了几个方向的扩展这里一并分享。第一个方向是给静态网页加“手”。有些业务前端是个 CDN 上的静态站没有正经后端。我通过边缘函数Edge Function实现 WebMCP 端点工具逻辑写在边缘函数里比如表单提交、邮件发送、短信验证码。网页本身还是静态托管但 Agent 已经可以调用这些能力了。这样能把 WebMCP 应用到个人主页、落地页、活动页等轻量站点上。第二个方向是现有网页的渐进式改造。我不建议一次性把所有页面都接上 WebMCP而是选择高频、低风险、结果可校验的功能先接。像“查询订单状态”这种只读操作适合第一批接入“删除数据”这种高危操作等稳定运行后再考虑开放并且要有二次审批机制。我一般会先梳理一份“功能清单”把每个功能的名称、输入、输出、权限要求列出来再逐一实现。第三个方向是让多个网页组成工具矩阵。一个 Agent 对话里可能同时用到 A 网页的“查询库存”、B 网页的“创建采购单”、C 网页的“计算物流费用”。如果每个网页都实现了 WebMCPAgent 可以通过统一路由把这些能力组合起来。我在内部做了个很简陋的工具注册中心集中展示所有已接入的 WebMCP 端点Agent 先在这里发现能力再路由到具体页面效果比让 Agent 自己乱搜 URL 好很多。最后说一点个人体会。WebMCP 真正难的不是接口代码而是把网页能力抽象成 AI 能正确理解的工具描述。这个抽象过程要求你足够了解自己的业务也要足够了解 Agent 的脾气。我反复调整过description里的措辞比如把“附近门店”改成“按当前定位返回最近 3 家支持自提的门店”Agent 的调用准确率才明显上来。一旦你把页面的核心功能梳理成清晰稳定的工具清单后面的自动化流程会顺畅得多。这也是我为什么愿意把这套思路整理出来的原因WebMCP 这种“网页原生工具化”的做法大概率会越来越普及早一点把流程跑通后面就能少踩不少坑。