--- title: "The MCP server" type: reference tags: [signpost, docs, reference] proposed_by: tok_bc09b340db294c709763789154d8113d model: kmem-docs-publisher extracted_at: "2026-10-08T22:27:44.445Z" --- # The MCP server kmem speaks the Model Context Protocol at `/mcp`. This document is the tool reference, the measured tool-definition footprint, and the setup for each client. The credential is a **connection token** — `Authorization: Bearer sp_…`. A client gets one in one of two ways: - **ChatGPT, Claude and the Gemini web app sign in.** They add kmem as a remote MCP server, find the org's authorization server from the `401`, and a person approves the connection on a kmem page. kmem mints the token at the code exchange. This is hosted OAuth, [ADR 0019](./ADR/0019-hosted-oauth.md), which supersedes [ADR 0002](./ADR/0002-bearer-not-oauth.md) for these clients. - **Every other client takes a token that a person creates in Connect** and pastes into the client's configuration. ADR 0002's argument still holds for them. [SECURITY.md](./SECURITY.md) says what a connection can do and how to revoke one. The second agent door is Quick Connect, for a client that cannot speak MCP: [API.md](./API.md) is its endpoint reference. The [user guide](./user-guide/110-connect-agents.md) is the same material for a person who connects an agent. [`COMPATIBILITY.md`](./COMPATIBILITY.md) holds the client matrix. Read it before you promise that a client works. --- ## Connect ``` POST https://{org}.kmem.app/mcp Authorization: Bearer sp_... ``` Streamable HTTP. One endpoint, one URL, and no session to resume. Every request carries the header. No handshake comes before it. Create a connection in the web app under **Connect**, on the **MCP** tab, or with `POST /api/tokens`. That request takes `label`, `scope` and `expires_in`. The scope is the connection's mode, and it decides what the agent may do: | Mode in the web app | `scope` | What the agent may do | | --- | --- | --- | | Read only | `read` | Answer questions. It writes nothing. | | Read and propose | `suggest` | Also write, and every write, a new doc included, waits in Review for a person. | | Read, propose and write | `write` | Also write directly, while the org's **Agent writes** setting is on. | **Agent writes is an org setting, `org_routing.agent_writes`, and it starts `off`** ([ADR 0022](./ADR/0022-agent-writes.md)). While it is off, every agent in the org is capped at `suggest`: a `write` connection proposes, and `POST /api/tokens` refuses a new `write` connection with `403 agent-writes-off`. The switch is read on every request, so turning it off takes effect on the next call. A self-hosted project with no org reads as `on`, and its `SIGNPOST_WRITE_POLICY` decides. On the shipped policy, `conflicts`, a `write` connection with the switch on applies a new doc at a free address and an edit to the current version. It queues a deletion, a stale or blind edit, a create at a taken address and a doc that replaces a live one. A connection that is not in write mode queues everything. Every write response says which happened, in `outcome` and in `because`, and carries `write_policy` and `agent_writes` beside them. See [ADR 0017](./ADR/0017-conflict-only-review.md) and ADR 0022. A connection reads what the person who made it can read: every Open doc, and the Private docs that are shared with that person ([ADR 0026](./ADR/0026-two-visibility-levels.md)). There is no choice of docs when you create one. `POST /api/tokens` still accepts a `reach` field from an older client, and ignores it. A connection follows its person on every request. It reads nothing that person cannot open, and it stops on the next request after that person leaves the org or is removed ([ADR 0020](./ADR/0020-restricted-spaces.md)). The expiry is 7, 30 or 90 days, or never. The web app selects seven days. A connection with no expiry ends when somebody revokes it or when its person leaves the org. The token is the credential, so prefer a short expiry. ### Signing in (hosted OAuth) A client that signs in needs only the endpoint. On an org's own host: 1. The client calls `/mcp` with no credential and gets `401`. The `WWW-Authenticate` challenge names `resource_metadata` and `scope="read suggest write"`. 2. The client reads `/.well-known/oauth-protected-resource/mcp` (the `resource_metadata` URL) and `/.well-known/oauth-authorization-server`, registers itself at `/oauth/register` (dynamic client registration; every client is public and uses PKCE with `S256`), and sends the person to `/oauth/authorize`. 3. The person signs in to kmem and sees the approval page. It shows the host the browser returns to, and marks any host other than `claude.ai` and `chatgpt.com` as unknown. The person names the connection, chooses a mode (the page preselects at most "Read and propose", and disables write while Agent writes is off) and an expiry (30 days preselected). 4. The client exchanges the code at `/oauth/token`. kmem mints the connection token then, so a code nobody exchanged leaves no live connection. kmem issues **no refresh token**. When the connection expires, the person connects again from the client. The connection appears in Connect as "Via {client} (sign-in)", and a person revokes it there like any other. `/oauth/*` and `/.well-known/oauth-*` answer `404` on the apex and on a self-hosted instance: hosted OAuth exists only on an org's own host. --- ## The seven tools Seven, and no more. Every tool definition a server publishes is spent from the client's context window before the conversation starts, so kmem caps the total and measures it. See [the footprint](#the-footprint) below. | Tool | Arguments | Returns | Scope | | --- | --- | --- | --- | | `recall` | `query`, `budget_tokens=4000`, `types?`, `since?`, `include_superseded=false` | A ContextPack: ranked claims, conflicts, gaps | `read` | | `read` | `ref`, `section?` | One note, one section, or one block | `read` | | `browse` | `path?`, `depth=1`, `changed_since?`, `queued=false` | Folders and note titles. Never note text | `read` | | `remember` | `content`, `kind`, `title?`, `folder?`, `permalink?`, `links?`, `supersedes?` | The note it created, or the proposal it queued | `suggest` | | `edit` | `ref`, `op`, `content?`, `key?`, `value?`, `reason?`, `expected_sha?` | The new content SHA | `suggest` | | `verify` | `ref` or `refs`, `state`, `note?` | How many claims it marked | `suggest` | | `link` | `from`, `rel_type`, `to` | The edge, and whether it resolved | `suggest` | The scope column names the **minimum** capability a tool needs. The door admits the call, and the clamp then decides to apply it, to queue it, or to refuse it: [ADR 0016](./ADR/0016-direct-write-by-default.md). Every result carries both `content`, which a model reads, and `structuredContent`, which a program reads. Both hold the same object. A refusal is a result with `isError: true` and a body of `{ error: { code, message, fix } }`. A refusal is never a protocol error, because the client's transport handles a protocol error and often tells the model only that something failed. ### Annotations | Tool | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` | | --- | --- | --- | --- | --- | | `recall` | true | false | true | false | | `read` | true | false | true | false | | `browse` | true | false | true | false | | `remember` | false | false | **false** | false | | `edit` | false | **true** | false | false | | `verify` | false | false | true | false | | `link` | false | false | true | false | `remember` is not idempotent, because two calls write two notes. `edit` is destructive, because `replace_block` with empty content removes the block and `delete_note` tombstones the whole note. `edit` is not idempotent, because two `append` calls append twice. `link` is idempotent, because the same edge twice is one edge: the second call returns `outcome: "unchanged"` and writes nothing. --- ### `recall` Call this before you answer anything about the project. ```json { "query": "why did we choose session cookies?", "budget_tokens": 3000 } ``` It returns a **ContextPack**: ```json { "query": "why did we choose session cookies?", "budget_tokens": 3000, "used_tokens": 812, "claims": [ { "ref": "kmem://backend/auth-decision#blk_8bcbdac3", "note": "backend/auth-decision", "kind": "decision", "heading": "Session handling", "text": "We chose session cookies over JWT, because revocation is simpler.", "version": "a3f9c1d2e4b5", "status": "current", "updated": "2026-08-13", "provenance": { "author_kind": "human", "author": "ankur", "created_at": "…", "state": "verified" }, "freshness": 0.82, "score": 0.91, "related": ["kmem://backend/session-store"] } ], "conflicts": [], "gaps": [], "index_health": { "embedding_coverage": 1, "lag_seconds": 2 }, "budget": { "requested": 3000, "used": 812, "report": 0, "dropped": 0 }, "partial": false } ``` `version` is the first twelve hex characters of the note's `content_sha`. It tells you that a cited note moved. It is **not** a concurrency token. The concurrency token is `expected_sha`, which `read` returns at all sixty-four characters. `budget.report` is what `conflicts` and `gaps` cost. `used_tokens` deliberately does not charge for them, so the response is larger than `used_tokens` by that much. `partial` is `true` when the pack holds less than everything that matched. **Never ignore `gaps`.** When it is not empty, say what is missing. Do not fill the gap in. An agent that gets nothing invents an answer, and a knowledge base that permits that becomes a hallucination amplifier. `types` restricts the answer to `note`, `decision`, `convention`, `state` or `reference`. `since` is an ISO date. `include_superseded` admits claims that a later note replaced. Use it only when you ask what the answer used to be and why it changed. **Recall can answer from any doc text, whoever wrote it.** An agent's unverified text and an accepted text come back like any other text that matches. With the org's **Verification** setting on, a claim's state changes its rank: verified text ranks above accepted text, which ranks above unverified text. With it off, every claim ranks the same ([ADR 0027](./ADR/0027-verification-is-tracking.md), [ADR 0018](./ADR/0018-verification-is-an-org-choice.md)). A change that waits in Review is never in an answer, from `recall` or from any other tool. kmem ignores `include_unverified` and keeps it for older clients. Its description in the tool list says so. Do not send it. ### `read` ```json { "ref": "kmem://backend/auth-decision" } { "ref": "backend/auth-decision", "section": "Rules" } { "ref": "kmem://backend/auth-decision#blk_8bcbdac3" } ``` The tool accepts a bare permalink as well as a full `kmem://` ref, and a legacy `sp://` ref names the same note. Every ref in a result uses `kmem://`. A note returns its canonical Markdown once, plus a block list. The block list carries addresses, kinds and verification states, and **not** the text again. The words are in `markdown`, and a second copy would double the cost of every read. `section` returns one heading and everything below it, the heading line included. The match ignores case and whitespace. A block ref returns that block's text, its author, and its state. ### `browse` ```json { "path": "backend", "depth": 2 } { "changed_since": "2026-09-01" } { "queued": true } ``` Structure only, never content. Folders come back flat. The path already carries the nesting, and a flat list is both cheaper and directly reusable as the next `browse(path)`. `changed_since` lists only the notes written since an ISO date, newest first. `queued: true` lists what this session queued for a person, instead of the tree. ### `remember` ```json { "content": "We chose D1 over Postgres because it is on the platform.", "kind": "decision", "title": "Storage choice", "folder": "backend/decisions", "links": ["backend/session-store"], "supersedes": "backend/old-storage-decision" } ``` Capture decisions, conventions, constraints and current state. These are the things that otherwise need an explanation again next session. Do not capture transient debugging. **`remember` always creates.** It takes no `ref` and has no append mode. That is deliberate. To add to a note that already exists, call [`edit`](#edit) with `op: "append"`. One capability behind two tools would cost every client tokens for a choice it should not have to make. When a note this link can read already holds the address that the title derives to, the result says so and names the call that would have appended instead: ```json { "outcome": "created", "permalink": "deploy-freeze-2", "address_in_use": { "ref": "kmem://deploy-freeze", "title": "Deploy freeze", "suggestion": "A note already exists at kmem://deploy-freeze, so this one is addressed deploy-freeze-2 instead. To add to the existing note rather than keep two, call edit with ref kmem://deploy-freeze and op append." } } ``` The field is absent when the address was free. It is also absent when this link cannot read the note that holds the address. A report there would disclose, through a suggestion, the existence that the read scope exists to withhold. The field costs nothing from the footprint below, because it is a field on a result rather than a word in a schema. - `kind` is one of `note`, `decision`, `convention`, `state`, `reference`. It decides the note's freshness half-life, so a `state` note decays faster than a `convention`. - `title` defaults to the first line of `content`, cut on a word boundary. kmem derives the address from the title. A title already in use gets `-2`, and kmem overwrites nothing. - `permalink` sets the address exactly and overrides the derived one. kmem refuses a permalink that another note holds. - `links` become real relations. kmem keeps a link to a note that does not exist yet, and attaches it when somebody writes that note. - `supersedes` writes the edge **and** marks every claim in the superseded note `superseded`, so retrieval no longer ranks it as current. ### `edit` Eight operations on one signature. | `op` | Needs | Does | | --- | --- | --- | | `append` | `content` | Adds Markdown at the end of the note | | `prepend` | `content` | Adds Markdown at the top of the body, below any frontmatter | | `replace_block` | a `#blk_` ref and `content` | Replaces one block's source range. Empty content removes it | | `replace_note` | `content` and `expected_sha` | Replaces the whole note, pinned to the version the caller read. The direct-write verb of a connection in write mode ([ADR 0022](./ADR/0022-agent-writes.md)) | | `set_property` | `key`, and `value` to set it | Sets or removes one frontmatter key | | `delete_note` | `reason` | Tombstones the whole note. kmem keeps the reason forever | | `withdraw` | a `prop_` id as `ref` | Takes back a write this session queued. `reason` is optional | | `retract` | `reason` | Removes a live note this session wrote that carries no trust decision | ```json { "ref": "kmem://backend/auth-decision", "op": "append", "content": "- [note] Keys rotate monthly #ops" } { "ref": "kmem://backend/auth-decision#blk_8bcbdac3", "op": "replace_block", "content": "…" } { "ref": "kmem://backend/auth-decision", "op": "set_property", "key": "owner", "value": "ankur" } ``` Pass `expected_sha` — the `content_sha` you read — and kmem never overwrites a version you did not see. On this door a stale `expected_sha` is not refused: the change goes to the clamp with your sha, and the clamp queues it in Review with a sentence that says the doc moved on. An update with no `expected_sha` is a blind overwrite, and it queues too. `replace_block` needs no sha, because its block id comes from the content. kmem pins a whole-note operation to the version it read even when you pass nothing, because that operation computed its new source from that version. kmem refuses a field that the named op does not read, rather than drops it. The refusal names the ops that do read that field. `replace_block` keeps every other block's id. That is what keeps their provenance and their inbound refs on the same claims. `edit` changes a note that exists and never creates one. When nothing is at the address, the refusal names `remember` as the repair rather than sends the caller back to `browse`: ```json { "error": { "code": "note-not-found", "message": "No live note exists at backend/runbook.", "fix": "Check the address with browse or recall. If nothing is there yet, call remember to create the note — edit and link change a note that already exists and never create one." } } ``` `link` says the same about the note that the edge is written **into**. The target of a `link` is exempt: kmem keeps an edge to a note that does not exist yet and attaches it later (invariant I3). Under a `suggest` scope this session's own writes are not live, so `retract` cannot reach them. Call `withdraw` with the `prop_` id instead. `browse` with `queued: true` lists those ids. ### `verify` ```json { "ref": "kmem://backend/auth-decision#blk_8bcbdac3", "state": "verified", "note": "confirmed in staging" } { "ref": "kmem://backend/auth-decision", "state": "stale" } { "refs": ["kmem://a/one", "kmem://a/two"], "state": "stale" } ``` `state` is `verified`, `stale`, `superseded` or `unverified`. A block ref marks one claim. A note ref marks every claim in that note. `refs` marks many notes in one call, which matters against the rate limit: ten notes are one call. Pass `ref` or `refs`, never both. Verification does **not** change the note's bytes. The words that somebody checked stay byte-identical, and that is the whole reason the mark is worth anything. In an org whose **Verification** setting is off, `verified` and `unverified` still record, but no answer and no surface reads them: every claim weighs the same, and no trust mark shows. `stale` and `superseded` keep their effect in both modes. The tool description says so. ### `link` ```json { "from": "kmem://backend/auth-decision", "rel_type": "depends_on", "to": "backend/session-store" } ``` `rel_type` is one snake_case verb. `depends_on`, `supersedes`, `implements`, `blocks`, `part_of` and `relates_to` are the suggested set. Any verb parses, because a team's vocabulary is the team's own. kmem writes the edge as a relation line in the source of the note it starts from. That is what makes the edge survive an export, a git clone, and any Markdown editor. The target need not exist yet. --- ## The resource **`signpost://brief`** — this team's curated conventions, plus how to use the knowledge base. It is a resource rather than a tool, because a client can attach it to the conversation before the model says anything. The rules a team wants followed must not depend on a model that decides to ask for them. In Claude Desktop the resource is under the paperclip. In Claude Code it is `@kmem:signpost://brief`. The conventions are the same ones Quick Connect serves, from the same renderer, so the two cannot disagree. A note reaches this resource only if all three of these hold: 1. it is live and its `type` is `convention`; 2. its frontmatter says `briefing: true` — the approval act, visible in a diff and revocable by one deleted line; 3. **a named person wrote or verified every block of it.** `kmem://signpost/conventions` is the usual place for the first one, and nothing more. A convention anywhere else that meets the three is included. A note at that address that does not meet them is not. The third gate is the one that matters most, and it is why this resource applies the same rule as the Quick Connect document rather than one of its own. A client attaches a resource to the conversation *before the model speaks*, so the model follows its text with no tool-call decision in between. That makes it the highest-leverage place in the product for an instruction, for a teammate and for anybody who holds a write link alike. The brief resolves before a team publishes anything, and it says so, with the "how to use this" half intact. A knowledge base on its first day holds no conventions yet, and that is a normal state rather than an error. The brief names anything it left out, and there are four reasons for that: the note is over the 200-line cap, it holds lines nobody approved, it is unreadable, or it holds a `signpost:managed` marker comment. kmem drops nothing silently. ## The prompts | Prompt | Arguments | For | | --- | --- | --- | | `capture-decision` | `decision`, `rationale?` | The end of an argument | | `session-summary` | `focus?` | The end of a sitting | In Claude Code they are `/mcp__kmem__capture-decision` and `/mcp__kmem__session-summary`, once the client registers the server under the name `kmem`. Another client lists them in its own prompt menu or slash menu. Both prompts open with one instruction: call `recall` first. A capture that does not check for an existing note produces a second note. The second note says the same thing with a different date, and a knowledge base with two answers to one question is worse than one with none. `session-summary` names what to leave out as plainly as what to keep. Without that, a summary reliably includes the debugging, which is the part that is worth nothing in a week. --- ## The footprint **1,827 tokens** against a hard cap of **3,500**. That number is the size of the whole `tools/list` payload — names, descriptions, JSON Schemas and annotations — counted with `cl100k_base`. The chunker and the ContextPack budget packer use the same tokenizer. Two checks enforce it: ```bash pnpm test:worker # test/mcp/footprint.test.ts asks the real handler for a real # tools/list and fails above the cap pnpm mcp:footprint:check # fails when README.md does not print the measured number pnpm mcp:footprint # measure it and rewrite that line ``` Both run in CI. The first is the stronger of the two, because it measures the bytes that cross the wire rather than a reconstruction of them. Four decisions keep the number small: no `outputSchema`, because it would roughly double the total to describe a shape the caller receives in full anyway; no `title` on any tool; no example in any description; and `edit` as one flat object rather than a seven-branch union, because Zod renders a union as `oneOf` with a full copy of the shape per branch. `/health` reports the same number under `mcp`, live, so an operator reads it without a command. --- ## Protocol Both eras, from one definition: - **2026-07-28** — the current revision. A per-request `_meta` envelope, the SEP-2243 `Mcp-Method` and `Mcp-Name` headers, and `server/discover` in place of `initialize`. - **2025-11-25 and earlier** — served by the SDK's stateless fallback, so a client that ships today works unchanged. `GET` and `DELETE` on `/mcp` answer `405`. kmem serves this endpoint per request and holds no session, so there is no session to resume or to delete. That is correct rather than missing: the platform can evict an isolate between any two requests, so a server that held session state would hold it right up until it did not. ### Origin and host validation kmem checks a request against the hostnames this deployment serves. It derives them from `SIGNPOST_BASE_URL`, the organization's own `{org}.kmem.app` hostname, the localhost set, and the deployment's `workers.dev` hostname. - **No `Origin` header** → allowed. Every command-line client sends none. - **This deployment's own origin** → allowed. - **Any other origin, an opaque `Origin: null`, or a malformed one** → `403`. - **A `Host` this deployment does not serve** → `403`, naming the ones it does. ### Limits | Bound | Value | | --- | --- | | Requests per minute, per link | 60 | | Requests per hour, per link | 1,000 | | Concurrent `recall`, `remember` or `edit`, per link | 4 | | Request body | 4 MB | | `recall` query | 2,000 characters | | Note Markdown | 1,000,000 characters | kmem answers a refused request with `429` and `Retry-After`. See [ADR 0006](./ADR/0006-rate-limiting.md). ### Errors | Situation | Answer | | --- | --- | | No `Authorization` header | `401`, `WWW-Authenticate: Bearer realm="kmem"`. On an org's own host the challenge also names `resource_metadata` and `scope="read suggest write"`, which is how a client that signs in finds the authorization server | | Unknown, revoked or malformed connection | `401`, identical in all three cases | | A connection whose person left the org, or was removed | `401`, the same as a revoked connection | | Expired connection | `401`, naming the date it expired | | A `read`-scope connection calls a write tool | `403`, `WWW-Authenticate: … error="insufficient_scope", scope="suggest"`, plus `resource_metadata` where hosted OAuth answers. The `fix` then says that a client that signed in widens its connection only through a new approval | | A `401` on the apex | The `fix` names the org's own address, because no org lives on the apex | | Rate limited | `429` with `Retry-After` | | Body too large | `413` | | The tool ran and refused | `200` with `isError: true` and `{ error: { code, message, fix } }` | Every successful request appends one row to `audit_log` with the connection's **name**, the operation, the IP and the user agent. A refused request appends nothing: there is no name to attribute it to, and a record of the credential somebody presented is exactly what [SECURITY.md](./SECURITY.md) forbids. --- ## Per-client setup The examples use the org subdomain `acme` and the placeholder `YOUR_TOKEN`. Connect fills in both. The server name is `kmem` throughout, and the prompt names and the resource name above follow from it. **A fence tagged `kmem:` is exactly the text that `apps/web/src/screens/connect/clients.ts` prints** for the endpoint `https://acme.kmem.app/mcp` and the token `YOUR_TOKEN`, and the [user guide](./user-guide/110-connect-agents.md) carries the same fences. Change the recipe in `clients.ts`, and change both documents with it. `KMEM_TOKEN` is the one name for the environment variable that holds a token in a client's configuration (`TOKEN_VARIABLE` in `clients.ts`). This document used `SIGNPOST_TOKEN` for the same job until 2026-09-16. The agent kit's hooks and the stdio bridge still read `SIGNPOST_URL` and `SIGNPOST_TOKEN`, which [AGENT-INTEGRATION.md](./AGENT-INTEGRATION.md) describes. Connect lists the clients in this order. It marks every client **"Not yet tested with kmem"** except Claude Code and Codex. The steps for the others come from each vendor's documentation, checked on 2026-09-16, and a vendor page is not a measurement. | Client | Route | Tested with kmem | Guide page | | --- | --- | --- | --- | | ChatGPT | Hosted OAuth | No | [Connect ChatGPT](./user-guide/120-connect-chatgpt.md) | | Claude (web and desktop) | Hosted OAuth, custom connector | No | [Connect Claude](./user-guide/130-connect-claude.md) | | Claude Code | Token, one command | Yes | [Connect Claude Code](./user-guide/140-connect-claude-code.md) | | Cursor | Token, `~/.cursor/mcp.json` | No | [Connect Cursor](./user-guide/150-connect-cursor.md) | | Codex | Token in `KMEM_TOKEN`, `~/.codex/config.toml` | Yes | [Connect Codex](./user-guide/160-connect-codex.md) | | OpenCode | Token, `~/.config/opencode/opencode.json` | No | [Connect OpenCode](./user-guide/170-connect-opencode.md) | | OpenClaw | Token in `KMEM_TOKEN`, `~/.openclaw/openclaw.json` | No | [Connect OpenClaw, Hermes or DeepSeek Harness](./user-guide/180-connect-openclaw-hermes-or-deepseek-harness.md) | | Hermes | Token, `~/.hermes/config.yaml` | No | [Connect OpenClaw, Hermes or DeepSeek Harness](./user-guide/180-connect-openclaw-hermes-or-deepseek-harness.md) | | DeepSeek Harness | Token in `KMEM_TOKEN`, `$DSH_HOME/cordis.patch.yml` | No | [Connect OpenClaw, Hermes or DeepSeek Harness](./user-guide/180-connect-openclaw-hermes-or-deepseek-harness.md) | | Gemini | The web app by hosted OAuth, Gemini CLI by token | No | [Connect Gemini](./user-guide/190-connect-gemini.md) | | Other | Any remote MCP client that sends a header | No | [Use Quick Connect and other clients](./user-guide/200-use-quick-connect-and-other-clients.md) | ### ChatGPT ChatGPT adds kmem as a remote MCP server and signs in. It needs developer mode, which ChatGPT Plus, Pro, Business, Enterprise and Edu have on the web; on a workspace plan an admin allows it first. 1. In ChatGPT, open Settings, then Security and login, and turn on Developer mode. 2. In Plugins, select the plus button, enter a name and a description, and put `https://acme.kmem.app/mcp` under Connection. If ChatGPT asks for authentication, choose OAuth. 3. Sign in to kmem, name the connection, choose its mode and expiry, and allow it. 4. In a new conversation, add kmem from the tools menu. Connect offers no Quick Connect for ChatGPT: [COMPATIBILITY.md](./COMPATIBILITY.md) records that route failing there, once with an invented answer. ### Claude (web and desktop) Claude adds kmem as a custom connector and signs in. 1. In Claude, open Customize, then Connectors. Select the plus button, then Add custom connector. 2. Enter a name and `https://acme.kmem.app/mcp`, then select Add. 3. If Claude asks how to register, choose Register automatically. kmem accepts dynamic client registration, so no client id or secret is needed. 4. Select Connect, sign in to kmem, and approve the connection. 5. In a conversation, turn on kmem in the connectors menu. On Claude Free a person can add one custom connector. On Team or Enterprise, a Claude owner adds kmem under Connectors in the admin settings first, and each member then selects Connect. Connect offers Quick Connect as the route for a member whose owner has not added kmem yet, marked "Quick Connect not yet tested with Claude". A Claude Desktop setup that cannot use a custom connector can still send a bearer token through the third-party `mcp-remote` bridge, in `claude_desktop_config.json` (Settings, then Developer, then Edit config). Nobody has run this against kmem: ```json { "mcpServers": { "acme-kmem": { "command": "npx", "args": [ "-y", "mcp-remote", "https://acme.kmem.app/mcp", "--header", "Authorization: Bearer YOUR_TOKEN" ] } } } ``` ### Claude Code ```sh kmem:claudeCode claude mcp add --transport http --scope user acme-kmem https://acme.kmem.app/mcp --header "Authorization: Bearer YOUR_TOKEN" ``` The server name is the org's address with `-kmem` after it: `acme-kmem` for `acme.kmem.app`. Each org gets its own name, so one machine can hold several orgs. `--scope user` puts it in every project on the machine. Without it, `claude mcp add` uses local scope, which is active only in the directory it ran in. `claude mcp remove kmem --scope user` removes it. `--scope project` writes `.mcp.json` in the repository, so the team shares it. If you commit that file, use an environment variable rather than a literal token: ```json { "mcpServers": { "acme-kmem": { "type": "http", "url": "https://acme.kmem.app/mcp", "headers": { "Authorization": "Bearer ${KMEM_TOKEN}" } } } } ``` Then in a session: - `/mcp` — check that it connected, and see the seven tools. - `@kmem:signpost://brief` — pull the conventions into the conversation. - `/mcp__kmem__capture-decision` and `/mcp__kmem__session-summary` — the two prompts. ### Cursor Merge this into `~/.cursor/mcp.json`, which serves every project: ```json kmem:cursor { "mcpServers": { "acme-kmem": { "url": "https://acme.kmem.app/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` Connect never offers the project file `.cursor/mcp.json`, because a team commits it. A project file that must name kmem uses `"Bearer ${env:KMEM_TOKEN}"` instead of a literal token. Check that the server is on in Cursor's MCP settings, then send the check prompt in Agent. ### Codex Merge this into `~/.codex/config.toml`, and set `KMEM_TOKEN` in the environment that starts Codex: ```toml kmem:codex [mcp_servers.acme-kmem] url = "https://acme.kmem.app/mcp" bearer_token_env_var = "KMEM_TOKEN" ``` Or add it with a command, which writes the same section: ```bash export KMEM_TOKEN=YOUR_TOKEN codex mcp add acme-kmem --url https://acme.kmem.app/mcp --bearer-token-env-var KMEM_TOKEN ``` `bearer_token_env_var` names the variable rather than the value, so the file holds no credential. Codex reads the variable when it connects, and names it in the error when it is empty or unset. `[mcp_servers.kmem.http_headers]` takes a literal `Authorization = "Bearer ..."` instead, and `[mcp_servers.kmem.env_http_headers]` sources any header from an environment variable. ### OpenCode Merge this into `~/.config/opencode/opencode.json`, then start OpenCode again: ```json kmem:openCode { "mcp": { "acme-kmem": { "type": "remote", "url": "https://acme.kmem.app/mcp", "oauth": false, "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` `"oauth": false` stops OpenCode from probing for a sign-in that this configuration does not use. ### OpenClaw Merge this into `~/.openclaw/openclaw.json` (JSON5), set `KMEM_TOKEN`, and run `openclaw mcp probe kmem`, which connects and counts the tools: ```json5 kmem:openClaw { mcp: { servers: { kmem: { url: "https://acme.kmem.app/mcp", transport: "streamable-http", headers: { Authorization: "Bearer ${KMEM_TOKEN}" }, }, }, }, } ``` OpenClaw's documentation keeps credentials out of configuration literals, so the header names the variable. ### Hermes Merge this into `~/.hermes/config.yaml`, then run `/reload-mcp` in Hermes: ```yaml kmem:hermes mcp_servers: kmem: url: "https://acme.kmem.app/mcp" headers: Authorization: "Bearer YOUR_TOKEN" ``` ### DeepSeek Harness DeepSeek Harness (`dsh`) reads remote MCP servers through its `@deepseek-ai/dsh-mcp-client` plugin. Merge this entry into `$DSH_HOME/cordis.patch.yml` (into the list, if the file holds one), set `KMEM_TOKEN`, and start it again, for example with `dsh web`: ```yaml kmem:deepSeek - insert: - id: mcp-kmem name: '@deepseek-ai/dsh-mcp-client' config: serverName: kmem transport: streamable-http url: "https://acme.kmem.app/mcp" headers: Authorization: !!js "'Bearer ' + process.env.KMEM_TOKEN" ``` ### Gemini **The Gemini web app** takes a custom app only for a personal Google Account in the US, in English, with Keep Activity on. In Settings, then Connected Apps, then Add a custom app, enter `https://acme.kmem.app/mcp`, then sign in to kmem and approve the connection. In a chat, type `@` and choose kmem. **Gemini CLI** takes a token. Merge this into `~/.gemini/settings.json`, start Gemini CLI again, and check `/mcp`: ```json kmem:gemini { "mcpServers": { "acme-kmem": { "httpUrl": "https://acme.kmem.app/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` `httpUrl` selects streamable HTTP. Connect offers Quick Connect as the route for everybody else, marked "Quick Connect not yet tested with Gemini". ### VS Code Copilot Connect has no tab for VS Code, and nobody has run this against kmem. `.vscode/mcp.json` in the workspace: ```json { "inputs": [ { "type": "promptString", "id": "kmem-token", "description": "kmem connection token", "password": true } ], "servers": { "acme-kmem": { "type": "http", "url": "https://acme.kmem.app/mcp", "headers": { "Authorization": "Bearer ${input:kmem-token}" } } } } ``` The `inputs` block is what lets a team commit this file. VS Code asks for the token once and stores it in its own secret storage. Open Chat, switch to **Agent** mode, and the tools appear under the tool picker. ### Anything else Any client that speaks streamable HTTP MCP and can send an `Authorization` header works: add the endpoint as a remote server and set the header to `Bearer` and the token. A client that can start only a local stdio server can use the bridge in [AGENT-INTEGRATION.md](./AGENT-INTEGRATION.md). A client that cannot speak MCP at all uses Quick Connect. Pi and Grok Bot document no remote MCP setup that kmem can use, so Connect sends both to Quick Connect. To check a token from a terminal, Connect prints these two commands, which read the token from `KMEM_TOKEN`: ```bash npx @modelcontextprotocol/inspector --cli https://acme.kmem.app/mcp --transport http --header "Authorization: Bearer $KMEM_TOKEN" --method tools/list curl -sS -X POST https://acme.kmem.app/mcp -H "Authorization: Bearer $KMEM_TOKEN" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` A tool list proves the token works. It does not complete the connection check in Connect, because it reads no doc. A `POST` without `Content-Type` is refused with `415`. The client matrix, with dates, is [COMPATIBILITY.md](./COMPATIBILITY.md). --- ## What this server tells the model at connect time ``` kmem is this team's knowledge base. Call recall before answering anything about this project, and remember anything that would otherwise need re-explaining next session. When recall returns gaps, say what is missing rather than filling it in. ``` Forty tokens, carried for the whole session. The conventions themselves are the `signpost://brief` resource, which costs nothing until somebody reads it. --- ## Conformance ```bash npx @modelcontextprotocol/inspector https://{org}.kmem.app/mcp ``` Pass the bearer token in the Inspector's Authentication panel. `test/mcp/protocol.test.ts` covers the same ground in CI against the real handler: both eras, `server/discover`, `tools/list`, `resources/list`, `prompts/list`, the `405` on a session operation, and the malformed-body cases.