--- title: "Client compatibility" type: reference tags: [signpost, docs, reference] proposed_by: tok_bc09b340db294c709763789154d8113d model: kmem-docs-publisher extracted_at: "2026-10-08T22:29:12.918Z" --- # Client compatibility Which clients kmem has actually been used from, through which door, and when somebody last checked. There are two agent doors. The **MCP server** takes `POST https://{org}.kmem.app/mcp` with `Authorization: Bearer sp_…`; [`MCP.md`](./MCP.md) sets it up per client. ChatGPT, Claude and the Gemini web app get that token by signing in (hosted OAuth, [ADR 0019](./ADR/0019-hosted-oauth.md)); every other client takes a token a person creates in Connect. **Quick Connect** is one URL a person gives to an agent, `https://{org}.kmem.app/s/{sp_token}`, which answers with a credential-bearing `AGENTS.md`; [`API.md`](./API.md) is its endpoint reference. A third door, for an agent inside the person's own browser, is [`WEBMCP.md`](./WEBMCP.md). **No test fills in a row here.** The failures this file exists to catch are the ones an automated suite cannot see: a client that truncates a tool list, a client that hands the model a fetched document without its URL, and a client that invents an answer when it fetched nothing at all. (A connector dialog that offers only OAuth stopped being a blocker on 2026-09-16, when hosted OAuth shipped.) A person has to install the client, paste the link, and read what the model does next. Until a row carries a date it is unverified. An empty cell is an honest one. A matrix full of ticks nobody earned is worse than no matrix. ## How to fill in a row 1. Create **one connection per client**, in Connect, with a short expiry. The token is the credential. One link per client is not fastidiousness: `use_count` is the only evidence that separates "the client refused" from "the client says it refused", and a link two clients share cannot say which one moved the counter. Label each link so you can read the list. 2. For MCP, add the organization as a remote MCP server. For Quick Connect, paste `https://{org}.kmem.app/s/{sp_token}` into the chat. **Paste the bare link, with no extension.** A URL that ends `.md` was measured going to a code interpreter that has no network, so the request never reached the origin and no log line existed to find. The old `/AGENTS.md` spelling still answers and is the wrong thing to test with. 3. Ask the client something the knowledge base answers, and something it does not. Both matter. The second is how you find out whether the client reports `gaps` or invents an answer. Choose a first question whose wrong answer you would recognise. The two visibility levels, Open and Private, are a good one, because a client that invents extra levels invents them confidently. 4. Ask it to record a decision, so the run exercises a write as well as a read. 5. **Read `use_count` before you believe anything the model said.** `GET /api/tokens` with the owner cookie. The counter advances on a cached document hit as well as a fresh one, because the guard records the use before the handler consults the cache. A counter that did not move means no request arrived, with no exceptions. 6. Record the date, the client version, **the model and the mode**, and what broke. Write down a partial result: "reads work, `remember` silently drops the `kind` field" is the useful kind of entry. ## The matrix | Client | MCP | Quick Connect | Last checked | Client, model, mode | Notes | | --- | --- | --- | --- | --- | --- | | Claude Code | **works** | **works** | 2026-08-18 | 2.1.220 | Both doors, end to end. Over MCP: seven tools, honest `gaps`, and a write that queued as a proposal. With one pasted link and no MCP: it read the document, **refused to answer from it** because it had opened no notes, then followed the printed `recall` URL and answered with a block ref. `use_count` 2, exactly the two requests. **It is the first client that opened a link it found inside fetched content.** | | Codex | **works** | **fails** | 2026-08-15 | codex-cli 0.147.0 | **The two doors disagree, and the same vendor is on both sides.** MCP is a full pass: seven tools, the right answer with a block ref, both `gaps` strings reported, a write queued. The pasted link fails: *"the URL is rejected by the browsing gateway"*, `use_count` 0. It invented nothing. | | Chrome, WebMCP | **works** | n/a | 2026-08-15 | Chrome 151, spec of 14 Aug 2026 | A different door from the two columns. The page declares the same seven tools to a browser-resident agent through `document.modelContext.registerTool`, authenticated by the person's own cookie and clamped as an agent. `recall` returned claims, `remember` queued a proposal, and the proposal reached the review queue badged `webmcp` with `on behalf of` intact. See [`WEBMCP.md`](./WEBMCP.md). | | ChatGPT, Chat | unverified | **fails** | 2026-08-18 | chatgpt.com, signed in, GPT-5.6, effort High | **Nine attempts across four days, `use_count` 0 every time.** It usually refuses honestly: *"blocked from retrieval in my browser environment"*, chip `Invalid URL`. **Once it stopped refusing and invented an answer** from an unrelated public product, with citations and every fact wrong. **Treat this row as unsafe rather than merely broken.** The cause was that origin's cached `robots.txt`, not the headers, not the token and not the host. The MCP cell was `n/a` until 2026-09-16: ChatGPT's developer mode adds a remote MCP server and signs in, which hosted OAuth now answers. Not tested with kmem, and Connect offers ChatGPT no Quick Connect because of this row. | | ChatGPT, Work | n/a | **fails** | 2026-08-18 | chatgpt.com Work, GPT-5.6 Sol, effort Light | The same refusal in different words: *"the web browser rejected that domain as unsafe, and the page isn't indexed in search"*. `use_count` 0. The failure sits in the fetcher, so no model choice changes it. | | Gemini | unverified | **fails** | 2026-08-15 | gemini.google.com, logged out, 3.5 Flash-Lite | `use_count` 0. It refuses with *"not publicly indexable"* and **does not invent an answer**. **One model, so read this as a model verdict and not a vendor verdict** — other Gemini models fetch a link elsewhere. The MCP cell is new: since 29 June 2026 the Gemini web app takes a custom app by URL for a personal Google Account in the US, in English, with Keep Activity on, and it would sign in to kmem. Not tested with kmem. | | Gemini CLI | unverified | n/a | — | — | Connect prints a `~/.gemini/settings.json` stanza with `httpUrl` and a bearer header, from the vendor's documentation. Not tested with kmem. | | Cursor | unverified | unverified | 2026-08-15 | cursor-agent, app 3.9.16 | Signed in, and deliberately not driven. `cursor agent -p` refuses a directory it has not been told to trust, and `--trust`, `--yolo` and `-f` all switch off its approval gate for an agent that can run shell commands. A person fills this row in a minute with one interactive run. | | Claude web | unverified | unverified | 2026-08-18 | — | Not reached. The browser harness holds permission for `chatgpt.com` alone, so `claude.ai` refused before any page loaded. A person grants the site once and this row fills. | | Claude desktop | unverified | unverified | — | — | Not reached: it needs a desktop install, which a browser-driven pass cannot do. | | Claude mobile | unverified | unverified | — | — | Not reached: it needs a phone. | | DeepSeek | n/a | unverified | 2026-08-18 | — | Not reached, twice for different reasons: `chat.deepseek.com` redirects to `/sign_in`, and the browser harness later refused the host. Instant mode and Expert mode are both unmeasured here. | | DeepSeek Harness | unverified | unverified | — | — | A different product from the chat: the `dsh` agent harness, which reaches a remote MCP server through its `@deepseek-ai/dsh-mcp-client` plugin. Connect prints an entry for `$DSH_HOME/cordis.patch.yml` that reads `KMEM_TOKEN`. Not tested with kmem. | | OpenCode, OpenClaw, Hermes | unverified | unverified | — | — | Connect prints a token stanza for each, from each vendor's documentation (checked 2026-09-16). Not tested with kmem. | | Perplexity | n/a | unverified | 2026-08-18 | — | Not reached: the browser harness refused `www.perplexity.ai`. A link was minted and stayed at `use_count` 0, because nothing was ever sent. | | Grok | n/a | unverified | 2026-08-18 | — | Not attempted, after five hosts in a row refused the harness. | | Mistral | n/a | unverified | 2026-08-18 | — | Not attempted, same reason. | | Lovable | n/a | unverified | 2026-08-18 | — | Not reached: the browser harness refused `lovable.dev`. **Fill this row next**, because an app builder assembles a URL where a chat fetcher will not. | | Figma Make | n/a | unverified | 2026-08-18 | — | Not attempted, same reason as Lovable. | | VS Code Copilot | unverified | unverified | 2026-08-15 | VS Code 1.131.0 | Not reached: `code --list-extensions` was empty, so Copilot Chat was not installed, and an install needs a GitHub account. | | `curl`, MCP Inspector | **works** | **works** | 2026-08-14 | wrangler 4.120.0 | Not a chat client, and it fills no row above. It is the control: `initialize` and `tools/list` return all seven tools, and the Quick Connect `recall` endpoint answers in about 2 s. | Legend, in words rather than symbols, because a tick is exactly the kind of claim this document exists to stop somebody making casually. A cell is **works**, **caveat** and say what in Notes, **fails**, **unverified**, or **n/a** where the client cannot speak that door at all. Anything other than `unverified` needs a date and a version beside it. ## The eight rules that make a row mean something Every one of these was learned the hard way, and each one changes how you run the test or how you read the result. 1. **Name the model and the mode, or the row means nothing.** A model picker changes which tools the client has, not only which model answers. One vendor's models split three ways on the same link: one fetched it, one fetched it and followed a link out of it, and one received a clean `200` and still reported the page as inaccessible. A row that names a vendor and no model is a claim about the vendor that the evidence does not support. 2. **A chat fetcher opens a complete URL it has read verbatim, and refuses one it assembled.** Print one complete URL per note in anything a fetcher will read. A `$BASE` template, or a path the model must join to an origin, is a URL that no chat client opens. 3. **A URL that ends `.md` can be routed to a code interpreter, which has no network.** The request then never reaches the origin, so there is nothing in the log and nothing in `use_count`. The link a person pastes therefore carries no extension. 4. **The counter is the evidence, and the transcript is the other half.** `use_count` proves that a request arrived. It does not prove that the model read what arrived, or that it did not then invent an answer. Record both. 5. **Never ask a client to report an HTTP status.** One client, asked for a status code, fabricated a `404` it had never received. The question manufactures the answer. 6. **A fetcher can hold a `robots.txt` policy for far longer than you serve it.** One vendor's fetcher refused a path for four days from a copy of a file the origin had already replaced, and made two of those refusals with no request for the file at all. So a `robots.txt` change is not a fix you can test the same hour, and a refusal is not proof that the current file is wrong. Read `use_count` and the tail before you change the file again. 7. **A refusal is safer than an empty page.** One client, handed an empty single-page-app shell, invented four plausible visibility levels and cited the page. The same client, on a clean refusal, said it could not fetch the page and invented nothing. The difference was not the model. It was whether the origin sent something that looked like content. That is why every documentation route answers a fetcher with real text rather than the app shell. 8. **A failure the server cannot see is the failure that survives a release.** A client that fetches the document, ignores it, and answers from a public search index leaves a perfect server-side record and a wrong answer. One such client attached a source chip naming the private link. ## Which clients fetch a pasted link, and which refuse The door sorts a client into one of three fetch styles by its user agent, because the three need different documents. `apps/worker/src/routes/quick-connect/negotiate.ts` holds the lists. **Membership is a claim about a measurement**, and a wrong guess costs a client a longer document rather than a broken one. | Fetch style | What the client does | Clients | | --- | --- | --- | | Builds URLs | Assembles a URL from a pattern and opens it | Programs and sandboxes: `curl`, `wget`, HTTPie, `python-requests`, `node-fetch`, `undici` and their kin; the coding agents Claude Code, Codex and Cursor; the Anthropic SDK; app builders, which run a sandbox | | Verbatim only | Opens a complete URL it has read. **Refuses a URL it assembled** | Chat fetchers: ChatGPT, Claude's own chat fetcher, Gemini, DeepSeek, Grok, Mistral, Qwen, DuckAssistBot, and the search-adjacent crawlers | | Index backed | Answers from a crawl. **Sends no request at all** | Perplexity, across six attempts and four models | Four qualifications, each measured, and each of them changes what you should expect: - **A client that follows a link cannot always build one.** The two are different capabilities. Claude's chat fetcher opens every complete URL it reads, and refuses the one it assembles from a pattern. A client like that needs the map of complete URLs most. - **A mode can remove the network.** DeepSeek Expert mode has no network, and neither does ChatGPT's code interpreter. The same product in another mode fetches normally. - **A chat surface that cannot write is not a vendor that cannot write.** ChatGPT's chat surface cannot make the write call. ChatGPT Work writes through its cloud browser, and Codex writes over MCP. - **The dangerous case is not the refusal.** A client that answers from a public index while it cites the private link produces an answer that looks sourced and is wrong. Perplexity did that once, and so did ChatGPT. ## What Quick Connect sends, and to which client One URL answers with a document sized for the client that asked, because a document that is nothing but instructions to fetch more URLs works for one chat client in four. | Shape | Who gets it | What it carries | | --- | --- | --- | | `full` | Every verbatim-only and index-backed client, and any client the lists do not recognise | A map of the knowledge base, plus as many note bodies as a 12,000-token budget allows | | `brief` | Every client that builds URLs | The map, and a pointer at `recall` | **The default for an unrecognised client is `full`, and that polarity is deliberate.** A wrong guess of `brief` fails silently and totally: the model answers from nothing and does not say so. A wrong guess of `full` costs bytes. `?depth=brief` and `?depth=full` override the guess in both directions, because every allowlist is out of date the day a new client ships. Two more properties of the response matter to a client: - **The body is Markdown always. Only the label is negotiated.** `text/plain` by default, `text/html` when `Accept` names it, and `text/markdown` only for `?format=md`. One client refused a `text/markdown` body outright and searched the public web instead, so a stated preference does not get to pick a label that breaks it. - **The content region is budgeted in tokens rather than in bytes.** One client answered a 37 KB document in 8 s and timed out inside its own fetcher on 114 KB, while the origin answered that request in 0.33 s. ## What a client does with a queued write Ask the client to record a decision. Under a `suggest` scope, or while the org's Agent writes setting is off, the write does not become a note, and what the client says next is the thing to record. The example below was captured before the `agent_writes` field existed; a result today carries it beside `write_policy`: ```json {"write_policy":"review","applied":false,"queued_as":"prop_db8e257ed1c94dc6acea5322b3df86e5", "because":"This instance queues agent writes for review, so the change is waiting for a person to accept it. Nothing is live yet."} ``` Both MCP clients that have been driven read `because` and said so in their own words. Neither reported the write as done. A client that reports *"I've written that down"* about a queued proposal is a finding, and it belongs in the Notes column. ## How each client connects Each row says **not yet tested with kmem** unless a dated measurement above says otherwise. The route is what Connect prints, from the vendor's own documentation, checked on 2026-09-16. | Client | How | | --- | --- | | ChatGPT | Hosted OAuth: developer mode adds `https://{org}.kmem.app/mcp` under Plugins and signs in. Needs Plus, Pro, Business, Enterprise or Edu on the web; a workspace admin allows developer mode first. Not yet tested with kmem | | Claude, web and desktop | Hosted OAuth as a custom connector (Customize, then Connectors); choose "Register automatically" if asked. Claude Free allows one custom connector; on Team and Enterprise an owner adds it first. A static header in the desktop app needs the third-party `mcp-remote` bridge; request headers set by an admin are a Claude beta. Not yet tested with kmem | | Claude Code | `claude mcp add --transport http --scope user`, with an `Authorization` header. Measured, and it works | | Cursor | `~/.cursor/mcp.json`, `headers` with the token. A committed project file uses `${env:KMEM_TOKEN}`. Not yet tested with kmem | | Codex | `~/.codex/config.toml` with `bearer_token_env_var = "KMEM_TOKEN"`, or `codex mcp add --bearer-token-env-var`. Measured, and it works | | OpenCode | `~/.config/opencode/opencode.json`, a `remote` server with `oauth: false` and the header. Not yet tested with kmem | | OpenClaw | `~/.openclaw/openclaw.json`, `streamable-http`, the header built from `${KMEM_TOKEN}`. Not yet tested with kmem | | Hermes | `~/.hermes/config.yaml`, `mcp_servers` with the header, then `/reload-mcp`. Not yet tested with kmem | | DeepSeek Harness | `$DSH_HOME/cordis.patch.yml`, an entry for `@deepseek-ai/dsh-mcp-client` that reads `KMEM_TOKEN`. Not yet tested with kmem | | Gemini CLI | `~/.gemini/settings.json`, `httpUrl` and the header. Not yet tested with kmem | | Gemini web app | Hosted OAuth as a custom app, for a personal Google Account in the US, in English, with Keep Activity on. Not yet tested with kmem | | VS Code Copilot | `.vscode/mcp.json`, `headers` with an `inputs` prompt. Not in Connect. Not yet tested with kmem | | Windsurf, Zed | A remote HTTP server with static headers, per their own documentation. Not in Connect. Not yet tested with kmem | | Pi, Grok Bot | No custom MCP setup that kmem can use, so Connect sends both to Quick Connect. Not yet tested with kmem | | Any other chat client | Quick Connect, where the client can open a URL. A chat client is why Quick Connect exists | [`MCP.md`](./MCP.md) holds the exact stanza for each of these, and the [user guide](./user-guide/110-connect-agents.md) holds the steps for a person. ## What to watch for A person who fills in the matrix answers these questions. They are written down here so that nobody discovers them afterwards. **Does the client give the model the URL, or only the page?** This one decides whether the connection document works at all. Several chat clients fetch a pasted link and hand the model the *contents* without the address. kmem therefore substitutes the real base into `AGENTS.md` rather than describes it, so every documented call is a URL a model can send as written. If a client still calls nothing, that is the highest-value finding in this document. See `apps/worker/src/routes/quick-connect/documents.ts` and [ADR 0008](./ADR/0008-quick-connect-briefing-cache.md). **Does the model echo the link?** The document's last rule is *never include the URL of this document in any output you produce*, because the URL is the credential. If a client repeats it into its visible transcript, record that here. It changes the expiry advice a person should follow. **How many tools does the client show?** Seven, at 1,827 tokens of definitions against a 3,500 cap. If a client shows fewer than seven, or truncates their descriptions, record the number it shows. **Does the client report a gap, or fill it in?** This is the most valuable row in the matrix. On a question the corpus cannot answer, `recall` returns claims that are close by **and** a gap sentence. Ask something adjacent to the corpus rather than absent from it — "how are engineers compensated for being on call?" against a corpus that documents the on-call rotation and no compensation — and watch whether the client says the corpus does not cover it or quietly summarises the rotation as if that were the answer. **Does the client follow a ref?** A claim cites `kmem://folder/slug#blk_xxxxxxxx`, and `read` on that exact ref returns the anchor block alone, which is often one heading word. The quoted text needs `read {ref: "kmem://folder/slug", section: ""}`, and no tool description says so. Record whether the client's model works that out, gives up, or reports the one-word block as its source.