首页 / 文章

WebMCP的实际应用:将任务看板转化为AI智能体的工具。

WebMCP的实际应用:一个任务看板通过声明式工具、脚本化工具、可动态更新的工具列表、MCP桥接功能以及拒绝处理机制,转变为面向AI智能体的工具平台。

发布者:REACTAPP.TOP阅读约 14 分钟3318 词

来源: WebMCP: When Websites Become AI Tools

猜测您网站内容的智能体

目前,与网站交互的AI智能体大多是在猜测。它会截取屏幕截图,寻找类似按钮的元素,或者解析DOM结构,希望类名能如其表面所示。点击之后,它会检查页面是否发生了表明操作成功的变动。这虽是个巧妙的办法,但基础十分薄弱,因为网站从未向智能体提供任何信息,所有结论都是推断出来的。

WebMCP是Chrome提出的解决方案,旨在终结这种猜测行为。页面会主动说明自身的功能,智能体则读取这些说明,并使用结构化的参数来调用相应功能,而非直接瞄准屏幕上的某个区域。虽然变化看似微小,但却改变了处理方式:智能体不再需要从像素和标记中重建应用程序的功能,而是直接从应用程序本身获取这些功能信息。

为验证这一方法在实践中的可行性,我们构建了一个小型任务板,并让一个真实智能体去操作它。最具参考价值的结果出现在该智能体直接拒绝某个请求时。

三种角色

