Before MCP, every agent framework (LangChain, CrewAI, custom code) needed its own bespoke integration code to talk to any given tool or memory backend — a combinatorial mess of N frameworks × M tools needing custom glue each time. MCP standardizes this: any MCP-compatible client can call any MCP-compatible server's tools through one consistent protocol, so your memory service only needs to implement the protocol once to be usable everywhere.
# Simplified illustration of MCP server structure — consult the official
# MCP SDK docs (modelcontextprotocol.io) for the exact current API,
# since this evolves; the shape below is the stable conceptual pattern.
from mcp.server import Server
import chromadb
app = Server("memory-service")
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(name="mcp_memory")
@app.tool()
def remember(fact: str, user_id: str) -> str:
"""Store a new fact for a given user."""
fact_id = f"{user_id}_{collection.count()}"
collection.add(documents=[fact], ids=[fact_id], metadatas=[{"user_id": user_id}])
return f"Stored: {fact}"
@app.tool()
def recall(query: str, user_id: str, k: int = 3) -> list:
"""Retrieve relevant facts for a given user."""
results = collection.query(
query_texts=[query], n_results=k,
where={"user_id": user_id} # multi-tenancy filter
)
return results['documents'][0]
Note the where={"user_id": user_id} filter — this is your multi-tenancy boundary. Any query, no matter how it's phrased, is scoped so it can never retrieve another user's facts. This single line is the difference between a safe multi-tenant memory service and a serious data leak.
At minimum, every stored memory needs a tenant/user identifier attached as metadata, and every single retrieval call must filter by it — never rely on "the query probably won't match another user's data" as your isolation strategy; that's not isolation, that's hoping. For stricter isolation (e.g., enterprise customers), consider separate collections/indexes per tenant entirely, rather than a shared index with metadata filtering, since a filtering bug is a much smaller blast radius than a shared-index bug.
Look at how Mem0's SDK is shaped for inspiration:
from mem0 import Memory
m = Memory()
m.add("User's favorite color is blue", user_id="user123")
results = m.search("what color does the user like", user_id="user123")
Three methods. That's it. The lesson: your memory service's external API surface should be this minimal, even if the internals (extraction, routing, hybrid retrieval, graph) are complex. Complexity should live inside your service, not leak into every developer's integration code.
- Build the minimal MCP server above (or follow the current official SDK quickstart, since the exact API surface evolves) exposing
rememberandrecall. - Connect it to a real MCP client and verify you can store and retrieve a fact through the protocol, not just by calling your Python functions directly.
- Deliberately try to make user A's query retrieve user B's data — verify your multi-tenancy filter actually blocks this.