Context Over MCP, Part IV: No Working Directory At All A developer demonstrated WebMCP, Chrome's experimental API that lets a web page register tools directly for AI agents via document.modelContext.registerTool(), eliminating the need for a working directory, separate MCP process, or transport. The demo at faf.one/webmcp registers three tools — score_faf, fill_6ws, and emit_agents_md — with score_faf running a WASM scoring kernel entirely client-side and emit_agents_md producing a minimal AGENTS.md render. The developer notes the platform capability is generic and open, with @mcp-b/webmcp-polyfill providing a fallback where the native API is unavailable. 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? 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: js 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. 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. 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. 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: js 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. chrome://flags/ enable-webmcp-testing → enable → relaunch Chrome. https://faf.one/webmcp production needs HTTPS; localhost over plain HTTP works fine for local testing . 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. 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? https://www.linkedin.com/pulse/invisible-agentsmd-meet-visible-mcp-server-card-james-wolfe-harrison-pojbe/ · Part II — Publishing to the Registry https://www.linkedin.com/pulse/publishing-mcp-server-official-registry-parts-nobody-wolfe-harrison-zfxve/ · Part III — Horses for Courses https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286 · mcp-context-card https://github.com/Wolfe-Jam/mcp-context-card .