Project memory for AI agents. When an agent pushes to git, it registers what it did and why — not the diff, the reasoning. Any other agent on the project reads that context via MCP before touching the same code.
No external model API required. The agent writing the change is the model.
Two processes, split by what each can own:
core/— Go (Fiber v3 + pgx). One long-lived daemon per machine. Owns PostgreSQL and the full-text search. Plain HTTP+SQL — it never runs git, never speaks MCP, never calls a model. Auth via static Bearer token.mcp/— Node (MCP SDK). The per-agent shell, spawned over stdio by Claude Code, one process per conversation. Speaks MCP, runsgitlocally in the repo (the part Go can't do), forwards tool calls to the Core. Holds no state.
In register_change: the agent writes the semantic fields (what/why/decisions/
risks); the MCP layer adds the mechanical ones from git (sha/branch/files/diff);
the Core upserts by (repository_id, sha) and regenerates the FTS vector.
Everything is wired through the Makefile (make lists all targets):
make up # Postgres + Redis + Ollama (pulls the embed model on first run)
make build # Core + CLI -> bin/, and the MCP server -> mcp/dist
make dev # up + run the Core in the foreground on 127.0.0.1:8742make dev is the one-liner for local work: it brings the stack up and runs the
Core with Redis + embeddings enabled. In another terminal:
make smoke # end-to-end MCP cycle against the running Core
make test # go vet + integration tests + MCP typecheckWire another repo into Cortex with the CLI:
./bin/cortex init --project myproj --token <token>
# writes .cortex.yml and merges .mcp.json without clobbering other MCP serversManual equivalent (no make)
docker compose up -d
cd core && CORTEX_REDIS=redis://127.0.0.1:6380 CORTEX_OLLAMA=http://127.0.0.1:11434 go run ./cmd/server
cd mcp && npm install && npm run buildCortex is multi-tenant: an org is the tenant boundary, owns its projects, and issues tokens with a role. One org never sees another's data — even with the same project name.
- Roles:
read(GET search/context/recent/patterns/check),write(+ the POST endpoints),admin(everything; today used only from the CLI). - Auth: every API token is an
api_keysrow; the bearer is sha256-hashed and looked up per request. Revoke a token and it's401immediately. - Admin is a CLI that talks straight to Postgres via
CORTEX_DSN(no HTTP), so bootstrap has no chicken-and-egg:
cortex admin org create acme
cortex admin token create --org acme --role write --label ci-bot
# -> ctx_live_… (shown ONCE — store it now)
cortex admin token list --org acme
cortex admin token revoke <id-prefix>Wire the issued token into an agent with cortex init --token <ctx_live_…> (it
goes into .mcp.json as CORTEX_TOKEN). The token determines the org; the
project field of each MCP tool is a name within that org.
In dev, the Core seeds an org cortex + token dev-token on startup when
CORTEX_DEV=1 (set by make dev/make core), so local flows just work.
One command brings up the full stack including the Core in Docker (built from
core/Dockerfile as a static binary). The Core talks to Postgres/Redis/Ollama by
service name; a real token is required.
make prod CORTEX_TOKEN=<your-secret> # build images, recreate containers, wait healthy
make prod-ps # status
make prod-logs # tail Core logs
make prod-down # stop (data volumes preserved)make prod recreates containers with fresh images but preserves data volumes
(it never wipes the project memory). It refuses to start with the default
dev-token. The dev-only Core runs on the host (make dev); the core service
is gated behind the compose prod profile so it never starts in dev.
CORTEX_TOKEN in prod is the bootstrap admin secret — there's no dev seed.
Once up, mint real per-agent tokens with cortex admin token create (see
Multi-tenancy & tokens) rather than sharing the admin
token.
- stdio (default):
node mcp/dist/index.js— spawned per-agent by Claude Code. - Streamable HTTP (remote / CI):
npm run start:http(portCORTEX_MCP_HTTP_PORT, default 9000, at/mcp; optionalCORTEX_MCP_TOKENbearer gate). SSE is not used — it was removed server-side from the MCP SDK.
All semantic fields (what/why/decisions/risks, knowledge, features, ADRs) are stored in English, regardless of the conversation language. Cortex is a tool for agents, so a single language keeps the FTS (and any future embedding model) on one English-tuned index — mixing languages degrades search. This is enforced where the agent actually reads it: the MCP tool schema descriptions instruct "write in English". (A Claude Code hook can't force a tool call, so the schema is the right enforcement point, not a hook.)
When CORTEX_REDIS is set, Core publishes a JSON ChangeEvent to
cortex:{project}:changes after every registered change, so other agents/services
on the project can react. No Redis configured → publishing is a no-op.
Search is FTS by default. When CORTEX_OLLAMA is set (e.g.
http://127.0.0.1:11434), Core also embeds each change/knowledge entry with a
local model (nomic-embed-text, 768-dim, via Ollama — no external API) and
fuses keyword + vector rankings with Reciprocal Rank Fusion. This surfaces
relevant prior work even when the query shares no keywords with it. docker compose up provisions Ollama and pulls the model automatically; embeddings are
generated off the write path and search degrades to FTS if Ollama is down.
CORTEX_OLLAMA=http://127.0.0.1:11434 CORTEX_REDIS=redis://127.0.0.1:6380 \
go run ./core/cmd/server.claude/skills/ ships two skills so agents use Cortex without being told:
- cortex-recall — consult context (get_project_context / search_knowledge / check_feature_exists) before starting work.
- cortex-log — record what & why (register_change / add_decision / upsert_feature / add_knowledge) after committing.
Write: register_change, add_knowledge, add_decision, upsert_feature.
Read: search_knowledge (commits + knowledge), get_recent_changes,
get_project_context, get_patterns, check_feature_exists.
Resource: cortex://context/{project} (mountable project summary).
Prompts: analyze-impact, whats-left-for-feature.
Smoke tests of the full cycle live in mcp/scripts/e2e*.mjs. The Go store has
an integration test gated on CORTEX_TEST_DSN:
CORTEX_TEST_DSN="postgres://cortex:cortex@127.0.0.1:5433/cortex?sslmode=disable" \
go test ./core/internal/store -run TestStore -v