-
Notifications
You must be signed in to change notification settings - Fork 327
Docker-first defaults: 18080 port, project scoping, MCP update/delete #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Docker-first defaults: 18080 port, project scoping, MCP update/delete #112
Conversation
|
Hi salacoste, thank you for your contribution. However, we require mandatory tests for every new pull request. Please conduct the tests and send the results along with the script. |
|
Ran the mandatory PR tests locally using the provided script added in this PR. Script
Command
Results (PASS)
The script writes a full log to |
|
Hi salacoste, i don't see the code of the script. Also the script should explicity test the changes you made, each function of it. |
|
thanks for the review. I ran a local validation that explicitly tests the changes introduced in this PR (MCP/HTTP service + new MCP tools + transport compatibility + Where to find the script (in repo):
How to reproduce locally:
What the script covers:
Attached to this comment:
PR112-validation-report.md PR112 Validation Report
Test Script (full)#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOG_DIR="${ROOT_DIR}/test-results"
STAMP="$(date -u +"%Y%m%dT%H%M%SZ")"
RUN_DIR="${LOG_DIR}/pr112-${STAMP}"
LOG_FILE="${RUN_DIR}/run.log"
REPORT_FILE="${LOG_DIR}/PR112-validation-report.md"
mkdir -p "${RUN_DIR}"
exec > >(tee -a "${LOG_FILE}") 2>&1
echo "[pr-tests] start: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "[pr-tests] repo: ${ROOT_DIR}"
echo "[pr-tests] run_dir: ${RUN_DIR}"
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "[pr-tests] missing required command: $1" >&2
exit 127
fi
}
section() {
echo
echo "============================================================"
echo "[pr-tests] $1"
echo "============================================================"
}
pick_free_port() {
python3 - <<'PY'
import socket
for port in range(18081, 18151):
s = socket.socket()
try:
s.bind(("127.0.0.1", port))
print(port)
raise SystemExit(0)
except OSError:
continue
finally:
try:
s.close()
except Exception:
pass
raise SystemExit("no free port in range 18081-18150")
PY
}
expect_http_code() {
local want="$1"
local got="$2"
local label="$3"
if [[ "${got}" != "${want}" ]]; then
echo "[pr-tests] ❌ ${label}: expected HTTP ${want}, got ${got}" >&2
return 1
fi
echo "[pr-tests] ✅ ${label}: HTTP ${got}"
}
extract_mcp_session_id() {
local headers_file="$1"
python3 - <<'PY' "${headers_file}"
import re, sys
headers = open(sys.argv[1], "r", encoding="utf-8", errors="ignore").read().splitlines()
for line in headers:
m = re.match(r"(?i)^mcp-session-id:\\s*(.+?)\\s*$", line.strip())
if m:
print(m.group(1).strip())
raise SystemExit(0)
print("")
raise SystemExit(0)
PY
}
json_extract() {
local file="$1"
local expr="$2"
python3 - <<'PY' "${file}" "${expr}"
import json, sys
data = json.load(open(sys.argv[1], "r", encoding="utf-8"))
expr = sys.argv[2].strip()
def get_path(obj, path):
cur = obj
for part in path.split("."):
if part.endswith("]"):
name, idx = part[:-1].split("[", 1)
if name:
cur = cur[name]
cur = cur[int(idx)]
else:
cur = cur[part]
return cur
val = get_path(data, expr)
if isinstance(val, (dict, list)):
print(json.dumps(val))
else:
print(val)
PY
}
run_in_docker_node() {
local image="node:22-bullseye"
docker run --rm \
-v "${ROOT_DIR}:/repo" \
-w "/repo/packages/openmemory-js" \
-e OM_DB_URL="sqlite:///:memory:" \
-e OM_TIER="fast" \
-e OM_VEC_DIM="1536" \
"${image}" \
bash -lc 'set -euo pipefail; apt-get update -y >/dev/null; apt-get install -y python3 make g++ >/dev/null; node -v; npm -v; npm ci; npm run build; npx tsx tests/test_omnibus.ts'
}
run_in_docker_python() {
local image="python:3.11"
docker run --rm \
-v "${ROOT_DIR}:/repo" \
-w "/repo/packages/openmemory-py" \
"${image}" \
bash -lc 'set -euo pipefail; python --version; pip --version; pip install -e ".[dev]"; pytest tests/test_omnibus.py -v'
}
run_mcp_http_smoke() {
local image_tag="$1"
local api_key="pr112-test-key"
local default_user="pr112-user"
local host_port
host_port="$(pick_free_port)"
local container_port="18080"
local container_name="openmemory-pr112-${STAMP}"
local volume_name="openmemory-pr112-data-${STAMP}"
section "Run container for MCP/HTTP smoke (port ${host_port} -> ${container_port})"
echo "[pr-tests] building/running with api_key=${api_key} default_user=${default_user}"
# Persist identifiers outside of function scope because EXIT traps run
# after locals are unset (macOS bash + set -u).
PRTEST_CONTAINER_NAME="${container_name}"
PRTEST_VOLUME_NAME="${volume_name}"
docker volume create "${volume_name}" >/dev/null
cleanup() {
if [[ "${KEEP_DOCKER:-}" == "1" ]]; then
echo "[pr-tests] KEEP_DOCKER=1 set; skipping container/volume cleanup"
return
fi
if [[ -n "${PRTEST_CONTAINER_NAME:-}" ]]; then
docker rm -f "${PRTEST_CONTAINER_NAME}" >/dev/null 2>&1 || true
fi
if [[ -n "${PRTEST_VOLUME_NAME:-}" ]]; then
docker volume rm -f "${PRTEST_VOLUME_NAME}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
docker run -d --rm \
--name "${container_name}" \
-e "OM_PORT=${container_port}" \
-e "OM_API_KEY=${api_key}" \
-e "OM_DEFAULT_USER_ID=${default_user}" \
-e "OM_USE_SUMMARY_ONLY=false" \
-e "OM_MAX_PAYLOAD_SIZE=1048576" \
-e "OM_MODE=standard" \
-e "OM_TIER=hybrid" \
-e "OM_EMBEDDINGS=synthetic" \
-e "OM_EMBEDDING_FALLBACK=synthetic" \
-e "OM_METADATA_BACKEND=sqlite" \
-e "OM_VECTOR_BACKEND=sqlite" \
-e "OM_DB_PATH=/data/openmemory.sqlite" \
-v "${volume_name}:/data" \
-p "${host_port}:${container_port}" \
"${image_tag}" >/dev/null
local base="http://127.0.0.1:${host_port}"
section "Wait for /health"
local deadline=$((SECONDS + 60))
until curl -fsS "${base}/health" >/dev/null 2>&1; do
if (( SECONDS > deadline )); then
echo "[pr-tests] ❌ healthcheck timeout; container logs:" >&2
docker logs "${container_name}" >&2 || true
exit 1
fi
sleep 1
done
curl -fsS "${base}/health" | tee "${RUN_DIR}/health.json" >/dev/null
echo "[pr-tests] ✅ healthy: ${base}"
section "Auth required (HTTP)"
local code
code="$(curl -sS -o /dev/null -w "%{http_code}" "${base}/memory/all")"
expect_http_code "401" "${code}" "GET /memory/all without key"
code="$(curl -sS -o /dev/null -w "%{http_code}" -H "x-api-key: ${api_key}" "${base}/memory/all")"
expect_http_code "200" "${code}" "GET /memory/all with key"
section "HTTP CRUD: add/get/patch/delete"
curl -sS -H "Content-Type: application/json" -H "x-api-key: ${api_key}" \
--data "$(python3 - <<'PY'
import json
print(json.dumps({
"content": "http-add-content",
"tags": ["pr112", "http"],
"metadata": {"source": "pr112-run-pr-tests"},
"user_id": "pr112-user",
}))
PY
)" \
"${base}/memory/add" | tee "${RUN_DIR}/http-memory-add.json" >/dev/null
local http_id
http_id="$(python3 - <<'PY' "${RUN_DIR}/http-memory-add.json"
import json, sys
data = json.load(open(sys.argv[1]))
print(data["id"])
PY
)"
echo "[pr-tests] http memory id: ${http_id}"
curl -sS -H "x-api-key: ${api_key}" \
"${base}/memory/${http_id}?user_id=${default_user}" | tee "${RUN_DIR}/http-memory-get.json" >/dev/null
curl -sS -X PATCH -H "Content-Type: application/json" -H "x-api-key: ${api_key}" \
--data "$(python3 - <<'PY'
import json
print(json.dumps({
"content": "http-updated-content",
"tags": ["pr112", "http", "updated"],
"metadata": {"source": "pr112-run-pr-tests", "updated": True},
"user_id": "pr112-user",
}))
PY
)" \
"${base}/memory/${http_id}" | tee "${RUN_DIR}/http-memory-patch.json" >/dev/null
curl -sS -H "x-api-key: ${api_key}" \
"${base}/memory/${http_id}?user_id=${default_user}" | tee "${RUN_DIR}/http-memory-get-after-patch.json" >/dev/null
local http_content
http_content="$(python3 - <<'PY' "${RUN_DIR}/http-memory-get-after-patch.json"
import json, sys
data = json.load(open(sys.argv[1]))
print(data.get("content",""))
PY
)"
if [[ "${http_content}" != "http-updated-content" ]]; then
echo "[pr-tests] ❌ HTTP PATCH did not update content (got '${http_content}')" >&2
exit 1
fi
echo "[pr-tests] ✅ HTTP PATCH updated content"
curl -sS -X DELETE -H "x-api-key: ${api_key}" \
"${base}/memory/${http_id}?user_id=${default_user}" | tee "${RUN_DIR}/http-memory-delete.json" >/dev/null
code="$(curl -sS -o /dev/null -w "%{http_code}" -H "x-api-key: ${api_key}" "${base}/memory/${http_id}?user_id=${default_user}")"
expect_http_code "404" "${code}" "GET /memory/:id after delete"
section "MCP transport: GET /mcp (SSE headers)"
set +e
curl -sS -D "${RUN_DIR}/mcp-sse.headers" -o /dev/null \
--max-time 2 \
-H "Accept: text/event-stream" \
-H "x-api-key: ${api_key}" \
"${base}/mcp"
local curl_rc=$?
set -e
if [[ "${curl_rc}" -ne 0 && "${curl_rc}" -ne 18 && "${curl_rc}" -ne 28 ]]; then
echo "[pr-tests] ❌ SSE probe curl failed with rc=${curl_rc}" >&2
exit 1
fi
python3 - <<'PY' "${RUN_DIR}/mcp-sse.headers"
import sys
hdr = open(sys.argv[1], "r", encoding="utf-8", errors="ignore").read().lower()
assert "200" in hdr.splitlines()[0]
assert "content-type:" in hdr
assert "text/event-stream" in hdr
print("ok")
PY
echo "[pr-tests] ✅ SSE headers OK"
section "MCP initialize + tools/list"
curl -sS -D "${RUN_DIR}/mcp-init.headers" -o "${RUN_DIR}/mcp-init.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
--data "$(python3 - <<'PY'
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":1,
"method":"initialize",
"params":{
"protocolVersion":"2024-11-05",
"capabilities":{},
"clientInfo":{"name":"pr112-run-pr-tests","version":"0.0.0"}
}
}))
PY
)" \
"${base}/mcp"
local mcp_sid
mcp_sid="$(extract_mcp_session_id "${RUN_DIR}/mcp-init.headers")"
if [[ -n "${mcp_sid}" ]]; then
echo "[pr-tests] mcp-session-id: ${mcp_sid}"
else
echo "[pr-tests] mcp-session-id: (none; stateless transport)"
fi
mcp_session_header() {
if [[ -n "${mcp_sid}" ]]; then
printf '%s\n' "-H" "mcp-session-id: ${mcp_sid}"
fi
}
curl -sS -D "${RUN_DIR}/mcp-tools.headers" -o "${RUN_DIR}/mcp-tools.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
"${base}/mcp"
python3 - <<'PY' "${RUN_DIR}/mcp-tools.json"
import json, sys
data = json.load(open(sys.argv[1]))
names = [t["name"] for t in data["result"]["tools"]]
need = ["openmemory_store","openmemory_query","openmemory_list","openmemory_get","openmemory_reinforce","openmemory_update","openmemory_delete"]
missing = [n for n in need if n not in names]
if missing:
raise SystemExit("missing tools: " + ", ".join(missing))
print("ok")
PY
echo "[pr-tests] ✅ tools/list includes update/delete"
section "MCP: store (default user_id from env) -> get -> update -> get -> reinforce -> delete -> get"
curl -sS -o "${RUN_DIR}/mcp-store.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<'PY'
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":3,
"method":"tools/call",
"params":{
"name":"openmemory_store",
"arguments":{
"content":"mcp-store-content",
"tags":["pr112","mcp"],
"metadata":{"source":"pr112-run-pr-tests"}
}
}
}))
PY
)" \
"${base}/mcp"
local mcp_store_json
mcp_store_json="$(json_extract "${RUN_DIR}/mcp-store.json" "result.content[1].text")"
echo "${mcp_store_json}" > "${RUN_DIR}/mcp-store.payload.json"
local mcp_id
mcp_id="$(python3 - <<'PY' "${RUN_DIR}/mcp-store.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
print(payload["hsg"]["id"])
PY
)"
local mcp_user
mcp_user="$(python3 - <<'PY' "${RUN_DIR}/mcp-store.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
print(payload.get("user_id") or "")
PY
)"
echo "[pr-tests] stored mcp id: ${mcp_id} (user_id='${mcp_user}')"
if [[ "${mcp_user}" != "${default_user}" ]]; then
echo "[pr-tests] ❌ MCP default user_id mismatch (expected '${default_user}', got '${mcp_user}')" >&2
exit 1
fi
echo "[pr-tests] ✅ MCP store used default user_id"
curl -sS -o "${RUN_DIR}/mcp-get.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":4,
"method":"tools/call",
"params":{"name":"openmemory_get","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
"${base}/mcp"
local mcp_get_payload
mcp_get_payload="$(json_extract "${RUN_DIR}/mcp-get.json" "result.content[0].text")"
echo "${mcp_get_payload}" > "${RUN_DIR}/mcp-get.payload.json"
python3 - <<'PY' "${RUN_DIR}/mcp-get.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
assert payload["content"] == "mcp-store-content"
print("ok")
PY
echo "[pr-tests] ✅ MCP get returned full content"
section "MCP: large content is not truncated (OM_USE_SUMMARY_ONLY=false)"
python3 - <<'PY' > "${RUN_DIR}/mcp-big-content.txt"
tail = "<<<TAIL-MUST-SURVIVE>>>"
print(("x" * 20000) + tail)
PY
curl -sS -o "${RUN_DIR}/mcp-store-big.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
big = open("${RUN_DIR}/mcp-big-content.txt", "r").read()
print(json.dumps({
"jsonrpc":"2.0",
"id":41,
"method":"tools/call",
"params":{
"name":"openmemory_store",
"arguments":{
"content": big,
"tags":["pr112","mcp","big"],
"metadata":{"source":"pr112-run-pr-tests","kind":"big"}
}
}
}))
PY
)" \
"${base}/mcp"
local mcp_big_store_json
mcp_big_store_json="$(json_extract "${RUN_DIR}/mcp-store-big.json" "result.content[1].text")"
echo "${mcp_big_store_json}" > "${RUN_DIR}/mcp-store-big.payload.json"
local mcp_big_id
mcp_big_id="$(python3 - <<'PY' "${RUN_DIR}/mcp-store-big.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
print(payload["hsg"]["id"])
PY
)"
curl -sS -o "${RUN_DIR}/mcp-get-big.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":42,
"method":"tools/call",
"params":{"name":"openmemory_get","arguments":{"id":"${mcp_big_id}"}}
}))
PY
)" \
"${base}/mcp"
local mcp_big_payload
mcp_big_payload="$(json_extract "${RUN_DIR}/mcp-get-big.json" "result.content[0].text")"
echo "${mcp_big_payload}" > "${RUN_DIR}/mcp-get-big.payload.json"
python3 - <<'PY' "${RUN_DIR}/mcp-get-big.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
content = payload["content"]
assert content.rstrip("\n").endswith("<<<TAIL-MUST-SURVIVE>>>")
assert len(content) > 20000
print("ok")
PY
echo "[pr-tests] ✅ MCP get preserved big content tail"
curl -sS -o "${RUN_DIR}/mcp-delete-big.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":43,
"method":"tools/call",
"params":{"name":"openmemory_delete","arguments":{"id":"${mcp_big_id}"}}
}))
PY
)" \
"${base}/mcp"
curl -sS -o "${RUN_DIR}/mcp-update.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":5,
"method":"tools/call",
"params":{
"name":"openmemory_update",
"arguments":{
"id":"${mcp_id}",
"content":"mcp-updated-content",
"tags":["pr112","mcp","updated"],
"metadata":{"source":"pr112-run-pr-tests","updated":True}
}
}
}))
PY
)" \
"${base}/mcp"
curl -sS -o "${RUN_DIR}/mcp-get-after-update.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":6,
"method":"tools/call",
"params":{"name":"openmemory_get","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
"${base}/mcp"
local mcp_get2_payload
mcp_get2_payload="$(json_extract "${RUN_DIR}/mcp-get-after-update.json" "result.content[0].text")"
echo "${mcp_get2_payload}" > "${RUN_DIR}/mcp-get-after-update.payload.json"
python3 - <<'PY' "${RUN_DIR}/mcp-get-after-update.payload.json"
import json, sys
payload = json.loads(open(sys.argv[1]).read())
assert payload["content"] == "mcp-updated-content"
assert payload["metadata"]["updated"] is True
assert "updated" in payload["tags"]
print("ok")
PY
echo "[pr-tests] ✅ MCP update applied (content/tags/metadata)"
curl -sS -o "${RUN_DIR}/mcp-reinforce.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":7,
"method":"tools/call",
"params":{"name":"openmemory_reinforce","arguments":{"id":"${mcp_id}","boost":0.2}}
}))
PY
)" \
"${base}/mcp"
curl -sS -o "${RUN_DIR}/mcp-delete.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":8,
"method":"tools/call",
"params":{"name":"openmemory_delete","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
"${base}/mcp"
curl -sS -o "${RUN_DIR}/mcp-get-after-delete.json" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
--data "$(python3 - <<PY
import json
print(json.dumps({
"jsonrpc":"2.0",
"id":9,
"method":"tools/call",
"params":{"name":"openmemory_get","arguments":{"id":"${mcp_id}"}}
}))
PY
)" \
"${base}/mcp"
local after_delete_text
after_delete_text="$(json_extract "${RUN_DIR}/mcp-get-after-delete.json" "result.content[0].text")"
if [[ "${after_delete_text}" != *"not found"* ]]; then
echo "[pr-tests] ❌ MCP get after delete did not report not found" >&2
echo "[pr-tests] payload: ${after_delete_text}" >&2
exit 1
fi
echo "[pr-tests] ✅ MCP delete removed memory"
section "MCP transport: DELETE /mcp closes session"
code="$(curl -sS -o /dev/null -w "%{http_code}" -X DELETE \
-H "Accept: application/json, text/event-stream" \
-H "x-api-key: ${api_key}" \
$(mcp_session_header) \
"${base}/mcp")"
if [[ "${code}" != "200" && "${code}" != "204" ]]; then
echo "[pr-tests] ❌ DELETE /mcp unexpected HTTP ${code}" >&2
exit 1
fi
echo "[pr-tests] ✅ DELETE /mcp returned HTTP ${code}"
section "Persistence: restart container keeps stored memories"
curl -sS -H "Content-Type: application/json" -H "x-api-key: ${api_key}" \
--data "$(python3 - <<'PY'
import json
print(json.dumps({
"content": "persistence-check-content",
"tags": ["pr112", "persist"],
"metadata": {"source": "pr112-run-pr-tests"},
"user_id": "pr112-user",
}))
PY
)" \
"${base}/memory/add" | tee "${RUN_DIR}/persist-add.json" >/dev/null
local persist_id
persist_id="$(python3 - <<'PY' "${RUN_DIR}/persist-add.json"
import json, sys
data = json.load(open(sys.argv[1]))
print(data["id"])
PY
)"
docker rm -f "${container_name}" >/dev/null
docker run -d --rm \
--name "${container_name}" \
-e "OM_PORT=${container_port}" \
-e "OM_API_KEY=${api_key}" \
-e "OM_DEFAULT_USER_ID=${default_user}" \
-e "OM_USE_SUMMARY_ONLY=false" \
-e "OM_MAX_PAYLOAD_SIZE=1048576" \
-e "OM_MODE=standard" \
-e "OM_TIER=hybrid" \
-e "OM_EMBEDDINGS=synthetic" \
-e "OM_EMBEDDING_FALLBACK=synthetic" \
-e "OM_METADATA_BACKEND=sqlite" \
-e "OM_VECTOR_BACKEND=sqlite" \
-e "OM_DB_PATH=/data/openmemory.sqlite" \
-v "${volume_name}:/data" \
-p "${host_port}:${container_port}" \
"${image_tag}" >/dev/null
deadline=$((SECONDS + 60))
until curl -fsS "${base}/health" >/dev/null 2>&1; do
if (( SECONDS > deadline )); then
echo "[pr-tests] ❌ healthcheck timeout after restart; container logs:" >&2
docker logs "${container_name}" >&2 || true
exit 1
fi
sleep 1
done
code="$(curl -sS -o /dev/null -w "%{http_code}" -H "x-api-key: ${api_key}" "${base}/memory/${persist_id}?user_id=${default_user}")"
expect_http_code "200" "${code}" "GET /memory/:id after restart (persistence)"
curl -sS -H "x-api-key: ${api_key}" \
"${base}/memory/${persist_id}?user_id=${default_user}" | tee "${RUN_DIR}/persist-get-after-restart.json" >/dev/null
python3 - <<'PY' "${RUN_DIR}/persist-get-after-restart.json"
import json, sys
data = json.load(open(sys.argv[1]))
assert data.get("content") == "persistence-check-content"
print("ok")
PY
echo "[pr-tests] ✅ persistence content verified"
}
write_report() {
section "Write Markdown report"
local big_len="unknown"
if [[ -f "${RUN_DIR}/mcp-big-content.txt" ]]; then
big_len="$(python3 -c "print(len(open('${RUN_DIR}/mcp-big-content.txt','r').read()))")"
fi
{
echo "# PR112 Validation Report"
echo
echo "- Generated (UTC): $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "- Repo: ${ROOT_DIR}"
echo "- Commit: $(git -C "${ROOT_DIR}" rev-parse HEAD)"
echo "- Script: scripts/run-pr-tests.sh"
echo "- Run dir: ${RUN_DIR}"
echo
echo "## Test Script (full)"
echo
echo "\`\`\`bash"
cat "${ROOT_DIR}/scripts/run-pr-tests.sh"
echo "\`\`\`"
echo
echo "## What Was Tested"
echo
echo "### SDK (containerized)"
echo "- Node SDK omnibus: build + tests/test_omnibus.ts"
echo "- Python SDK omnibus: pytest tests/test_omnibus.py"
echo
echo "### Service (Docker image)"
echo "- Build image from packages/openmemory-js/Dockerfile"
echo "- Start container + /health"
echo "- Auth required for HTTP API"
echo "- HTTP CRUD: /memory/add, /memory/:id (GET/PATCH/DELETE)"
echo "- MCP transport: GET SSE headers, POST JSON-RPC, DELETE /mcp"
echo "- MCP tools: tools/list includes update/delete; store/get/update/reinforce/delete flow"
echo "- Default user_id from OM_DEFAULT_USER_ID for MCP calls without user_id"
echo "- Persistence: restart container with same volume keeps data"
echo
echo "## Results (artifacts)"
echo
echo "- Log: ${LOG_FILE}"
echo "- Directory with captured responses: ${RUN_DIR}/"
echo
echo "### Health"
echo "\`\`\`json"
cat "${RUN_DIR}/health.json"
echo "\`\`\`"
echo
echo "### HTTP CRUD"
echo "**Add** \`${RUN_DIR}/http-memory-add.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/http-memory-add.json"
echo "\`\`\`"
echo
echo "**Get (before patch)** \`${RUN_DIR}/http-memory-get.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/http-memory-get.json"
echo "\`\`\`"
echo
echo "**Patch** \`${RUN_DIR}/http-memory-patch.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/http-memory-patch.json"
echo "\`\`\`"
echo
echo "**Get (after patch)** \`${RUN_DIR}/http-memory-get-after-patch.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/http-memory-get-after-patch.json"
echo "\`\`\`"
echo
echo "**Delete** \`${RUN_DIR}/http-memory-delete.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/http-memory-delete.json"
echo "\`\`\`"
echo
echo "### MCP (SSE headers probe)"
echo "\`\`\`text"
sed -n '1,30p' "${RUN_DIR}/mcp-sse.headers"
echo "\`\`\`"
echo
echo "### MCP initialize"
echo "**Headers** \`${RUN_DIR}/mcp-init.headers\`"
echo "\`\`\`text"
sed -n '1,50p' "${RUN_DIR}/mcp-init.headers"
echo "\`\`\`"
echo
echo "**Body** \`${RUN_DIR}/mcp-init.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-init.json"
echo "\`\`\`"
echo
echo "### MCP tools/list"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-tools.json"
echo "\`\`\`"
echo
echo "### MCP store/get/update/delete (small content)"
echo "**Store response** \`${RUN_DIR}/mcp-store.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-store.json"
echo "\`\`\`"
echo
echo "**Store payload (parsed)** \`${RUN_DIR}/mcp-store.payload.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-store.payload.json"
echo "\`\`\`"
echo
echo "**Get payload (parsed)** \`${RUN_DIR}/mcp-get.payload.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-get.payload.json"
echo "\`\`\`"
echo
echo "**Update response** \`${RUN_DIR}/mcp-update.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-update.json"
echo "\`\`\`"
echo
echo "**Get-after-update payload (parsed)** \`${RUN_DIR}/mcp-get-after-update.payload.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-get-after-update.payload.json"
echo "\`\`\`"
echo
echo "**Reinforce response** \`${RUN_DIR}/mcp-reinforce.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-reinforce.json"
echo "\`\`\`"
echo
echo "**Delete response** \`${RUN_DIR}/mcp-delete.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-delete.json"
echo "\`\`\`"
echo
echo "**Get-after-delete response** \`${RUN_DIR}/mcp-get-after-delete.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/mcp-get-after-delete.json"
echo "\`\`\`"
echo
echo "### MCP large content (truncation regression check)"
echo "- Stored content length: ${big_len}"
echo "- Retrieved payload file (contains full content): ${RUN_DIR}/mcp-get-big.payload.json"
echo "- Store response: ${RUN_DIR}/mcp-store-big.json"
echo "- Get response: ${RUN_DIR}/mcp-get-big.json"
echo
echo "### Persistence (volume survives restart)"
echo "**Add** \`${RUN_DIR}/persist-add.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/persist-add.json"
echo "\`\`\`"
echo
echo "**Get after restart** \`${RUN_DIR}/persist-get-after-restart.json\`"
echo "\`\`\`json"
cat "${RUN_DIR}/persist-get-after-restart.json"
echo "\`\`\`"
echo
echo "## How To Reproduce"
echo
echo "\`\`\`bash"
echo "scripts/run-pr-tests.sh"
echo "\`\`\`"
} > "${REPORT_FILE}"
echo "[pr-tests] ✅ report: ${REPORT_FILE}"
}
section "Versions"
require_cmd git
require_cmd python3
git -C "${ROOT_DIR}" rev-parse HEAD
git -C "${ROOT_DIR}" status -sb
if command -v docker >/dev/null 2>&1; then
docker version
else
echo "[pr-tests] docker not found; cannot run containerized tests." >&2
exit 127
fi
section "Node SDK (packages/openmemory-js)"
run_in_docker_node
section "Python SDK (packages/openmemory-py)"
run_in_docker_python
section "Docker Build (packages/openmemory-js/Dockerfile)"
docker build -t openmemory-prtest:local "${ROOT_DIR}/packages/openmemory-js"
section "MCP/HTTP Smoke (Docker image)"
require_cmd curl
run_mcp_http_smoke "openmemory-prtest:local"
write_report
section "Done"
echo "[pr-tests] ✅ all checks passed"
echo "[pr-tests] log: ${LOG_FILE}"
echo "[pr-tests] report: ${REPORT_FILE}"What Was TestedSDK (containerized)
Service (Docker image)
Results (artifacts)
Health{"ok":true,"version":"2.0-hsg-tiered","embedding":{"provider":"synthetic","fallback_chain":["synthetic"],"dimensions":256,"mode":"simple","batch_support":false,"advanced_parallel":false,"embed_delay_ms":200,"configured":true,"type":"synthetic"},"tier":"hybrid","dim":256,"cache":3,"expected":{"recall":98,"qps":"700-800","ram":"0.5gb/10k","use":"For high accuracy"}}```
### HTTP CRUD
**Add** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-add.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","primary_sector":"semantic","sectors":["semantic"],"chunks":1}```
**Get (before patch)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-get.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","content":"http-add-content","primary_sector":"semantic","sectors":["semantic"],"tags":["pr112","http"],"metadata":{"source":"pr112-run-pr-tests"},"created_at":1767676950407,"updated_at":1767676950407,"last_seen_at":1767676950407,"salience":0.4,"decay_lambda":0.005,"version":1,"user_id":"pr112-user"}```
**Patch** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-patch.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","updated":true}```
**Get (after patch)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-get-after-patch.json`
```json
{"id":"5f1aca76-1b6b-4068-a8a6-ec6a5758af02","content":"http-updated-content","primary_sector":"semantic","sectors":["semantic"],"tags":["pr112","http","updated"],"metadata":{"source":"pr112-run-pr-tests","updated":true},"created_at":1767676950407,"updated_at":1767676950490,"last_seen_at":1767676950407,"salience":0.4,"decay_lambda":0.005,"version":2,"user_id":"pr112-user"}```
**Delete** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/http-memory-delete.json`
```json
{"ok":true}```
### MCP (SSE headers probe)
```text
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,DELETE,OPTIONS
Access-Control-Allow-Headers: Content-Type,Authorization,x-api-key,mcp-session-id,mcp-protocol-version,last-event-id
Access-Control-Expose-Headers: mcp-session-id
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
Date: Tue, 06 Jan 2026 05:22:30 GMT
Transfer-Encoding: chunked
MCP initializeHeaders Body {"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true},"resources":{"listChanged":true},"logging":{},"completions":{}},"serverInfo":{"name":"openmemory-mcp","version":"2.1.0"}},"jsonrpc":"2.0","id":1}```
### MCP tools/list
```json
{"result":{"tools":[{"name":"openmemory_query","description":"Query OpenMemory for contextual memories (HSG) and/or temporal facts","inputSchema":{"type":"object","properties":{"query":{"type":"string","minLength":1,"description":"Free-form search text"},"type":{"type":"string","enum":["contextual","factual","unified"],"default":"contextual","description":"Query type: 'contextual' for HSG semantic search (default), 'factual' for temporal fact queries, 'unified' for both"},"fact_pattern":{"type":"object","properties":{"subject":{"type":"string","description":"Subject pattern (entity) - use undefined for wildcard"},"predicate":{"type":"string","description":"Predicate pattern (relationship) - use undefined for wildcard"},"object":{"type":"string","description":"Object pattern (value) - use undefined for wildcard"}},"additionalProperties":false,"description":"Fact pattern for temporal queries. Used when type is 'factual' or 'unified'"},"at":{"type":"string","description":"ISO date string for point-in-time queries (default: now). Queries facts valid at this time"},"k":{"type":"integer","minimum":1,"maximum":32,"default":8,"description":"Maximum results to return (for HSG queries)"},"sector":{"type":"string","enum":["episodic","semantic","procedural","emotional","reflective"],"description":"Restrict search to a specific sector (for HSG queries)"},"min_salience":{"type":"number","minimum":0,"maximum":1,"description":"Minimum salience threshold (for HSG queries)"},"user_id":{"type":"string","minLength":1,"description":"Isolate results to a specific user identifier"}},"required":["query"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_store","description":"Persist new content into OpenMemory (HSG contextual memory and/or temporal facts)","inputSchema":{"type":"object","properties":{"content":{"type":"string","minLength":1,"description":"Raw memory text to store"},"type":{"type":"string","enum":["contextual","factual","both"],"default":"contextual","description":"Storage type: 'contextual' for HSG only (default), 'factual' for temporal facts only, 'both' for both systems"},"facts":{"type":"array","items":{"type":"object","properties":{"subject":{"type":"string","minLength":1,"description":"Fact subject (entity)"},"predicate":{"type":"string","minLength":1,"description":"Fact predicate (relationship)"},"object":{"type":"string","minLength":1,"description":"Fact object (value)"},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"Confidence score (0-1, default 1.0)"},"valid_from":{"type":"string","description":"ISO date string for fact validity start (default: now)"}},"required":["subject","predicate","object"],"additionalProperties":false},"description":"Array of facts to store in temporal graph. Required when type is 'factual' or 'both'"},"tags":{"type":"array","items":{"type":"string"},"description":"Optional tag list (for HSG storage)"},"metadata":{"type":"object","additionalProperties":{},"description":"Arbitrary metadata blob"},"user_id":{"type":"string","minLength":1,"description":"Associate the memory with a specific user identifier"}},"required":["content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_reinforce","description":"Boost salience for an existing memory","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to reinforce"},"boost":{"type":"number","minimum":0.01,"maximum":1,"default":0.1,"description":"Salience boost amount (default 0.1)"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_list","description":"List recent memories for quick inspection","inputSchema":{"type":"object","properties":{"limit":{"type":"integer","minimum":1,"maximum":50,"default":10,"description":"Number of memories to return"},"sector":{"type":"string","enum":["episodic","semantic","procedural","emotional","reflective"],"description":"Optionally limit to a sector"},"user_id":{"type":"string","minLength":1,"description":"Restrict results to a specific user identifier"}},"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_get","description":"Fetch a single memory by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to load"},"include_vectors":{"type":"boolean","default":false,"description":"Include sector vector metadata"},"user_id":{"type":"string","minLength":1,"description":"Validate ownership against a specific user identifier"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_update","description":"Update an existing memory (content, tags, metadata)","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to update"},"content":{"type":"string","minLength":1,"description":"New memory content (omit to keep current)"},"tags":{"type":"array","items":{"type":"string"},"description":"Replace tags (omit to keep current)"},"metadata":{"type":"object","additionalProperties":{},"description":"Replace metadata (omit to keep current)"},"user_id":{"type":"string","minLength":1,"description":"Validate ownership against a specific user identifier"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}},{"name":"openmemory_delete","description":"Delete a memory by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"Memory identifier to delete"},"user_id":{"type":"string","minLength":1,"description":"Validate ownership against a specific user identifier"}},"required":["id"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]},"jsonrpc":"2.0","id":2}```
### MCP store/get/update/delete (small content)
**Store response** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-store.json`
```json
{"result":{"content":[{"type":"text","text":"Stored memory 3872378d-8561-4ac4-bc6b-bfacc31d320f (primary=semantic) across sectors: semantic [user=pr112-user]"},{"type":"text","text":"{\n \"type\": \"contextual\",\n \"hsg\": {\n \"id\": \"3872378d-8561-4ac4-bc6b-bfacc31d320f\",\n \"primary_sector\": \"semantic\",\n \"sectors\": [\n \"semantic\"\n ]\n },\n \"user_id\": \"pr112-user\"\n}"}"}```
**Store payload (parsed)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-store.payload.json`
```json
{
"type": "contextual",
"hsg": {
"id": "3872378d-8561-4ac4-bc6b-bfacc31d320f",
"primary_sector": "semantic",
"sectors": [
"semantic"
]
},
"user_id": "pr112-user"
}Get payload (parsed) {
"id": "3872378d-8561-4ac4-bc6b-bfacc31d320f",
"content": "mcp-store-content",
"primary_sector": "semantic",
"salience": 0.4,
"decay_lambda": 0.005,
"created_at": 1767676952745,
"updated_at": 1767676952745,
"last_seen_at": 1767676952745,
"user_id": "pr112-user",
"tags": [
"pr112",
"mcp"
],
"metadata": {
"source": "pr112-run-pr-tests"
}
}Update response {"result":{"content":[{"type":"text","text":"Updated memory 3872378d-8561-4ac4-bc6b-bfacc31d320f"},{"type":"text","text":"{\n \"id\": \"3872378d-8561-4ac4-bc6b-bfacc31d320f\",\n \"updated\": true,\n \"user_id\": \"pr112-user\"\n}"}]},"jsonrpc":"2.0","id":5}```
**Get-after-update payload (parsed)** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-after-update.payload.json`
```json
{
"id": "3872378d-8561-4ac4-bc6b-bfacc31d320f",
"content": "mcp-updated-content",
"primary_sector": "semantic",
"salience": 0.4,
"decay_lambda": 0.005,
"created_at": 1767676952745,
"updated_at": 1767676953217,
"last_seen_at": 1767676952745,
"user_id": "pr112-user",
"tags": [
"pr112",
"mcp",
"updated"
],
"metadata": {
"source": "pr112-run-pr-tests",
"updated": true
}
}Reinforce response {"result":{"content":[{"type":"text","text":"Reinforced memory 3872378d-8561-4ac4-bc6b-bfacc31d320f by 0.2"}]},"jsonrpc":"2.0","id":7}```
**Delete response** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-delete.json`
```json
{"result":{"content":[{"type":"text","text":"Deleted memory 3872378d-8561-4ac4-bc6b-bfacc31d320f"}]},"jsonrpc":"2.0","id":8}```
**Get-after-delete response** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-after-delete.json`
```json
{"result":{"content":[{"type":"text","text":"Memory 3872378d-8561-4ac4-bc6b-bfacc31d320f not found."}]},"jsonrpc":"2.0","id":9}```
### MCP large content (truncation regression check)
- Stored content length: 20024
- Retrieved payload file (contains full content): /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-big.payload.json
- Store response: /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-store-big.json
- Get response: /xxx/OpenMemory/test-results/pr112-20260106T052123Z/mcp-get-big.json
### Persistence (volume survives restart)
**Add** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/persist-add.json`
```json
{"id":"dbb0cb4e-adb4-4455-9247-7c9dd6c033fc","primary_sector":"semantic","sectors":["semantic"],"chunks":1}```
**Get after restart** `/xxx/OpenMemory/test-results/pr112-20260106T052123Z/persist-get-after-restart.json`
```json
{"id":"dbb0cb4e-adb4-4455-9247-7c9dd6c033fc","content":"persistence-check-content","primary_sector":"semantic","sectors":["semantic"],"tags":["pr112","persist"],"metadata":{"source":"pr112-run-pr-tests"},"created_at":1767676953482,"updated_at":1767676953482,"last_seen_at":1767676953482,"salience":0.4,"decay_lambda":0.005,"version":1,"user_id":"pr112-user"}```
## How To Reproduce
```bash
scripts/run-pr-tests.sh |
Summary
This PR makes OpenMemory easier to run and integrate in multi-project setups (Docker-first) and adds missing MCP operations for updating and deleting memories.
Key changes
18080(configurable viaOM_PORT) and improved compose healthchecks.PORT(withOM_PORTtaking precedence).user_id.openmemory_updateandopenmemory_delete(docs updated)./mcpin addition to POST (better compatibility with clients like Claude Code).OM_DEFAULT_USER_ID/OPENMEMORY_DEFAULT_USER_ID/OPENMEMORY_USER_IDwhen a client doesn’t provideuser_id.user_id(body) andx-om-user-id/x-openmemory-user-idheaders.Migration notes
8080, update URLs/port mappings or setOM_PORT=8080.OM_USE_SUMMARY_ONLY=true(it stores only the summarized extract).Docs
README.mdincludes Docker + integration guide and MCP tool list.docs/multi-project.mdanddocs/mcp.mdprovide detailed workflows.