Part I: an MCP client has no working directory, so it never sees your AGENTS.md.
Part II: publish the server so a client can find it.
Part III: keep facts as data and instructions as prose, so what it finds is worth seeing.
Now take the working directory away completely. WebMCP's client is a browser
tab, and a tab doesn't fail to find a working directory. It doesn't have the
concept.
So what does "expose your context" mean when there's no repo, no process, and
no server — just a page?
The API is not FAF's
document.modelContext.registerTool() is Chrome's own experimental API
(chrome://flags/#enable-webmcp-testing). A page registers a tool, an agent
inspecting that page calls it, done — no separate MCP process, no transport,
no working directory to have or lack. Where the native API isn't available,
@mcp-b/webmcp-polyfill — a real, independent package, not FAF's — provides
the same registerTool surface as a fallback:
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 doesn't care what you register. This is the part that matters more
than any specific tool: the platform capability is generic, open, and not
owned by anyone's product. What follows is one demonstration of it, not the
only shape it can take.
Three tools, two ways to register them
faf.one/webmcp registers three:
| Tool | Returns |
|---|---|
score_faf |
{ score, tier, populated, active, gaps } — 0–100, in-browser |
fill_6ws |
a human_context: YAML fragment from a form |
emit_agents_md |
{ markdown } — a minimal AGENTS.md render |
Those are Part III's three moves, in a tab: fill in the six W's, score what's
still blank, and write the AGENTS.md from the facts.
score_faf and emit_agents_md are registered in script with
registerTool(), and both carry annotations: { readOnlyHint: true }. That's
not a comment. It's part of the registration, and an agent can read it before
calling anything: the same trust signal MCP tool annotations give a client, now
on a web page instead of a server.
fill_6ws is a plain HTML form with toolname, tooldescription and
toolautosubmit attributes: the browser turns the form into the tool. If the
form isn't picked up, the page registers fill_6ws in script instead.
score_faf runs entirely client-side. The scoring kernel is WASM
(faf_wasm_sdk), compiled once from the same Rust source the CLI and the
edge workers use — no server round-trip, no MCP process to spin up:
export function scoreYaml(yaml: string): string {
if (!ready) throw new ToolError('kernel_not_ready', 'WASM scoring kernel is not initialized');
return score_faf(yaml);
}
And the result mapper refuses to pad the picture. Ignored slots — fields that
don't apply to this project — aren't counted as missing:
/** Score is populated/active. Ignored slots are not missing — 13/13, not 13/21. */
gapsFromSlots only reports what's genuinely empty. A tool that's willing to
inflate its own denominator to look better is exactly the kind of thing a
"structured facts" argument falls apart without.
The part that's honest about its own limits
emit_agents_md renders a real AGENTS.md — Setup & build, Run the tests,
Stack, 6Ws — from the same .faf fields the CLI reads. But the file's own
comment says what it isn't:
// Minimal renderer — not the full faf-cli AGENTS.md compiler.
It doesn't do Guardrails tiers, branch-aware Commit & PR, or any of the
richer sections the real compiler produces — because a browser tab is a
different environment with a different budget than a CLI process, and a
renderer that pretended otherwise would be lying about what ran. The output
says so too — a plain, one-line stamp at the foot: compiled from. Not "compiled with faf-cli." A different tool
application/vnd.faf+yaml
made this.
The fetch surface is an allowlist, not a promise
score_faf and emit_agents_md can take a URL instead of pasted YAML. That
means the page fetches something the agent asked for — and a page that
fetches whatever a caller names is a real risk, not a hypothetical one. The
check is explicit, and it isn't just a pre-check — it runs again after
redirects, on whatever URL the request actually landed on:
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;
}
Two hosts only — faf.one, raw.githubusercontent.com — exact match, not a
suffix (their own comment: "ide.faf.one is not faf.one"). HTTPS only,
256KB cap. And a URL containing /mcp in its path is rejected outright, by
name — the tool doesn't just happen to avoid talking to an MCP server, it
refuses to. A github.com/owner/repo link gets rewritten to the raw file
host before any of this runs, so a redirect can't land the fetch somewhere
this check never saw: fetchAllowedYaml re-validates the final URL after
following redirects, not just the one it started with.
Try it
-
chrome://flags/#enable-webmcp-testing→ enable → relaunch Chrome. - Open
https://faf.one/webmcp(production needs HTTPS;localhostover plain HTTP works fine for local testing). - Install the Model Context Tool Inspector and open it on that tab.
Five things should be true:
[ ] Inspector lists score_faf, fill_6ws, emit_agents_md
[ ] score_faf on the loaded fixture returns a numeric score
[ ] Bad YAML returns { error, message } — not a thrown exception
[ ] The 6Ws form returns YAML and does not navigate the page
[ ] Network tab shows no call to any MCP server or /mcp URL —
the only network activity is the one allowlisted YAML fetch
That last one is the actual claim of this piece, made checkable: nothing here
talks to a server. The tab did it.
What Part I asked, answered somewhere Part I didn't expect
Part I's problem was a client that can't see your AGENTS.md because it has
no way to walk to it. WebMCP's problem is stricter: there's no walking to
anything, ever — so the only way to expose context is to hand it over
directly, as tool calls, from wherever the process already lives. score_faf,
fill_6ws, emit_agents_md are one answer to that, running in the browser
because the browser is where this particular client already is.
The API that makes it possible isn't FAF's, and doesn't have to be. Register
whatever a page's own good judgment says an agent visiting it should be able
to call.
Series: Part I — Invisible AGENTS.md? · Part II — Publishing to the Registry · Part III — Horses for Courses · mcp-context-card.
Top comments (1)
The browser tab having no concept of a working directory is a sharper constraint than the native one - there is nothing to approximate. registerTool from the page flips the discovery problem nicely: context travels with the thing being looked at instead of living one filesystem away. The question I would poke next: what does write access look like in that model? A page exposing tools is one trust decision; a page exposing mutable state is another.