Skip to main content
Glama
JrmHsr

mcp-persistent-context

by JrmHsr

MCP Persistent Context

A lightweight memory layer for custom MCP servers — persist user context across LLM sessions with minimal token cost.

4 tools. ~600 schema tokens. Key-value dedup. Multi-tenant. Cross-MCP.

Why?

LLMs forget everything between sessions. Every conversation starts from zero.

The official @modelcontextprotocol/server-memory solves this with a knowledge graph (entities, relations, observations). It's powerful — but can be overkill for simple context persistence:

server-memory

This project

Tools

9 (~1500 schema tokens)

4 (~600 schema tokens)

Read cost

read_graph returns full graph

Paginated, filtered, compact

Multi-tenant

No

Yes (client_id)

Cross-MCP

No

Yes (namespace)

Dedup

By entity name

By key within (client_id, namespace, category)

TTL

No

Optional per-entry expiration

They are complementary. Use server-memory when you need entity relationships. Use this when you need fast, cheap, structured context for custom MCP projects.

Related MCP server: Samantha

Which version should I use?

Do you have 1 MCP server or multiple?

  1 MCP server ──→ Embed (tools_memory.py)
                    Copy into your project, register tools, done.

  2+ MCP servers ─→ Standalone (mcp_memory_server.py)
                    Run as a separate MCP. Domain servers stay clean.
                    Context is shared across all MCPs.

Embedded

Standalone

File

tools_memory.py

mcp_memory_server.py

Setup

Import + register in your server

Run as separate process

Cross-MCP

No (lives inside one MCP)

Yes (shared by all MCPs)

Schema cost

Adds ~600 tokens to your MCP

~600 tokens in its own MCP

Best for

Single MCP projects

Multi-MCP architectures

Quick Start

Standalone server

pip install "mcp[cli]"
python mcp_memory_server.py --transport streamable-http --port 8770

For Claude Desktop (stdio):

{
  "mcpServers": {
    "memory": {
      "command": "python",
      "args": ["path/to/mcp_memory_server.py"],
      "env": {
        "MEMORY_DIR": "/path/to/memory_data",
        "MAX_ENTRIES_PER_CLIENT": "500"
      }
    }
  }
}

Embedded in your MCP server

from mcp.server.fastmcp import FastMCP
from tools_memory import register_memory_tools
from pathlib import Path
from datetime import datetime

mcp = FastMCP("My App")

# Register your domain tools
@mcp.tool()
def do_something(query: str) -> str:
    return process(query)

# Register memory tools (4 tools added to your server)
register_memory_tools(
    mcp,
    memory_dir=Path("./memory_data"),
)

mcp.run()

Tools

save_memory

save_memory(
    category="business_context",
    type="insight",
    content="AS=0 | persona=seniors | monetization=affiliate",
    reason="Client business context for SEO strategy.",
    client_id="_default",
    namespace="general",
    ttl_days=0
)
→ "Saved. business_context | insight | 4 entries"

get_memory

get_memory(client_id="acme_corp")
→ Memory 'acme_corp' (4/4):
  2026-02-26 INSIGHT | general/business_context | AS=0 | persona=seniors | monetization=affiliate
  2026-02-26 DECISION | seo/domain_context | pillar=cloud_computing | approach=editorial_first
  2026-02-26 EXCLUSION | seo/domain_context | exclude=serverless | reason=off_topic
  2026-02-15 ACTION | general/project_config | stack=React+Node | deploy=Vercel [90d]

delete_memory

delete_memory(content_match="persona", client_id="acme_corp")
→ "Deleted: AS=0 | persona=seniors | monetization=affiliate
   3 entries remaining"

memory_status

memory_status(client_id="acme_corp")
→ "'acme_corp': 3 entries | ns: general, seo | cat: business_context, domain_context | 2026-02-15 → 2026-02-26 | 1 with TTL"

Content Format

key=value | key=value | key=value

Why key=value, not JSON?

  • 2-3x fewer tokens ({"key":"value"} = 7 tokens, key=value = 3)

  • Enables key-based dedup without NLP

  • LLMs naturally produce and parse it

  • Works across any domain

Key=value is recommended, not enforced. The server warns if no = is detected, but still saves the entry. Some use cases need free text (e.g. content="Client confirmed budget by phone"). The dedup engine simply skips entries without parseable keys.

Examples across domains:

# Marketing / SEO
"AS=0 | persona=seniors | monetization=affiliate+partnerships"

# Healthcare
"allergy=penicillin | blood_type=O+ | primary_care=Dr.Smith"

# Software Engineering
"stack=React+Node | deploy=Vercel | CI=GitHub_Actions"

# Legal
"jurisdiction=FR | entity=SAS | fiscal_year=calendar"

# Education
"level=grade10 | learning_style=visual | weakness=algebra"

Key-Based Dedup

Same (client_id, namespace, category) + overlapping key → merge, don't duplicate:

Existing:  "AS=0 | persona=seniors"
Incoming:  "AS=12 | site=launched"
Result:    "AS=12 | persona=seniors | site=launched"

No parseable keys → append as new entry (no dedup attempted).

Integration Examples

Example 1: Domain MCP delegates memory to standalone server

Your domain MCP does its job. Memory lives elsewhere.

# my_domain_mcp.py — zero memory logic
@mcp.tool()
def analyze_data(query: str) -> str:
    results = run_analysis(query)
    return json.dumps(results)

Claude's system prompt handles the memory calls:

