Skip to main content
Normalized for Mintlify from knowledge-base/neurigraph-memory-architecture/neurigraph-tool-references/01-MCP-Knowledge-Graph-Memory-Server.mdx.

Clean-Room Specification 01: MCP Knowledge Graph Memory Server

Document Purpose

This specification describes a persistent knowledge graph memory server that exposes graph operations (entities, relations, observations) through the Model Context Protocol (MCP). An AI coding model should be able to read this document and produce a functionally identical, working implementation without any additional references.

1. System Overview

1.1 What This System Does

This is a single-file TypeScript application that provides an AI assistant with persistent memory by storing a knowledge graph on disk. The server exposes 9 tools via MCP that allow an AI to create, query, and delete entities, relations, and observations. All data is stored in a single JSONL (JSON Lines) file — one JSON object per line.

1.2 Core Architecture

1.3 Key Design Decisions

  1. Full file read/write on every operation: loadGraph() reads the entire file; saveGraph() writes the entire file. There is no incremental append or partial update. This is intentional simplicity — the file is small enough for typical use.
  2. No database: Pure file-based storage. No SQLite, no external dependencies for persistence.
  3. Single-file implementation: The entire server is one TypeScript file (~300 lines). No separate modules.
  4. JSONL format: One JSON object per line. Entities first, then relations. Each object has a type discriminator field ("entity" or "relation").
  5. Deduplication by name: Entity names are unique identifiers. No numeric IDs. Entity deduplication uses exact string match on name. Relation deduplication uses exact match on the tuple (from, to, relationType).
  6. Case-insensitive search: The searchNodes method lowercases the query and compares against lowercased entity fields.
  7. Cascading deletes: Deleting an entity also removes all relations where that entity appears as either from or to.

2. Data Model

2.1 Core Types

2.2 JSONL File Format

The persistence file stores one JSON object per line. Each object includes a type discriminator field that is only present in the file format, not in the in-memory data structures. Entity line format:
Relation line format:
Complete file example (4 lines):
CRITICAL: Write order — When saving, ALL entity lines are written first, then ALL relation lines. This is the canonical ordering.

2.3 Loading and Saving

Loading (loadGraph):
  1. Read the entire file as a UTF-8 string
  2. Split by newline character (\n)
  3. Filter out empty lines
  4. Parse each line as JSON
  5. Use a reduce operation to accumulate into a KnowledgeGraph:
    • If item.type === "entity": strip the type field, push to entities array
    • If item.type === "relation": strip the type field, push to relations array
IMPORTANT: When loading, the type field is stripped from each object. The in-memory Entity and Relation objects do NOT contain a type property. The type field only exists in the serialized JSONL format. Saving (saveGraph):
  1. Concatenate all entity lines, then all relation lines, joined by \n
  2. Write the entire string to the file (overwriting completely)

3. KnowledgeGraphManager Class — Complete Method Specifications

3.1 Constructor and Initialization

The class takes a single constructor parameter: the file path (string) for the JSONL storage file.

3.2 loadGraph(): Promise<KnowledgeGraph>

Reads and parses the entire JSONL file. Algorithm:

3.3 saveGraph(graph: KnowledgeGraph): Promise<void>

Writes the entire graph to disk. Algorithm:

3.4 createEntities(entities: Entity[]): Promise<Entity[]>

Adds new entities, skipping any whose name already exists. Algorithm:
Return value: The array of entities that were actually created (excluding duplicates).

3.5 createRelations(relations: Relation[]): Promise<Relation[]>

Adds new relations, skipping exact duplicates. Algorithm:
Note: Two relations with the same from and to but different relationType values are NOT duplicates. They are distinct relations.
Adds observation strings to existing entities, skipping duplicate observation strings. Algorithm:
CRITICAL ERROR BEHAVIOR: If an entity name is not found, the method throws an error. This aborts the entire operation — no partial saves occur if the error happens mid-iteration (because the save happens after the loop).

3.7 deleteEntities(entityNames: string[]): Promise<void>

Removes entities AND all relations connected to those entities (cascading delete). Algorithm:
Return value: void (no return data).
Removes specific observation strings from entities. Algorithm:
IMPORTANT DIFFERENCE from addObservations: deleteObservations does NOT throw an error when an entity is not found. It silently ignores the deletion request for non-existent entities.

3.9 deleteRelations(relations: Relation[]): Promise<void>

Removes specific relations by exact match on all three fields. Algorithm:

3.10 readGraph(): Promise<KnowledgeGraph>

Returns the complete graph. Algorithm:
This is just a passthrough to loadGraph().

3.11 searchNodes(query: string): Promise<KnowledgeGraph>

