Skip to main content
Version: 0.12.0

Lucenia as the memory layer for Claude Code (MCP)

Give an AI coding agent (Claude Code) a durable, team-shared memory backed by your Lucenia cluster. Conversations and decisions survive context compaction and carry across sessions, scoped to your team's org and protected by a least-privilege personal access token (PAT). All embeddings and data stay in your cluster.

The agent connects to Lucenia's built-in MCP server (POST /_plugins/_ml/mcp), which exposes five memory tools — memory_remember, memory_recall, memory_forget, memory_drift_check, memory_rollup — as a streamable-HTTP MCP endpoint.

Replace $CLUSTER with your cluster's HTTPS endpoint and $ADMIN/$ADMIN_PW with an admin credential. Commands use curl -k for a self-signed cluster cert; drop -k once you front the cluster with a CA-signed certificate.

0. Prerequisites (one-time, admin)

Memory is disabled until an embedding model is configured, and the MCP transport is a separate switch. Enable both (one _cluster/settings call), then confirm a real model is active:

curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_cluster/settings" \
-H 'Content-Type: application/json' -d '{"persistent":{
"plugins.ml_commons.mcp_server_enabled": true,
"plugins.memory.embedding.provider": "bedrock",
"plugins.memory.embedding.model": "amazon.titan-embed-image-v1",
"plugins.memory.embedding.region": "us-east-1",
"plugins.memory.embedding.dimension": 1024
}}'

curl -k -u "$ADMIN:$ADMIN_PW" "$CLUSTER/_plugins/_memory/stats"
# expect: "mode":"amazon.titan-embed-image-v1","dimension":1024,"semantic":true
# ("mode":"disabled" means no model is configured; "hashing-v1" is the dev-only embedder)

One-stanza alternative (recommended for the operator). If you run the Lucenia Kubernetes operator, declare the whole feature in the LuceniaCluster instead of PUTing settings by hand — the operator fans it out to the same cluster settings:

spec:
memory:
embedding: { provider: bedrock, model: amazon.titan-embed-image-v1, region: us-east-1, dimension: 1024 }
tenancy: { orgBackendRolePrefix: "org-" } # see step 2
mcp: true # expose over the MCP endpoint

Omitting the spec.memory block leaves memory unmanaged (existing settings untouched).

0b. Choose your embedder — sovereignty is a dial

Recall quality comes from the embedding model, and which model you choose decides whether any data leaves your network. All three postures below give you a working memory layer; they differ only in where the embedding step runs.

Memory is disabled until you configure a model — with no provider set, remember/recall return a clear "not configured" error (there is no silent low-quality default). Pick a posture and set it:

PostureProviderEgressBest for
Fully sovereign / air-gappedSelf-hosted OpenAI-compatible server (vLLM, TEI)NoneClassified, defense, regulated, or any team that wants zero external calls
Your cloud accountBedrock / Vertex / Azure in your accountStays in your VPC/project/tenantYou already run on a cloud and accept in-account managed models
Development onlyBuilt-in hashing (explicit provider: hashing)NoneTrying it out; recall is lexical-only, not semantic — not for production

For the fully sovereign vibe-coding experience, point memory at a self-hosted embedding model on your own GPUs. Any endpoint that speaks the OpenAI /embeddings shape works — for example a vLLM or Text Embeddings Inference (TEI) server hosting a model like BAAI/bge-large-en-v1.5 (1024-dim) or nomic-embed-text-v1.5:

# optional: only if your embedding server requires auth
# bin/lucenia-keystore add plugins.memory.embedding.api_key (then restart the node)

curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_cluster/settings" \
-H 'Content-Type: application/json' -d '{"persistent":{
"plugins.memory.embedding.provider": "http",
"plugins.memory.embedding.endpoint": "http://embeddings.internal.svc:8080/v1/embeddings",
"plugins.memory.embedding.model": "BAAI/bge-large-en-v1.5",
"plugins.memory.embedding.dimension": 1024
}}'

Set dimension to the model's true output width — it is locked into the memory index at creation, so set it before the first remember. Confirm the dial with GET /_plugins/_memory/stats ("semantic": true means a real model is active). Full provider matrix — self-hosted, Bedrock, Vertex, Azure, and multimodal image memory — is in Inference memory embeddings, and the sovereignty model behind it is in Security, privacy, and data sovereignty.

1. Provision the team org (admin)

Each org gets physically-isolated memory indices. Provision engineering with the same model/dimension as the deployment embedder:

curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_plugins/_memory/org/engineering" \
-H 'Content-Type: application/json' \
-d '{"model":"amazon.titan-embed-image-v1","dimension":1024,"provider":"bedrock","region":"us-east-1"}'

2. Source the org from a backend role (admin)

A PAT carries its user's backend roles but not custom attributes, so derive the org from a backend-role prefix. With prefix org-, a user in backend role org-engineering resolves to org engineering:

curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_cluster/settings" \
-H 'Content-Type: application/json' \
-d '{"persistent":{"plugins.memory.tenancy.org_backend_role_prefix":"org-"}}'

Using OIDC/JWT instead of native users? Leave this unset and keep the default plugins.memory.tenancy.org_attribute=attr.jwt.org — the org comes from the org claim in the token.

3. Roles: memory + index + self-service PATs (admin)

memory_read_write ships with the product (full memory verbs + MCP access). Add a scoped index role and a self-service api-key role:

