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
$CLUSTERwith your cluster's HTTPS endpoint and$ADMIN/$ADMIN_PWwith an admin credential. Commands usecurl -kfor a self-signed cluster cert; drop-konce 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
LuceniaClusterinstead 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 endpointOmitting the
spec.memoryblock 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:
| Posture | Provider | Egress | Best for |
|---|---|---|---|
| Fully sovereign / air-gapped | Self-hosted OpenAI-compatible server (vLLM, TEI) | None | Classified, defense, regulated, or any team that wants zero external calls |
| Your cloud account | Bedrock / Vertex / Azure in your account | Stays in your VPC/project/tenant | You already run on a cloud and accept in-account managed models |
| Development only | Built-in hashing (explicit provider: hashing) | None | Trying 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 theorgclaim 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>"):
| Request | Expect |
|---|---|
POST /_plugins/_memory/remember / recall | 200 |
POST /_plugins/_ml/mcp (tools/list) | 200 |
PUT /eng-anything | 200 |
PUT /prod-secrets (other namespace) | 403 |
GET /_cluster/settings | 403 |
GET /_plugins/_security/api/internalusers | 403 |
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 inorg-engineeringshare 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, configurableretention_daysand complianceredact_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
complianceingest processor (GDPR/ HIPAA/PCI redaction) before it becomes a memory. - Rollup / drift: periodically
memory_rollupto summarize and compact old memories, andmemory_drift_checkto 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.