Performs case-insensitive substring search across entity names, types, and observations. Algorithm:
Key details:
  • The search is SUBSTRING matching, not exact match. If query is “john”, it matches “Johnny”, “john_smith”, etc.
  • The search is case-INSENSITIVE — both query and target are lowercased before comparison.
  • Relations are included if EITHER endpoint matches — not just both.

3.12 openNodes(names: string[]): Promise<KnowledgeGraph>

Retrieves specific entities by exact name match, plus their connected relations. Algorithm:
Key details:
  • Entity lookup is EXACT string match (case-sensitive), unlike searchNodes which is case-insensitive substring.
  • Relations are returned if AT LEAST ONE endpoint matches a requested name. This means you may get relations pointing to/from entities that are NOT in the returned entities list.

4. MCP Tool Definitions

The server registers exactly 9 tools. Each tool has a name, description, input schema (defined with Zod), and a handler function. Below is the complete specification for each.

4.1 create_entities

Description: “Create multiple new entities in the knowledge graph” Input Schema:
Handler:
  1. Extract entities from parsed input arguments
  2. Call manager.createEntities(entities)
  3. Return the result (array of created entities) as a JSON-stringified text content response
Response format: MCP text content containing JSON array of the newly created entities.

4.2 create_relations

Description: “Create multiple new relations between entities in the knowledge graph. Relations are directed edges.” Input Schema:
Handler:
  1. Extract relations from parsed input arguments
  2. Call manager.createRelations(relations)
  3. Return result as JSON-stringified text content

4.3 add_observations

Description: “Add new observations to existing entities in the knowledge graph” Input Schema:
Handler:
  1. Extract observations from parsed input arguments
  2. Call manager.addObservations(observations)
  3. Return result as JSON-stringified text content
Error case: If entityName doesn’t match any existing entity, this will throw and the MCP framework surfaces the error to the caller.

4.4 delete_entities

Description: “Delete multiple entities and their associated relations from the knowledge graph” Input Schema:
Handler:
  1. Extract entityNames from parsed input arguments
  2. Call manager.deleteEntities(entityNames)
  3. Return confirmation message: "Entities deleted successfully"

4.5 delete_observations

Description: “Delete specific observations from entities in the knowledge graph” Input Schema:
Handler:
  1. Extract deletions from parsed input arguments
  2. Call manager.deleteObservations(deletions)
  3. Return confirmation message: "Observations deleted successfully"

4.6 delete_relations

Description: “Delete multiple relations from the knowledge graph” Input Schema:
Handler:
  1. Extract relations from parsed input arguments
  2. Call manager.deleteRelations(relations)
  3. Return confirmation message: "Relations deleted successfully"

4.7 read_graph

Description: “Read the entire knowledge graph”
Handler:
  1. Call manager.readGraph()
  2. Return the full KnowledgeGraph object as JSON-stringified text content

4.8 search_nodes

Description: “Search for nodes in the knowledge graph based on a query” Input Schema:
Handler:
  1. Extract query from parsed input arguments
  2. Call manager.searchNodes(query)
  3. Return the matching KnowledgeGraph as JSON-stringified text content

4.9 open_nodes

Description: “Open specific nodes in the knowledge graph by their names” Input Schema:
Handler:
  1. Extract names from parsed input arguments
  2. Call manager.openNodes(names)
  3. Return the matching KnowledgeGraph as JSON-stringified text content

5. MCP Server Setup and Transport

5.1 Server Initialization

The server uses the MCP SDK (@modelcontextprotocol/sdk) with the following setup:
Server creation:

5.2 Tool Registration Pattern

Each tool is registered using server.tool() with this signature:
IMPORTANT: The input schema passed to server.tool() is a flat object of Zod schemas, NOT a nested Zod object. For example:

5.3 Server Startup

The server communicates entirely over stdin/stdout using the MCP protocol (JSON-RPC). There is no HTTP server, no port binding.

5.4 Process Entry Point

The startup flow:
  1. Determine the memory file path (see Section 6)
  2. Instantiate KnowledgeGraphManager with the file path
  3. Register all 9 tools
  4. Create StdioServerTransport and connect

6. File Path Resolution and Migration

6.1 Memory File Path Resolution

The storage file path is determined by a function ensureMemoryFilePath(): Algorithm:

6.2 Legacy Migration (.json → .jsonl)

The system migrates from an older .json format to the current .jsonl format: Algorithm:
Note: The migration copies the raw content byte-for-byte. It does NOT re-parse and re-serialize. This works because the original format was already one-JSON-per-line (despite the .json extension).

7. Dependencies and Build Configuration

