WebMCP将工具注册API直接引入浏览器,探讨在无工作目录、无进程环境下暴露上下文的架构思路,是MCP协议边界的重要技术讨论。
现在把工作目录彻底拿走。WebMCP 的客户端是一个浏览器标签页,而标签页不会存在"找不到工作目录"的问题。它根本没有这个概念。
所以当没有仓库、没有进程、没有服务器——只有一个页面的时候——"暴露你的上下文"意味着什么?
document.modelContext.registerTool() 是 Chrome 自带的实验性 API(chrome://flags/#enable-webmcp-testing)。一个页面注册一个工具,访问该页面的 agent 调用它,完成——不需要单独的 MCP 进程、不需要传输层、不需要"有没有"工作目录的概念。在原生 API 不可用的地方,@mcp-b/webmcp-polyfill——一个真实的、独立发布的包,不是 FAF 的——提供了相同的 registerTool 接口作为兜底:
const native = getDocumentContext();
if (native && typeof native.registerTool === 'function') {
return { context: native, source: 'native' };
}
try {
const { initializeWebMCPPolyfill } = await import('@mcp-b/webmcp-polyfill');
initializeWebMCPPolyfill();
} catch {
return { context: null, source: 'none' };
}
const polyfilled = getDocumentContext();
if (polyfilled && typeof polyfilled.registerTool === 'function') {
return { context: polyfilled, source: 'polyfill' };
}
return { context: null, source: 'none' };
WebMCP 不关心你注册了什么。这才是比任何一个具体工具都重要的部分:平台能力是通用的、开放的,不属于任何人的产品。以下只是它的一种实现演示,不是它唯一可能的形态。
faf.one/webmcp 注册了三个工具:
这些就是第三部分的三个动作,在一个标签页里:补全六个 W、打分还有哪些空白、以及用事实写出一份 AGENTS.md。
score_faf 和 emit_agents_md 通过脚本用 registerTool() 注册,两者都带有注解:{ readOnlyHint: true }。这不是注释。它是注册的一部分,agent 在调用任何东西之前就能读到它——MCP 工具注解给客户端的信任信号,现在出现在网页上而不是服务器上。
fill_6ws 是一个普通的 HTML 表单,带有 toolname、tooldescription 和 toolautosubmit 属性:浏览器把它转成工具。如果表单没被识别,页面就改用脚本注册 fill_6ws。
score_faf 完全在客户端运行。评分内核是 WASM(faf_wasm_sdk),从 CLI 和边缘 worker 使用的同一份 Rust 源码编译而来——没有服务器往返、没有要启动的 MCP 进程:
export function scoreYaml(yaml: string): string {
if (!ready) throw new ToolError('kernel_not_ready', 'WASM scoring kernel is not initialized');
return score_faf(yaml);
}
结果映射器拒绝给画面注水。被忽略的槽位——当前项目不适用的字段——不计入缺失:
/** Score is populated/active. Ignored slots are not missing — 13/13, not 13/21. */
gapsFromSlots 只报告真正为空的内容。一个愿意膨胀自己的分母来让自己看起来更好的工具,正是"结构化事实"论点失去它就会崩溃的那种东西。
emit_agents_md 从 CLI 读取的同一组 .faf 字段渲染出一份真正的 AGENTS.md——Setup & build、Run the tests、Stack、6Ws。但文件本身的注释说明了它不是什么:
// Minimal renderer — not the full faf-cli AGENTS.md compiler.
它不做 Guardrails 分层、不做分支感知的 Commit & PR、不做真正编译器产生的任何更丰富的章节——因为浏览器标签页是和 CLI 进程不同的环境、有不同的预算,一个假装自己能做到更多的渲染器就是在撒谎。输出也说明了这一点——底部一个简单的单行戳记:compiled from application/vnd.faf+yaml。不是"compiled with faf-cli"。这是另一个工具生成的。
score_faf 和 emit_agents_md 可以接收 URL 而不是粘贴的 YAML。这意味着页面会 fetch agent 请求的东西——而一个什么都按调用方指定的名字去 fetch 的页面是真实的风险,不是假设的。检查是显式的,而且不只是预检——它在重定向之后会再次检查,以请求实际到达的 URL 为准:
export function assertAllowedUrl(urlString: string): URL {
const url = new URL(urlString);
if (url.protocol !== 'https:') {
throw new ToolError('invalid_url', 'url must use https');
}
if (!ALLOWED_HOSTS.has(url.hostname)) {
throw new ToolError('invalid_url', `host not allowlisted: ${url.hostname}`);
}
if (url.pathname.includes('/mcp')) {
throw new ToolError('invalid_url', 'mcp endpoints are not allowed');
}
return url;
}
只有两个主机——faf.one、raw.githubusercontent.com——精确匹配,不是后缀(他们自己的注释:"ide.faf.one 不是 faf.one")。仅 HTTPS,256KB 上限。路径中包含 /mcp 的 URL 被直接按名字拒绝——工具不只是"恰好"不去和 MCP 服务器通信,它是主动拒绝。一个 github.com/owner/repo 链接在以上检查运行之前会被改写成 raw 文件主机,所以重定向不可能把 fetch 落到这个检查从未见过的地方:fetchAllowedYaml 在跟随重定向之后验证最终 URL,而不是只验证它最初发出的那个。
chrome://flags/#enable-webmcp-testing → enable → 重启 Chrome。
打开 https://faf.one/webmcp(生产环境需要 HTTPS;本地测试用纯 HTTP 的 localhost 即可)。
安装 Model Context Tool Inspector 并在那个标签页上打开它。
应该有五个为真:
[ ] Inspector 列出了 score_faf、fill_6ws、emit_agents_md
[ ] 在加载的 fixture 上调用 score_faf 返回一个数字分数
[ ] 格式错误的 YAML 返回 { error, message }——而不是抛出异常
[ ] 6Ws 表单返回 YAML 且不导航页面
[ ] Network 标签页没有任何对任何 MCP 服务器或 /mcp URL 的调用——
唯一的网络活动是那一次白名单内的 YAML fetch
最后一条是这篇文章的实际主张,并且是可以检验的:这里没有任何东西和服务器通信。标签页自己完成的。
第一部分的问题是:一个客户端因为没有路径可以走到你的 AGENTS.md,所以永远看不到它。WebMCP 的问题更严格:永远没有任何路径可以走到任何地方——所以暴露上下文的唯一方式是把它们直接交出去,作为工具调用,从进程原本就在的地方发出。score_faf、fill_6ws、emit_agents_md 是对这个问题的 一种回答,运行在浏览器里,因为这个特定的客户端本来就在浏览器里。
使这一切成为可能的 API 不是 FAF 的,也不需要是。注册什么工具,由页面自己的判断来决定——判断一个来访的 agent 应该被允许调用什么。
系列文章:第一部分 — 看不见的 AGENTS.md?· 第二部分 — 发布到注册表 · 第三部分 — 各取所需 · mcp-context-card。