Skip to content

feat: add mempalace_sync_status MCP tool and freshness hook#256

Closed
rusel95 wants to merge 6 commits intoMemPalace:developfrom
rusel95:feat/sync-mcp-tool
Closed

feat: add mempalace_sync_status MCP tool and freshness hook#256
rusel95 wants to merge 6 commits intoMemPalace:developfrom
rusel95:feat/sync-mcp-tool

Conversation

@rusel95
Copy link
Copy Markdown

@rusel95 rusel95 commented Apr 8, 2026

Summary

Relates to #224 — Adds a read-only MCP tool that lets any AI agent check whether palace memories are up to date before trusting search results. Combined with the freshness hook, this creates an automatic "staleness awareness" loop.

This is the companion to #251 (CLI sync command). While mempalace sync is for humans, mempalace_sync_status is for AI agents — structured JSON output, no side effects, MCP-native.

Changes

MCP Tool: mempalace_sync_status

// Request
{"name": "mempalace_sync_status", "arguments": {"directory": "/path/to/project"}}

// Response (fresh)
{"status": "fresh", "total_source_files": 58, "fresh": 58, "stale": 0, "missing": 0}

// Response (stale)
{
  "status": "stale",
  "fresh": 47, "stale": 6, "missing": 0,
  "stale_files": [{"file": "README.md", "drawers": 55, "wing": "my-project"}],
  "remine_commands": ["mempalace mine /path --wing my-project --force"],
  "message": "6 files changed since last mine. Run the remine_commands to refresh."
}

Read-only — reports freshness without modifying the palace. AI agents can use this to:

  1. Check freshness before trusting search results
  2. Suggest re-mine commands to the user
  3. Flag when palace memories may be outdated

Hook: mempal_freshness_hook.sh

Stop hook that checks palace freshness once per session. If stale files are detected, blocks the AI from stopping and prompts it to call mempalace_sync_status for details.

Configurable:

  • CHECK_DIR — scope to a specific directory
  • CHECK_INTERVAL — minimum seconds between checks (default: 3600)

Test plan

  • test_sync_status_empty_palace — empty palace returns "empty" status
  • test_sync_status_no_palace — missing palace returns error
  • test_sync_status_fresh_files — unchanged files detected as fresh
  • test_sync_status_stale_file — modified file detected with correct name and re-mine command
  • test_sync_status_missing_file — deleted source file reported as orphaned
  • test_sync_status_legacy_no_hash — legacy drawers without hash handled gracefully
  • test_sync_status_directory_filter — directory filter scopes check correctly
  • All 108 existing tests pass (0 regressions)
$ python -m pytest tests/ -q
108 passed in 85s

🤖 Generated with Claude Code

@bgauryy
Copy link
Copy Markdown

bgauryy commented Apr 8, 2026

PR Review: feat: add mempalace_sync_status MCP tool and freshness hook

Executive Summary

Aspect Value
PR Goal Add a read-only MCP tool for AI agents to check palace freshness, plus a bash stop-hook that blocks the AI when memories are stale
Files Changed 3 (+408/-0)
Risk Level 🔴 HIGH — multiple runtime-breaking dependency gaps and cross-platform incompatibility
Review Effort 3/5 — moderate complexity, clear intent, but requires cross-PR dependency verification
Recommendation 🔄 REQUEST_CHANGES

Affected Areas: mempalace/mcp_server.py (new tool), hooks/mempal_freshness_hook.sh (new file), tests/test_mcp_server.py

Business Impact: AI agents calling mempalace_sync_status will always get "fresh" status today because no miner writes content_hash. The freshness hook will silently fail on macOS. Both features are non-functional without companion changes.

Flow Changes: Adds a new read-only MCP tool in the SYNC/FRESHNESS section (between graph tools and write tools). No existing flows are modified. The tool scans all drawers via col.get() batch pagination and compares stored content_hash against live file content.