7.1 Runtime Dependencies

That’s it — ONE runtime dependency (plus its transitive dependencies including zod).

7.2 Package Configuration

Key details:
  • ESM module ("type": "module")
  • Single entry point compiled to dist/index.js
  • The build script compiles TypeScript and makes the output executable (chmod 755)
  • The bin field allows npx execution

7.3 TypeScript Configuration

  • Target: ES2022 or later (uses top-level await)
  • Module: ESM (Node16 or NodeNext module resolution)
  • Strict mode enabled
  • Output to dist/ directory

7.4 Shebang Line

The source file begins with:
This allows direct execution as a CLI tool.

8. Complete Behavioral Test Specifications

These test cases define the exact expected behavior. An implementation must pass all of these.

8.1 Entity Operations

Test: Create basic entities
  • Expected: Returns array with one entity. Graph now contains one entity.
Test: Entity deduplication by name
  • Setup: Create entity with name “Alice”
  • Input: Create entity with name “Alice” again (same or different type/observations)
  • Expected: Returns empty array (no new entity created). Only one “Alice” in graph.
Test: Multiple entities, some duplicates
  • Setup: Create entity “Alice”
  • Input: Create entities [“Alice”, “Bob”]
  • Expected: Returns array with only “Bob”. Both exist in graph.
Test: Entity name matching is case-SENSITIVE
  • Setup: Create entity “alice”
  • Input: Create entity “Alice” (capital A)
  • Expected: Returns [“Alice”]. Both “alice” and “Alice” exist as separate entities.

8.2 Relation Operations

Test: Create basic relation
  • Expected: Returns array with one relation.
Test: Relation deduplication
  • Input: Create same relation again
  • Expected: Returns empty array. Only one relation in graph.
Test: Same endpoints, different relationType
  • Expected: Returns the new relation. Both relations exist (they are distinct).
Test: Directionality matters
  • Expected: Returns the new relation. Both exist (A→B and B→A are different).

8.3 Observation Operations

Test: Add observations to existing entity
  • Setup: Create entity “Alice” with observations [“Is a student”]
  • Input: Add observations to “Alice”: [“Likes pizza”]
  • Entity “Alice” now has observations: [“Is a student”, “Likes pizza”]
Test: Observation deduplication
  • Setup: Entity “Alice” with observations [“Is a student”]
  • Input: Add observations to “Alice”: [“Is a student”, “Likes pizza”]
  • Only the new, non-duplicate observation is added.
Test: Add observations to non-existent entity
  • Input: Add observations to “Nonexistent”: [“anything”]
  • Expected: THROWS Error “Entity with name Nonexistent not found”
Test: Delete observations
  • Setup: Entity “Alice” with observations [“Is a student”, “Likes pizza”]
  • Input: Delete observations from “Alice”: [“Likes pizza”]
  • Expected: Entity “Alice” now has observations: [“Is a student”]
Test: Delete observations from non-existent entity
  • Input: Delete observations from “Nonexistent”: [“anything”]
  • Expected: NO error. Silent no-op. (Contrast with addObservations which DOES throw.)

8.4 Delete Operations

Test: Delete entity cascades to relations
  • Setup: Entities “Alice” and “Bob”. Relation: Alice → Bob “knows”
  • Input: Delete entities [“Alice”]
  • Expected: “Alice” entity removed. The “knows” relation is ALSO removed (cascade).
Test: Delete entity cascade — relation connected on ‘to’ side
  • Setup: Entities “Alice” and “Bob”. Relation: Bob → Alice “reports_to”
  • Input: Delete entities [“Alice”]
  • Expected: “Alice” removed. “reports_to” relation ALSO removed (Alice was the ‘to’ endpoint).
Test: Delete entity preserves unrelated relations
  • Setup: Entities A, B, C. Relations: A→B, B→C
  • Input: Delete entities [“A”]
  • Expected: A removed. A→B removed. B→C preserved (neither endpoint is A).
Test: Delete relations by exact match
  • Setup: Relations: A→B “knows”, A→B “likes”
  • Expected: “knows” removed. “likes” preserved.

8.5 Search Operations

Test: Search by entity name (case-insensitive)
  • Setup: Entity “John_Smith”
  • Input: Search query “john”
  • Expected: Returns entity “John_Smith”
Test: Search by entity type (case-insensitive)
  • Setup: Entity with entityType “Person”
  • Input: Search query “person”
  • Expected: Returns the entity
Test: Search by observation content (case-insensitive)
  • Setup: Entity with observation “Works at Google”
  • Input: Search query “google”
  • Expected: Returns the entity
