# mem4dev documentation

mem4dev is the context layer for AI-assisted development. It records the decisions and activity of your projects, connects them to each other and to your code, and shares them with your team and your AI assistants (Claude Code, Claude Desktop, Cursor, VS Code or any MCP client) through a native MCP server or a REST API.

Contents:

- [Connect over MCP](#mcp): add the mem4dev MCP server to your AI client with OAuth.
- [Create an API key](#api-keys): read-only or read-and-write keys, per scope.
- [Example with curl](#curl): list scopes, log an entry, search, edit and link.
- [Scopes and tags](#scopes-tags): how entries are organized, shared and isolated.
- [Relations and references](#relations): connect entries to each other and to commits, pull requests and issues.
- [Public limits](#limits) and [plans](#plans).

This documentation is also available as raw Markdown (`/docs/index.md`). Spanish version: [/docs/es/](https://mem4dev.com/docs/es/).

## Connect over MCP {#mcp}

mem4dev exposes a public Model Context Protocol (MCP) server so an AI assistant can read the context of your projects and, if you allow it, write to it.

- **Server URL**: `https://app.mem4dev.com/mcp`
- **Transport**: streamable HTTP (JSON-RPC 2.0)
- **Authentication**: OAuth 2.1 with PKCE (S256) and Dynamic Client Registration. You never paste a password or a token into the client.

### Add it to your client

Claude Code:

```bash
claude mcp add --transport http mem4dev https://app.mem4dev.com/mcp
```

Cursor (`~/.cursor/mcp.json`):

```json
{ "mcpServers": { "mem4dev": { "url": "https://app.mem4dev.com/mcp" } } }
```

VS Code (`.vscode/mcp.json`):

```json
{ "servers": { "mem4dev": { "type": "http", "url": "https://app.mem4dev.com/mcp" } } }
```

Claude Desktop and other clients: add a remote/custom connector with the same URL.

### Authorize

The first time the client connects, the mem4dev consent screen opens in your browser (log in if needed). You choose:

1. **Access type**: *read-only* (the default) or *read and write*.
2. **Scopes**: all your scopes (including future ones) or only the ones you select.

You can revoke access at any time from your account. A client authorized as read-only that tries a write tool gets an explicit error telling it to authorize again with write access.

### Tools

| Tool | Access | What it does |
|---|---|---|
| `list_scopes` | read | Lists your scopes (call it first to get the `scope_id`). |
| `get_plan_usage` | read | Your plan, its limits and how much you have used (scopes, entries, interactions this month, attachments). |
| `get_usage_guide` | read | Recommended instructions for using this MCP (cheap queries with `compact`, obsolete entries, relations, how to write entries). Call it first, once per conversation. |
| `get_scope_context` | read | The context you wrote for the scope (who is who, rules, vocabulary), its query instructions, who you are in it and whether it is shared. Read it once per scope. |
| `query_entries` | read | Natural-language search, with optional tags, date range, text filter, relations (`related_to`), references (`ref`) and compact format; excludes obsolete entries unless you ask for them. |
| `get_entry` | read | Full text, attachments, obsolete marks, relations and references of an entry (even if it is obsolete). |
| `get_related_entries` | read | The entries related to a given one, following the relations up to 3 hops. |
| `list_tags` | read | Custom tags of a scope, with how many entries each one has. |
| `get_attachment_download_url` | read | Temporary download link (5 minutes) for an attachment. |
| `add_entries_bulk` | write | Logs up to 20 entries in one call, with their tags, relations and references. |
| `update_entry` | write | Edits the text, date, tags, relations and references of an entry, or marks it as obsolete (the previous version is kept). |
| `link_entries` | write | Relates an existing entry to others in the same scope, or adds references to commits, pull requests or issues, without touching its text. |
| `delete_entry` | write | Sends an entry to the trash (one per call). |

Tools that touch entries take a `scope_id`; data from other scopes or other accounts is never returned. `list_scopes` includes the scopes other people shared with you (marked as shared): you write in them only if your permission allows it, and the limits of the scope owner's plan apply.

Every tool description ends with the catalog version (today `2026-10-22.2`): use it to check which version your client received. If you see an older date, your client has the list cached; reconnect the server.

Files can only be attached from the web app, not through MCP or the API. Through MCP you can read and download existing attachments; deleting them is also web-only.

### Deleting entries

`delete_entry` does not delete right away: the entry leaves the searches and stays 30 days in a trash that only the scope owner sees and can recover from the web app. It cannot be recovered through MCP. If the entry was the only replacement of other entries, those stay obsolete unless you send `reactivate_obsolete: true`; the response carries `kept_obsolete` or `reactivated`.

### Obsolete entries

An entry stops being current when a newer one replaces it. The recommended way is the `replaces` relation (see [Relations and references](#relations)): the new entry makes the old one obsolete by itself, and removing the relation reactivates it. You can also mark it by hand with `update_entry`: `obsolete: true` and, if you know it, `replaced_by` with the id of the new entry (it must be a live entry in the same scope). You do not need to resend the text to mark it. It is never deleted: it stays in the history, `get_entry` always returns it, and marking it creates a new version, like any edit. `add_entries_bulk` also accepts `obsolete` and `replaced_by` per entry.

`query_entries` and `get_related_entries` **exclude obsolete entries by default** and answer `omitted_obsolete` (how many matched) so the AI knows history exists; with `include_obsolete: true` they include them. Every returned entry carries `obsolete` and `replaced_by`. The Ask chat in the web app also excludes them, with an «Include obsolete entries» checkbox to see them.

### Cheaper searches with less noise

Besides `question`, `tags`, `date_from`, `date_to`, `limit` and `offset`, `query_entries` accepts:

- `format`: `"full"` (the default, full text) or `"compact"`: per entry only `id`, `event_date`, `tags`, `obsolete`, `replaced_by`, `score`, `title` (the first `# ...` line) and `fragments` (up to 3 excerpts of about 200 characters around the `filter` matches or, without a filter, from the «Decision» section). Ask for the full text afterwards with `get_entry`.
- `filter`: a list of terms (any one is enough) or an object `{ "all": [...], "any": [...] }`; and `filter_regex`: an optional regular expression (up to 200 characters, no nested quantifiers, with a cost limit). Both apply to the full text, ignoring case and accents.
- `related_to`: `{ "entry_id", "types"?, "depth"? }` limits the search to entries related to another one. `ref`: `{ "type"?, "value", "repo"? }` limits the search to entries that point to that commit, pull request, issue, branch or URL (a hash is found by a prefix of 7 or more characters).
- `explain`: `true` adds to each entry its raw semantic score and lexical score (`null` = no match on that signal) and the `match_type` (`qdrant` = meaning only, `lexical` = words only, `hybrid` = both). Only with `question`. If your client cannot send lists or objects, `filter` also accepts them written as JSON text.
- `candidates`: how many candidates to gather per source before filtering (default 50, maximum 200). With `filter` or `filter_regex` and no `candidates`, the maximum is used. The order is: candidates → drop obsolete → `filter` → sort by score → cut to `limit`, so the filter does not lose entries that would have fallen outside the top 20.

The response reports `total_candidates` (the union of the semantic and keyword candidates, so it can exceed the cap), `total_after_filter`, `omitted_obsolete`, `candidates_limit` (the cap applied to each source) and `candidates_truncated` (true = the candidates above the minimum score filled that cap, so unevaluated entries may exist: raise `candidates` or narrow by dates or tags). `has_more` is true when there are more results after this page. With `filter` and no `question`, only the `candidates` most recent entries that meet tags and dates are evaluated, and `total_matches` counts those that meet the filter.

### Context, authors and ranking

Every scope has a **context** written in the web app (who is who, the project, rules, vocabulary) and, if you want, query instructions. `get_scope_context` hands them over so any AI can use them, as the Ask tab does; it also says who you are in the scope (`current_user`) and whether it is shared. `get_usage_guide` and the connection instructions tell the AI to read both once, first.

A scope can be **shared** by several people. Every entry carries `author` (who wrote it), in full and compact format, so an AI can tell what Ana did from what Luis did.

With `question`, results come sorted by `score` (0 to 1), highest first, and on equal score the most recent first. The order does **not** depend on `limit`: the first N entries are the same with any `limit`, and `offset` reads further while `has_more` is true. The `score` mixes two signals, each normalized on its own scale: 65% semantic similarity and 35% word match (rare terms and title terms weigh more), plus a small recency weight; an entry with both signals never scores lower than one with a single signal of equal value. What matters is the order inside the response, not the absolute value.

### MCP limits

Every tool has its own limit per hour and per account: `query_entries` 20; `get_related_entries` 60; `add_entries_bulk`, `update_entry`, `delete_entry` and `link_entries` 30 each. Besides, every MCP call counts as one interaction of your plan's month (like every API call and every question in Ask; what you do from the web app does not count).

## Create an API key {#api-keys}

API keys let scripts, CI jobs and your own tools use the REST API without logging in through the browser. For AI assistants, prefer [MCP](#mcp).

### Steps

1. Log in at `https://app.mem4dev.com/` and open **API keys** (`https://app.mem4dev.com/api-keys`).
2. Create a key and give it a name you will recognize (for example `ci-notes`).
3. Choose which scopes it can access. Read access is always included in an assigned scope; **write access is optional and set per scope**.
4. Copy the secret now. It looks like `mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` and is shown **only once**.
5. Revoke the key from the same page when you no longer need it.

### Use it

Send it as a Bearer token:

```http
Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Base URL: `https://app.mem4dev.com/api/v1`

### Endpoints

| Method and path | Permission | What it does |
|---|---|---|
| `GET /scopes` | read | Lists the scopes your key can use. |
| `GET /entries` | read | Lists entries by scope and date range (`scope_id`, `date_from`, `date_to`, `limit`). Every entry carries `relations` and `refs`. |
| `POST /entries/search` | read | Natural-language search (equivalent to `query_entries`). |
| `GET /entries/related` | read | Entries related to a given one (equivalent to `get_related_entries`). |
| `GET /entries/versions` | read | Edit history of an entry. |
| `GET /tags` | read | Tags of a scope, with their count. |
| `GET /plan` | read | Your plan, limits and usage. |
| `POST /entries/bulk` | write | Logs up to 20 entries, with tags, relations and references. |
| `PUT` or `PATCH /entries` | write | Edits an entry: text, date, tags, obsolete, relations and references. |
| `POST /entries/links` | write | Relates an entry to others or adds references, without touching its text (equivalent to `link_entries`). |
| `DELETE /entries` | write | Sends an entry to the trash (`reactivate=1` reactivates the entries that were obsolete only because of it). |

### Permissions

| Key configuration | Can |
|---|---|
| Assigned scope, read-only | The read endpoints in the table above |
| Assigned scope, read and write | Everything above plus the write endpoints |
| Unassigned scope | `403 forbidden` |

A key only reaches the scopes that the account that created it has access to: its own and those other people shared with it (in the latter, writing depends on the permission you were given). If you lose access to a shared scope, the key loses it too.

### Errors

Errors are JSON: `{ "error": { "code": "forbidden", "message": "..." } }`

| HTTP | code | Meaning |
|---|---|---|
| 400 | `invalid_request` | A field is missing or has an invalid format. |
| 401 | `unauthorized` | Missing, invalid or revoked key. |
| 402 | `plan_limit` | A limit of the plan of the account that owns the scope was reached (entries or interactions this month). Nothing is deleted: only creating more is blocked. |
| 403 | `forbidden` | No access (or no write access) to that scope. |
| 404 | `not_found` | The scope or the entry does not exist. |
| 429 | `rate_limited` | Limit exceeded: 120 requests per hour, per key and endpoint. |

## Example with curl {#curl}

Replace `mk_live_...` with your [API key](#api-keys). Base URL: `https://app.mem4dev.com/api/v1`.

### 1. List the scopes your key can use

```bash
curl https://app.mem4dev.com/api/v1/scopes -H "Authorization: Bearer mk_live_..."
```

```json
{ "ok": true, "scopes": [ { "scope_id": 1, "scope_name": "Work", "can_read": true, "can_write": true } ] }
```

### 2. Log an entry (needs write access)

```bash
curl -X POST https://app.mem4dev.com/api/v1/entries/bulk -H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" -d '{"scope_id": 1, "entries": [{"entry_text": "# Upload limit\n\nDecision: cap uploads at 50 MB.", "tags": ["adr"]}]}'
```

Up to 20 entries per call; each one is validated separately and the response reports `saved_count`, `failed_count` and a result per entry.

### 3. Log an entry that replaces another and points to a commit

```bash
curl -X POST https://app.mem4dev.com/api/v1/entries/bulk -H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" -d '{"scope_id": 1, "entries": [{"entry_text": "# New upload limit\n\nDecision: 100 MB.", "relations": [{"type": "replaces", "target": 4821}], "refs": [{"type": "commit", "value": "a1b2c3d", "repo": "mem4dev-app"}]}]}'
```

Entry 4821 becomes obsolete by itself and stops showing up in searches. If a relation or reference fails, the entry is saved anyway and the error goes in `link_errors`.

### 4. Search in natural language

```bash
curl -X POST https://app.mem4dev.com/api/v1/entries/search -H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" -d '{"scope_id": 1, "question": "what did we decide about file uploads?", "date_from": "2026-09-01"}'
```

Omit `question` to filter only by `tags` and dates. Results come 20 at a time (use `offset` to paginate). Obsolete entries are excluded unless you send `include_obsolete: true`.

### 5. Read by date range

```bash
curl "https://app.mem4dev.com/api/v1/entries?scope_id=1&date_from=2026-09-01&limit=50" -H "Authorization: Bearer mk_live_..."
```

### 6. Follow the relations of an entry

```bash
curl "https://app.mem4dev.com/api/v1/entries/related?scope_id=1&entry_id=4821&depth=2&include_obsolete=1" -H "Authorization: Bearer mk_live_..."
```

### 7. Relate an existing entry (needs write access)

```bash
curl -X POST https://app.mem4dev.com/api/v1/entries/links -H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" -d '{"scope_id": 1, "entry_id": 4830, "add_relations": [{"type": "implements", "target": 4821}], "add_refs": [{"type": "pull_request", "value": "128", "repo": "mem4dev-app"}]}'
```

### 8. Edit and delete (need write access)

```bash
curl -X PUT https://app.mem4dev.com/api/v1/entries -H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" -d '{"scope_id": 1, "entry_id": 4821, "entry_text": "Corrected text.", "add_tags": ["adr"]}'
```

```bash
curl -X DELETE "https://app.mem4dev.com/api/v1/entries?scope_id=1&entry_id=4821" -H "Authorization: Bearer mk_live_..."
```

Deleting sends the entry to the trash: the scope owner can recover it from the web app for 30 days.

### 9. Check your plan and usage

```bash
curl https://app.mem4dev.com/api/v1/plan -H "Authorization: Bearer mk_live_..."
```

Returns the plan, its limits, what you have used and what is left (scopes, entries, interactions this month and storage). If a limit is reached, writes and searches answer `402 plan_limit`.

Also available: `GET /entries/versions?scope_id=&entry_id=` (edit history) and `GET /tags?scope_id=`.

Limits: 120 requests per hour per key and endpoint. See the [public limits](#limits).

## Scopes and tags {#scopes-tags}

### Scopes

A scope is a separate space inside your account: for example *Work*, *Client A*, *Side project*. Every scope has its own entries and its own AI context.

- Every account and every scope are isolated. Queries, MCP responses and API calls only return data from the scope you ask for, and only if your key or MCP authorization includes it.
- Every API or MCP call that touches entries takes a `scope_id`. Get yours with `GET /api/v1/scopes` or the MCP tool `list_scopes`.
- Access is granted per scope: an API key has read (always) and write (optional) per scope; an MCP authorization covers all scopes or the ones you select.

### Shared scopes

The owner of a scope can share it with other people by email, with read-only or read-and-write permission. It is available on every plan.

- Whoever accepts the invitation sees the shared scope next to their own; in the API and in MCP it appears marked as shared, with their role.
- Whoever writes can edit and delete any entry in the scope, like the owner; what is deleted stays 30 days in a trash that only the owner sees and can recover. Only the owner invites, revokes and manages the scope, and invitations expire after 7 days.
- The limits (entries, interactions this month, attachments and people) are those of the owner's plan, not those of whoever queries.
- An API key or MCP authorization of an invited person reaches that scope only while that person keeps access.
- In every entry, `author` says who wrote it.

### Entries

An entry is free text (up to 5,000 characters) with an `event_date` (today in your time zone by default; it cannot be in the future). Edits keep a version history. For an AI to make the most of them, write each entry with a title (`# ...`) and a «Decision» section, so it can be understood on its own.

### Tags

Custom tags organize entries inside a scope, beyond free text.

- Assign tags when creating an entry (`tags` in `entries/bulk`) or later (`add_tags` / `remove_tags` when editing).
- List the tags of a scope, with how many entries each one has: `GET /api/v1/tags?scope_id=1` or MCP `list_tags`. Reuse an existing tag instead of creating a near-identical one.
- Filter by tags in `POST /api/v1/entries/search` or in MCP `query_entries`. Several tags combine with AND (the entry must have all of them); they can be combined with a question and a date range.

## Relations and references {#relations}

An entry can be related to others in the **same scope** and point to your code. Relations never cross scopes.

### Relation types

| Type | Inverse | Meaning |
|---|---|---|
| `replaces` | `replaced_by` | The new entry replaces the old one. The old one is obsolete while the relation exists. |
| `caused_by` | `causes` | A decision or work session comes from an incident or problem. |
| `implements` | `implemented_by` | A work session carries out a decision. |
| `refines` | `refined_by` | An entry adjusts part of another, which stays current. |
| `continues` | `continued_by` | An entry is the next part of a long text split across several entries. Neither becomes obsolete. |
| `relates_to` | `relates_to` | Free, symmetric association. |

Every relation is stored once; the inverse is computed when reading. You can use either name when creating it.

### External references

An entry can point to: `commit` (a hash of 7 to 64 characters), `pull_request`, `issue` (number or key, like `PROJ-123`), `branch` and `url`. Every reference accepts optional `repo` and `title`. mem4dev only stores the pointer: it does not query your repository.

### How to create them

- When saving: `relations` and `refs` on every entry of `add_entries_bulk` (MCP) or `POST /entries/bulk` (API). A relation points to an existing entry with `target` or to another entry of the same batch with `target_index` (position from 0).
- When editing: `add_relations`, `remove_relations`, `add_refs` and `remove_refs` in `update_entry` or `PUT /entries`.
- Without touching the text: `link_entries` (MCP) or `POST /entries/links` (API).
- A relation or reference that fails does not prevent saving the entry: the error goes in `link_errors`.

### How to read them

- `get_entry` and `GET /entries` return `relations` (type → ids) and `refs` of every entry.
- `get_related_entries` and `GET /entries/related` walk the relations: `types`, `direction` (`out`, `in` or `both`), `depth` (1 to 3), `limit` (25 by default, maximum 50), `include_obsolete` and `with_refs`. They return per entry only the id, date, title, `obsolete`, the relation seen from the entry you came through (`relation`), that entry (`via`) and the depth; ask for the full text with `get_entry`.
- `query_entries` and `POST /entries/search` accept `related_to` and `ref` to search only among related entries or entries that point to a commit, pull request or issue.

### Limits and permissions

- Up to 50 relations and 20 references per entry, and up to 20 of each per call.
- You need write permission. In a shared scope, whoever writes must be the author of at least one of the two entries (in a replacement, of the one that becomes obsolete); the owner can do it with all of them.
- A call that changes something counts as one interaction of the plan; with the limit reached it answers `402 plan_limit`.

## Public limits {#limits}

| Item | Limit |
|---|---|
| Entry text | 5,000 characters |
| Entries per bulk call | 20 |
| Relations and references per entry | 50 relations, 20 references |
| Attachments | 10 MB per file, 5 per entry (upload from the web app only; MCP can read and download them) |
| Attachment types | pdf, jpg, jpeg, png, webp, docx, xlsx, pptx, txt, md, zip, rar, skill |
| REST API | 120 requests per hour, per API key and endpoint (`GET /scopes`, `GET /tags` and `GET /plan`: no limit of their own) |
| MCP | per tool and per hour: `query_entries` 20, `get_related_entries` 60, `add_entries_bulk`, `update_entry`, `delete_entry` and `link_entries` 30 each |
| Search results | 20 per page |
| Interactions | Every API call, every MCP call and every question in Ask counts 1, against the plan of the data owner. What you do from the web app does not count. |

## Plans {#plans}

| Plan | Own scopes | Entries | Interactions per month | Attachments | People per shared scope | Scopes from other accounts you can accept |
|---|---|---|---|---|---|---|
| Free | 1 | 1,000 | 1,000 | 100 MB | 1 | 1 |
| Individual (from USD 7.99 a month) | 10 | 10,000 | 10,000 | 1 GB | 10 | 10 |

The Free plan needs no credit card and has no feature restrictions (MCP and API included); its limits are on volume. Individual has higher tiers within the same plan. Individual is available now; Teams is coming soon. Sharing scopes is available on every plan, and the limits are set by the scope owner's plan. At the entries limit only creating more is blocked; at the interactions limit the API, MCP and Ask pause until it renews. Nothing is ever deleted and the web app stays readable. Check your usage with the MCP tool `get_plan_usage` or `GET /api/v1/plan`.