# create + read/write/search on eng-* indices only
curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_plugins/_security/api/roles/engineering_index" \
-H 'Content-Type: application/json' -d '{
"cluster_permissions":["cluster:monitor/main"],
"index_permissions":[{"index_patterns":["eng-*"],
"allowed_actions":["create_index","crud","search","indices:admin/mapping/put","indices:admin/get"]}]}'

# let users mint/revoke their OWN PATs (the subset guard still prevents privilege escalation)
curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_plugins/_security/api/roles/apikey_self" \
-H 'Content-Type: application/json' \
-d '{"cluster_permissions":["security:apikey/create","security:apikey/list","security:apikey/get","security:apikey/invalidate"]}'

4. User + group mapping (admin)

Create the non-admin engineer. Two backend roles: engineering (the group, mapped to roles) and org-engineering (drives the memory org):

curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_plugins/_security/api/internalusers/eng-claude" \
-H 'Content-Type: application/json' \
-d '{"password":"<STRONG_PASSWORD>","backend_roles":["engineering","org-engineering"]}'

# map the engineering group -> the three scoped roles
for role in memory_read_write engineering_index apikey_self; do
curl -k -u "$ADMIN:$ADMIN_PW" -XPUT "$CLUSTER/_plugins/_security/api/rolesmapping/$role" \
-H 'Content-Type: application/json' -d '{"backend_roles":["engineering"]}'
done

5. Mint the 365-day PAT — as the user (least privilege by construction)

Minting the token as eng-claude makes it inherit only that user's roles. It can do memory + eng-* indices and nothing else — no cluster admin, no other namespaces:

curl -k -u "eng-claude:<STRONG_PASSWORD>" -XPUT "$CLUSTER/_plugins/_security/api/apikey" \
-H 'Content-Type: application/json' \
-d '{"name":"claude-code-eng","expiration":"365d"}'
# → copy the "api_key" value (shown once). Revoke anytime: DELETE /_plugins/_security/api/apikey/{id}

Verify least privilege (using -H "Authorization: ApiKey <PAT>"):

RequestExpect
POST /_plugins/_memory/remember / recall200
POST /_plugins/_ml/mcp (tools/list)200
PUT /eng-anything200
PUT /prod-secrets (other namespace)403
GET /_cluster/settings403
GET /_plugins/_security/api/internalusers403

6. Connect Claude Code

Reach the cluster (ClusterIP → port-forward; leave running):

kubectl -n <namespace> port-forward svc/<cluster-svc> 9200:9200

Add the MCP server (streamable-HTTP transport + PAT header):

claude mcp add --transport http --scope user lucenia-memory \
https://localhost:9200/_plugins/_ml/mcp \
--header "Authorization: ApiKey <PAT>"

TLS: the cert is self-signed and its SAN is the internal cluster hostname, so for a port-forwarded dev connection launch Claude Code with verification off:

NODE_TLS_REJECT_UNAUTHORIZED=0 claude

Production: front the cluster with an ingress/LB presenting a CA-signed cert for a real hostname, point the MCP URL at that hostname, and drop the env var (optionally trust a private CA via NODE_EXTRA_CA_CERTS).

Verify:

claude mcp list          # lucenia-memory: connected
# in the REPL: /mcp -> shows the 5 memory tools

7. Make it survive compaction (the important part)

The tools are available, but the agent only uses them if instructed. Add this to your project (or user) CLAUDE.md:

## Persistent memory (Lucenia)
You have a durable, team-shared memory via the `lucenia-memory` MCP server. Use it so
context survives compaction and carries across sessions.

- At the START of a session and immediately AFTER any context compaction, call
`memory_recall` with a query describing the current task to reload prior decisions,
file locations, and gotchas before acting.
- Proactively `memory_remember` durable facts as you work: architectural decisions and
their rationale, where key code lives (`file:symbol`), non-obvious constraints and
gotchas, chosen approaches, and open TODOs. One self-contained fact per memory.
- Prefer recalling over re-deriving. If unsure whether something was decided, recall first.
- Store POINTERS, not payloads — never secrets, credentials, or large code blobs.
- Memory is shared with the engineering team, so write memories useful to teammates.

Optionally, force a recall at every session start with a hook in .claude/settings.json:

{ "hooks": { "SessionStart": [{ "hooks": [{ "type": "prompt",
"prompt": "Call memory_recall for the current working directory and open task, then summarize what you already know before proceeding." }] }] } }

8. Production hardening checklist

  • CA-signed cert + stable hostname (ingress/LB) so you can drop NODE_TLS_REJECT_UNAUTHORIZED.
  • Per-engineer PATs, not a shared token — each engineer mints their own (apikey_self), so revocation and the audit trail are per-person. All in org-engineering share the memory.
  • PAT rotation: 365 days is a ceiling; rotate quarterly. Revoke instantly via DELETE /_plugins/_security/api/apikey/{id}.
  • Audit who did what: enable plugins.memory.audit.enabled=true (per-org, async, configurable retention_days and compliance redact_profile) — every action is recorded in the per-org .plugins-memory-audit-<org> index, bound to the verified identity. See Security, privacy, and data sovereignty.
  • Compliance: run sensitive content through the compliance ingest processor (GDPR/ HIPAA/PCI redaction) before it becomes a memory.
  • Rollup / drift: periodically memory_rollup to summarize and compact old memories, and memory_drift_check to surface stale/contradicted facts.
  • Manage security config via the operator (spec.security.config.securityConfigFiles) rather than ad-hoc REST calls, so the roles/users above are declarative and versioned.