# Engrava MCP [![CI](https://github.com/sovantica/engrava-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sovantica/engrava-mcp/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/engrava-mcp.svg)](https://pypi.org/project/engrava-mcp/) [![Python](https://img.shields.io/pypi/pyversions/engrava-mcp.svg)](https://pypi.org/project/engrava-mcp/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) **The [Model Context Protocol](https://modelcontextprotocol.io) server for [Engrava](https://github.com/sovantica/engrava)** — expose an agent memory database to any MCP client (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, …) over stdio. `engrava-mcp` is a standalone, runnable package that consumes Engrava's public API. It is the one way to run Engrava as a memory server; the `engrava` library itself ships no MCP code. ```bash uv tool install engrava-mcp # recommended for daily use: a persistent install engrava-mcp # spawned by your MCP client over stdio ``` `uvx engrava-mcp` runs it without an install step, and `pip install engrava-mcp` works too. `uvx` keeps its environment in a cache. When that cache is cold, the first start waits for the download; see [Optional providers](#optional-providers) for the `[local]` extra, where the download is largest. Installing `engrava-mcp` pulls in `engrava` transitively, so you also get the `import engrava` library in the same environment. ## Compatibility `engrava-mcp` follows Engrava's version: **`engrava-mcp X.Y.z` targets `engrava X.Y`** and requires `engrava >=X.Y,=0.5,<0.6` | | `0.6.x` | `>=0.6,<0.7` | | `0.7.x` | `>=0.7,<0.8` | The dependency range is the source of truth. Normal installs resolve a compatible `engrava` automatically; if you pin `engrava` yourself, keep it within that range. If no matching `engrava-mcp` exists yet for an `engrava` newer than the table's last row, that pairing is **not yet verified/supported** — not broken; stay on a supported pair until a matching `engrava-mcp` ships. ## Which package do I want? | Goal | Install | |---|---| | Build on the Engrava Python API (memory DB in your own code) | `pip install engrava` | | Run Engrava as a memory server for an MCP client | `uvx engrava-mcp` (or `pip install engrava-mcp`) | There is no third option. ## Migrating from `engrava[mcp]` The server used to ship inside Engrava as the `engrava[mcp]` extra and an in-`engrava` `engrava-mcp` command. As of Engrava 0.5.0 it lives here instead. | Before | After | |---|---| | `pip install "engrava[mcp]"` | `pip install engrava-mcp` (or `uvx engrava-mcp`) | | `engrava-mcp` (installed by engrava) | `engrava-mcp` (installed by this package) | | client `mcp.json`: `"command": "engrava-mcp"` | client `mcp.json`: `"command": "uvx", "args": ["engrava-mcp"]` | - **Watch out:** `pip install "engrava[mcp]"` against Engrava 0.5 **does not fail** — pip ignores the now-unknown extra and quietly installs bare `engrava`, so it can look like the server installed when it did not. Install `engrava-mcp` instead. - Update any pinned requirement strings (`engrava[mcp]>=...`) to depend on `engrava-mcp`, not just reinstall. - **Your store configuration is unchanged** — the same `engrava.yaml` / env vars work exactly as before (see [Configuration](#configuration)). ## Configuration The server resolves its store from environment variables, in priority order: | Variable | Meaning | |---|---| | `ENGRAVA_MCP_CONFIG` | Path to an `engrava.yaml`. Built with the full configuration — embedding provider, vector backend, journal, TTL. The thought/edge journal is configured here: set `journal: enabled: true` to turn it on. **Recommended.** | | `ENGRAVA_DB_PATH` | Path to a bare SQLite database file. Use an absolute path: a relative one resolves against the server's working directory, which the client chooses. Zero-config quick-start; no embedding provider is configured, so semantic (vector) search is inert — full-text search, the graph, and MindQL still work. This route builds the store with no journal; use `ENGRAVA_MCP_CONFIG` for that. "Zero-config" means Engrava's default search policy, so `search_memory`'s `recency_now` is honoured on this route too — recency is scored against the timestamp you supply, under Engrava's default search weights. | | `ENGRAVA_MCP_READ_ONLY` | When set to `1` / `true` / `yes`, the write tools are not registered and no read makes a write of its own — including a deferred access-count update a store with access tracking on would otherwise buffer and flush on close. Every tool's `readOnlyHint` annotation is therefore accurate under this mode, on every configuration route. Read-only mode governs the tools, not how the database is opened. At startup the server still opens the file read-write, creates it if it is missing, and upgrades its schema to the one its Engrava version uses, so a database file the server cannot write cannot be served in this mode either. | **Recommended:** give the MCP server the same `engrava.yaml` your application uses. The `yaml` is the only place to declare an embedding provider (and its model / key), which the server needs to embed a *new query* at search time for semantic search. With only `ENGRAVA_DB_PATH` set, the server emits a startup warning that semantic search is inert and points you at `ENGRAVA_MCP_CONFIG`. At search time, the server embeds the query with the provider this `yaml` declares. Whether this server's writes embed anything is decided by the same `yaml`'s `embeddings.auto_embed`, which Engrava leaves off by default. With it off, a thought created through `store_thought` gets no embedding, so `search_memory`'s vector ranking cannot match it — its keyword ranking still can — and an `update_thought` leaves whatever embedding the thought already had as it was, not refreshed. With it on, creating a thought, or changing its `essence` or `content`, also calls the provider. ### Store-hook extensions need the config path Engrava extensions that hook the store — anything wired through an `engrava.yaml`'s `hooks:` section — are attached only on the `ENGRAVA_MCP_CONFIG` launch. `ENGRAVA_DB_PATH` opens a bare database and carries no configuration, so it runs with Engrava's default hooks and cannot attach a store-hook extension. That is deliberate: it is an intentionally minimal read/write facade. Installing such an extension and starting with `ENGRAVA_DB_PATH` therefore leaves its store hooks unattached in this server. When an installed package advertises any extension, the server emits a startup warning naming it — it reports what is advertised, not what each one does, since it never loads them itself — so you can tell the difference between "nothing advertised" and "advertised but nothing wired it here". If reading the installed-package metadata raises an ordinary error, the server attempts to log that instead and carries on starting. Both go through Python's `logging`, so whether and where they surface is up to your logging configuration. To wire a store hook, launch with `ENGRAVA_MCP_CONFIG` pointing at an `engrava.yaml` with a `hooks:` section: ```yaml hooks: class: "my_package.hooks.MyHooks" ``` ### Example `engrava.yaml` ```yaml database: path: /absolute/path/to/memory.db embeddings: provider: openai-compatible # or: ollama, sentence-transformer, huggingface model: text-embedding-3-small api_key: ${OPENAI_API_KEY} ``` A relative `database.path` resolves against the server process's working directory, which the MCP client chooses, not against the yaml's folder. ## Client setup Point your MCP client at the server over stdio. For example, a typical `mcp.json` entry: ```json { "mcpServers": { "engrava": { "command": "engrava-mcp", "env": { "ENGRAVA_MCP_CONFIG": "/absolute/path/to/engrava.yaml" } } } } ``` A `${VAR}` value in the `engrava.yaml`, such as `${OPENAI_API_KEY}`, is read from the server's own environment, so add that variable to the same `env` block, unless the client is known to pass its own environment through. This assumes `uv tool install engrava-mcp`. If your client cannot find the command, give its absolute path; `uv tool dir --bin` prints the directory. Without an install, use `"command": "uvx", "args": ["engrava-mcp"]`. Use `ENGRAVA_DB_PATH` instead of `ENGRAVA_MCP_CONFIG` for the zero-config quick-start, and add `"ENGRAVA_MCP_READ_ONLY": "1"` for an app-writes / agent-reads deployment. ### Running without uvx ```bash engrava-mcp # console script python -m engrava_mcp # module run python -m engrava_mcp.server # module run (server module directly) ``` ## Optional providers For an MCP deployment, prefer an embedding provider that runs outside the server process: Ollama (`provider: ollama`) or an OpenAI-compatible endpoint (`provider: openai-compatible`). The default install already covers both, and the server then loads no embedding model itself. The default install supports the vector backend and HTTP-based embedding providers (OpenAI / Ollama) once configured in the `yaml`. Heavier providers are opt-in extras that mirror Engrava's own extras: ```bash uvx --from "engrava-mcp[local]" engrava-mcp # sentence-transformers (local model) uvx --from "engrava-mcp[hf]" engrava-mcp # HuggingFace Inference API uvx --from "engrava-mcp[openai]" engrava-mcp # OpenAI-compatible embeddings deps uvx --from "engrava-mcp[ollama]" engrava-mcp # Ollama embeddings deps ``` `[local]` runs the model inside the server process and installs PyTorch, which can add several gigabytes. A first start on a cold `uvx` cache waits for that download; later starts reuse the cache. `uv tool install "engrava-mcp[local]"` pays the download once, at install time. A model that is not already in the local model cache is downloaded when it is first loaded. ## The surface - **Tools (13):** `get_thought`, `search_memory`, `search_keywords`, `list_memory`, `query_memory`, `memory_stats`, `get_edges`, `list_edges` (read); `store_thought`, `update_thought`, `link_thoughts`, `delete_thought`, `delete_edge` (write, gated by `ENGRAVA_MCP_READ_ONLY`). - **Resources (3):** `engrava://thought/{thought_id}`, `engrava://stats`, `engrava://recent`. - **Prompts (3):** `summarize_recent_memory`, `find_related`, `reflect_on_topic`. `query_memory` accepts only MindQL `FIND` queries; raw SQL and every other command are rejected. It returns at most 5000 rows. A `limit` argument replaces the query's own `LIMIT`; without one, a `LIMIT` outside 1–5000 is refused. `get_edges` traverses a thought's edges by direction (`IN` / `OUT` / `BOTH`); with `limit`, at most that many edges, the highest-weight ones first, and without it, every edge. `list_edges` browses edges filtered by type, source, or metadata. `link_thoughts` accepts optional edge `metadata` (JSON fields that `list_edges` can filter on). `search_memory` accepts an optional `recency_now` (ISO-8601 timestamp) giving the moment to measure age against (transaction time); recency takes part in the ranking only when you pass it. ## Development ```bash pip install -e ".[dev]" ruff check src/ tests/ ruff format --check src/ tests/ mypy --strict src/ pytest --cov --cov-fail-under=90 ``` ## License MIT