Overview
Installing promptev-context-engine (Python 3.12+) or @promptev/context-engine (Node 22+) puts a context-engine executable on your path. The four subcommands behave the same in both; the few differences are called out where they occur.
| Command | What it does | Needs |
|---|
| migrate | Create or upgrade the engine’s tables in a Postgres database. | A Postgres driver |
| mcp | Serve the MCP tool surface over streamable HTTP, for trying it out. | The MCP extra or peer, a Postgres driver, CE_ config |
| check-acl-exposure | Measure ACL-filtered retrieval accuracy on your own corpus. Offline and read-only. | A Postgres driver |
| bm25-enable | Opt this database in to the experimental BM25 keyword ranking: install its triggers and count the stored chunks. | A Postgres driver |
| bm25-disable | Remove the BM25 triggers and statistics. | A Postgres driver |
| bm25-compact | Fold the BM25 statistics rows each chunk write adds, one per source. | A Postgres driver |
| install-skill | Install the agent skill so a coding assistant uses the library correctly. | Nothing extra |
Python dependencies: the Postgres driver is the [postgres] extra (psycopg2). mcp also needs the [mcp] extra (the official MCP Python SDK, mcp 2.2 or later), which brings uvicorn. install-skill needs nothing beyond the base install.
TypeScript dependencies: the Postgres driver is the optional peer pg. mcp also needs the optional peers @modelcontextprotocol/server (^2.2.0) and @modelcontextprotocol/node (^2.1.0), the official MCP SDK. install-skill needs nothing beyond the package.
Running the command
Install the package and call context-engine, or run it once without installing. Remember the driver either way.
# Installed in your environment
pip install "promptev-context-engine[postgres]"
context-engine migrate --database-url postgresql://user:pass@localhost:5432/mydb
# Or without installing, via uv
uvx --from "promptev-context-engine[postgres]" context-engine migrate \
--database-url postgresql://user:pass@localhost:5432/mydb# Installed in your project
npm install @promptev/context-engine pg
npx context-engine migrate --database-url postgresql://user:pass@localhost:5432/mydb
# Or without installing (pg is an optional peer, so name it)
npx -p @promptev/context-engine -p pg context-engine migrate \
--database-url postgresql://user:pass@localhost:5432/mydbcontext-engine --help and context-engine <command> --help print the flags.
migrate
Creates or upgrades the engine’s schema. Run it on every deploy: it is idempotent, and the migration history ships inside the package, so you need no alembic.ini or copied scripts.
| Flag | Default | Meaning |
|---|
| --database-url <url> | required | Postgres connection URL. |
| --dim <n> | 1536 | Embedding vector width. It becomes the width of every embedding column and is recorded in context_engine_meta. It must match your embedding model. |
| --graph | off | Also create the graph tables (entities, relationships, chunk entities, communities, and the record of removals a Neo4j backend still owes). Run it before starting workers: the engine looks for the removals table once per process. |
context-engine migrate --database-url postgresql://user:pass@localhost:5432/mydb --dim 1536
# Later, add the graph tables on top of the same database
context-engine migrate --database-url postgresql://user:pass@localhost:5432/mydb --dim 1536 --graph
npx context-engine migrate --database-url postgresql://user:pass@localhost:5432/mydb --dim 1536
# Later, add the graph tables on top of the same database
npx context-engine migrate --database-url postgresql://user:pass@localhost:5432/mydb --dim 1536 --graph
What it does
Extensions. Creates vector, pg_trgm and unaccent if missing, so the database role needs permission to CREATE EXTENSION (or an administrator creates them first).
Base schema, always. Documents, chunks, meta, structured keys, and the tool, tool call and tool approval tables.
Graph schema, with --graph. Running it later with --graph adds the graph tables on top. Running it again without the flag is a no-op, never a downgrade.
Its own version table. Progress is recorded in context_engine_alembic_version, so it does not collide with your application’s Alembic history in the same database.
Indexes without write locks. Every index after the initial schema is built with CREATE INDEX CONCURRENTLY, so ingest keeps writing while it builds, and an index left invalid by an interrupted build is dropped and rebuilt by the next run. The CLI prints a line per index to stderr, because a concurrent build waits for open write transactions and can be silent for minutes otherwise; SELECT * FROM pg_stat_progress_create_index shows how far along it is. A lock_timeout or statement_timeout on the connection applies to these builds, and migrate does not override it.
Self-healing. Every statement is idempotent, so a failed migrate is safe to re-run, and re-running is the repair: the database converges to the current schema.
Safe when servers start together. migrate holds one advisory lock for the whole run, so a second process migrating the same database waits, finds everything applied and exits cleanly. On a database that is already up to date it takes no table lock, so a migrate at every boot never queues your traffic, and a repair that does need a lock gives up after 5 seconds instead of waiting behind open transactions.
The dimension is fixed once recorded. If the database already records a different embedding_dim, migrate refuses before running any DDL, because changing the width would invalidate every stored vector. Use a fresh database for a different embedding width.
The same step is available from code:
from context_engine.cli import run_migrate
run_migrate("postgresql://user:pass@localhost:5432/mydb", dim=1536, graph=False)import { runMigrate } from "@promptev/context-engine";
await runMigrate("postgresql://user:pass@localhost:5432/mydb", { dim: 1536, graph: false });mcp
Serves the engine’s MCP tools (search_knowledge_base, search_tools and execute_tool) over streamable HTTP. It is a quick way to try the MCP surface, not a deployment target.
Security: unauthenticated, anonymous, loopback by default
This server has no authentication. Every request runs as an anonymous caller (empty principals) over the whole corpus (UNSCOPED), so anyone who can reach it can read every unrestricted document and execute every unrestricted tool. It binds 127.0.0.1 by default for that reason. Binding any other host prints a warning that says so. To serve MCP for real, mount the app in your own server with a principals resolver behind your own auth (below).
| Flag | Default | Meaning |
|---|
| --database-url <url> | required | Postgres connection URL. Every other setting comes from CE_ environment variables. |
| --host <host> | 127.0.0.1 | Interface to bind. Widen it only if you accept that anyone who can reach it can use it. |
| --port <port> | 8080 | Port to serve on. |
| --dim <n> | from env, else detected | Overrides embedding.dim after the config is loaded. |
| --graph | off | Sets graph.enabled after the config is loaded. The graph still needs its CE_GRAPH__* variables (at least the extraction LLM); the flag does not supply them. |
pip install "promptev-context-engine[postgres,mcp]"
export CE_EMBEDDING__PROVIDER=openai
export CE_EMBEDDING__MODEL=text-embedding-3-small
export CE_EMBEDDING__API_KEY=sk-...
context-engine mcp --database-url postgresql://user:pass@localhost:5432/mydb --port 8080
# streamable HTTP endpoint: http://127.0.0.1:8080/mcp
npm install @promptev/context-engine pg @modelcontextprotocol/server @modelcontextprotocol/node
export CE_EMBEDDING__PROVIDER=openai
export CE_EMBEDDING__MODEL=text-embedding-3-small
export CE_EMBEDDING__API_KEY=sk-...
npx context-engine mcp --database-url postgresql://user:pass@localhost:5432/mydb --port 8080
# prints: context-engine mcp listening on http://127.0.0.1:8080
Configuration it reads
Everything but the URL comes from the environment. The command builds ContextEngineConfig from CE_ variables (Python constructs it directly; TypeScript calls ContextEngineConfig.fromEnv), so CE_EMBEDDING__PROVIDER and CE_EMBEDDING__MODEL are required. CE_LLM__* turns on the LLM-backed actions, and CE_ENABLE_CODE_EXECUTION governs compute. See the configuration reference.
Paths. The Python server is uvicorn serving the MCP app, with the endpoint at /mcp. The TypeScript server is a Node HTTP server that hands every request to the MCP handler. Both answer the 2026-07-28 protocol version and the older initialize handshake, and neither issues a session id.
Loopback answers only loopback. Bound to 127.0.0.1, localhost or ::1, the server answers only those names at its own port in the Host header. Without that check any web page could reach this unauthenticated, tool-running surface by DNS rebinding. Bound elsewhere with --host, it sets no Host list and warns instead.
Serving MCP for real
from context_engine import create_mcp_app, UNSCOPED
# Your own ASGI app, behind your own auth: principals come from the caller.
app.mount("/mcp", create_mcp_app(engine, principals=my_principals, scope=UNSCOPED))import { createMcpApp, UNSCOPED } from "@promptev/context-engine";
// Behind your own auth: principals come from the caller.
const handler = await createMcpApp(engine, { principals: myPrincipals, scope: UNSCOPED });principals and scope are both required: there is no default caller identity. Pass a narrower scope than UNSCOPED when the tool should reach only some sources. The knowledge tool reference covers the tool’s actions.
check-acl-exposure
Answers one question against your own database: for callers who can see only part of the corpus, does the vector leg return what an exact search would? It compares a bare scoped query and the engine’s own retrieval path, both scored against an exact scan computed inside Postgres.
Offline
No embedding provider is called. Query vectors are sampled from embeddings already stored, and no data leaves the process.
Read-only
No temp tables, no index changes, no ANALYZE. The only session settings are SET LOCAL.
| Flag | Default | Meaning |
|---|
| --database-url <url> | required | Postgres connection URL of a migrated database with embedded documents. |
| --samples <n> | 100 | Query vectors to sample from embeddings already stored. |
| --top-k <n> | 10 | Result depth scored against the exact answer. |
| --max-scopes <n> | 8 | How many of the most common ACL values to test. |
context-engine check-acl-exposure --database-url postgresql://user:pass@localhost:5432/mydb
# In a deployment pipeline: fails the step only when the verdict is "exposed"
context-engine check-acl-exposure --database-url "$DATABASE_URL" --samples 200 --top-k 20
npx context-engine check-acl-exposure --database-url postgresql://user:pass@localhost:5432/mydb
# In a deployment pipeline: fails the step only when the verdict is "exposed"
npx context-engine check-acl-exposure --database-url "$DATABASE_URL" --samples 200 --top-k 20
Verdicts
The report prints recall and the share of empty results per scope, then a verdict from the worst scope tested:
| Verdict | When | Exit code |
|---|
| ok | The worst scope’s recall@k is at least 0.99. | 0 |
| degraded | The worst scope’s recall@k is at least 0.90 and below 0.99. | 0 |
| exposed | The worst scope’s recall@k is below 0.90. | 1 |
| no-data | No tested scope had eligible rows. | 0 |
On exposed the report suggests what to check: pgvector 0.8 or newer, a GIN index on the ACL column, and that searches go through the library rather than raw SQL.
bm25-enable, bm25-disable, bm25-compact
The bm25 keyword ranking is experimental and opt-in per database. Measured on five public datasets it is not a default and does not improve default search. Nothing changes for a database that does not use it: the migration adds two small tables and some SQL functions and never touches the chunk table. bm25-enable installs four triggers on the chunk table (taking its lock briefly, with a 2-second timeout and retries) and counts the chunks already stored, one source at a time, without blocking ingest. Run it again at any time; it recounts. Until it has finished, a bm25 search ranks with ts_rank_cd and reports why. Enabled, each chunk write adds one small statistics row and two writers never wait on each other. A source is compacted on its own after a write once it holds more than search.bm25.compact_after_deltas rows (default 1000); bm25-compact does the same for every source on demand, which matters only when that setting is turned off. Recounts and compactions take a per-source lock that only maintenance uses, so bm25-enable, bm25-compact and the automatic compaction are safe to overlap with each other and with ingest, for example several replicas enabling at startup. bm25-disable removes the triggers and statistics. Each takes --database-url only. The library calls are enable_bm25(), disable_bm25() and compact_bm25_stats()enableBm25(), disableBm25() and compactBm25Stats() on the engine.
context-engine bm25-enable --database-url "$DATABASE_URL"
context-engine bm25-compact --database-url "$DATABASE_URL" # only needed with automatic compaction off
context-engine bm25-disable --database-url "$DATABASE_URL"
npx context-engine bm25-enable --database-url "$DATABASE_URL"
npx context-engine bm25-compact --database-url "$DATABASE_URL" # only needed with automatic compaction off
npx context-engine bm25-disable --database-url "$DATABASE_URL"
install-skill
Copies the agent skill shipped inside the package (a SKILL.md stating the library’s invariants, such as TRUSTED instead of the deprecated principals=None) into a .claude/skills/context-engine/ directory. It is an explicit command rather than an install hook, because the file becomes instructions inside your agent.
| Flag | Default | Meaning |
|---|
| --global | off | Install under your home directory: ~/.claude/skills/context-engine/SKILL.md. |
| --dir <dir> | current directory | Install under this directory instead: <dir>/.claude/skills/context-engine/SKILL.md. Wins over --global. |
| --print | off | Write the skill to stdout instead of a file, for agents that do not read .claude/skills/. |
| --force | off | Overwrite an existing skill file you have edited. |
context-engine install-skill # -> ./.claude/skills/context-engine/SKILL.md
context-engine install-skill --global # -> ~/.claude/skills/context-engine/SKILL.md
context-engine install-skill --dir ../app
context-engine install-skill --print > context-engine-skill.md
npx context-engine install-skill # -> ./.claude/skills/context-engine/SKILL.md
npx context-engine install-skill --global # -> ~/.claude/skills/context-engine/SKILL.md
npx context-engine install-skill --dir ../app
npx context-engine install-skill --print > context-engine-skill.md
Safe to re-run. If the file already matches the shipped skill it prints “Already up to date”. If you have edited it, the command refuses and exits 1 rather than overwrite your changes; pass --force to replace it.
Exit codes
| Command | Exit status |
|---|
| migrate | 0 on success. Non-zero on any error, including a --dim that differs from the recorded one. |
| mcp | Runs until interrupted. Non-zero if it cannot start (for example a missing extra or missing CE_EMBEDDING__*). |
| check-acl-exposure | 1 when the verdict is exposed. 0 for ok, degraded and no-data. Non-zero on a connection or query error. |
| bm25-enable / bm25-disable / bm25-compact | 0 on success, after printing what it did. Non-zero on a connection or query error, or when the chunk table’s lock could not be taken after every retry. |
| install-skill | 0 when installed, already up to date, or printed. 1 when the target exists, differs from the shipped skill and --force was not given. |
| any | A missing required flag or an unknown option is a usage error: exit 2 from the Python CLI (argparse), 1 from the TypeScript CLI (commander). No command or an unknown command prints the usage and exits the same way: 2 in Python, 1 in TypeScript. --help exits 0. |