You have access to two MCP servers: Domain and Memory.
At session start: call get_memory() to load user context.
When the user shares business context, preferences, or decisions:
  call save_memory() with key=value content.

Claude sees both MCPs, calls get_memory() at start, calls domain tools for work, calls save_memory() when the user shares context. The domain MCP never touches memory.

Example 2: Single MCP with embedded memory

# my_mcp_server.py
from mcp.server.fastmcp import FastMCP
from tools_memory import register_memory_tools

mcp = FastMCP("My App")

@mcp.tool()
def do_something(query: str) -> str:
    result = process(query)
    # Trigger reminder in response
    return f"{result}\n\nMEMORY: save_memory() if user shared context."

register_memory_tools(mcp, memory_dir=Path("./data"))
mcp.run()

Example 3: Multi-tenant with namespace filtering

# User works with client "acme_corp" across multiple domains

# Session 1 (SEO context)
save_memory(client_id="acme_corp", namespace="seo",
            category="business_context", type="insight",
            content="AS=45 | market=US | vertical=saas",
            reason="SEO baseline metrics")

# Session 2 (Ads context) — can read SEO memory too
get_memory(client_id="acme_corp")
# → returns BOTH seo and ads entries

get_memory(client_id="acme_corp", namespace="ads")
# → returns only ads entries

Categories

Recommended (cover most domains):

Category

What it stores

business_context

Company, market, monetization, personas

project_config

Stack, architecture, conventions

user_preference

Workflow, tone, formatting style

domain_context

Domain-specific decisions

analysis_context

Recurring findings, baselines

content_strategy

Editorial guidelines, content types

Custom: Use x_ prefix (x_medical_history, x_legal_discovery). The server warns on unknown categories but does not reject them.

Types

Type

When to use

decision

User chose between options

exclusion

User explicitly rejected something

insight

Factual context about user/project

action

User committed to a plan

anomaly

Unexpected finding worth remembering

TTL (Time-To-Live)

save_memory(..., ttl_days=90)  # expires in 90 days
save_memory(..., ttl_days=0)   # permanent (default)
  • Permanent: Business identity, user preferences, architecture decisions

  • 90 days: Campaign context, quarterly goals

  • 30 days: Temporary constraints, short-term priorities

Expired entries are pruned automatically on get_memory.

Architecture: 1 Memory MCP, N Domain MCPs

┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│  MCP SEO    │   │  MCP Ads    │   │  MCP Email  │
│  0 memory   │   │  0 memory   │   │  0 memory   │
│  tools      │   │  tools      │   │  tools      │
└──────┬──────┘   └──────┬──────┘   └──────┬──────┘
       │                 │                 │
       └────────┬────────┴────────┬────────┘
                │                 │
         ┌──────┴──────┐         │
         │ MCP Memory  │◄────────┘
         │ 4 tools     │
         │ shared ctx  │
         │ ~600 tok    │
         └─────────────┘

Benefits:

  • Schema tokens: ~600 total (not ~600 x N)

  • 1 get_memory at session start (not N)

  • Context from SEO visible in Ads and vice versa

  • Domain MCPs stay focused on their job

Triggering Memory Calls

System prompt instructions alone do NOT reliably trigger LLM memory calls.

Strategy A — Dedicated Memory MCP (recommended for multi-MCP):

Add to system prompt:

At session start: call get_memory() to load user context.
After state-changing tools: if the user shared context, call save_memory().

Strategy B — Embedded in domain MCP (for single-MCP setups):

Inject short reminders in tool responses:

MEMORY: context shared? → save_memory() | correction? → delete_memory()

Keep trigger text under 25 tokens per tool response.

What to persist

Persist

Don't persist

User decisions and preferences

Tool outputs or raw data

Business constraints

Intermediate calculations

Explicit corrections

Session-specific state

What changes future behavior

What can be re-derived

Server-Side Guards

Guard

Rule

Key dedup

Same (client_id, ns, category) + overlapping key → merge

Truncate

content capped at 500 chars

Prune

Max entries per client (default: 200, configurable)

TTL

Expired entries pruned on read

Content warning

Soft warn if no = detected (does not reject)

Category warning

Soft warn on non-standard categories (does not reject)

Configuration

Variable

Default

Description

MEMORY_DIR

./memory_data

Base directory for memory files

MEMORY_PORT

8770

HTTP port (streamable-http transport)

MAX_CONTENT_LEN

500

Max characters per content field

MAX_ENTRIES_PER_CLIENT

200

Max entries per client before pruning oldest

Storage

{MEMORY_DIR}/{client_id}/memory.json

Each entry:

{
  "namespace": "seo",
  "category": "business_context",
  "type": "insight",
  "content": "AS=0 | persona=seniors | monetization=affiliate",
  "reason": "Client business profile.",
  "date": "2026-02-26T14:30:00",
  "ttl_days": 90
}

Implementation Checklist

  • 4 tools: save_memory, get_memory, delete_memory, memory_status

  • content format: key=value | key=value (soft warn if no =)

  • Key-based dedup on (client_id, namespace, category)

  • content truncated at MAX_CONTENT_LEN (default 500)

  • Max MAX_ENTRIES_PER_CLIENT entries (default 200)

  • TTL pruning on get_memory

  • client_id defaults to _default

  • namespace defaults to general

  • Categories: recommended set + custom x_ prefix (warn, don't reject)

  • type enum: decision, exclusion, insight, action, anomaly

License

MIT

A
license - permissive license
-
quality - not tested
D
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JrmHsr/mcp-persistent-context'

If you have feedback or need assistance with the MCP directory API, please join our Discord server