Astro 博客主题 Stalux 集成 WebMCP:纯前端注册 7 个工具,让 AI 代理直接用你的博客
AI 辅助声明:本文的代码实现与排查过程使用了 AI 编程助手(ZCode)辅助进行源码编写、调试与文档撰写。所有结论均经过构建与浏览器实测验证。
我在给 Astro 博客主题 Stalux 做 AI 改造时,受Mayx的博客启发接触到了 WebMCP——一个还在 W3C 社区组草案阶段的规范,目标是让网页直接向浏览器里的 AI 代理暴露工具。当时我的第一反应是:这是给动态网站准备的吧,我这种纯静态 Astro 博客也想凑这个热闹?
后来发现想多了。WebMCP 恰恰是纯前端零后端的典范——我的博客连服务器都没有,全是构建产物,反而把 WebMCP 的能力发挥得淋漓尽致。
这篇文章记录 Astro 博客主题 Stalux 从零集成 WebMCP 的完整过程:工具设计、i18n、agent 实测踩坑,以及 webmcp.com 扫描评级从 A- 的优化历程。
WebMCP 是什么
先说清楚概念。MCP(Model Context Protocol)是 Anthropic 提出的标准协议,让 AI 应用接入外部工具和数据源。而 WebMCP 是 W3C 社区组(webmachinelearning/webmcp)推进的草案,把类似的能力下沉到浏览器层面——网页通过原生 API 暴露工具给 AI 代理,浏览器作为安全边界管好权限和来源。
| 对比项 | MCP | WebMCP |
|---|---|---|
| 协议层 | AI 应用 ↔ 工具服务器 | 网页 ↔ 浏览器代理 |
| 部署 | 需要跑 MCP server | 页面 JS 注册即可,零后端 |
| 安全边界 | server 自己管 | 浏览器权限策略 |
| 状态 | 已广泛采用 | W3C 社区组草案,Chrome 149 Origin Trial |
核心 API 是 document.modelContext.registerTool(),注册的 ModelContextTool 字典长这样:
| 字段 | 必填 | 说明 |
|---|---|---|
name |
是 | 工具唯一标识,snake_case |
title |
否 | 展示名 |
description |
是 | 自然语言描述,模型靠它决定何时调用 |
inputSchema |
否 | JSON Schema 参数契约 |
execute |
是 | 真正干活的函数,返回 JSON |
annotations |
否 | readOnlyHint(是否只读)、untrustedContentHint(输出是否不可信) |
纯前端零后端:为什么静态站能实现
我的博客数据源全是构建期静态产物,一个后端都没有:
| 数据源 | 用途 | 形态 |
|---|---|---|
/api/posts.json |
文章元信息索引(标题/日期/分类/标签/摘要/URL) | 构建期生成 JSON |
/pagefind/ |
全文搜索索引 | Pagefind 构建期生成 |
/posts/{abbrlink}.md |
单篇文章原始 Markdown | 构建期导出 |
/llms.txt、/llms-full.txt |
站点信息 + 全站 Markdown 镜像 | 构建期生成 |
这些文件都是 public/ 或静态路由下的产物,浏览器直接 fetch 就能拿到。工具注册进 document.modelContext 后,execute 内部就是从这些静态端点取数据——没有任何一个请求发到“自己的服务器”,因为压根没有。
这就是 WebMCP 的设计初衷:一个静态博客不需要任何后端,写一段 registerTool() 的 JS 就能让 AI 代理直接使用。
工具设计:从 4 个到 7 个
第一版(v1.11.0)我注册了 4 个只读工具:
const tools = [ listPostsTool(), // stalux_list_posts 分页列出文章 searchPostsTool(), // stalux_search_posts 全文搜索 readPostTool(), // stalux_read_post 读文章 Markdown siteInfoTool(), // stalux_site_info 站点信息];每个工具都是 { name, title, description, inputSchema, annotations, execute } 六元组。以搜索为例:
execute: async (input) => { const keyword = String(input.keyword ?? "").trim(); if (!keyword) return err("BAD_INPUT", "请提供搜索关键词"); // 动态加载 Pagefind 索引(运行时按需,构建期产物) const pagefind = await loadPagefind(); const res = await pagefind.search(keyword); const posts = await Promise.all( res.results.slice(0, limit).map(async (r) => { const data = await r.data(); return { url: data.url, title: data.title, excerpt: data.excerpt.slice(0, 240) }; }), ); return { ok: true, keyword, count: posts.length, posts };},4 → 7 个的演进
后来提交到 webmcp.com 扫描(WebMCP 目录站),拿了 A-(79 分),usability 维度只有 65。对照评分方法论(usability 占 60% 权重,看“工具有用、命名清晰、描述好”),问题很明确:工具太少、覆盖面窄。
于是 v1.12.0 扩到 7 个,补上了元信息定位类工具:
| 工具 | 功能 | 数据源 | 成本提示 |
|---|---|---|---|
stalux_list_posts |
分页列表(含元信息) | /api/posts.json |
不返回正文 |
stalux_get_post |
按 abbrlink/关键词取单篇元信息 | /api/posts.json |
比读正文便宜得多 |
stalux_current_post |
当前浏览文章上下文 | /api/posts.json |
“这篇文章”场景 |
stalux_random_post |
随机推荐一篇 | /api/posts.json |
探索用 |
stalux_search_posts |
全文搜索 | Pagefind | 命中后转 read/get |
stalux_read_post |
读原始 Markdown | /posts/{id}.md |
获取 abbrlink 先用前两个 |
stalux_site_info |
站点信息 + llms 入口 | site.yml | 批量获取全站数据 |
配套新增了 /api/posts.json 元信息索引端点——完整字段(标题/abbrlink/日期/分类/标签/摘要/字数/URL),是 get/current/random 三个工具的廉价数据源。
i18n:工具描述跟随站点语言
Astro 博客主题 Stalux 是 i18n 主题,中英文站都有。工具描述是给模型看的说明书,语言必须跟随站点:
const SITE_LANG = window.__STALUX_SITE_INFO__?.lang ?? "zh-CN";const IS_EN = SITE_LANG === "en" || SITE_LANG === "en-us" || SITE_LANG === "en-gb";
/** 按站点语言选择文案:pick(中文, English) */function pick(zh: string, en: string): string { return IS_EN ? en : zh;}
// 用法title: pick("列出博客文章", "List blog posts"),description: pick( "分页列出博客的全部已发布文章,返回标题、永久链接、日期、分类、标签、摘要与文章页 URL。", "Paginated list of all published posts, returning title, abbrlink, date, categories, tags, description and post page URL.",),实测验证:英文站(stalux.needhelp.icu)工具显示 “List blog posts” / “Get post info”,中文站(xingwangzhe.fun)显示 “列出博客文章” / “查看文章信息”。
Agent 实测的坑
坑一:agent 反复翻页撞 OUT_OF_RANGE(v1.14.0 修复)
webmcp.com 的 “Run live agent test” 实测暴露了一个真实缺陷:agent 不知道总页数,只能一页页翻,翻过头就撞 OUT_OF_RANGE 报错,10 次调用耗尽还没完成任务。
修复:越界自动回退最后一页,而不是硬报错:
let clamped = false;let resolvedPage = page;if (page > totalPages) { resolvedPage = totalPages; clamped = true; // 标记,agent 能感知被夹取}const posts = list.slice((resolvedPage - 1) * pageSize, ...).map(briefMeta);return { ok: true, page: resolvedPage, pageSize, total, totalPages, clamped, posts };同时在描述里提示 agent 用 pageSize: 50 一次拿完,减少翻页。
坑二:dev 模式 pagefind import 报错(v1.17.0 顺带修复)
这个坑不是 WebMCP 迭代期间暴露的,而是 v1.17.0 升级 Astro 7.2、做增量构建确定性改造时顺带发现的——但因为它直接影响 stalux_search_posts 在 dev 环境的使用,一并记录在这里:
[ERROR] [vite] Internal server error: Failed to resolve import "/pagefind/pagefind.js" from "src/scripts/webmcp.ts"动态 import("/pagefind/pagefind.js") 在 build 时 @vite-ignore 能过,但 dev 模式 vite:import-analysis 还是会解析绝对路径。修复:用完整 URL 构造,让 Vite 彻底跳过:
const url = new URL("/pagefind/pagefind.js", location.origin).href;const mod = await import(/* @vite-ignore */ url);效果
| 指标 | 结果 |
|---|---|
| 工具数 | 7 个(全部 readOnlyHint: true) |
| webmcp.com 扫描 | 4 tools detected(早期)、可执行性 100% |
| 语言 | 中英双语跟随站点 |
| 后端 | 零(纯静态产物) |
| 增量构建兼容 | 不影响(确定性修复后 HTML 1254 restored) |
Commit 记录
| Commit | 版本 | 内容 |
|---|---|---|
5ac6d18b |
1.11.0 | WebMCP 工具注册(4 只读工具)+ polyfill |
d4774092 |
1.12.0 | 工具 4→7 + /api/posts.json 元信息索引 |
e5d74e57 |
1.13.0 | 工具描述中英双语(跟随站点 lang) |
62308da4 |
1.14.0 | list_posts 越界自动回退最后一页(clamped) |
参考文档
| 文档 | 链接 |
|---|---|
| WebMCP 规范仓库 | github.com/webmachinelearning/webmcp |
| WebMCP polyfill | @mcp-b/webmcp-polyfill |
| WebMCP 目录站 | webmcp.com |
| Chrome 149 Origin Trial | Chrome Platform Status |
总结
WebMCP 还远没到正式标准,但方向是对的:网页自己成为 AI 代理可安全调用的工具,浏览器负责身份与权限。对静态博客而言,它几乎是零成本的——数据源早就躺在构建产物里,只是以前只有人能读,现在 agent 也能用了。
说实话,最让我意外的是“纯前端零后端”这个特性。GitHub Pages 上挂着的静态博客,配上 WebMCP,就成了 agent 可以直接调用的接口。等 Chrome 原生支持落地(目前 Origin Trial),polyfill 自动让位,这套东西就能跑进每个访问者的浏览器里。
如果你想给自己的站点也接上,思路很直接:列出现有的静态数据源,设计工具描述(记得带成本提示),registerTool 一把梭。去 webmcp.com 扫一下,看看评分能拿几级。
Astro 博客主题 Stalux 集成 WebMCP:纯前端注册 7 个工具,让 AI 代理直接用你的博客
作者:xingwangzhe
本文链接:https://xingwangzhe.fun/posts/stalux-webmcp-integration/
本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论