Ratings

Aspect Score
Correctness 2/5 — depends on unimplemented content_hash and --force flag
Security 3/5 — path traversal risk in shell hook via SESSION_ID
Performance 4/5 — batch pagination is sensible, but scans entire collection
Maintainability 4/5 — clean code, good test coverage for what exists

PR Health


High Priority Issues

(Must fix before merge)

🐛 #1: content_hash is never written — tool reports "fresh" for everything

Location: mempalace/mcp_server.py (tool_sync_status) | Confidence: ✅ HIGH

The tool reads meta.get("content_hash", "") from drawer metadata, but neither miner.py nor convo_miner.py writes this field. Every drawer falls into the no_hash path, so fresh == 0, stale == 0, missing == 0 (for existing files), and the final else branch returns "status": "fresh" — regardless of actual file state.

This is presumably intended to land after PR #251, but there's no guard or warning for this scenario. At minimum, the tool should return an honest status when all drawers lack hashes.

-    else:
-        result["status"] = "fresh"
-        result["message"] = "All source files are up to date."
+    elif no_hash == len(source_files):
+        result["status"] = "unknown"
+        result["message"] = f"All {no_hash} source files lack content_hash (mined before sync support). Re-mine with latest version to enable freshness tracking."
+    else:
+        result["status"] = "fresh"
+        result["message"] = "All source files are up to date."

🐛 #2: Generated re-mine commands use non-existent --force flag

Location: mempalace/mcp_server.py:~line 340 | Confidence: ✅ HIGH

The re-mine commands include --force (e.g. mempalace mine /path --wing x --force), but cmd_mine() in cli.py does not accept a --force argument. Any AI agent or user executing these commands will get an argument error.

-                cmd = f"mempalace mine {parent} --wing {wing} --force"
-                if mode == "convos":
-                    cmd = f"mempalace mine {parent} --mode convos --wing {wing} --force"
+                cmd = f"mempalace mine {parent} --wing {wing}"
+                if mode == "convos":
+                    cmd = f"mempalace mine {parent} --mode convos --wing {wing}"

Or add --force to the CLI in this PR / ensure PR #251 adds it.


🐛 #3: grep -oP breaks on macOS (BSD grep has no -P flag)

Location: hooks/mempal_freshness_hook.sh:76 | Confidence: ✅ HIGH

STALE_COUNT=$(echo "$SYNC_OUTPUT" | grep -oP 'Stale \(changed\):\s+\K\d+' 2>/dev/null || echo "0")

macOS ships BSD grep which does not support Perl-compatible regex (-P). The || echo "0" fallback masks the failure, meaning stale count is always 0 on macOS — the hook will never block.

-STALE_COUNT=$(echo "$SYNC_OUTPUT" | grep -oP 'Stale \(changed\):\s+\K\d+' 2>/dev/null || echo "0")
+STALE_COUNT=$(echo "$SYNC_OUTPUT" | sed -n 's/.*Stale (changed):[[:space:]]*\([0-9]*\).*/\1/p' 2>/dev/null)
+STALE_COUNT="${STALE_COUNT:-0}"

🐛 #4: Directory filter false positive — prefix matching without path boundary

Location: mempalace/mcp_server.py (tool_sync_status, directory filter) | Confidence: ✅ HIGH

source_files = {
    sf: info for sf, info in source_files.items()
    if _resolve_path_safe(sf).startswith(dir_resolved)
}

startswith without a trailing separator causes false matches: a filter directory /home/user/project will also match files under /home/user/project_other/. This silently includes files from unrelated directories.

     if directory:
         dir_resolved = str(Path(directory).expanduser().resolve())
+        if not dir_resolved.endswith(os.sep):
+            dir_resolved += os.sep
         source_files = {
             sf: info for sf, info in source_files.items()
-            if _resolve_path_safe(sf).startswith(dir_resolved)
+            if _resolve_path_safe(sf).startswith(dir_resolved) or _resolve_path_safe(sf) == dir_resolved.rstrip(os.sep)
         }

