سرور حافظه‌ی MCP یک فایل JSON خطی روی دیسک نگه می‌دارد که هر خط آن یک موجودیت یا یک رابطه است. در این نوشته نصب بسته، نوشتن یک کلاینت کوچک JSON-RPC و چهار فراخوانی واقعی را می‌بینید. دو نکته‌ی مهم هم روشن می‌شود: نام فیلد ورودی در مستندات با کدی که واقعا اجرا می‌شود یکی نیست، و سرور رابطه‌ی معلق را بی‌صدا قبول می‌کند.

نصب و آنچه واقعا روی دیسک می‌نشیند

بسته‌ی رسمی روی npm با نام @modelcontextprotocol/server-memory منتشر شده است. نسخه‌ی latest در لحظه‌ی خواندن این نوشته، ۲ اکتبر ۲۰۲۶، برابر 2026.8.31 است که در ۳۱ اوت ۲۰۲۶ منتشر شده.[1][2] همین بسته ۱۴ نسخه در رجیستری دارد و نخستین نسخه‌اش ۲۱ نوامبر ۲۰۲۴ ثبت شده است.[3]

نصب سراسری، همان چیزی است که خواننده اجرا می‌کند:

$ npm install -g @modelcontextprotocol/server-memory@2026.8.31
added 95 packages in 2s

34 packages are looking for funding
  run `npm fund` for details

$ command -v mcp-server-memory
/root/.hermes/tools/node-26.7.0-linux-x64/bin/mcp-server-memory

بسته تنها یک وابستگی دارد: @modelcontextprotocol/sdk با بازه‌ی ^1.30.0.[3] نکته‌ی مهم این است که نامی که npm می‌سازد mcp-server-memory است، نه نام بسته. دستور server-memory وجود ندارد و خطای command not found می‌دهد.

سرور روی ورودی و خروجی استاندارد کار می‌کند و هیچ درگاهی باز نمی‌کند. آن را که اجرا کنید، فقط این را روی خروجی خطا می‌نویسد و منتظر ورودی JSON-RPC می‌ماند:

$ mcp-server-memory
 Knowledge Graph MCP Server running on stdio

اگر آن را در ترمینال رها کنید، هیچ اتفاقی نمی‌افتد. برای خروج، کلیدترکیبی Ctrl+C.

نخستین فراخوانی: کلاینت چقدر کوچک می‌شود

سرور فقط یک برنامه‌ی خط فرمان نیست؛ یک سرور MCP است و باید با یک کلاینت حرف بزند. کوچک‌ترین کلاینت، سه مرحله دارد: نخست اعلام نسخه‌ی پروتکل، بعد فرستادن اعلان آمادگی، و در پایان فراخوانی ابزار. همین سه مرحله در قالب یک فایل پایتون جا می‌شود.

import json, os, subprocess