Test: Search returns connected relations
  • Setup: Entities A and B. Relation A→B. Only A matches search.
  • Input: Search query matching only A
  • Expected: Returns entity A and relation A→B (because A is an endpoint)
Test: Search includes relations where at least one endpoint matches
  • Setup: Entities A, B, C. Relations: A→B, B→C. Search matches only B.
  • Input: Search query matching only B
  • Expected: Returns entity B, relation A→B, and relation B→C
Test: Search with no matches
  • Input: Search query “xyznonexistent”

8.6 Open Nodes Operations

Test: Open specific nodes
  • Setup: Entities A and B
  • Input: Open nodes [“A”]
  • Expected: Returns entity A (not B)
Test: Open nodes returns connected relations
  • Setup: Entities A, B. Relation A→B.
  • Input: Open nodes [“A”]
  • Expected: Returns entity A and relation A→B
Test: Open nodes — name matching is case-SENSITIVE
  • Setup: Entity “Alice”
  • Input: Open nodes [“alice”]
  • Expected: Returns empty (no match — exact case required)
Test: Open nodes with multiple names
  • Setup: Entities A, B, C. Relations: A→B, B→C, A→C.
  • Input: Open nodes [“A”, “C”]
  • Expected: Returns entities A and C. Returns relations A→B (A matches), B→C (C matches), A→C (both match).

8.7 Persistence Tests

Test: Data survives save/load cycle
  • Create entities and relations
  • Load graph from disk
  • Verify all data matches
Test: JSONL format correctness
  • Create entities and a relation
  • Read the raw file
  • Verify each line is valid JSON
  • Verify entity lines have "type":"entity"
  • Verify relation lines have "type":"relation"
  • Verify entities come before relations in the file
Test: Type field stripped on load
  • Write JSONL manually with type fields
  • Load graph
  • Verify loaded entities/relations do NOT contain type property

8.8 File Path and Migration Tests

Test: Default path
  • No MEMORY_FILE_PATH env var
Test: Absolute path from env var
  • Set MEMORY_FILE_PATH to “/tmp/custom.jsonl”
  • Expected path: /tmp/custom.jsonl
Test: Relative path from env var
  • Set MEMORY_FILE_PATH to “data/graph.jsonl”
Test: Migration from .json to .jsonl
  • Create a file at “memory.json” with valid content
  • Run ensureMemoryFilePath()
  • Expected: “memory.jsonl” now exists with same content. “memory.json” is deleted.
Test: No migration if .jsonl already exists
  • Both “memory.json” and “memory.jsonl” exist
  • Run ensureMemoryFilePath()
  • Expected: Neither file modified (JSONL takes priority)

When this server is used with an AI assistant (e.g., Claude Desktop), the following system prompt pattern is recommended to guide the AI in using the memory tools effectively:

10. Edge Cases and Implementation Notes

10.1 Thread Safety

There is NO concurrency protection. If two operations happen simultaneously, they will both loadGraph() and then saveGraph(), with the second write overwriting the first. This is acceptable for single-client MCP usage.

10.2 Empty Graph

10.3 File Encoding

All file reads and writes use UTF-8 encoding.

10.4 No Validation of Referential Integrity

createRelations does NOT verify that the from and to entity names actually exist in the graph. You can create relations referencing non-existent entities. Only addObservations validates entity existence.

10.5 No Pagination

All operations return full result sets. readGraph() returns every entity and relation. There is no pagination, limits, or cursor mechanism.

10.6 MCP Error Handling

When a tool handler throws an error (e.g., addObservations for non-existent entity), the MCP SDK framework catches it and returns it as an error response to the calling AI. The server itself does not crash — the error is per-tool-call.

10.7 Process Lifecycle

The server runs as a long-lived process, communicating over stdin/stdout. It stays alive until the parent process (e.g., Claude Desktop) terminates the connection.

11. Implementation Checklist

To build a functionally identical system, implement these in order:
  1. Data types: Define Entity, Relation, KnowledgeGraph interfaces
  2. KnowledgeGraphManager class:
    • loadGraph() — JSONL parsing with type field stripping
    • saveGraph() — JSONL serialization with type field addition
    • All 9 operation methods exactly as specified in Section 3
  3. File path resolution: ensureMemoryFilePath() with env var support and .json→.jsonl migration
  4. MCP server setup: Initialize McpServer with name “memory”, version “1.0.0”
  5. Tool registration: Register all 9 tools with Zod schemas as specified in Section 4
  6. Transport: Create StdioServerTransport and connect
  7. Package configuration: ESM module with shebang line and bin entry
  8. Test suite: All test cases from Section 8
Total expected implementation size: ~300 lines of TypeScript in a single file.