Medium Priority Issues

(Should fix, not blocking)

🏗️ #5: Shell hook depends on unmerged mempalace sync CLI command

Location: hooks/mempal_freshness_hook.sh:73 | Confidence: ✅ HIGH

SYNC_OUTPUT=$(python3 -m mempalace sync $SYNC_ARGS 2>/dev/null || echo "")

The sync subcommand does not exist in cli.py on main. It's presumably from PR #251. The 2>/dev/null || echo "" masks the error, but the hook becomes a no-op (always allows). The install docs should note this dependency, or the PR should be rebased onto #251.


🚨 #6: SESSION_ID path traversal in shell hook stamp file

Location: hooks/mempal_freshness_hook.sh:54-55 | Confidence: ⚠️ MED

SESSION_ID=$(echo "$INPUT" | python3 -c "..." 2>/dev/null || echo "unknown")
STAMP_FILE="/tmp/mempalace_freshness_${SESSION_ID}"

SESSION_ID comes from JSON stdin (attacker-controllable if the hook is invoked with crafted input). A value like ../../etc/cron.d/evil would write to /tmp/mempalace_freshness_../../etc/cron.d/evil. While the file only contains a Unix timestamp, it could overwrite arbitrary files the user owns.

+# Sanitize session ID to prevent path traversal
+SESSION_ID=$(echo "$SESSION_ID" | tr -cd 'A-Za-z0-9_-')
 STAMP_FILE="/tmp/mempalace_freshness_${SESSION_ID}"

🎨 #7: Misleading "fresh" when no drawers have content_hash

Location: mempalace/mcp_server.py (status logic) | Confidence: ✅ HIGH

Related to #1 but specifically about UX: when no_hash_legacy > 0 and fresh == 0, the response includes "no_hash_legacy": N in the result dict but the status says "fresh" and message says "All source files are up to date." This is contradictory — the consumer has to inspect no_hash_legacy to realize nothing was actually verified. The status should reflect the ambiguity. (See fix in #1.)


🎨 #8: stale_files uses basename only — ambiguous for same-named files

Location: mempalace/mcp_server.py:~line 348 | Confidence: ⚠️ MED

result["stale_files"] = [
    {"file": Path(s["file"]).name, "drawers": s["drawers"], "wing": s["wing"]}
    for s in stale[:20]
]

If two stale files share the same name in different directories (e.g. src/utils.py and lib/utils.py), the output shows two entries with "file": "utils.py" and no way to distinguish them. Consider including a relative or abbreviated path.

-            {"file": Path(s["file"]).name, "drawers": s["drawers"], "wing": s["wing"]}
+            {"file": s["file"], "drawers": s["drawers"], "wing": s["wing"]}

🐛 #9: Shell hook doesn't quote $CHECK_DIR — breaks with spaces

Location: hooks/mempal_freshness_hook.sh:72 | Confidence: ✅ HIGH

SYNC_ARGS="$SYNC_ARGS --dir $CHECK_DIR"

If CHECK_DIR contains spaces (e.g. /Users/me/My Projects), the unquoted expansion splits into multiple arguments. Word splitting will break the command.

-    SYNC_ARGS="$SYNC_ARGS --dir $CHECK_DIR"
+    SYNC_ARGS="$SYNC_ARGS --dir \"$CHECK_DIR\""

Low Priority Issues

(Nice to have)

#10: Shell hook parses JSON from stdin twice

Location: hooks/mempal_freshness_hook.sh:44,53 | Confidence: ✅ HIGH

The hook pipes $INPUT through python3 -c twice — once for stop_hook_active and once for session_id. Each invocation spawns a Python interpreter. Could be merged into a single parse.

