Skip to content

fix+docs(memory): atomic file write + expanded tool descriptions - #4049

Closed
yakuphanycl wants to merge 1 commit into
modelcontextprotocol:mainfrom
yakuphanycl:fix/memory-server-atomic-write-and-descriptions
Closed

fix+docs(memory): atomic file write + expanded tool descriptions#4049
yakuphanycl wants to merge 1 commit into
modelcontextprotocol:mainfrom
yakuphanycl:fix/memory-server-atomic-write-and-descriptions

Conversation

@yakuphanycl

Copy link
Copy Markdown

Summary

External MCP audit of src/memory/ (using the mcp-audit skill) surfaced one Low state-handling finding and one Info discoverability finding. Batched into a single PR per the audit's disclosure SOP §2.

No behavioural change to any tool dispatch path, no schema change, no new dependencies.

Findings addressed

F-002 — Non-atomic file write (Low, state-handling axis)

KnowledgeGraphManager.saveGraph() truncate-and-wrote the live memory file directly via fs.writeFile(memoryFilePath, ...). Combined with the absence of any concurrency control, this means:

  1. Crash-mid-write: a process killed between the truncate and the buffer flush leaves the live file empty or partial. Next read returns an empty graph (the loadGraph ENOENT branch swallows the error and returns {entities: [], relations: []}), silently masking data loss.
  2. Concurrent reader: a tool call that begins reading while another is mid-write can observe a torn JSONL file (lines that don't parse).

Fix: write to <memoryFilePath>.tmp.<pid>.<ts> then fs.rename into place. Rename is atomic on POSIX and a same-volume best-effort on Windows. The live file is now always either the pre-existing valid snapshot or the new valid snapshot, never torn.

Two new tests under describe('atomic write'):

  • Live file always parses as valid JSONL after every save.
  • No <file>.tmp.* stragglers left after a successful save.

Scope note: this fix does not address the broader loadGraph→mutate→saveGraph in-process race (two parallel tool calls each read the same pre-state, one's mutation is overwritten). That requires an in-process write mutex and is flagged as a follow-up in the audit's next-session backlog. The atomic-write fix is a strict improvement that lands cleanly without that scope.

F-003 — In-code Tool() descriptions lack when-to-use + return-shape (Info)

All 9 server.registerTool descriptions were one-line WHAT-statements ("Create multiple new entities in the knowledge graph"). README has more context, but agents reading the MCP tool surface only see the in-code description.

Rewrote all 9 to a consistent shape:

  • WHAT (preserved)
  • when-to-use guidance (e.g., read_graph: "for targeted lookups prefer search_nodes (by query) or open_nodes (by name) since payload size grows with the graph")
  • return-shape hint
  • silent-skip behaviour documented (duplicate creates, non-existent deletes, unknown observation removals all silently skip — was inferable from tests but not from the surface)

Findings deferred (not in this PR)

  • F-001 — No MCP-layer dispatch test (Low). 9/9 tools have store-layer tests via KnowledgeGraphManager, but none round-trip through the server.registerTool handler dispatch. Same gap as audit of src/git. Worth a separate PR with an SDK-client integration fixture.
  • F-002 follow-up — In-process write mutex (Low). Atomic write addresses crash-mid-write and torn reads; an in-process mutex is needed to prevent the loadGraph→mutate→saveGraph race that can lose mutations across parallel tool calls. Recommend a small async-mutex-style serializer or a chained Promise pattern.

Test plan

  • Atomic-write fix uses standard fs.writeFile + fs.rename — no new APIs, no type changes.
  • Description changes are pure string concatenation, no schema touched.
  • New tests added in the same fixture style as existing tests.
  • Local verification skipped on Windows due to a workspace-install symlink block (EISDIR: illegal operation on a directory, symlink ... server-sequential-thinking) — same constraint documented in the auditor's filesystem case-study. Linux CI will run cleanly.
  • Maintainer review of description wording — open to tightening if too verbose.

Audit reference

  • Severity rubric and disclosure SOP applied: yakuphanycl/wrg-skills
  • Audit case-study (pending publication on the auditor's side): wrg-skills/docs/case-studies/memory-mcp-2026-04-26.md

🤖 Generated with Claude Code

External MCP audit (using https://github.com/yakuphanycl/wrg-skills/tree/main/skills/mcp-audit)
of `src/memory/` surfaced one Low (state-handling) and one Info finding.
Batched per the audit's disclosure SOP §2.

## Fix — non-atomic file write (Low, state-handling axis)

`KnowledgeGraphManager.saveGraph()` truncate-and-wrote the live memory file
directly via `fs.writeFile(memoryFilePath, ...)`. A crash mid-write (or a
reader racing the writer) could observe an empty or torn file. There is no
existing concurrency control, so the live file is the only canonical store.

Changed to write-temp-then-rename:
  - Write all JSONL lines to `<memoryFilePath>.tmp.<pid>.<ts>`
  - `fs.rename` the temp into place — atomic on POSIX, same-volume
    best-effort on Windows.
  - On failure, best-effort `fs.unlink` the temp so a half-written file
    doesn't leak alongside the live one.

This is a strict improvement: the live file is now always either the
pre-existing valid snapshot or the new valid snapshot, never a torn one.
It does NOT solve the loadGraph→mutate→saveGraph in-process race (two
parallel tool calls can still each see the same pre-state and one's
mutation can be lost). A follow-up to add an in-process write mutex is
recommended; flagged in the audit's next-session backlog.

Two new tests under `describe('atomic write')`:
  - Live file always parses as valid JSONL after a successful save.
  - No `<file>.tmp.*` stragglers left in the directory after a successful
    save.

## Docs — expand 9 Tool() descriptions (Info)

The in-code `server.registerTool` descriptions were one-line WHAT
statements ("Create multiple new entities in the knowledge graph"). The
README has more context, but agents reading the MCP tool surface only
see the in-code description. Rewrote all 9 to follow a consistent shape:

  - WHAT (preserved)
  - when-to-use guidance (one short clause; e.g., for `read_graph`:
    "for targeted lookups prefer search_nodes (by query) or open_nodes
    (by name) since payload size grows with the graph")
  - return-shape hint
  - silent-skip behaviour (the fact that duplicate creates,
    non-existent deletes, and unknown observation removals are silently
    skipped is now documented in the description, not just inferable
    from tests)

No behavioural change to any tool dispatch path, no schema change, no
new dependencies.
@yakuphanycl yakuphanycl closed this by deleting the head repository May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant