FalkorDB
Solutions
Use cases
Why FalkorDB
Developers

Shared Memory Graph for MCP Agents: How to Give Multiple Agents a Single Source of Truth

Title card: Shared Memory Graph for MCP Agents

The previous post in this series covered single-agent memory: one agent, one user, one graph. That model holds until you add a second agent.

Once two agents share a user, a task, or an organizational context, per-agent memory becomes the problem. Agent A writes a decision. Agent B writes a contradicting one. Neither knows what the other stored. The user sees inconsistency, and the debugging session traces back to the fact that you never defined who owns what.

Shared multi-agent memory requires an explicit graph schema, server-side write ownership, and provenance on every update.

Per-Agent Memory vs. Shared Memory

Per-agent memory is scoped. Each agent maintains its own store, isolated from every other agent. That isolation is useful when agents are independent: no cross-contamination, no coordination overhead, clean deletes. The retrieval architecture post covers this pattern in depth.

Shared memory is a different architectural problem. Multiple agents read from and write to the same graph. A researcher agent that discovers a new entity makes it immediately available to a writer and a reviewer without any message-passing or sync protocol. That’s the upside. The downside is that without explicit write ownership, agents silently overwrite each other’s facts.

Dimension Per-agent memory Shared memory graph
Isolation Complete Explicit boundaries required
Consistency Trivial (one writer) Requires write ownership rules
Freshness Stale across agents Current on next read
Conflict risk None High without provenance
Use case Single-agent, single-user Multi-agent coordination

In a shared graph, the database is no longer the isolation boundary. The graph schema and the MCP server layer are.

The Graph Schema

A shared memory graph needs a schema that covers the full coordination surface: agents, users, tasks, entities, and decisions.

Nodes

  • Agent: one node per agent type. Properties: agent_id, role, capabilities[].
  • User: the human in the loop. Properties: user_id, created_at.
  • Task: a unit of work. Properties: task_id, status, created_at, closed_at (plus provenance fields).
  • Entity: any named thing agents discover or reference. Properties: name, type, embedding (plus provenance fields).
  • Decision: a conclusion that affects downstream work. Properties: decision_id, content, confidence, agent_id, written_at, source, valid_from, valid_to, superseded_by.

Relationships

(Agent)-[:ASSIGNED_TO]->(Task)
(User)-[:OWNS]->(Task)
(Agent)-[:WROTE {created_by, created_at}]->(Decision)
(Decision)-[:REFERENCES]->(Entity)
(Entity)-[:RELATED_TO]->(Entity)
(Task)-[:PRODUCED]->(Decision)
(Agent)-[:DISCOVERED]->(Entity)

Every node carries three mandatory provenance properties: agent_id, written_at, and source. Every relationship carries created_by and created_at. These fields are the audit trail that answers “which agent wrote this, when, and based on what.”

The entity side of the schema is intentionally generic. Agents can discover new entity types without a schema migration. Specificity lives in the type property and in relationship labels.

Read and Write Boundaries

Every agent gets read access to the full shared graph. Write access should be narrow, and it should be enforced server-side, not by trusting a role claim from the calling agent.

Each agent owns the node types it creates. A researcher writes Entity and Decision nodes. A planner writes Task nodes. A reviewer can supersede a Decision by closing the existing one and writing a replacement. Crossing those lines is how contradictions enter the graph.

The role-to-permission mapping lives on the server, keyed by agent_id. An agent cannot escalate its own permissions by passing a different role in the request.

Write permissions by role

Agent role Can write Cannot write
Researcher Entity, Decision Task, Agent
Writer Decision (drafts) Entity, Task
Reviewer Decision (supersede existing) Entity, Task
Planner Task Entity, Decision content

All roles have full read access via ro_query. No MERGE or CREATE runs outside the MCP write tools.

When a Decision node contains bad data, the server-side role map tells you immediately which agent type wrote it. That’s the debugging advantage of write ownership: you don’t trace logs across four agents.

Preventing Duplicate and Contradictory Memories

Two agents discovering the same entity is expected. Two agents storing it under different names is a bug. “Acme Corp,” “ACME,” and “acme.com” are the same entity. If your graph has three nodes for them, every traversal from that entity misses two-thirds of the connected facts.

Resolve entities at write time. Before writing a new Entity node, run a vector similarity search against existing Entity embeddings and a name normalization pass. If a match exceeds the similarity threshold, merge into the existing node. This deduplication step is not in the minimal code example below; it belongs in a preprocessing layer before the graph write.

For contradictory Decisions, close the old fact before writing the new one. A reviewer agent superseding a researcher’s decision runs this as a single chained query, so both steps succeed or neither does:

// Match both nodes first, then SET and CREATE atomically
MATCH (d:Decision {decision_id: $old_id})
WHERE d.valid_to IS NULL
MATCH (a:Agent {agent_id: $agent_id})
SET d.valid_to = $now, d.superseded_by = $new_id
CREATE (d2:Decision {
  decision_id: $new_id,
  content: $content,
  agent_id: $agent_id,
  written_at: $now,
  valid_from: $now,
  source: $source,
  confidence: $confidence
})
CREATE (a)-[:WROTE {created_by: $agent_id, created_at: $now}]->(d2)

Both MATCHes run before any write, so a missing Agent node causes the whole query to return nothing rather than leaving the old Decision closed with no replacement. FalkorDB runs each query atomically and serializes writes to a graph, so two concurrent supersedes can’t both close the same Decision. A query filtered on valid_to IS NULL returns exactly one current Decision.

MCP Memory Server Architecture with FalkorDB

The MCP memory server is the single entry point for all graph writes. Agents call tools. The server looks up the calling agent’s role from a server-side map, checks write permissions, and executes the Cypher. No agent touches FalkorDB directly.

Below is a minimal FastMCP server exposing write_memory and read_memory. This example shows agent_id as a tool parameter for readability; in production it should come from the authenticated MCP session so agents can’t pass a peer’s ID. The role map is keyed by agent_id on the server. This example omits entity deduplication, relationship creation, and supersede logic to stay readable; all three belong in a production implementation. For the production MCP server, see the FalkorDB MCP server docs, which covers deployment and configuration.

from mcp.server.fastmcp import FastMCP
from falkordb import FalkorDB
import time

db = FalkorDB(host="localhost", port=6379)
graph = db.select_graph("shared_agent_memory")
mcp = FastMCP("falkordb-memory")

# Role is looked up server-side by agent_id, not supplied by the caller
AGENT_ROLES: dict[str, str] = {
    "agent-researcher-01": "researcher",
    "agent-writer-01":     "writer",
    "agent-reviewer-01":   "reviewer",
    "agent-planner-01":    "planner",
}

WRITE_PERMISSIONS: dict[str, set[str]] = {
    "researcher": {"Entity", "Decision"},
    "writer":     {"Decision"},
    "reviewer":   {"Decision"},
    "planner":    {"Task"},
}

@mcp.tool()
def write_memory(
    agent_id: str,
    node_id: str,
    content: str,
    source: str,
    confidence: float = 1.0,
) -> dict:
    """Write a Decision node to the shared memory graph with provenance.
    Entity and Task writes use separate tools, not shown.
    agent_id should come from the authenticated MCP session in production.
    """
    role = AGENT_ROLES.get(agent_id)
    if role is None:
        return {"error": f"Unknown agent_id: {agent_id}"}

    allowed = WRITE_PERMISSIONS.get(role, set())
    if "Decision" not in allowed:
        return {"error": f"Role '{role}' cannot write Decision nodes."}

    now = int(time.time() * 1000)

    graph.query(
        "CREATE (n:Decision {"
        "  decision_id: $node_id, content: $content,"
        "  agent_id: $agent_id, written_at: $now,"
        "  source: $source, confidence: $confidence,"
        "  valid_from: $now"
        "})",
        {
            "node_id": node_id,
            "content": content,
            "agent_id": agent_id,
            "now": now,
            "source": source,
            "confidence": confidence,
        },
    )
    return {"decision_id": node_id, "written_at": now}

@mcp.tool()
def read_memory(cypher: str, params: dict | None = None) -> list:
    """Query the shared memory graph. Read-only.
    Return scalar properties in your query (e.g. RETURN d.content, d.agent_id),
    not nodes or edges, which are not JSON-serializable.
    """
    result = graph.ro_query(cypher, params or {})
    # ro_query enforces read-only at the database level
    return [list(row) for row in result.result_set]

if __name__ == "__main__":
    mcp.run()

ro_query enforces read-only access at the FalkorDB level, so no keyword blocklist is needed. The role check in write_memory runs before any database call. The reviewer’s supersede right is in the permissions table; the supersede tool itself is omitted here for brevity.

What This Architecture Gives You

A shared memory graph built on this pattern delivers four concrete properties.

Immediate consistency across agents. When a researcher writes a new Entity, the writer and reviewer see it on their next read. No sync, no polling, no message bus for memory updates.

One current version per fact. The chained supersede query means valid_to IS NULL always returns exactly one active Decision. History is preserved for audit; it’s just not surfaced in retrieval.

A traceable write trail. Every node carries agent_id, written_at, and source. Every WROTE relationship carries created_by and created_at. When an answer is wrong, you trace it to the specific write and the agent that made it. For a deeper look at how this maps to enterprise auditability requirements, see the enterprise agents context layer reference.

Write ownership enforced at the server. The MCP server is the only path to the graph. The role map lives on the server, keyed by the authenticated session identity. Permissions are checked before any Cypher executes.

Post 6 in this series covers RBAC for agents.

Try FalkorDB Cloud or run it locally with one Docker command:

docker run -p 6379:6379 -p 3000:3000 -it --rm falkordb/falkordb:latest

Then open localhost:3000 to see the shared memory graph in FalkorDB Browser.