实验中的每一刻都涉及三种角色之一。

  • 页面扮演提供者角色。它负责注册各种工具,每个工具都会包含一个名称、针对模型的描述、描述参数的 JSON Schema,以及执行具体任务的函数。
  • 浏览器则充当注册表的角色。它会为每个标签页保存工具列表,尽可能从 HTML 中提取 Schema,并决定何时显示或隐藏某个工具。
  • 该代理即为用户。它通过getTools()向浏览器查询可用工具,再使用executeTool()执行选定的工具。DOM不在其操作范围内,而且它也无法实现页面从未提供的功能。
  • 最后这一点正是该设计的核心。Chrome的文档已对相关API有足够清晰的描述,但阅读说明并不等同于真正理解,因此最快的方法就是构建尽可能简小的版本。

    演示:三个文件构成的任务看板

    该应用由 index.html、app.js 和 style.css 组成。无需任何构建或安装步骤。用户可以添加任务、标记完成状态以及筛选列表,这类项目通常只需一个下午就能完成。新之处在于,通过浏览器提供的一个对象,该页面还能以一组可调用工具的形式呈现给人工智能代理。

    const mc = document.modelContext;   // undefined if WebMCP is off
    

    那一行代码就是整个入口点。当对象存在时,页面可以注册工具;而当对象不存在时,页面就仅作为一个任务板使用,这也是它应有的降级表现。

    为让两端都清晰可见,演示页面将它们并排展示。左侧是任务板,即提供方;右侧则是一个“模拟代理”面板,它扮演消费者的角色,像真实代理一样发起 getTools() 和 executeTool() 调用,从而无需额外的连接配置。在下面的每张截图中都能看到该面板。

    WebMCP 在 Chrome Platform Status 中仍被标记为“提议中”状态,且默认并未在任何地方启用,因此首先需要设置一个标志位。

    打开 chrome://flags/#enable-webmcp-testing,将其设置为“已启用”后再重新启动浏览器。否则 document.modelContext 的值将为未定义。Chrome 还提供了从 Chrome 149 开始支持的 WebMCP 来源测试功能。该标志适用于本地实验,而来源测试则是 Chrome 推荐用于更广泛测试的方式。

    先完成循环

    在看到 API 的输出结果后,就能更容易理解它的工作原理。Claude Code 被连接到开发板后,收到了一条指令,要求它完成四项任务:查找信息并开展研究、撰写博客大纲、编写包含示例的完整博客内容,以及发布博客。随后便出现了四项任务。第二条指令要求它将第3、4和5项任务标记为已完成,这也成功实现了。

    Claude没有截取该界面的截图,也没有读取其HTML代码。它向浏览器查询可用的工具,找到了add-task函数,随后使用结构化的参数调用了该函数四次。以下是从浏览器内部视角看到的同一时刻的画面。

    这四个新任务现在出现在界面的第3到第6位,而开发者工具面板(先选择“应用程序”,再选择“WebMCP”)则列出了该页面当前支持的所有六种工具。其中clear-completed函数在刚才还不存在,其原因很快就会显现出来。这三项已完成的任务会被逐次记录下来。

    每个调用都会显示其参数({"id":3}、{"id":4}和{"id":5}),摘要行会显示总共有三次调用,且没有失败、取消或仍在运行的调用。在这里调试WebMCP最为快捷,因为面板会准确显示代理发送的内容。

    因此,在终端中输入的一句命令会变成浏览器标签页中的四个结构化函数调用。接下来的问题是,页面需要做些什么才能实现这一点。

    发布工具的两种方式

    第一种方式完全不需要JavaScript,只需在现有的表单中添加四个额外的属性即可。

    <form id="add-form"
          toolname="add-task"
          tooldescription="Add a new task to the user's task board."
          toolautosubmit>
      <input name="title" required maxlength="80"
             toolparamdescription="Short description of the task to add.">
      <select name="priority"
              toolparamdescription="How urgent the task is.">
        <option value="low">low</option>
        <option value="normal" selected>normal</option>
        <option value="high">high</option>
      </select>
      <button type="submit">Add</button>
    </form>
    

    那个表单就是 Claude 调用了四次的 add-task 工具。字段名称、required 属性以及 <select> 选项都会被浏览器读取,浏览器会自行构建 JSON Schema。智能体只能看到一个必填的字符串和一个包含三个值的枚举,而实际上并没有人手动编写过该 Schema。在现有的应用中,这很可能是最简单的 WebMCP 实验,因为普通的 HTML 表单已经为它奠定了基础。唯一需要补充的,就是向智能体说明该表单的含义。

    有一个细节需要稍加理解:同一个 submit 处理程序既服务于点击“添加”按钮的用户,也服务于调用该工具的智能体,而事件会表明是哪种情况。

    $('#add-form').addEventListener('submit', (e) => {
      e.preventDefault();
      const task = addTask(new FormData(e.target).get('title'), /* ... */);
    
      if (!e.agentInvoked) {
        e.target.reset();          // human — clear the box
      } else {
        e.respondWith?.(Promise.resolve(
          text(`Added task #${task.id}: "${task.title}".`)
        ));
      }
    });
    

    agentInvoked标志可以告诉你是谁按下了按钮,而respondWith()则会在不跳转页面的情况下向智能体返回结果。存在一条包含两个调用方的代码路径,这是个不错的设计选择,因为没人希望应用中的每个操作都有“AI版本”。只要合理,相同的逻辑就应该同时服务于人类用户和智能体。

    声明式路径或许没那么有趣,但它却更可能吸引人们的注意,因为它展示效果出色且无需额外思考。表单无法表达的内容之处,才是有趣的部分。那就是第二种路径——registerTool,它的写法很像为后端MCP服务器编写的代码。

    await mc.registerTool({
      name: 'list-tasks',
      description: 'List the tasks on the board. Use this before acting so you know the task IDs.',
      inputSchema: {
        type: 'object',
        properties: {
          status: { type: 'string', enum: ['all', 'open', 'done'] },
        },
      },
      annotations: { readOnlyHint: true },
      async execute({ status = 'all' }) {
        const rows = visible(tasks, status);
        return text(rows.map((t) => `#${t.id} [${t.done ? 'x' : ' '}] ${t.title}`).join('\n'));
      },
    });
    

    仔细看看那个description。它是为模型而非开发者编写的,像“在执行操作前使用此功能以便了解任务ID”这样的描述能提示何时调用该功能。如今描述已成为API接口的一部分,模糊的描述会导致智能体只能靠猜测行事。

    readOnlyHint标志表示无需先询问用户即可安全调用该工具。list-tasks和estimate-task带有此标志,而complete-task和clear-completed则没有,因为它们会改变现有状态。

    工具的处理速度也可能较慢,智能体也可能会在过程中改变主意。estimate-task工具会故意等待三秒,并将AbortSignal作为第二个参数传入。

    async execute({ id }, { signal }) {
      const t = findTask(tasks, id);
    
      if (!t) {
        return text(`No task #${id}.`);
      }
    
      await sleep(3000, signal); // throws if the agent aborts
    
      return text(
        `Task #${t.id} is about ${
          t.priority === 'high' ? '2 hours' : '30 minutes'
        }.`
      );
    }
    

    尊重该信号即可实现取消功能;若忽略它,即便代理已离开,工具仍会继续运行。

    页面打开时会变化的工具列表

    与发送静态清单相比,在脚本中注册工具的最大优势在于列表允许在运行时发生变化。该平台有一个名为clear-completed的工具,用于删除所有已完成的任务。这种设计只有在任务真正完成时才有意义,这也解释了为何在早前的截图中,三个任务被标记为完成后立即出现了该工具。因此它仅在这些时刻存在。

    let clearCtl = null;
    
    async function syncClearTool() {
      const has = tasks.some((t) => t.done);
    
      if (has && !clearCtl) {
        const ctl = new AbortController();
        clearCtl = ctl;
    
        await mc.registerTool(
          {
            name: 'clear-completed',
            /* ... */
          },
          {
            signal: ctl.signal
          }
        );
      } else if (!has && clearCtl) {
        const ctl = clearCtl;
        clearCtl = null;
    
        setTimeout(() => ctl.abort(), 0); // abort() IS unregister
      }
    }
    

    并不存在 unregisterTool() 这个函数。在注册时传入一个 AbortSignal,一旦中止该信号就会移除对应工具。这样的设计非常巧妙:用于取消正在进行的调用的信号同时也能撤销相应功能,一个机制完成了两项任务。

    将某个任务标记为已完成后,clear-completed 会出现在代理的列表中;再取消该标记,该工具就会消失。代理会通过 toolchange 事件得知这一变化,无需主动轮询。

    mc.addEventListener('toolchange', refreshTools);
    

    因此,工具列表并非一次性发布的清单。它描述的是应用在当前状态下的功能。

    这与后端 MCP 服务器有何不同

    接着,人们要求 Claude Code 将看板筛选为已完成的任务。

    智能体调用了 filter-tasks 函数并传入参数 {"status":"done"},此时“已完成”按钮变为可用状态,看板上仅显示已完成的任务。实际上智能体并未获取经过筛选的数据副本,它只是改变了用户所看到的界面内容。

    后端MCP服务器无法实现这一点。由于位于网络边界的另一侧,它只能操作存储的数据,完全不知道当前各个屏幕上显示的内容。WebMCP工具则存在于标签页内部,共享该标签页的DOM、会话及Cookie,因此代理与用户可以查看同一屏幕,且任意一方都可能对其进行修改。这既是其显著优势,也潜藏着风险,相关风险内容将在下文单独阐述。这也正是为何将WebMCP称为“浏览器内的MCP”会低估其价值——工具的所在位置至关重要,因为它与用户正在查看的页面状态紧密相连。

    连接外部代理

    “模拟代理”面板在开发过程中能提供帮助,但由于它本身就是被调用的页面,因此作用十分有限。真正的测试需要浏览器之外的工具,该工具只能查看getTools()返回的信息。为此有了agent.mjs,这是一个小型命令行工具,借助了Chrome DevTools Protocol的相关功能,且没有任何依赖项,因为Node 22已内置全局WebSocket。按其设计,它只能获取getTools()返回的信息,并且只能通过executeTool()来执行操作,无法进行DOM抓取。

    node agent.mjs list
    node agent.mjs call add-task '{"title":"Ship the demo","priority":"high"}'
    

    那两条命令会列出相关工具,然后调用add-task并传入标题和优先级。生成这些截图的设置还更进一步:Claude Desktop、Claude Code和ChatGPT使用的是MCP协议而非WebMCP,因此mcp-bridge.mjs充当适配器,而且由于两种协议的对应关系几乎完全一致,这个适配器也非常简单:tools/list对应getTools(),tools/call对应executeTool()。Claude Code则是通过.mcp.json文件来识别并使用该适配器的。

    {
      "mcpServers": {
        "webmcp-board": {
          "command": "node",
          "args": ["C:\\projects\\web-mcp-demo\\mcp-bridge.mjs"],
          "env": { "PAGE_URL": "https://tusharkanjariya.github.io/web-mcp-demo/" }
        }
      }
    }
    

    因此,整个流程是 Claude Code 通过 MCP 与桥接层通信,桥接层再通过 CDP 与 Chrome 通信,最后 Chrome 调用页面注册的工具。这共有四层跳转,而最终的页面无法判断另一端是哪个客户端。

    Claude Code
        ↓
    MCP
        ↓
    mcp-bridge.mjs
        ↓
    Chrome DevTools Protocol
        ↓
    WebMCP
        ↓
    my task board
    

    还有一个便利之处:如果在调试端口没有响应,该工具会自行启动 Chrome,并在对应标签页不存在时打开该页面。MCP 客户端可以从头开始启动所有功能。

    拒绝

    随后,该代理被要求删除一个任务。

    截图的上半部分显示了之前的过滤请求。下半部分则是关键部分:Claude 表示该平台上不存在 delete-task 工具,唯一可用的删除功能是 clear-completed,但该功能也会同时清除任务 #2、#3、#4 和 #5,而它不会这么做。

    并非护栏、系统提示或安全层导致了拒绝响应。根本不存在删除单个任务的功能,而该智能体也注意到了这一点。对于一个简单的测试应用中的单次交互过度解读是错误的,但这确实改变了问题的呈现方式。那些通过像素和坐标来操控页面的智能体会寻找类似删除的功能并尝试使用,因为屏幕抓取型智能体只能依靠即兴应对。而这个智能体统计了自己的六种工具,发现没有一种可用于删除任务,又意识到最接近的选项会带来附带损害,于是暂停并提出了疑问。

    工具界面就是界限所在。并没有人制定过禁止删除内容的规则。页面从未提供过删除功能,这一点就足够说明问题了,尽管WebMCP并不能让智能体自动具备安全性。

    Chrome与规范之间的分歧

    运行该程序后发现了文档描述与浏览器实现之间的差异,解决这些问题花了一整个下午的时间。其中三个问题与数据结构有关。

    • executeTool()的参数:规范中说明应为对象形式,但Chrome 151要求输入JSON字符串,否则会触发“无法解析输入参数”的错误。
    • RegisteredTool.inputSchema:规范中规定为对象类型,但Chrome返回的是JSON字符串,因此必须在读取properties之前先对其进行解析。
    • executeTool()最终返回的值:规范中说明应为DOMString类型,确实如此,但实际上它是一个序列化的{content:[…]}封装结构,需要解包两次才能获取到实际文本。

    一旦了解这些规则,其实都没有什么难度。真正造成问题的两个问题是生命周期相关的错误,而且只有在真实浏览器加载页面时才会出现。

    在智能体触发的提交操作正在进行时调用 form.reset() 会以“工具执行因表单重置而被取消”为由终止该工具调用。那些像大多数处理程序那样在提交后清空输入内容的普通处理程序,会导致所有由智能体驱动的 add-task 操作失败。任务虽已创建,但回复却从未到达,正因如此,前面展示的处理程序仅对人工提交的表单进行重置。

    在 Chrome 153 之前的版本中,移除某个工具的同时也会取消其仍在运行的所有调用。只要工具是因执行成功而自行被移除,这并无危害。clear-completed 会删除所有已完成的任务,导致没有任何任务剩余,进而使该工具自行退出。在 151 版本中,该功能会在返回结果之前就中断自身的响应。解决办法是使用 setTimeout(() => ctl.abort(), 0):将中止操作推迟一个时间间隔,可以让调用先完成。这种方法虽然易于理解且有效,但通过一个时间间隔来延迟清理工作,这类补丁在 Chrome 发布几版后或许需要重新审视。

    发布演示版本

    该演示完全是静态的,因此 GitHub Pages 可以原样提供它,而 HTTPS 则能提供 WebMCP 所需要的安全环境。这样一来,每个人都能使用功能正常的任务板,但却无法使用功能正常的 WebMCP,因为那些未设置相关标志的访问者仍然会看到 document.modelContext 为未定义状态。要解决这个问题,需要在 <head> 中添加一个来源试用令牌,并将其注册为第一方令牌,因为 Chrome 会拒绝将第三方令牌放在元标签中。在没有任何标志的纯净配置下,modelContext 会与全部六种工具一起以“object”形式返回。该试用版本的有效期截至 Chrome 156 版本。

    没有合适答案的安全问题

    WebMCP 工具必须在已登录的标签页中运行。这既是其设计初衷,也是存在的问题所在。

    该规范中包含一个untrustedContentHint,但它仅起到提示作用。如何处理提示注入问题目前仍未解决,且该API的任何部分都不存在授权边界。如果攻击者控制的文本能够显示在页面上并被客户端读取,readOnlyHint也无法起到作用。

    该桥接服务也可以通过隧道以HTTP方式提供,这样ChatGPT的连接器就能访问它。虽然它能正常工作,但使用前需谨慎考虑,因为该隧道会暴露一个无身份验证的端点,任何猜到地址的人都可以在你已登录的浏览器标签页中运行相关工具。实验20分钟没问题,但让它整夜运行则不可取。

    在自己的应用中尝试

    由于规模较小,网页应用的维护者只需一个下午就能了解该 API。选择任何现有的表单,为其添加 toolname 和 tooldescription 属性,然后在 DevTools 中打开 WebMCP 面板,就能看到该工具出现。这样花费的约十分钟所学到的知识,比再花一小时阅读还要多。

    其意义远超语法层面。如实描述应用的功能已成为产品本身的一部分,而非事后撰写的文档。由于控制板不支持单任务删除,因此智能体也从未执行过删除操作。getTools()返回的结果决定了智能体对应用功能的认知,而这种认知正逐渐转化为界面本身。演示版本包含三个文件中的六种工具,且没有依赖项,不妨思考一下自己的页面会如何描述自身。如需重现该效果,可开启chrome://flags/#enable-webmcp-testing标志,然后执行node agent.mjs list命令。若能得到六种工具的响应,即表示你正在以智能体的方式与网页交互。

    快速解答

    什么是 WebMCP?这是一种拟议中的浏览器 API,允许页面通过 document.modelContext 将自身的功能作为可调用的工具发布给 AI 智能体。智能体可以直接调用这些工具,从而无需进行 DOM 抓取或协调点击操作。

    它与普通的 MCP 服务器有何不同?后端服务器运行在网络边界之外,基于存储的数据进行处理。而 WebMCP 工具则在浏览器标签页中运行,因此能够改变用户看到的内容,并且会复用页面的会话和权限。调用 filter-tasks 就明显改变了界面内容。

    页面如何注册工具?要么在现有表单中添加 toolname 和 tooldescription 属性,让浏览器从这些字段中推断出架构;要么传入名称、描述、JSON Schema 以及 execute 函数,调用 document.modelContext.registerTool()。表单适用于简单的操作,而 registerTool 则能处理表单无法表达的各类功能。

    需要设置标志吗?目前需要。请启用该标志并重新启动 Chrome,或者使用 --enable-features=WebMCP 参数启动它。对于未设置该标志的访问者,页面 <head> 中的第一方源试用令牌即可使用。

    Claude Desktop 或 ChatGPT 能调用这些工具吗?它们本身无法直接调用,因为这些工具使用的是 MCP 而非 WebMCP。有一个小型适配器将 tools/list 映射为 getTools(),将 tools/call 映射为 executeTool()。这里的适配器代码约有 130 行,无需任何依赖,且可通过标准输入输出在本地客户端中使用。

    它安全吗?这个问题尚无定论。untrustedContentHint 和 readOnlyHint 只是提示信息,并不能作为授权边界,而且相关规范也尚未解决提示注入的问题。在构建工具接口时,应假设用户可能会诱使智能体调用你提供的任何功能。

    相关阅读

  • Serena MCP Server:专为AI助手设计的符号感知代码编辑功能 — 通过Docker和SSE搭建Serena,让Claude Code、VS Code和OpenCode具备基于LSP的符号搜索、安全重命名功能,同时以更少的令牌消耗实现持久化内存支持。