proc = subprocess.Popen(
    ["mcp-server-memory"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1,
    env=dict(os.environ, MEMORY_FILE_PATH=os.path.expanduser("~/graph.json")),
)

_n = 0
def send(method, params=None, notify=False):
    global _n
    _n += 1
    msg = {"jsonrpc": "2.0", "method": method}
    if not notify:
        msg["id"] = _n
    if params is not None:
        msg["params"] = params
    proc.stdin.write(json.dumps(msg) + "\n")   # هر پیام یک خط، با پایان خط
    proc.stdin.flush()
    return None if notify else json.loads(proc.stdout.readline())

def tool(name, **args):
    # فراخوانی ابزار هم یک پیام درست با نام و arguments است
    r = send("tools/call", {"name": name, "arguments": args})
    if "error" in r:
        raise SystemExit(f"{name} failed: {r['error']}")
    return r["result"]["content"][0]["text"]

send("initialize", {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {"name": "hoosh", "version": "0.0.0"},
})
send("notifications/initialized", notify=True)

دو نکته این کد را از یک اسکریپت شکسته جدا می‌کند. نخست، هر پیام باید با یک خط جدید تمام شود و بعد فوراً flush شود، وگرنه سرور آن را نمی‌بیند. دوم، اعلان notifications/initialized شماره‌ی id ندارد؛ اگر به آن شماره بدهید، سرور منتظر پاسخی می‌ماند که هرگز نمی‌آید و کلاینت شما قفل می‌شود.

در پاسخ به initialize، سرور نام و نسخه‌ی خودش را معرفی می‌کند:

=== initialize ===
serverInfo: {"name": "memory-server", "version": "0.6.3"}
protocolVersion: 2024-11-05

نسخه‌ی serverInfo برابر 0.6.3 است، در حالی که نسخه‌ی بسته روی npm 2026.8.31 است. این دو یک عدد نیستند: اولی نسخه‌ی سرور است و دومی نسخه‌ی بسته.[2][3]

نوشتن گراف: سه فراخوانی که واقعا کار می‌کنند

سرور نُه ابزار می‌دهد که همه از یک فایل خطی می‌خوانند و در آن می‌نویسند. فهرست کامل، از خود سرور و بدون حدس:

=== tools/list -> 9 tools ===
create_entities, create_relations, add_observations, delete_entities,
delete_observations, delete_relations, read_graph, search_nodes, open_nodes

سه فراخوانی اول گراف را می‌سازند. ورودی هر سه یک آرایه است، نه یک شیء:

tool("create_entities", entities=[
    {"name": "HooshDotCom", "entityType": "site",
     "observations": ["Persian AI news site", "static nginx"]},
    {"name": "getphoto.py", "entityType": "script",
     "observations": ["fetches one WebP photo per post"]},
])

tool("create_relations", relations=[
    {"from": "getphoto.py", "to": "HooshDotCom",
     "relationType": "supplies photo to"},
])

tool("add_observations", observations=[
    {"entityName": "getphoto.py",
     "contents": ["rejects photos of screens and faces"]},
])

نتیجه‌ی واقعی این است که فایل روی دیسک یک JSON خطی است، یعنی JSON Lines. هر خط مستقل و به‌تنهایی معتبر است:

$ cat ~/.hoosh-graph.json
{"type":"entity","name":"HooshDotCom","entityType":"site","observations":["Persian AI news site","static nginx"]}
{"type":"entity","name":"getphoto.py","entityType":"script","observations":["fetches one WebP photo per post"]}
{"type":"relation","from":"getphoto.py","to":"HooshDotCom","relationType":"supplies photo to"}

همین شکل ذخیره، دو خاصیت عملی دارد. می‌توانید فایل را با grep و awk بگردید بدون آنکه کل گراف را در حافظه بیاورید، و یک خط خراب، کل فایل را از کار نمی‌اندازد.

خواندن گراف و آنچه درباره‌ی آن یاد می‌گیریم

حالا گراف را بخوانید. search_nodes روی نام، نوع و متن مشاهده‌ها جست‌وجو می‌کند و نتیجه، موجودیت‌های منطبق به‌همراه رابطه‌های خودشان را برمی‌گرداند:

print(tool("search_nodes", query="photo"))

{
  "entities": [
    {
      "name": "getphoto.py",
      "entityType": "script",
      "observations": [
        "fetches one WebP photo per post",
        "rejects photos of screens and faces"
      ]
    }
  ],
  "relations": [
    {
      "from": "getphoto.py",
      "to": "HooshDotCom",
      "relationType": "supplies photo to"
    }
  ]
}

جست‌وجوی واژه‌ی photo موجودیت getphoto.py را برگرداند، چون واژه در نام آن بود. موجودیت HooshDotCom در نتیجه نیست، با اینکه در فایل وجود دارد: نتیجه هر موجودیت را همراه رابطه‌های خودش می‌دهد و نه کل گراف را.

دو اشتباهی که سرور بی‌صدا قبول می‌کند

این بخش مهم‌ترین یافته‌ی این نوشته است. دو خطای زیر خطا نمی‌دهند و نتیجه هم درست به نظر می‌رسد.

نام فیلد در مستندات با کد یکی نیست

برای add_observations، مستندات بسته فیلد observations را درون هر عضو آرایه نشان می‌دهد. طرح‌واره‌ی واقعی که سرور با tools/list برمی‌گرداند، فیلد دیگری را الزامی می‌کند:

### schema: add_observations
{
  "properties": {
    "observations": {
      "type": "array",
      "items": {
        "properties": {
          "entityName": { "type": "string" },
          "contents": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["entityName", "contents"]
      }
    }
  },
  "required": ["observations"]
}

فراخوانی با شکل مستندشده، خطای روشنی می‌دهد و هیچ چیز هم نمی‌نویسد:

MCP error -32602: Input validation error: Invalid arguments for tool
add_observations: Invalid input: expected array, received undefined at
observations[0].contents

با contents همان فراخوانی درست کار می‌کند و پاسخ، فهرست مشاهده‌های تازه‌افزوده را برمی‌گرداند:

[
  {
    "entityName": "getphoto.py",
    "addedObservations": [
      "rejects photos of screens and faces"
    ]
  }
]

قاعده‌ی عملی: پیش از نوشتن کلاینت، tools/list را صدا بزنید و طرح‌واره‌ی واقعی را بخوانید. متن مستندات می‌تواند از کد عقب بماند.

رابطه‌ی معلق بی‌صدا پذیرفته می‌شود

اگر رابطه‌ای بسازید که مقصدش وجود ندارد، سرور آن را قبول می‌کند و پاسخ شادمانه برمی‌گرداند:

tool("create_relations", relations=[
    {"from": "getphoto.py", "to": "NoSuchEntity", "relationType": "reports to"},
])

[
  {
    "from": "getphoto.py",
    "to": "NoSuchEntity",
    "relationType": "reports to"
  }
]

برای همین، خط آخر فایل هم یک رابطه‌ی معلق است و در ادامه، search_nodes آن را کنار نتیجه‌ی واقعی برمی‌گرداند. جست‌وجو مقصد ناموجود را حذف نمی‌کند.

{"type":"relation","from":"getphoto.py","to":"NoSuchEntity","relationType":"reports to"}

برای تست منفی، یک فراخوانی ناموجود هم بزنید. این یکی برخلاف دو مورد قبل، خطای درست می‌دهد:

=== NEGATIVE: unknown tool ===
{"content": [{"type": "text", "text": "MCP error -32602: Tool delete_everything not found"}],
 "isError": true}

پس دفاع درست، اعتبارسنجی سمت خودتان است: پیش از create_relations با search_nodes یا open_nodes مطمئن شوید هر دو سر رابطه وجود دارند.

وصل کردن به کلاینت واقعی و سه حدی که باید بدانید

این سرور روی ورودی و خروجی استاندارد کار می‌کند و هیچ درگاهی باز نمی‌کند. پس پیکربندی‌اش در بیشتر کلاینت‌ها فقط یک فرمان و یک مسیر است. در این پیکربندی، مسیر فایل از متغیر محیطی می‌آید تا مسیر یکتا بماند:

{
  "mcpServers": {
    "memory": {
      "command": "mcp-server-memory",
      "env": { "MEMORY_FILE_PATH": "/srv/agent-graph.json" }
    }
  }
}

متغیر MEMORY_FILE_PATH تعیین می‌کند فایل کجا بنشیند. اگر آن را ندهید، سرور در پوشه‌ی جاری فایلی می‌سازد، و آن پوشه هر بار که نشست را از مسیر دیگری شروع کنید عوض می‌شود. برای نگه‌داری حافظه‌ی چند نشست، مسیر ثابت را در پیکربندی بنویسید.

این الگو را در ساخت سرور و کلاینت MCP با SDK پایتون دیده بودیم؛ آن‌جا پیام‌ها را در همان فرمت می‌فرستادیم، ولی اینجا سرور را نصب کردیم و به آن واقعا وصل شدیم.

سه حد را باید بدانید، چون هر سه از نام «حافظه» انتظار بزرگی می‌سازند. نخست، سرور چیزی را جمع‌بندی یا بازیابی معنایی نمی‌کند: search_nodes جست‌وجوی متنی ساده است و تفسیر گراف با خود شماست. دوم، مقیاس را تست نکرده‌ام: روی این ماشین چند موجودیت و چند رابطه اجرا شد، نه هزاران. سوم، فایل خطی است و هر نوشتن، کل فایل را بازنویسی می‌کند؛ برای حافظه‌ی هم‌زمان چندنویسنده‌ای مناسب نیست.

برای حافظه‌ای که خودش خلاصه می‌کند و از یک فایل خطی بزرگ‌تر می‌شود، به سرور دیگری نیاز دارید. این یکی برای نگه‌داری گراف کوچک و صریح، با چند موجودیت که خودتان نامشان را انتخاب می‌کنید، مناسب است. برای سنجیدن اینکه یک سرور پیش از نصب چقدر به کانتکست اضافه می‌کند، پست ارزیابی سرور MCP پیش از نصب عددهای دیگری نشان می‌دهد.

منابع

  1. مخزن مرجع سرورهای MCP، پوشه‌ی memory — خوانده در ۲ اکتبر ۲۰۲۶
  2. صفحه‌ی بسته‌ی server-memory در npm — خوانده در ۲ اکتبر ۲۰۲۶
  3. متادیتای رجیستری npm برای server-memory: نسخه‌ها، تاریخ انتشار و وابستگی‌ها — خوانده در ۲ اکتبر ۲۰۲۶
  4. مشخصات MCP نسخه‌ی ۲۰۲۴-۱۱-۰۵: روش initialize و اعلان initialized — خوانده در ۲ اکتبر ۲۰۲۶
  5. مخزن modelcontextprotocol/servers — خوانده در ۲ اکتبر ۲۰۲۶
  6. بسته‌ی SDK که سرور حافظه به آن تکیه می‌کند — خوانده در ۲ اکتبر ۲۰۲۶