mcp-persistent-context

mcp-persistent-context

A lightweight memory layer for MCP servers that persists user context across LLM sessions with minimal token cost, supporting key-value dedup, multi-tenant namespaces, and optional TTL.

Category
访问服务器

README

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.

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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选