{"slug": "context-over-mcp-part-iv-no-working-directory-at-all", "title": "Context Over MCP, Part IV: No Working Directory At All", "summary": "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.", "body_md": "Part I: an MCP client has no working directory, so it never sees your `AGENTS.md`. \n\nPart II: publish the server so a client can find it.\n\nPart III: keep facts as data and instructions as prose, so what it finds is worth seeing.\n\nNow take the working directory away completely. WebMCP's client is a browser\n\ntab, and a tab doesn't fail to find a working directory. It doesn't have the\n\nconcept.\n\nSo what does \"expose your context\" mean when there's no repo, no process, and\n\nno server — just a page?\n\n`document.modelContext.registerTool()` is Chrome's own experimental API\n\n(`chrome://flags/#enable-webmcp-testing`). A page registers a tool, an agent\n\ninspecting that page calls it, done — no separate MCP process, no transport,\n\nno working directory to have or lack. Where the native API isn't available,\n\n`@mcp-b/webmcp-polyfill` — a real, independent package, not FAF's — provides\n\nthe same `registerTool` surface as a fallback:\n\n``` js\nconst native = getDocumentContext();\nif (native && typeof native.registerTool === 'function') {\n  return { context: native, source: 'native' };\n}\ntry {\n  const { initializeWebMCPPolyfill } = await import('@mcp-b/webmcp-polyfill');\n  initializeWebMCPPolyfill();\n} catch {\n  return { context: null, source: 'none' };\n}\nconst polyfilled = getDocumentContext();\nif (polyfilled && typeof polyfilled.registerTool === 'function') {\n  return { context: polyfilled, source: 'polyfill' };\n}\nreturn { context: null, source: 'none' };\n```\n\nWebMCP doesn't care what you register. This is the part that matters more\n\nthan any specific tool: the platform capability is generic, open, and not\n\nowned by anyone's product. What follows is one demonstration of it, not the\n\nonly shape it can take.\n\n`faf.one/webmcp` registers three:\n\n| Tool | Returns | \n|---|---|\n| `score_faf` | `{ score, tier, populated, active, gaps }` — 0–100, in-browser | \n| `fill_6ws` | a `human_context:` YAML fragment from a form | \n| `emit_agents_md` | `{ markdown }` — a minimal AGENTS.md render | \n\nThose are Part III's three moves, in a tab: fill in the six W's, score what's\n\nstill blank, and write the `AGENTS.md` from the facts.\n\n`score_faf` and `emit_agents_md` are registered in script with\n\n`registerTool()`, and both carry `annotations: { readOnlyHint: true }`. That's\n\nnot a comment. It's part of the registration, and an agent can read it before\n\ncalling anything: the same trust signal MCP tool annotations give a client, now\n\non a web page instead of a server.\n\n`fill_6ws` is a plain HTML form with `toolname`, `tooldescription` and\n\n`toolautosubmit` attributes: the browser turns the form into the tool. If the\n\nform isn't picked up, the page registers `fill_6ws` in script instead.\n\n`score_faf` runs entirely client-side. The scoring kernel is WASM\n\n(`faf_wasm_sdk`), compiled once from the same Rust source the CLI and the\n\nedge workers use — no server round-trip, no MCP process to spin up:\n\n```\nexport function scoreYaml(yaml: string): string {\n  if (!ready) throw new ToolError('kernel_not_ready', 'WASM scoring kernel is not initialized');\n  return score_faf(yaml);\n}\n```\n\nAnd the result mapper refuses to pad the picture. Ignored slots — fields that\n\ndon't apply to this project — aren't counted as missing:\n\n```\n/** Score is populated/active. Ignored slots are not missing — 13/13, not 13/21. */\n```\n\n`gapsFromSlots` only reports what's genuinely empty. A tool that's willing to\n\ninflate its own denominator to look better is exactly the kind of thing a\n\n\"structured facts\" argument falls apart without.\n\n`emit_agents_md` renders a real AGENTS.md — Setup & build, Run the tests,\n\nStack, 6Ws — from the same `.faf` fields the CLI reads. But the file's own\n\ncomment says what it isn't:\n\n```\n// Minimal renderer — not the full faf-cli AGENTS.md compiler.\n```\n\nIt doesn't do Guardrails tiers, branch-aware Commit & PR, or any of the\n\nricher sections the real compiler produces — because a browser tab is a\n\ndifferent environment with a different budget than a CLI process, and a\n\nrenderer that pretended otherwise would be lying about what ran. The output\n\nsays so too — a plain, one-line stamp at the foot: `compiled from`. Not \"compiled with faf-cli.\" A different tool\n\napplication/vnd.faf+yaml\n\nmade this.\n\n`score_faf` and `emit_agents_md` can take a URL instead of pasted YAML. That\n\nmeans the page fetches something the agent asked for — and a page that\n\nfetches whatever a caller names is a real risk, not a hypothetical one. The\n\ncheck is explicit, and it isn't just a pre-check — it runs again after\n\nredirects, on whatever URL the request actually landed on:\n\n``` js\nexport function assertAllowedUrl(urlString: string): URL {\n  const url = new URL(urlString);\n  if (url.protocol !== 'https:') {\n    throw new ToolError('invalid_url', 'url must use https');\n  }\n  if (!ALLOWED_HOSTS.has(url.hostname)) {\n    throw new ToolError('invalid_url', `host not allowlisted: ${url.hostname}`);\n  }\n  if (url.pathname.includes('/mcp')) {\n    throw new ToolError('invalid_url', 'mcp endpoints are not allowed');\n  }\n  return url;\n}\n```\n\nTwo hosts only — `faf.one`, `raw.githubusercontent.com` — exact match, not a\n\nsuffix (their own comment: *\"`ide.faf.one` is not `faf.one`\"*). HTTPS only,\n\n256KB cap. And a URL containing `/mcp` in its path is rejected outright, by\n\nname — the tool doesn't just happen to avoid talking to an MCP server, it\n\nrefuses to. A `github.com/owner/repo` link gets rewritten to the raw file\n\nhost before any of this runs, so a redirect can't land the fetch somewhere\n\nthis check never saw: `fetchAllowedYaml` re-validates the *final* URL after\n\nfollowing redirects, not just the one it started with.\n\n`chrome://flags/#enable-webmcp-testing` → enable → relaunch Chrome.`https://faf.one/webmcp` (production needs HTTPS; `localhost` over\nplain HTTP works fine for local testing).\nFive things should be true:\n\n```\n[ ] Inspector lists score_faf, fill_6ws, emit_agents_md\n[ ] score_faf on the loaded fixture returns a numeric score\n[ ] Bad YAML returns { error, message } — not a thrown exception\n[ ] The 6Ws form returns YAML and does not navigate the page\n[ ] Network tab shows no call to any MCP server or /mcp URL —\n    the only network activity is the one allowlisted YAML fetch\n```\n\nThat last one is the actual claim of this piece, made checkable: nothing here\n\ntalks to a server. The tab did it.\n\nPart I's problem was a client that can't see your `AGENTS.md` because it has\n\nno way to walk to it. WebMCP's problem is stricter: there's no walking to\n\nanything, ever — so the only way to expose context is to hand it over\n\ndirectly, as tool calls, from wherever the process already lives. `score_faf`,\n\n`fill_6ws`, `emit_agents_md` are one answer to that, running in the browser\n\nbecause the browser is where this particular client already is.\n\nThe API that makes it possible isn't FAF's, and doesn't have to be. Register\n\nwhatever a page's own good judgment says an agent visiting it should be able\n\nto call.\n\n*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).*", "url": "https://wpnews.pro/news/context-over-mcp-part-iv-no-working-directory-at-all", "canonical_source": "https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f", "published_at": "2026-09-14 16:26:34+00:00", "updated_at": "2026-09-14 16:55:34.978690+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["WebMCP", "Chrome", "faf.one", "document.modelContext.registerTool", "@mcp-b/webmcp-polyfill", "faf_wasm_sdk", "MCP"], "alternates": {"html": "https://wpnews.pro/news/context-over-mcp-part-iv-no-working-directory-at-all", "markdown": "https://wpnews.pro/news/context-over-mcp-part-iv-no-working-directory-at-all.md", "text": "https://wpnews.pro/news/context-over-mcp-part-iv-no-working-directory-at-all.txt", "jsonld": "https://wpnews.pro/news/context-over-mcp-part-iv-no-working-directory-at-all.jsonld"}}