Summary
The Memory server's create_relations does not check that the referenced entities exist, so it silently persists relations pointing at entities that were never created (dangling relations). add_observations in the same file does validate entity existence, so the behaviour is inconsistent.
Reproduction
Against @modelcontextprotocol/server-memory (current main), on a fresh graph:
- Call
create_relations with [{ "from": "Ghost_A", "to": "Ghost_B", "relationType": "knows" }] — no entities exist yet.
→ returns success (isError: false).
- Call
read_graph → entities: [], relations: [{ "from": "Ghost_A", "to": "Ghost_B", ... }] — a dangling relation.
For contrast, add_observations with { "entityName": "Ghost_C", "contents": [...] } correctly fails with Entity with name Ghost_C not found.
Live output:
create_relations(nonexistent) -> isError: False
returned: [ { "from": "Ghost_A", "to": "Ghost_B", "relationType": "knows" } ]
add_observations(nonexistent) -> isError: True
returned: Entity with name Ghost_C not found
read_graph -> entities: 0 | relations: 1
A minimal (~30-line) stdio reproduction using the MCP Python client is here: https://github.com/chrisbartoloburlo/llmcontract-mcp-audit/blob/main/servers/memory/probe.py
Root cause
src/memory/index.ts, createRelations filters only for duplicates and then pushes, with no existence check — unlike addObservations, which throws Entity with name ... not found:
async createRelations(relations: Relation[]): Promise<Relation[]> {
const graph = await this.loadGraph();
const newRelations = relations.filter(r => !graph.relations.some(er =>
er.from === r.from && er.to === r.to && er.relationType === r.relationType));
graph.relations.push(...newRelations); // no check that from/to exist
await this.saveGraph(graph);
return newRelations;
}
Impact
read_graph returns referentially-inconsistent state that persists to MEMORY_FILE_PATH. Downstream consumers that assume referential integrity (graph traversal, visualization, an agent reasoning over the graph) get silently corrupt data — whereas the analogous observation case fails loudly.
Suggested fix
In createRelations, validate that every relation's from/to names an existing entity (mirroring addObservations) and throw, or skip-and-report, otherwise.
Found with llmcontract, a session-type runtime monitor for agent/tool protocols: encoding the documented "entities before relations" workflow as a contract flags these sequences that the server accepts. Full case study (protocol, probe, replay tests): https://github.com/chrisbartoloburlo/llmcontract-mcp-audit/tree/main/servers/memory
Summary
The Memory server's
create_relationsdoes not check that the referenced entities exist, so it silently persists relations pointing at entities that were never created (dangling relations).add_observationsin the same file does validate entity existence, so the behaviour is inconsistent.Reproduction
Against
@modelcontextprotocol/server-memory(currentmain), on a fresh graph:create_relationswith[{ "from": "Ghost_A", "to": "Ghost_B", "relationType": "knows" }]— no entities exist yet.→ returns success (
isError: false).read_graph→entities: [],relations: [{ "from": "Ghost_A", "to": "Ghost_B", ... }]— a dangling relation.For contrast,
add_observationswith{ "entityName": "Ghost_C", "contents": [...] }correctly fails withEntity with name Ghost_C not found.Live output:
A minimal (~30-line) stdio reproduction using the MCP Python client is here: https://github.com/chrisbartoloburlo/llmcontract-mcp-audit/blob/main/servers/memory/probe.py
Root cause
src/memory/index.ts,createRelationsfilters only for duplicates and then pushes, with no existence check — unlikeaddObservations, which throwsEntity with name ... not found:Impact
read_graphreturns referentially-inconsistent state that persists toMEMORY_FILE_PATH. Downstream consumers that assume referential integrity (graph traversal, visualization, an agent reasoning over the graph) get silently corrupt data — whereas the analogous observation case fails loudly.Suggested fix
In
createRelations, validate that every relation'sfrom/tonames an existing entity (mirroringaddObservations) and throw, or skip-and-report, otherwise.Found with llmcontract, a session-type runtime monitor for agent/tool protocols: encoding the documented "entities before relations" workflow as a contract flags these sequences that the server accepts. Full case study (protocol, probe, replay tests): https://github.com/chrisbartoloburlo/llmcontract-mcp-audit/tree/main/servers/memory