How KnowledgeGraphManager Loads and Saves the Graph in the MCP Memory Server

The KnowledgeGraphManager class persists knowledge graphs as JSON Lines in a local file, loading entities and relations on demand and atomically saving mutations back to disk.

The MCP Memory server from the modelcontextprotocol/servers repository maintains persistent knowledge using a graph structure of interconnected entities. At the core of this persistence layer lies the KnowledgeGraphManager class implemented in src/memory/index.ts, which handles all disk I/O operations for the knowledge graph. Understanding how this manager loads and saves data is essential for developers building tools that interact with the memory system or extending the server's capabilities.

Determining the Memory File Path

Before any loading or saving occurs, the server must resolve where to store the graph data. The ensureMemoryFilePath() function handles this initialization logic at startup.

  • If the MEMORY_FILE_PATH environment variable is set, the function uses that value directly, resolving relative paths against the script location
  • If unset, it defaults to memory.jsonl in the working directory and migrates any legacy memory.json file if present
  • The resolved path is stored in the global MEMORY_FILE_PATH variable and passed to the KnowledgeGraphManager instance during initialization

This resolution happens during server startup (lines 14-44 of src/memory/index.ts), ensuring the manager knows exactly which file to monitor before handling any tool requests.

Loading the Knowledge Graph

When MCP tools like read_graph or search_nodes request data, they invoke knowledgeGraphManager.loadGraph(). This method implements a streaming JSON Lines parser that reconstructs the graph object from disk.

The method reads the entire file as UTF-8 text and splits it into non-empty lines. Each line represents a single JSON record containing either an entity or relation, distinguished by a type field. Entities are identified when item.type === "entity" and pushed to graph.entities, while relations are identified when item.type === "relation" and pushed to graph.relations.

If the file does not exist, the method catches the ENOENT error and gracefully returns an empty graph structure ({ entities: [], relations: [] }), allowing the server to start fresh without crashing.

Load Implementation Details

According to the source code at lines 71-93 of src/memory/index.ts, the loadGraph method operates as follows:

private async loadGraph(): Promise<KnowledgeGraph> {
  try {
    const data = await fs.readFile(this.memoryFilePath, "utf-8");
    const lines = data.split("\n").filter(l => l.trim() !== "");
    return lines.reduce((graph, line) => {
      const item = JSON.parse(line);
      if (item.type === "entity") {
        graph.entities.push({ name: item.name, entityType: item.entityType, observations: item.observations });
      }
      if (item.type === "relation") {
        graph.relations.push({ from: item.from, to: r.to, relationType: item.relationType });
      }
      return graph;
    }, { entities: [], relations: [] });
  } catch (error) {
    if ((error as any).code === "ENOENT") return { entities: [], relations: [] };
    throw error;
  }
}

Saving the Knowledge Graph

Mutations such as create_entities or delete_relations trigger saveGraph(graph), which serializes the entire in-memory graph back to disk. This method ensures that every entity and relation is written with its type discriminator to maintain the JSON Lines format.

The method maps every entity to a JSON string prefixed with type: "entity" and every relation with type: "relation", joining all records with newline characters. This write pattern replaces the entire file content, ensuring that deletions are properly persisted and the file remains valid even if the process encounters errors during the operation.

Save Implementation Details

The saveGraph implementation from lines 101-117 of src/memory/index.ts demonstrates the serialization logic:

private async saveGraph(graph: KnowledgeGraph): Promise<void> {
  const lines = [
    ...graph.entities.map(e => JSON.stringify({ type: "entity", name: e.name, entityType: e.entityType, observations: e.observations })),
    ...graph.relations.map(r => JSON.stringify({ type: "relation", from: r.from, to: r.to, relationType: r.relationType })),
  ];
  await fs.writeFile(this.memoryFilePath, lines.join("\n"));
}

Integration with MCP Tools

The persistence layer integrates seamlessly with the Model Context Protocol tool system through a consistent load-modify-save pattern. Read-only tools simply call loadGraph() and return the result, while mutation tools follow a specific transaction flow.

When handling a mutation request:

  1. The tool handler calls loadGraph() to retrieve the current state from memory.jsonl
  2. It modifies the in-memory graph by adding or removing entities and relations
  3. It invokes saveGraph(updatedGraph) to persist the changes back to disk

This pattern guarantees that the in-memory representation always remains synchronized with the on-disk JSON Lines file, preventing data loss across server restarts.

Summary

  • File resolution: The ensureMemoryFilePath() function determines the storage location via the MEMORY_FILE_PATH environment variable or defaults to memory.jsonl
  • JSON Lines format: The graph stores entities and relations as newline-delimited JSON records with type discriminators for efficient streaming
  • Graceful initialization: loadGraph() returns empty graphs for missing files by catching ENOENT errors, allowing clean server starts
  • Atomic persistence: saveGraph() writes the complete graph as a single string operation, ensuring the file is replaced atomically
  • Synchronous consistency: All tool operations follow load-modify-save cycles to maintain alignment between disk and memory states

Frequently Asked Questions

What file format does the Memory server use to store knowledge graphs?

The Memory server uses JSON Lines (JSONL) format in a file named memory.jsonl. Each line represents a single JSON object—either an entity with type: "entity" or a relation with type: "relation"—which allows the KnowledgeGraphManager to stream data efficiently without parsing an entire large JSON object into memory.

How does the KnowledgeGraphManager handle missing memory files?

When loadGraph() encounters an ENOENT error indicating the file does not exist, it returns an empty graph structure containing empty arrays for entities and relations rather than throwing an exception. This allows the server to start cleanly without existing data and create the file automatically upon the first save operation.

Can I change the location of the memory file?

Yes. Set the MEMORY_FILE_PATH environment variable before starting the server. The ensureMemoryFilePath() function in src/memory/index.ts checks this variable first and resolves relative paths against the script location, giving you full control over where the persistence file resides.

Is the save operation atomic?

The saveGraph() method writes the entire graph content as a single string joined by newlines using fs.writeFile(), which typically replaces the file atomically at the filesystem level. This prevents file corruption during write operations, ensuring that the memory.jsonl file remains valid even if the process crashes during the save.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →