` for agents without the tools. It sits between `BEGIN:onsystem-agent-rules` and `END:onsystem-agent-rules` comment markers, which a later run updates in place.
+
+## How the server finds your project
+
+The server finds the project from `--root`, a config file in the working directory, or the working directory itself when it holds a project (a `package.json` or `components.json`). Started at a [monorepo root](configuration.md#monorepo-roots), it serves every app and design-system package in it: the tools take the `path` of the file the agent is editing and answer for that file's project.
+
+MCP roots, which the 2026-07-28 protocol revision deprecates, are the fallback: when the working directory holds no project (Claude Desktop starts servers in `/`), a 2025-era client that reports roots has its first `file://` root used. When none of these gives a project (a client that starts servers in your home or the plugin's folder and reports no roots), each tool call's project is found from the absolute path it names, `path` (or `filename`, for `check_ui` with `code`): the nearest folder at or above it with a `package.json`, `components.json` or config, or the monorepo root above that, but never your home folder or the filesystem root. A project found this way loads a JSON config (`onsystem.config.json`) only: the server runs a JavaScript or TypeScript config only in a project it was started in. Until a call names one, the tools return an error that asks for it; after that, a call without one answers from the project found, as long as only one has been.
+
+The server speaks MCP over stdio, in both protocol eras: the 2026-07-28 revision (stateless, with `server/discover`) and the 2025 one (the `initialize` handshake), the era chosen by the client's first message.
diff --git a/site/test/clients-page.test.ts b/site/test/clients-page.test.ts
new file mode 100644
index 00000000..81b39d44
--- /dev/null
+++ b/site/test/clients-page.test.ts
@@ -0,0 +1,56 @@
+import { createElement } from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { describe, expect, it, vi } from 'vitest';
+
+import ClientsPage from '@/app/docs/clients/page';
+
+vi.mock('next/navigation', () => ({ usePathname: () => '/docs/clients' }));
+
+// /docs/clients is the setup page for every agent but Claude Code with the plugin: after the
+// install buttons comes the setup, and how the server finds the project waits for the end.
+
+const html = renderToStaticMarkup(createElement(ClientsPage));
+const text = (fragment: string) =>
+ fragment
+ .replace(/<[^>]+>/g, ' ')
+ .replace(/'/g, "'")
+ .replace(/"/g, '"')
+ .replace(/&/g, '&')
+ .replace(/\s+/g, ' ')
+ .trim();
+const at = (id: string) => {
+ const index = html.indexOf(`id="${id}"`);
+ if (index === -1) throw new Error(`no element with id ${id}`);
+ return index;
+};
+
+describe('/docs/clients', () => {
+ it('opens with the setup, right after the install buttons', () => {
+ // From the install buttons, the page's first links, to its first section.
+ const body = html.slice(html.indexOf('Install in VS Code'), at('plugins-for-other-agents'));
+ const lead = text(/([\s\S]*?)<\/p>/.exec(body)?.[1] ?? '');
+ expect(lead).toContain('Run it from the project, or pass --root');
+ expect(lead.split(' ').length).toBeLessThanOrEqual(50);
+ expect(text(body)).not.toContain('2026-07-28');
+ // The one .mcp.json, as a box above the first section.
+ expect(body).toMatch(/role="note"[\s\S]*\.mcp\.json/);
+ });
+
+ it('explains how the server finds the project after every client, before the docs search', () => {
+ const ids = [...html.matchAll(/
m[1]);
+ expect(ids.slice(-2)).toEqual([
+ 'how-the-server-finds-your-project',
+ 'search-these-docs-from-your-agent',
+ ]);
+ const protocol = html.indexOf('2026-07-28');
+ expect(protocol).toBeGreaterThan(at('how-the-server-finds-your-project'));
+ expect(at('other-clients')).toBeLessThan(at('how-the-server-finds-your-project'));
+ });
+
+ it('has the Antigravity and Gemini CLI setups under other clients', () => {
+ expect(at('antigravity-cli-and-ide')).toBeGreaterThan(at('other-clients'));
+ const gemini = text(html.slice(at('gemini-cli'), at('antigravity-cli-and-ide')));
+ expect(gemini).toContain('gemini mcp add -s user onsystem npx -y onsystem');
+ expect(gemini).toContain('only in trusted folders');
+ });
+});
diff --git a/test/clients-docs.test.ts b/test/clients-docs.test.ts
new file mode 100644
index 00000000..88280897
--- /dev/null
+++ b/test/clients-docs.test.ts
@@ -0,0 +1,102 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+import { describe, expect, it } from 'vitest';
+
+import { blocks, section } from './markdown.js';
+
+// docs/clients.md (/docs/clients on the website) is the setup page for every agent but Claude
+// Code with the plugin. It opens with the setup, offers one committed file that several clients
+// read, and keeps each client's commands to what that client runs today.
+
+const root = path.resolve(import.meta.dirname, '..');
+const read = (file: string) =>
+ fs.readFileSync(path.join(root, file), 'utf8').replace(/\r\n/g, '\n');
+const clients = read('docs/clients.md');
+const readme = read('README.md');
+
+/** What the page says before its first `##` heading. */
+const intro = clients.slice(0, clients.indexOf('\n## '));
+
+/** The body of the `` block whose summary is `title`. */
+function details(title: string): string {
+ const start = clients.indexOf(`${title}
`);
+ if (start === -1) throw new Error(`no block for ${title}`);
+ return clients.slice(start, clients.indexOf(' ', start));
+}
+
+/** The first JSON code block in `markdown`, parsed. */
+function json(markdown: string): unknown {
+ const [block] = blocks(markdown, 'json');
+ if (block === undefined) throw new Error('no JSON code block');
+ return JSON.parse(block);
+}
+
+describe('docs/clients.md', () => {
+ it('opens with the setup, and leaves how the server finds the project for the end', () => {
+ const lead = intro.split('\n\n')[1] ?? '';
+ expect(lead.split(/\s+/).length).toBeLessThanOrEqual(50);
+ expect(lead).toContain('Run it from the project, or pass `--root`');
+ expect([...clients.matchAll(/^## (.+)$/gm)].map((m) => m[1]).at(-1)).toBe(
+ 'How the server finds your project',
+ );
+ // The protocol eras and the roots fallback come after every client's setup.
+ const finding = section(clients, 'How the server finds your project');
+ for (const detail of ['2026-07-28', '`initialize`', '`file://`', 'MCP roots']) {
+ expect(intro, detail).not.toContain(detail);
+ expect(finding, detail).toContain(detail);
+ }
+ });
+
+ it('offers one .mcp.json to commit, at the top, naming the clients that read it', () => {
+ const box = /^> \[!TIP\]\n((?:>.*\n)+)/m.exec(intro)?.[1]?.replace(/^> ?/gm, '') ?? '';
+ expect(box).toContain('`.mcp.json`');
+ for (const client of ['Claude Code', 'VS Code', 'GitHub Copilot CLI', 'Grok Build']) {
+ expect(box, client).toContain(client);
+ }
+ expect(json(box)).toEqual({
+ mcpServers: { onsystem: { command: 'npx', args: ['-y', 'onsystem'] } },
+ });
+ // The Claude Code plugin registers the same tools: one or the other, not both.
+ expect(box).toMatch(/instead of the plugin/);
+ });
+
+ it('installs the Copilot CLI plugin from the marketplace, not by the deprecated direct install', () => {
+ const plugins = section(clients, 'Plugins for other agents');
+ expect(plugins).toContain(
+ '```sh\n copilot plugin marketplace add dgesteves/onsystem\n copilot plugin install onsystem@dgesteves\n ```',
+ );
+ expect(clients).not.toMatch(/copilot plugin install dgesteves\//);
+ });
+
+ it('says the hook runs in Codex too, on the files apply_patch changes', () => {
+ expect(clients).not.toMatch(/only (?:the )?Claude Code(?: plugin)? has the hook/i);
+ expect(intro).toContain('Claude Code and Codex CLI');
+ expect(section(clients, 'Plugins for other agents')).toContain('`apply_patch`');
+ });
+
+ it('adds Gemini CLI from the terminal, and says it starts servers only in trusted folders', () => {
+ const gemini = details('Gemini CLI');
+ expect(gemini).toContain(
+ 'gemini mcp add -s user onsystem npx -y onsystem --root /absolute/path/to/app',
+ );
+ expect(gemini).toContain('only in trusted folders');
+ expect(gemini).toContain('`/permissions`');
+ expect(gemini).toContain('`--skip-trust`');
+ });
+
+ it('has a block for Antigravity, which reads its own file, with the same server entry', () => {
+ const antigravity = details('Antigravity (CLI and IDE)');
+ expect(antigravity).toContain('`~/.gemini/config/mcp_config.json`');
+ expect(antigravity).toContain('`.agents/mcp_config.json`');
+ expect(json(antigravity)).toEqual(json(details('Gemini CLI')));
+ });
+});
+
+describe('the README', () => {
+ it('lists Antigravity with the other clients', () => {
+ const top = readme.slice(0, readme.indexOf(''));
+ expect(top).toContain('](https://onsystem.vercel.app/docs/clients#antigravity-cli-and-ide)');
+ expect(readme).toMatch(/\[configs for [^\]]*Antigravity[^\]]*\]/);
+ });
+});