-STOP_ACTIVE=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('stop_hook_active', False))" 2>/dev/null || echo "False")
-...
-SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('session_id', 'unknown'))" 2>/dev/null || echo "unknown")
+read -r STOP_ACTIVE SESSION_ID <<< $(echo "$INPUT" | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+print(d.get('stop_hook_active', False), d.get('session_id', 'unknown'))
+" 2>/dev/null || echo "False unknown")

🎨 #11: Missing test coverage for edge cases

Location: tests/test_mcp_server.py | Confidence: ⚠️ MED

Good coverage for the happy paths, but missing:

  • _resolve_path_safe with broken symlinks or permission errors
  • Batch pagination with >500 drawers (exercising the while offset < total loop)
  • Files with encoding issues (binary files stored as source_file)
  • Mixed scenario: stale + missing + fresh in one call
  • Directory filter with trailing slash vs without
  • Directory filter prefix false positive (see Using AAAK as language for agents #4)

🔗 #12: _resolve_path_safe duplicates resolve logic

Location: mempalace/mcp_server.py (new helper) | Confidence: ⚠️ MED

_resolve_path_safe is a standalone helper that just calls Path(p).resolve() with error handling. Similar path resolution patterns exist in miner.py and convo_miner.py. Consider moving to a shared utility (e.g. constants.py or a new utils.py) to avoid drift.


Flow Impact Analysis

                    ┌─────────────────────────────┐
                    │  AI Agent (Claude/Cursor)    │
                    └──────┬──────────────────┬────┘
                           │                  │
                   MCP call│          Stop Hook│ (bash)
                           ▼                  ▼
              ┌─────────────────┐  ┌──────────────────────┐
              │ tool_sync_status│  │mempal_freshness_hook  │
              │  (mcp_server.py)│  │  (.sh)                │
              └───────┬─────────┘  └──────────┬────────────┘
                      │                       │
                      │ col.get()             │ python3 -m mempalace sync --dry-run
                      ▼                       ▼
              ┌──────────────┐      ┌──────────────────┐
              │  ChromaDB    │      │  CLI sync command │ ← NOT ON MAIN (PR #251)
              │  (drawers)   │      │  (cli.py)         │
              └──────────────┘      └──────────────────┘
                      │
                      │ reads content_hash from metadata
                      ▼
              ┌──────────────────┐
              │  content_hash    │ ← NOT SET BY ANY MINER
              │  (drawer meta)   │
              └──────────────────┘

Dependency chain: This PR requires both:

  1. PR feat: add sync command and --force flag for incremental re-mining #251 (CLI sync command) — for the shell hook to work
  2. A miner change (not yet PR'd) — for content_hash to be populated

Without both, the MCP tool returns "fresh" unconditionally and the hook always allows stop.


Summary of Dependencies

Feature Depends On Status
tool_sync_status freshness detection content_hash in drawer metadata Missing — no miner writes it
Generated re-mine commands --force CLI flag Missing — not in cli.py
mempal_freshness_hook.sh mempalace sync --dry-run CLI PR #251 (not merged)
Shell hook on macOS grep -P (GNU grep) Incompatible — macOS uses BSD grep

Created by Octocode MCP https://octocode.ai 🔍🐙

@rusel95
Copy link
Copy Markdown
Author

rusel95 commented Apr 9, 2026

@bgauryy Rebased onto main and addressed all high-priority review items:

Fixed:

  1. "unknown" status for legacy drawers — returns honest "unknown" when all drawers lack content_hash instead of misleading "fresh"
  2. Removed --force flag from generated re-mine commands (doesn't exist in CLI)
  3. macOS compatibility — replaced grep -P (GNU-only) with sed in freshness hook
  4. Directory filter path boundary — added trailing separator to prevent /project matching /project_other
  5. SESSION_ID sanitization — prevent path traversal via tr -cd 'A-Za-z0-9_-'
  6. Quoted $CHECK_DIR — handles paths with spaces
  7. Full file path in stale_files — no more ambiguous basename-only output
  8. Test compatibility — fixed tests for upstream _patch_mcp_server signature change

All 124 tests pass.

@rusel95
Copy link
Copy Markdown
Author

rusel95 commented Apr 9, 2026

@bgauryy Pushed security and correctness fixes (99f0d4d):

  1. Removed eval injection — freshness hook no longer uses eval for command execution. Runs python3 directly with quoted args. Added EXIT trap for guaranteed JSON output on crashes.
  2. Fixed false "fresh" status — added "partial" status when some files are fresh but others lack content_hash. No more misleading "all files up to date" for partially-unknowable state.
  3. First non-empty hash wins — pagination now captures the first non-empty content_hash per file, preventing non-deterministic results from mixed-hash drawers.
  4. Removed dead code — the or _resolve_path_safe(sf) == dir_resolved.rstrip(os.sep) branch in directory filter was always False (file path ≠ dir path).

All 135 tests pass.

@rusel95
Copy link
Copy Markdown
Author

rusel95 commented Apr 9, 2026

Updated with sync skill for Claude Code and Codex plugins. Here's the MCP sync status flow:

flowchart TD
    A[mempalace_sync_status MCP tool] --> B[Get palace collection]
    B --> C[Scan drawers in batches of 500]
    C --> D[Collect unique source files + content hashes]
    
    D --> E{For each source file}
    E --> F{File exists on disk?}
    F -->|no| G[Mark as missing]
    F -->|yes| H{Has stored content_hash?}
    H -->|no| I[Mark as legacy — no hash]
    H -->|yes| J[Compare file_content_hash vs stored]
    J -->|match| K[Fresh ✓]
    J -->|mismatch| L[Stale]
    
    G --> M[Return sync report]
    I --> M
    K --> M
    L --> M
    
    M --> N["{ fresh, stale, missing, legacy, total }"]
Loading

Also added /mempalace:sync command for both .claude-plugin/ and .codex-plugin/ — delegates to mempalace instructions sync for guided sync workflow.

Ready for re-review.

rusel95 and others added 3 commits April 10, 2026 18:05
Exposes palace freshness checking to any AI agent via MCP. The tool
compares content_hash stored in drawer metadata against current file
content and returns structured JSON with stale/fresh/missing counts
plus actionable re-mine commands.

Also adds a Stop hook (mempal_freshness_hook.sh) that automatically
checks palace freshness once per session and prompts the AI to call
mempalace_sync_status if stale files are detected.

Changes:
- New MCP tool: mempalace_sync_status (read-only, no modifications)
  - Scans all source files for content changes via hash comparison
  - Returns status (fresh/stale/orphaned), counts, stale file list
  - Generates grouped re-mine commands
  - Supports directory filter for scoped checks
- New hook: mempal_freshness_hook.sh
  - Fires on Stop event, checks freshness once per session
  - Blocks AI if stale files found, suggesting sync_status call
  - Configurable CHECK_DIR and CHECK_INTERVAL
- 7 new MCP test cases covering empty, fresh, stale, missing,
  legacy, and directory-filtered scenarios

Relates to MemPalace#224

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1. Return "unknown" status when all drawers lack content_hash instead
   of misleading "fresh"
2. Remove non-existent --force flag from generated re-mine commands
3. Replace grep -P (GNU-only) with sed in shell hook for macOS compat
4. Fix directory filter prefix matching with path boundary separator
5. Sanitize SESSION_ID in hook to prevent path traversal
6. Quote $CHECK_DIR in hook to handle paths with spaces
7. Use full file path in stale_files output instead of ambiguous basename
8. Fix test compatibility with upstream _patch_mcp_server signature

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
… non-empty hash

1. Critical: removed eval-based command execution in freshness hook —
   now runs python3 directly with quoted args. Added EXIT trap for
   guaranteed JSON output.

2. Critical: added 'partial' status when some files are fresh but
   others lack content_hash — prevents misleading 'fresh' for
   partially-unknowable state.

3. Use first non-empty hash per file in pagination — mixed-hash
   drawers no longer produce non-deterministic results.

4. Removed dead code branch in directory filter (file path != dir path).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@rusel95 rusel95 force-pushed the feat/sync-mcp-tool branch from 99f0d4d to ff159f2 Compare April 10, 2026 15:05
Copy link
Copy Markdown

@web3guru888 web3guru888 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of #256feat: add mempalace_sync_status MCP tool and freshness hook

Scope: +427/−0 · 3 file(s) · touches core

  • hooks/mempal_freshness_hook.sh (added: +88/−0)
  • ⚠️ mempalace/mcp_server.py (modified: +155/−0)
  • tests/test_mcp_server.py (modified: +184/−0)

Technical Analysis

  • 🔌 MCP server dispatch changes — verify JSON-RPC compliance and backward compatibility
  • 🪟 Windows compatibility — verify path handling works cross-platform

Issues

  • ⚠️ Hardcoded filesystem path — breaks portability
  • ⚠️ Touches mempalace/mcp_server.py — Core MCP server — maintainer guards this closely

Suggestions

  • Magic number(s) 2025, 2026 — consider extracting to named constant(s)

Strengths

  • ✅ Includes test coverage

🟡 Needs attention — touches guarded files and has items to address.


🏛️ Reviewed by MemPalace-AGI · Autonomous research system with perfect memory · Showcase: Truth Palace of Atlantis

@rusel95
Copy link
Copy Markdown
Author

rusel95 commented Apr 13, 2026

@igorls @bensig @milla-jovovich This PR is ready for initial review. All issues from the Octocode bot review have been addressed:

  • ✅ Honest "unknown" status when all drawers lack content_hash
  • ✅ Removed non-existent --force from generated commands
  • ✅ macOS-compatible sed instead of grep -P
  • ✅ Directory filter path boundary fix
  • ✅ SESSION_ID sanitization (path traversal prevention)
  • ✅ Removed eval injection in freshness hook
  • ✅ Full file paths in stale_files output

This is the MCP companion to #251mempalace_sync_status for AI agents (read-only, no side effects) plus a freshness stop-hook.

Note: CI isn't running because the workflow only triggers on PRs targeting main, not develop.

@rusel95
Copy link
Copy Markdown
Author

rusel95 commented Apr 13, 2026

Local CI results (GitHub CI doesn't trigger on develop-targeted PRs):

$ python -m pytest tests/ -v --tb=short
598 passed, 1 failed, 106 deselected in 50.12s

The 1 failure is unrelated (test_cmd_repair_success from #239 — mock gap). All sync_status tests pass.

Ruff clean for this PR's files — the 7 errors are all in #251's code (cli.py, miner.py).

@igorls igorls added area/hooks Claude Code hook scripts (Stop, PreCompact, SessionStart) area/mcp MCP server and tools enhancement New feature or request labels Apr 14, 2026
Brings in tunnel functions, ChromaBackend, closets, halls, entity metadata,
and other v3.2.0 changes. Keeps sync_status MCP tool alongside new tunnel tools.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@rusel95
Copy link
Copy Markdown
Author

rusel95 commented May 4, 2026

Closing and reopening as a clean rebase onto current develop.

The branch had accumulated several merge commits from upstream syncs. The new branch (feat/sync-mcp-tool-v2) is a clean single commit on top of current develop with all the same changes and all review fixes from @bgauryy applied.

Dependency note: This tool requires content_hash to be stored in drawer metadata. Without it the tool returns status: unknown for all drawers — intentionally honest rather than falsely reporting fresh. The companion PR (feat/content-hash-foundation) should merge first to make this tool fully functional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/hooks Claude Code hook scripts (Stop, PreCompact, SessionStart) area/mcp MCP server and tools enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants