Files

140 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Memory Server implementing MCP protocol using FastMCP.
Provides basic memory operations with optional namespace support.
"""
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastmcp import FastMCP
import fnmatch
class MemoryServer:
"""A simple MCP server that stores key/value pairs in a JSON file.
The server exposes six tools:
* save store a value under a key.
* get retrieve a stored value.
* delete remove a key.
* list_keys list all keys with optional glob pattern.
* save_with_namespace same as ``save`` but prefixes the key with a namespace.
* get_by_namespace return all items belonging to a namespace.
"""
def __init__(self, storage_path: str | Path = "./memory_data.json"):
self.mcp = FastMCP("Memory-Server")
self.storage_path = Path(storage_path)
# Ensure directory exists for future writes
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
def _load(self) -> Dict[str, Any]:
"""Load the entire memory store from disk.
Returns an empty dict if file does not exist or is invalid.
"""
try:
if self.storage_path.exists():
with self.storage_path.open("r", encoding="utf-8") as f:
return json.load(f)
except Exception:
# Corrupted file start fresh
pass
return {}
def _save(self, data: Dict[str, Any]) -> None:
"""Persist the memory store to disk.
The file is written atomically by writing to a temp file first.
"""
tmp = self.storage_path.with_suffix(".tmp")
with tmp.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
tmp.replace(self.storage_path)
def _current_timestamp(self) -> str:
return datetime.now(timezone.utc).isoformat()
@property
def mcp_tool(self):
# Helper to expose the FastMCP instance for decorators
return self.mcp
# ---------- Basic tools ----------
@FastMCP.tool()
def save(key: str, value: Any) -> bool:
"""Store *value* under *key*.
The value is JSONserialisable. Existing keys are overwritten.
Returns ``True`` on success.
"""
data = self._load()
data[key] = {"value": value, "timestamp": self._current_timestamp()}
self._save(data)
return True
@FastMCP.tool()
def get(key: str) -> Optional[Dict[str, Any]]:
"""Retrieve the stored item for *key*.
Returns a dict with ``value`` and ``timestamp`` or ``None`` if not found.
"""
data = self._load()
return data.get(key)
@FastMCP.tool()
def delete(key: str) -> bool:
"""Remove *key* from the store.
Returns ``True`` if key existed and was removed, otherwise ``False``.
"""
data = self._load()
if key in data:
del data[key]
self._save(data)
return True
return False
@FastMCP.tool()
def list_keys(pattern: str = "*") -> List[str]:
"""Return all keys matching *pattern*.
The pattern supports Unix shell wildcards (``*``, ``?``).
"""
data = self._load()
return [k for k in data.keys() if fnmatch.fnmatch(k, pattern)]
# ---------- Namespace tools ----------
@FastMCP.tool()
def save_with_namespace(key: str, value: Any, namespace: str = "default") -> bool:
"""Store *value* under ``namespace:key``.
The key is prefixed with the namespace and a colon. Existing entries are overwritten.
Returns ``True`` on success.
"""
namespaced_key = f"{namespace}:{key}"
return self.save(namespaced_key, value)
@FastMCP.tool()
def get_by_namespace(namespace: str = "default") -> List[Dict[str, Any]]:
"""Return all items belonging to *namespace*.
Each returned dict contains ``key`` (without namespace prefix), ``value`` and ``timestamp``.
"""
data = self._load()
prefix = f"{namespace}:"
result: List[Dict[str, Any]] = []
for full_key, meta in data.items():
if full_key.startswith(prefix):
key_without_ns = full_key[len(prefix) :]
item = {"key": key_without_ns, "value": meta["value"], "timestamp": meta["timestamp"]}
result.append(item)
return result
# ---------- Server entry point ----------
if __name__ == "__main__":
server = MemoryServer()
# Run with stdio transport suitable for local testing and client usage.
server.mcp.run(transport="stdio", show_banner=False, log_level="ERROR")