
最近我有一个非常强烈的感受AI Agent 要真正落地功夫一半在大模型本身的推理能力上另一半其实在“它能不能摸到你系统里的工具”。如果你跟我一样折腾过让大模型去操作网页、查页面状态、触发业务流程那你大概率遇到过这个尴尬事——模型很聪明但你的网页对它来说是个“暗盒”。它能读懂你粘贴给它的 HTML却没办法随时、按需地调用网页里的真实功能。WebMCP 就是我这段时间整理出来的一套偏轻量的方法核心目标很简单让网页把自己能做的事情通过一个标准接口暴露给 AI Agent。Agent 不需要知道按钮挂在哪个 DOM 节点、接口地址藏在哪个目录它只管说“我要做什么”网页负责“去怎么做”。这套思路适合谁适合正在手工搭 Agent 工具链的开发者、准备把网站能力接入大模型的产品同学也包括那些想把内部系统、运营后台、个人知识库开放给 Agent 的爱好者。技术成熟度上大模型和多模态交互已经具备了量产落地条件Agent 从 demo 走向生产的关键恰恰就是工具层的标准化和小步快跑的落地姿势。下面我把这套方案从设计思路到实操过程、再到踩坑记录一次性整理出来。1. 为什么 WebMCP 值得自己搭一套1.1 大模型缺少的不是智商是一双能“动手”的手很多人在接触 Function Calling 之后都有一种豁然开朗的感觉原来可以让模型输出一个 JSON然后程序根据 JSON 去调用真实的函数。但真到了网页场景里问题马上来了。网页不是 Python 函数它有自己的生命周期按钮有时可见有时不可见表单可能要等接口返回才能提交一个页面上有成百上千个元素总不能每个都写一个函数让模型调用吧。我见过不少项目的做法是给 Agent 塞一堆硬编码函数比如check_order(),create_order(),query_user()。这种模式在接口固定、页面结构固定的系统里能跑但一套稍复杂的运营后台工具函数能堆出几十个且每一个都要手动维护参数说明。更麻烦的是页面升级、功能下架文档和代码往往不同步Agent 调用到一个已经失效的工具是常有的事。WebMCP 的出发点就是让“网页自己决定自己能提供哪些工具”Agent 实时去读一份“能力说明书”而不是靠开发者在两套系统之间来回同步。1.2 WebMCP 跟 MCP 是什么关系MCPModel Context Protocol现在已经被很多大模型生态接受它把“模型”和“外部工具”解耦工具方只要实现一套协议客户端就能统一调用。WebMCP 这个叫法并不是官方标准我理解的是把 MCP 的思想落地到网页场景里网页作为工具提供方暴露一个 manifest 入口里面写清楚有哪些工具、每个工具的参数格式、调用地址AI Agent 作为调用方先读 manifest再按规则去调用。本质上就是一个“网页版的工具开放协议”。跟传统后端 API 相比WebMCP 更强调“页面状态”与“业务动词”的表达。普通 API 返回的是数据WebMCP 返回的是带页面上下文的结构化结果普通 API 需要开发者逐个对接WebMCP 则希望 Agent 能自发现、自描述、自调用。拿点餐来打比方传统 API 是你直接进后厨说“给我炒一个宫保鸡丁”WebMCP 是先给你一份菜单你按菜单点菜厨师按标准流程出菜哪怕这家店是新开的你也不需要重新学一套沟通方式。1.3 适合用 WebMCP 的场景以及不适合的场景先说不适合的避免大家一上来就套错地方。如果你的系统本身就是一组标准的内部 REST API调用关系固定、权限模型简单那直接用 Function Calling 对接就好WebMCP 反而多了一层封装。低延迟数字运算、大规模数据流、音视频实时交互也不适合走这一类 HTTPS 请求应该用专门的 RPC 或 WebSocket 通道。适合的场景主要有这么几类第一页面状态检查比如 Agent 定期确认下单页的按钮是否可点击、公告栏是否正常展示第二自然语言驱动的页面操作比如用户对 Agent 说“帮我把筛选条件重置一下”第三内部系统智能助手运营后台、数据面板、CMS 编辑器这类页面工具非常杂乱正好用 WebMCP 统一暴露第四知识库或内容页面的二次加工比如从某个页面抽取结构化数据再由 Agent 决定下一步动作。总结起来只要“功能长在网页里、动作以页面状态为上下文”WebMCP 就能发挥价值。2. WebMCP 的核心设计协议、结构与鉴权2.1 一切从 manifest 开始先给 Agent 一份“能力菜单”我在设计 WebMCP 时把 manifest 当成整个协议的心脏。Agent 第一次接入时不需要提前知道你的页面有哪些功能只需要访问一个固定地址比如GET /mcp/manifest拿到一份 JSON里面写清楚协议版本、服务名称、支持的工具列表、鉴权方式。这样做最大的好处是解耦页面功能升级只要 manifest 同步更新Agent 就能感知到不需要重新发版。下面是我在一套演示系统里实际用过的 manifest 结构{ protocol: webmcp, version: 1.0, server_name: order-console-webmcp, server_url: /mcp, auth: { type: bearer, token_url: /auth/token }, tools: [ { name: get_element_status, description: 获取页面指定元素的可见性、可用性、文本内容用于判断按钮或区域当前是否能操作。, parameters: { type: object, properties: { element_id: { type: string, description: 页面元素的唯一 ID比如 submit-btn、order-list } }, required: [element_id] } }, { name: click_element, description: 模拟点击页面上的指定元素触发对应业务动作。只建议对按钮类元素使用。, parameters: { type: object, properties: { element_id: { type: string, description: 需要点击的元素 ID }, confirm: { type: boolean, description: 是否需要二次确认默认 false } }, required: [element_id] } } ] }每个字段都不是随便写的。name是 Agent 在决定调用哪个函数时直接使用的标识命名必须稳定一旦发布了就别随便改description是给大模型看的要写清楚“这个工具是干嘛的、什么时候用、参数有什么限制”因为模型读不懂代码只能靠这段描述来判断要不要调用parameters要符合 JSON Schema 规范这样不同的 Agent 框架都能直接转成自己的工具结构兼容性会好很多。2.2 invoke 调用极简接口但返回结构要统一manifest 只是“菜单”真正干活的是 invoke 接口。我采用的路径是POST /mcp/invoke请求体里带上session_id、tool和parameters。session_id很重要它让多次调用可以共享上下文比如 Agent 先查了按钮状态再点击按钮服务端能知道这是在同一个“会话场景”里发生的避免不同页面、不同用户的动作混在一起。一个标准的调用请求长这样POST /mcp/invoke Authorization: Bearer WEB_MCP_TOKEN Content-Type: application/json { session_id: sess_001, tool: get_element_status, parameters: { element_id: submit-btn } }对应的响应我建议统一成这样{ request_id: req_3fa9d81c, code: 0, tool: get_element_status, result: { element_id: submit-btn, visible: true, enabled: false, text: 处理中... }, elapsed_ms: 128 }request_id和elapsed_ms看起来不起眼排查问题时却帮了大忙。Agent 侧拿request_id去问服务端“我那次调用到底发生了什么”一查一个准elapsed_ms则让 Agent 感知到工具调用是不是出现了性能劣化。返回结果里的result我强烈建议只放结构化 JSON不要把整段 HTML 或者一大段文本扔给模型。模型处理 JSON 比处理标签文本稳定得多而且 token 消耗也更低。2.3 鉴权与权限控制别让 Agent 在页面上裸奔我在早期做 WebMCP 对接时注意力全放在功能上鉴权就顺手写了个固定的 Header 字符串。后来真正接进业务系统才发现这玩意儿的攻击面比你想象中大Agent 能调用的工具相当于有人拿着一张 API 通行证在操作你的页面。如果这个通行证长期有效、权限又不分粒度一旦泄露对方就能直接执行下单、改配置、删数据等危险操作。目前的实践是三层控制第一层接口本身必须有鉴权我推荐短期 token可以用 JWT 或者简单的签名方案有效期控制在 30 分钟到 2 小时避免长期令牌泄露风险第二层工具级权限控制manifest 里可以再加一个可选的required_permission字段比如click_element需要order:write调用时服务端校验角色权限第三层危险操作二次确认比如点击“删除按钮”这类高风险动作WebMCP 可以在响应中返回need_confirm: true要求 Agent 先跟用户确认再发起正式调用。2.4 错误码规范统一格式让 Agent 能自动恢复模型调用工具时经常会出现参数传错、目标不存在、权限不足等情况。一开始我的接口出错时直接抛 HTTP 500或者返回各种不规则的错误文案结果 Agent 看到异常信息就懵了只能干巴巴地跟用户说“出错了”。后来我总结出一套错误码分段用结构化错误响应替代裸异常code 区间含义示例1xxxx参数错误10001 缺少必填参数10002 参数格式不合法2xxxx权限错误20001 未认证20002 无权限调用该工具3xxxx资源状态错误30001 元素不存在30002 元素当前不可见5xxxx服务端内部错误50001 内部执行异常对应的响应结构如下{ code: 10001, message: missing_parameter, detail: element_id is required. }关键点在于detail要写得“对 Agent 友好”最好带上排查建议比如“请检查 element_id 是否拼写正确当前页面可用的元素包括 submit-btn、order-list”。这样 Agent 收到错误后能直接把细节内容作为上下文交给大模型让模型修正参数重新调用形成自动纠错闭环。3. 从零实现一个带 WebMCP 的网页服务3.1 技术选型FastAPI 内存状态先把链路跑通我给演示系统选型的原则很简单能用最少的代码验证完整链路。后端用 FastAPI是因为它对 Pydantic 校验、JSON 序列化、自动文档的支持都非常顺手几行代码就能搭出一个符合规范的 API。前端我用一个原生 HTML 页面加少量 JavaScript避免引入前端框架把示例复杂度拉高。状态存储先放在内存里用共享字典模拟页面元素状态等链路跑通了再替换成真实数据库或者浏览器自动化。为什么先强调“跑通链路”因为 WebMCP 最大的坑往往不是单个接口的写法而是 Agent、网页、后端三者之间能不能闭环。先把玩具版本跑通再往里面填真实业务这个顺序能省下大量调试时间。我见过太多人一上来就追求生产级架构结果一个月过去连一眼能从 Agent 发起到页面状态变更的完整请求链路都演示不了。3.2 后端核心代码manifest 和 invoke 的工程化写法下面这段代码是我整理过的最小可用实现已经可以复制下来直接跑。为了演示我定义了两个工具get_element_status查询页面元素状态click_element模拟点击一个元素并产生状态变化。实际页面里工具函数内部应该对接真实业务逻辑。# webmcp_demo.py from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel from typing import Optional import time import uuid app FastAPI(titleWebMCP Demo Server) WEB_MCP_TOKEN change-me-to-a-real-token # 模拟页面里的元素状态真实项目中这里应该对接数据库或实时页面状态 page_state { submit-btn: {visible: True, enabled: True, text: 提交订单}, order-list: {visible: True, enabled: True, text: 最近 3 笔订单}, } TOOLS [ { name: get_element_status, description: 获取页面指定元素的可见性、可用性和文本内容。, parameters: { type: object, properties: { element_id: { type: string, description: 页面元素 ID例如 submit-btn } }, required: [element_id], }, }, { name: click_element, description: 模拟点击页面上的指定元素触发对应业务动作。, parameters: { type: object, properties: { element_id: { type: string, description: 需要点击的元素 ID } }, required: [element_id], }, }, ] class InvokeRequest(BaseModel): session_id: str tool: str parameters: dict {} def require_token(authorization: Optional[str]): token authorization.replace(Bearer , ) if authorization else if token ! WEB_MCP_TOKEN: raise HTTPException(status_code401, detailinvalid_token) def success_result(tool: str, result: dict, elapsed_ms: int): return { request_id: str(uuid.uuid4()), code: 0, tool: tool, result: result, elapsed_ms: elapsed_ms, } app.get(/mcp/manifest) def get_manifest(): return { protocol: webmcp, version: 1.0, server_name: order-console-demo, server_url: /mcp, auth: {type: bearer, token_url: /auth/token}, tools: TOOLS, } app.post(/auth/token) def get_token(user: str demo): # 这里只做演示生产环境必须换成正式的身份认证并控制 token 有效期 expires int(time.time()) 3600 token fdemo-{expires} return {token: token, expires_at: expires} app.post(/mcp/invoke) def invoke(req: InvokeRequest, authorization: Optional[str] Header(None)): require_token(authorization) tool req.tool parameters req.parameters start time.time() if tool get_element_status: element_id parameters.get(element_id) if not element_id: raise HTTPException(status_code400, detailmissing_parameter: element_id) if element_id not in page_state: raise HTTPException(status_code404, detailelement_not_found) return success_result(tool, page_state[element_id], int((time.time() - start) * 1000)) if tool click_element: element_id parameters.get(element_id) if not element_id: raise HTTPException(status_code400, detailmissing_parameter: element_id) if element_id not in page_state: raise HTTPException(status_code404, detailelement_not_found) # 模拟点击之后的副作用提交按钮变成不可用文案改变 if element_id submit-btn: page_state[submit-btn][enabled] False page_state[submit-btn][text] 处理中... return success_result(tool, {clicked: True, state: page_state[element_id]}, int((time.time() - start) * 1000)) raise HTTPException(status_code400, detailtool_not_found)这套代码里我特意把 token 演示简化成了固定字符串代码里那段/auth/token只是示意。真实项目里建议换成一个成熟方案比如签发短期 JWTexp设置为当前时间加 2 小时同时把密钥放到环境变量里。另外page_state这个内存字典在真实系统中要替换成如果页面本身就是后端渲染状态以数据库为准如果页面是前端 SPA由前端通过事件接口把状态同步到后端如果要控制真实浏览器可以用 Playwright 或 Selenium 在服务端执行页面操作。3.3 前端页面如何跟 WebMCP 保持状态一致很多朋友会卡在“Agent 改了状态页面上看不到”“页面上的操作Agent 查不到”这类问题本质上是状态源不一致。我在演示里把所有状态统一收口到page_state前端页面加载时通过一个状态接口渲染Agent 调用 WebMCP 后直接修改同一个状态源页面再用轮询或 WebSocket 拿到最新状态。下面是我给页面写的最小示例!DOCTYPE html html langzh-CN head meta charsetUTF-8 title订单控制台/title /head body h1订单处理页面/h1 button idsubmit-btn提交订单/button div idorder-list最近 3 笔订单/div script async function syncState() { const res await fetch(/api/state); const state await res.json(); const btn document.getElementById(submit-btn); btn.disabled !state[submit-btn].enabled; btn.textContent state[submit-btn].text; document.getElementById(order-list).textContent state[order-list].text; } document.getElementById(submit-btn).addEventListener(click, async () { await fetch(/api/order/submit, { method: POST }); syncState(); }); setInterval(syncState, 3000); syncState(); /script /body /html在这个架构里“页面元素”和“后端状态”之间不是两个孤立世界而是由同一套状态来驱动。页面用户点了按钮接口会改状态Agent 调用 WebMCP 点击按钮也会改状态。无论谁来操作页面上都能同步看到结果Agent 拿到的状态也不会过期到离谱。这么做可能不是最高性能的方案但对中后台系统、运营工具这类场景简单可靠比极致实时更重要。3.4 参数选择与边界条件超时、并发、幂等任何工具接口都不能无限信任调用方尤其是 Agent 这种“非确定性调用方”。我在实现 WebMCP 时对三个边界条件做了强约束。超时控制上单工具执行时间最长不超过 30 秒。Agent 侧的超时时间要留足余量我一般设为 45 秒。如果一个工具确实需要执行超过 30 秒不要死等先返回status: pending再通过 Webhook 回调或者让 Agent 轮询结果。并发控制上同一个session_id内的调用我默认串行执行防止两个工具同时操作同一个页面元素导致状态错乱不同会话之间的调用可以并行。幂等控制上凡是会产生副作用的写操作请求体里都要支持request_id服务端记录已执行过的request_id重复请求直接返回上一次的结果不会重复触发业务动作。这一点在做“提交订单”这类工具时尤其重要Agent 一旦遇到网络超时大概率会重试没有幂等防护一单被提交两次的教训可是真金白银换来的。4. 把 AI Agent 接进来一次完整的调用实弹4.1 Agent 如何发现 WebMCP 工具动态发现替代硬编码Agent 跟 WebMCP 对接的方式有两种。一种是把 manifest 里的工具手动翻译成 Agent 代码里的函数适合工具数量少、结构固定的场景另一种是动态发现Agent 启动时先请求 manifest然后动态构建工具列表适合工具经常变更的场景。我更推荐第二种因为 WebMCP 的核心优势就是“网页自动描述能力”你再用手动翻译等于把这个优势丢掉了。下面是用 OpenAI Function Calling 风格的 Python 代码来动态构建工具的思路换成其他大模型平台也差不多import requests from openai import OpenAI client OpenAI() def load_webmcp_tools(server_url): resp requests.get(f{server_url}/mcp/manifest, timeout10) resp.raise_for_status() manifest resp.json() tools [] for tool in manifest[tools]: tools.append({ type: function, function: { name: tool[name], description: tool[description], parameters: tool[parameters], } }) return tools server_url http://127.0.0.1:8000 webmcp_tools load_webmcp_tools(server_url)这里需要注意manifest 里的parameters已经是 JSON Schema 格式直接塞给主流平台都能识别不需要再转结构。如果你用 LangChain也可以把工具包一层tool装饰器然后把它加入 Agent 的工具列表。4.2 跑通一次真实调用链从用户问题到页面状态变更为了让你更直观地理解完整流程我给你还原一次实操记录。假设用户对 Agent 说“检查一下订单提交按钮是否可用如果状态不对帮我重置一下。”第一步Agent 看完用户请求后通过 Function Calling 选择了工具get_element_status调用参数是{element_id: submit-btn}第二步WebMCP 服务端返回{visible: true, enabled: false, text: 处理中...}第三步Agent 发现按钮不可用认为这是一个异常状态于是决定调用click_element看能否触发一次重试WebMCP 返回{clicked: true, state: {visible: true, enabled: false, text: 处理中...}}第四步Agent 综合两次调用结果向用户输出结论“提交按钮目前不可点击按钮文本是‘处理中’说明系统里已经存在一笔正在处理的订单我没法直接重置。”这个过程中最让我惊讶的一点是Agent 并没有被“重置”这个动词带偏它通过页面状态判断出重置是不合理的动作从而拒绝执行。这正好说明了“结构化状态反馈”对 Agent 的价值。如果 WebMCP 返回的是一段杂乱的 HTML模型大概率会瞎猜可能直接说按钮正常也可能说点击成功但不会给出这么有分寸的判断。5. 常见问题与排查技巧实录5.1 Agent 调用超时链路卡住怎么办这是最常见的坑。先区分是“接口真的慢”还是“Agent 那边超时配置太短”。我在第一步会先用 curl 手动调用一次 WebMCP看响应时间。如果接口本身耗时小于 200ms那问题基本出在 Agent 侧的超时配置把工具调用的timeout调大即可如果接口真的慢就需要在工具内部做“快速失败”比如查数据库超过 5 秒就返回超时错误不要让整个 HTTP 请求挂在那里占住 Agent 的上下文窗口。注意给 Agent 做工具调用时宁可让它快速看到一个明确的错误也不要让它长时间无响应。大模型没有耐心很多 Agent 框架遇到响应超时会直接放弃后面再重试也不会结果就是你看着日志发呆。5.2 页面状态跟后端状态对不上我踩过的最大一个坑就是前端用户操作改了数据库但page_state还是旧值Agent 调用 WebMCP 拿到旧状态给出的判断完全错误。后来我把所有页面状态都改成“从唯一数据源读取”也就是数据库查询结果经过一次格式化后直接返回不再维护独立的缓存快照。如果确实需要缓存一定要加last_sync_at时间戳并在返回给 Agent 时明确标注“该状态是 10 秒前的快照”让 Agent 有判断空间。5.3 危险工具被 Agent 误触发这个问题在演示环境里不痛不痒生产环境里会出事。比如用户说“帮我把所有订单删除”Agent 如果只有一个delete_order工具它可能真的会逐个调用。我的方案是第一工具命名不要过于泛化用delete_order_with_confirm代替delete_order第二manifest 里给危险工具增加标记比如danger_level: high第三Agent 框架里对高危险工具做强制人工确认确认前不发起调用第四服务端对写操作统一记录审计日志谁调用了、参数是什么、结果是什么全都留下来。5.4 常见问题速查表现象可能原因处理建议Agent 拿到的 manifest 是旧的Agent 侧或浏览器有缓存加上?v2版本参数Agent 端缓存控制在 60 秒内工具调用一直返回 401token 过期或没正确传 Header检查 Authorization 前缀是否包含Bearer确认 token 有效期元素一直提示不存在页面结构变化元素 ID 被改建立元素 ID 映射表在 manifest 中同步更新Agent 把参数传成中文模型对英文参数名理解不到位加强description中的示例比如element_id: 例如 submit-btn多个 Agent 会话互相干扰没有隔离会话状态确认每个请求都带唯一session_id服务端按会话隔离状态点击工具执行了两次网络重试导致重复请求在 invoke 请求里增加request_id服务端做幂等去重6. 我的落地经验与后续扩展把这套 WebMCP 方案在几个小型项目里跑过之后我的体会是它不太像一个严格的标准规范更像我手里一套组织“网页能力”的方法论。真正要落地三点建议供你参考。第一一定要让 WebMCP 反映真实页面状态不要 mock 一套状态再跟页面脱节否则 Agent 再聪明也会被脏数据带偏。第二从只读工具开始接。先让 Agent 能查状态、查列表验证整个链路稳稳当当再逐步放开点击、提交、删除这些写操作。全读写一步到位出问题的时候你连锅都甩不干净。第三日志和审计要前置。每个 WebMCP 调用在线上环境都要能看到是谁调用的、调用了什么、结果如何这既是安全底线也是出问题后快速定位的救命稻草。这个方向后续可以延展的地方也不少。比如把 WebMCP manifest 转成标准 MCP server这样所有支持 MCP 协议的客户端都能直接使用再比如给长耗时工具增加 Webhook 回调能力Agent 提交任务后不用一直等任务完成由服务端主动通知还可以做一个可视化工具编排界面在页面上点选按钮、圈一下区域自动生成能力描述让非技术同学也能维护 Agent 的工具清单。最后分享一个调试小技巧在你准备把 Agent 接进来之前先用 curl 把自己当成 Agent把全套请求手动打一遍curl -s http://127.0.0.1:8000/mcp/manifest | python3 -m json.tool curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ -H Authorization: Bearer change-me \ -H Content-Type: application/json \ -d {session_id:debug-001,tool:get_element_status,parameters:{element_id:submit-btn}}能看到结构化响应再让 Agent 也走一遍同样的链路基本就能把“Agent 的问题”和“WebMCP 服务的问题”切分开。踩过几次坑之后我越来越相信一件事很多时候不是模型不够懂你的业务而是你的业务还没有给模型开一扇门。WebMCP 就是我想象中那扇门的样子——网页把自己的工具大大方方交给 AgentAgent 也终于能从“能说会道”变成“能干活”。