Repository navigation
Description
Which server?
memory (@modelcontextprotocol/server-memory)
Protocol era
Modern (2026-07-28)
MCP client
No response
The problem
For context: I've been reading the documentation, the implementation, and the existing tests side by side — this note is about a place where the three don't quite line up.
The memory server's existing tests pin the atomic save, the stale-lock handling, and save-failure behaviour well. What they don't pin is what happens when the data file itself is destroyed from outside the process between operations: an operator deleting it, a rotation truncating it, a directory appearing at its path, or the temp remnant of a killed run left beside it. The server's promises are "atomic save" and "never silently drop data"; the cases below pin how those promises behave under external destruction. All four pass against the current implementation — they fix behaviour that today rests on implication, not on a test.
Cases
- File deleted between operations — the next write recreates it.
loadGraphmaps ENOENT to an empty graph (src/memory/index.ts:502-510), so a vanished file is a fresh start and the write recreates it; only the new graph is read back. - File truncated to zero bytes — reads as an empty graph and the server keeps working. Zero lines is the natural empty-graph case of
loadGraph. - A directory appears at the data path — reads and writes fail loudly with EISDIR instead of silently reporting an empty graph (which would let a later save bury real data). Only ENOENT maps to empty (index.ts:506-513). The EISDIR case in file-lock.test.ts:349 covers the lock-acquisition stage; the load stage was unpinned.
- An orphaned temp remnant from a killed run — a file matching saveGraph's pattern (
<name>.<32 hex>.tmp) with corrupt content sitting beside the data file affects neither loads nor saves, and is left untouched. Saves write a fresh temp + rename (index.ts:596-599) and loads read only the exact data path.
Solution you have in mind (optional)
Proposed tests
For src/memory/__tests__/knowledge-graph.test.ts (or a new file in the same style), driving KnowledgeGraphManager against a temp directory:
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { promises as fs } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { KnowledgeGraphManager } from "../index.js";
describe("KnowledgeGraphManager against a file destroyed from outside", () => {
let manager: KnowledgeGraphManager;
let testFilePath: string;
let testDir: string;
beforeEach(async () => {
testDir = await fs.mkdtemp(
path.join(path.dirname(fileURLToPath(import.meta.url)), "overnight-cable-"),
);
testFilePath = path.join(testDir, "memory.jsonl");
manager = new KnowledgeGraphManager(testFilePath);
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it("recreates the file when it is deleted between operations", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
]);
await fs.unlink(testFilePath);
const created = await manager.createEntities([
{ name: "Bob", entityType: "person", observations: [] },
]);
expect(created).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.entities.map((e) => e.name)).toEqual(["Bob"]);
const onDisk = await fs.readFile(testFilePath, "utf-8");
expect(onDisk).toContain('"Bob"');
});
it("reads an externally truncated file as an empty graph and keeps working", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: ["x"] },
]);
await fs.writeFile(testFilePath, "");
const graph = await manager.readGraph();
expect(graph.entities).toEqual([]);
expect(graph.relations).toEqual([]);
await manager.createEntities([
{ name: "Carol", entityType: "person", observations: [] },
]);
const after = await manager.readGraph();
expect(after.entities.map((e) => e.name)).toEqual(["Carol"]);
});
it("fails loudly, not silently empty, when the data path is a directory", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
]);
await fs.unlink(testFilePath);
await fs.mkdir(testFilePath);
await expect(manager.readGraph()).rejects.toThrow(/EISDIR/);
await expect(
manager.createEntities([
{ name: "Bob", entityType: "person", observations: [] },
]),
).rejects.toThrow(/EISDIR/);
});
it("ignores a previous run's orphaned temp file beside the data file", async () => {
await manager.createEntities([
{ name: "Alice", entityType: "person", observations: [] },
]);
const stray = path.join(
testDir,
"memory.jsonl.0123456789abcdef0123456789abcdef.tmp",
);
await fs.writeFile(stray, '{"type":"entity","name":"Ghost",');
const graph = await manager.readGraph();
expect(graph.entities.map((e) => e.name)).toEqual(["Alice"]);
await manager.createEntities([
{ name: "Bob", entityType: "person", observations: [] },
]);
const after = await manager.readGraph();
expect(after.entities.map((e) => e.name)).toEqual(["Alice", "Bob"]);
expect(await fs.readFile(stray, "utf-8")).toContain("Ghost");
});
});Which MCP feature does it demonstrate? (optional)
No response
Alternatives or workarounds you have tried
No workaround covers this — the behaviour is currently only implied, not pinned by a test. Related issues checked during duplicate search: #4885 and #4827 — near hits, not duplicates.
Already built it locally?
Verification
I ran these against a byte-identical copy of v2/main @ 6cfff62 (Node 24.22.1, vitest 4.1.12): 4 passed. Full memory suite with these included: 18 files, 241 passed + 1 skipped. No behaviour change is proposed; if any case's expectation is not the intended contract, the test is the place to say so.
Prompt used
Per this repository's contribution practice, the prompt that produced these tests: "In src/memory, add tests to the knowledge-graph suite for the data file being destroyed from OUTSIDE the process between operations: (1) the file is deleted — the next write recreates it and reads back only the new graph; (2) the file is truncated to zero bytes — it reads as an empty graph and the server keeps working; (3) a directory appears at the data path — reads and writes fail with EISDIR rather than reporting an empty graph; (4) an orphaned temp file in saveGraph's naming pattern (<name>.<32 hex>.tmp) sits beside the data file — loads and saves are unaffected and the remnant is left untouched. Drive everything through KnowledgeGraphManager against a temp directory, in the style of tests/knowledge-graph.test.ts."
Before you submit
- I searched existing issues and this is not a duplicate.
- This is a request for this repository or one of its servers, not a new server, the MCP specification or an SDK.
Activity