feat:初步实现记忆skill

This commit is contained in:
ViperEkura 2026-04-26 15:56:39 +08:00
parent a4c65ee814
commit b419e988e6
4 changed files with 875 additions and 0 deletions

130
memory/SKILL.md Normal file
View File

@ -0,0 +1,130 @@
---
name: memory
description: >
This skill should be used when the user wants to store, retrieve, update, or delete
persistent memories with hierarchical categories. Supports MCP JSON-RPC interface
and CLI management with multi-format export.
---
# Memory Manager
Hierarchical memory storage with categories, MCP support, and multi-format export.
## Architecture
```
memory/
├── SKILL.md
├── memories/ # Root storage
│ ├── index.md # Root index
│ ├── work/ # Category: work
│ │ ├── project-a/ # Sub-category
│ │ │ └── <id>.md # Memories
│ │ └── <id>.md
│ ├── personal/ # Category: personal
│ └── preferences/ # Category: preferences
└── scripts/
└── store.py # Main storage (CLI + MCP)
```
## When to Use
- User asks to "remember", "save", or "store" something
- User wants to "recall" or "retrieve" stored information
- User wants to organize memories into categories
- User asks to "forget" or "delete" stored information
- Export memories in different formats
## CLI Usage
```bash
# Add memory to category
python store.py add -t "API Key" -c "sk-xxx" --category work/secrets --tags secret api
# Get memory
python store.py get abc12345 --category work/secrets
# List all (recursive)
python store.py list
# List specific category (no recursion)
python store.py list --category work --no-recursive
# Search in category
python store.py search "api" --category work
# Update memory
python store.py update abc12345 -c "new content"
python store.py update abc12345 --move new/category # Move to category
# Delete memory
python store.py delete abc12345
# List category structure
python store.py ls /
python store.py ls work
# Create/remove category
python store.py mkdir new/category
python store.py rmdir empty/category
# Export
python store.py export --format json -o memories.json
python store.py export --format markdown -o memories.md
python store.py export --format csv -o memories.csv
python store.py export --category work --format json
```
## Category Structure
- **Root (`/`)**: Default, memories without category
- **Nested (`work/projects/api`)**: Hierarchical categories using `/` separator
- **Case-sensitive**: `Work``work`
- **Auto-create**: Parent categories created automatically
## Memory File Format
```markdown
---
category: work/projects
created: 2024-01-01T00:00:00
id: abc12345
tags: [api, backend]
title: API Design
updated: 2024-01-01T00:00:00
---
Memory content goes here.
```
## MCP JSON-RPC Interface
Run as MCP server: `python store.py --mcp`
### Methods
| Method | Parameters | Description |
|--------|-----------|-------------|
| `add` | `title`, `content`, `category?`, `tags?` | Add memory |
| `get` | `id`, `category?` | Get memory |
| `list` | `category?`, `recursive?` | List memories |
| `search` | `query`, `category?` | Search memories |
| `update` | `id`, `category?`, `title?`, `content?`, `tags?`, `new_category?` | Update/Move |
| `delete` | `id`, `category?` | Delete memory |
| `ls` | `category?` | List category structure |
| `mkdir` | `category` | Create category |
| `rmdir` | `category` | Remove empty category |
| `export` | `format?`, `category?` | Export memories |
## Export Formats
- **JSON**: Array of memory objects
- **Markdown**: Human-readable with category sections
- **CSV**: Spreadsheet-compatible
## Tips
- Use categories to organize: `work/`, `personal/`, `preferences/`
- Add tags for cross-category search
- Use `ls /` to view full structure
- Export specific category: `--category work`

4
memory/memories/index.md Normal file
View File

@ -0,0 +1,4 @@
# Memory Index
| ID | Title | Tags |
|---|-------|------|

258
memory/scripts/manager.py Normal file
View File

@ -0,0 +1,258 @@
"""Memory Manager - CLI tool for managing persistent memories."""
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
MEMORY_DIR = Path(__file__).parent.parent / "memories"
INDEX_FILE = MEMORY_DIR / "index.md"
def ensure_dirs():
"""Ensure memory directory exists."""
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
if not INDEX_FILE.exists():
INDEX_FILE.write_text("# Memory Index\n\n", encoding="utf-8")
def generate_id(title: str) -> str:
"""Generate a short ID from title."""
import hashlib
return hashlib.md5(title.encode()).hexdigest()[:8]
def parse_frontmatter(content: str) -> tuple[dict, str]:
"""Parse YAML frontmatter from markdown content."""
pattern = r'^---\n(.*?)\n---\n?(.*)$'
match = re.match(pattern, content, re.DOTALL)
if match:
frontmatter_text = match.group(1)
body = match.group(2)
fm = {}
for line in frontmatter_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip().strip('[]"\'|')
if value.startswith('['):
fm[key] = [v.strip().strip('"\'') for v in value[1:-1].split(',')]
else:
fm[key] = value
return fm, body
return {}, content
def create_frontmatter(data: dict, body: str) -> str:
"""Create markdown with frontmatter."""
lines = ["---"]
for key, value in data.items():
if isinstance(value, list):
lines.append(f"{key}: [{', '.join(value)}]")
else:
lines.append(f"{key}: {value}")
lines.append("---\n")
if body.strip():
lines.append(body.strip())
return "\n".join(lines)
def add_memory(title: str, content: str, tags: list[str] = None) -> str:
"""Add a new memory."""
ensure_dirs()
memory_id = generate_id(title)
filepath = MEMORY_DIR / f"{memory_id}.md"
if filepath.exists():
print(f"Memory with title '{title}' already exists as {memory_id}", file=sys.stderr)
return memory_id
now = datetime.now().isoformat()
data = {
"id": memory_id,
"title": title,
"created": now,
"updated": now,
"tags": tags or []
}
md_content = create_frontmatter(data, content)
filepath.write_text(md_content, encoding="utf-8")
update_index(memory_id, title, tags or [])
print(f"Memory added: {memory_id}")
return memory_id
def update_index(memory_id: str, title: str, tags: list[str]):
"""Update the index file."""
lines = ["# Memory Index\n", "| ID | Title | Tags |", "|---|-------|------|"]
for md_file in sorted(MEMORY_DIR.glob("*.md")):
if md_file.name == "index.md":
continue
fm, _ = parse_frontmatter(md_file.read_text(encoding="utf-8"))
tags_str = ", ".join(fm.get("tags", []))
lines.append(f"| {fm.get('id', '')} | {fm.get('title', '')} | {tags_str} |")
INDEX_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
def get_memory(memory_id: str):
"""Get a specific memory."""
filepath = MEMORY_DIR / f"{memory_id}.md"
if not filepath.exists():
print(f"Memory {memory_id} not found", file=sys.stderr)
return None
content = filepath.read_text(encoding="utf-8")
fm, body = parse_frontmatter(content)
print(f"# {fm.get('title', 'Untitled')}")
print(f"ID: {memory_id}")
print(f"Created: {fm.get('created', 'N/A')}")
print(f"Updated: {fm.get('updated', 'N/A')}")
print(f"Tags: {', '.join(fm.get('tags', []))}")
print(f"\n---\n{body}")
return fm
def list_memories():
"""List all memories."""
ensure_dirs()
memories = []
for md_file in MEMORY_DIR.glob("*.md"):
if md_file.name == "index.md":
continue
fm, _ = parse_frontmatter(md_file.read_text(encoding="utf-8"))
memories.append(fm)
if not memories:
print("No memories found.")
return
print(f"{'ID':<10} {'Title':<30} {'Tags':<20} {'Updated'}")
print("-" * 80)
for m in sorted(memories, key=lambda x: x.get('updated', ''), reverse=True):
tags = ", ".join(m.get('tags', [])[:2])
print(f"{m.get('id', ''):<10} {m.get('title', ''):<30} {tags:<20} {m.get('updated', '')[:10]}")
def search_memories(query: str):
"""Search memories by keyword."""
ensure_dirs()
results = []
for md_file in MEMORY_DIR.glob("*.md"):
if md_file.name == "index.md":
continue
content = md_file.read_text(encoding="utf-8").lower()
if query.lower() in content:
fm, body = parse_frontmatter(md_file.read_text(encoding="utf-8"))
results.append(fm)
if not results:
print(f"No memories found matching '{query}'")
return
print(f"Found {len(results)} result(s):\n")
for m in results:
print(f"- [{m.get('id')}] {m.get('title')} ({', '.join(m.get('tags', []))})")
def update_memory(memory_id: str, title: Optional[str] = None, content: Optional[str] = None, tags: list[str] = None):
"""Update an existing memory."""
filepath = MEMORY_DIR / f"{memory_id}.md"
if not filepath.exists():
print(f"Memory {memory_id} not found", file=sys.stderr)
return
fm, body = parse_frontmatter(filepath.read_text(encoding="utf-8"))
if title:
fm['title'] = title
if content:
body = content
if tags is not None:
fm['tags'] = tags
fm['updated'] = datetime.now().isoformat()
filepath.write_text(create_frontmatter(fm, body), encoding="utf-8")
update_index(memory_id, fm.get('title', ''), fm.get('tags', []))
print(f"Memory updated: {memory_id}")
def delete_memory(memory_id: str):
"""Delete a memory."""
filepath = MEMORY_DIR / f"{memory_id}.md"
if not filepath.exists():
print(f"Memory {memory_id} not found", file=sys.stderr)
return
filepath.unlink()
update_index("", "", [])
print(f"Memory deleted: {memory_id}")
def main():
parser = argparse.ArgumentParser(description="Memory Manager CLI")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Add command
add_parser = subparsers.add_parser("add", help="Add a new memory")
add_parser.add_argument("--title", "-t", required=True, help="Memory title")
add_parser.add_argument("--content", "-c", required=True, help="Memory content")
add_parser.add_argument("--tags", nargs="*", default=[], help="Tags")
# Get command
get_parser = subparsers.add_parser("get", help="Get a memory")
get_parser.add_argument("id", help="Memory ID")
# List command
subparsers.add_parser("list", help="List all memories")
# Search command
search_parser = subparsers.add_parser("search", help="Search memories")
search_parser.add_argument("query", help="Search query")
# Update command
update_parser = subparsers.add_parser("update", help="Update a memory")
update_parser.add_argument("id", help="Memory ID")
update_parser.add_argument("--title", "-t", help="New title")
update_parser.add_argument("--content", "-c", help="New content")
update_parser.add_argument("--tags", nargs="*", help="New tags")
# Delete command
delete_parser = subparsers.add_parser("delete", help="Delete a memory")
delete_parser.add_argument("id", help="Memory ID")
args = parser.parse_args()
if args.command == "add":
add_memory(args.title, args.content, args.tags)
elif args.command == "get":
get_memory(args.id)
elif args.command == "list":
list_memories()
elif args.command == "search":
search_memories(args.query)
elif args.command == "update":
update_memory(args.id, args.title, args.content, args.tags)
elif args.command == "delete":
delete_memory(args.id)
else:
parser.print_help()
if __name__ == "__main__":
main()

483
memory/scripts/store.py Normal file
View File

@ -0,0 +1,483 @@
"""Memory Store - Hierarchical file-based memory storage with MCP support."""
import argparse
import json
import os
import re
import sys
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Optional, List
# === Hierarchical Memory Store ===
class MemoryStore:
"""Hierarchical file-based memory storage with categories."""
def __init__(self, memory_dir: Path):
self.memory_dir = Path(memory_dir)
self.memory_dir.mkdir(parents=True, exist_ok=True)
def _generate_id(self, title: str) -> str:
return hashlib.md5(title.encode()).hexdigest()[:8]
def _parse_frontmatter(self, content: str) -> tuple[dict, str]:
pattern = r'^---\n(.*?)\n---\n?(.*)$'
match = re.match(pattern, content, re.DOTALL)
if match:
fm_text = match.group(1)
body = match.group(2)
fm = {}
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip().strip('[]"\'')
if value.startswith('['):
fm[key] = [v.strip().strip('"\'') for v in value[1:-1].split(',') if v.strip()]
else:
fm[key] = value
return fm, body
return {}, content
def _create_frontmatter(self, data: dict, body: str) -> str:
lines = ["---"]
for key, value in sorted(data.items()):
if isinstance(value, list):
lines.append(f"{key}: [{', '.join(value)}]")
else:
lines.append(f"{key}: {value}")
lines.append("---\n")
if body.strip():
lines.append(body.strip())
return "\n".join(lines)
def _get_category_path(self, category: str = None) -> Path:
if not category or category == "/":
return self.memory_dir
path = self.memory_dir / category.lstrip("/")
path.mkdir(parents=True, exist_ok=True)
return path
def _get_memory_path(self, category: str, memory_id: str) -> Path:
return self._get_category_path(category) / f"{memory_id}.md"
def _relpath(self, path: Path) -> str:
"""Get relative path from memory_dir."""
try:
return str(path.relative_to(self.memory_dir)).replace("\\", "/")
except ValueError:
return str(path)
def add(self, title: str, content: str, category: str = None, tags: List[str] = None, id: str = None) -> str:
"""Add memory to category."""
memory_id = id or self._generate_id(title)
filepath = self._get_memory_path(category, memory_id)
now = datetime.now().isoformat()
data = {
"id": memory_id,
"title": title,
"created": now,
"updated": now,
"tags": tags or [],
"category": category or "/"
}
filepath.write_text(self._create_frontmatter(data, content), encoding="utf-8")
return memory_id
def get(self, memory_id: str, category: str = None) -> Optional[dict]:
"""Get memory by ID, optionally in category."""
if category:
filepath = self._get_memory_path(category, memory_id)
if filepath.exists():
return self._read_memory(filepath)
return None
# Search all categories
for md_file in self.memory_dir.rglob("*.md"):
fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
if fm.get("id") == memory_id:
result = fm
result["content"] = self._read_memory(md_file)["content"]
return result
return None
def _read_memory(self, filepath: Path) -> dict:
fm, body = self._parse_frontmatter(filepath.read_text(encoding="utf-8"))
fm["content"] = body.strip()
fm["category"] = self._relpath(filepath.parent)
return fm
def list(self, category: str = None, recursive: bool = True) -> List[dict]:
"""List memories in category."""
memories = []
cat_path = self._get_category_path(category)
pattern = "*.md" if recursive else "*.md"
for md_file in cat_path.glob(pattern):
if md_file.name.startswith("_"):
continue
fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
fm["category"] = self._relpath(md_file.parent)
memories.append(fm)
return sorted(memories, key=lambda x: (x.get('category', ''), x.get('updated', '')), reverse=True)
def search(self, query: str, category: str = None) -> List[dict]:
"""Search memories."""
results = []
query_lower = query.lower()
base_path = self._get_category_path(category)
for md_file in base_path.rglob("*.md"):
if md_file.name.startswith("_"):
continue
content = md_file.read_text(encoding="utf-8").lower()
if query_lower in content:
fm, body = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
fm["content"] = body.strip()
fm["category"] = self._relpath(md_file.parent)
results.append(fm)
return results
def update(self, memory_id: str, category: str = None, title: str = None,
content: str = None, tags: List[str] = None, new_category: str = None) -> bool:
"""Update memory, optionally move to new category."""
old_path = self._get_memory_path(category, memory_id)
if not old_path.exists():
# Try search across all
for md_file in self.memory_dir.rglob("*.md"):
fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
if fm.get("id") == memory_id:
old_path = md_file
category = self._relpath(md_file.parent)
break
else:
return False
fm, body = self._parse_frontmatter(old_path.read_text(encoding="utf-8"))
if title is not None:
fm['title'] = title
if content is not None:
body = content
if tags is not None:
fm['tags'] = tags
target_cat = new_category if new_category is not None else category
fm['updated'] = datetime.now().isoformat()
fm['category'] = target_cat or "/"
new_path = self._get_memory_path(target_cat, memory_id)
if new_path != old_path:
old_path.unlink()
new_path.write_text(self._create_frontmatter(fm, body), encoding="utf-8")
return True
def delete(self, memory_id: str, category: str = None) -> bool:
"""Delete memory."""
filepath = self._get_memory_path(category, memory_id)
if not filepath.exists():
# Search all
for md_file in self.memory_dir.rglob("*.md"):
fm, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
if fm.get("id") == memory_id:
filepath = md_file
break
else:
return False
filepath.unlink()
# Clean empty category dirs
cat_dir = filepath.parent
if cat_dir != self.memory_dir and not any(cat_dir.iterdir()):
cat_dir.rmdir()
return True
def ls(self, category: str = None) -> dict:
"""List category structure (like ls -la)."""
cat_path = self._get_category_path(category)
result = {
"path": self._relpath(cat_path),
"categories": [],
"memories": []
}
for item in sorted(cat_path.iterdir()):
if item.name.startswith("_"):
continue
if item.is_dir():
result["categories"].append(item.name)
else:
fm, _ = self._parse_frontmatter(item.read_text(encoding="utf-8"))
result["memories"].append({
"id": fm.get("id"),
"title": fm.get("title"),
"tags": fm.get("tags", [])
})
return result
def mkdir(self, category: str) -> bool:
"""Create category."""
cat_path = self._get_category_path(category)
if cat_path.exists():
return False
cat_path.mkdir(parents=True)
return True
def rmdir(self, category: str) -> bool:
"""Remove empty category."""
cat_path = self._get_category_path(category)
if cat_path == self.memory_dir or not cat_path.exists():
return False
# Check empty
items = list(cat_path.iterdir())
if any(not i.name.startswith("_") for i in items):
return False
for item in items:
if item.is_file():
item.unlink()
elif item.is_dir():
import shutil
shutil.rmtree(item)
cat_path.rmdir()
return True
def export(self, format: str = "json", category: str = None) -> str:
"""Export memories."""
memories = self.list(category=category)
if format == "json":
return json.dumps(memories, indent=2, ensure_ascii=False)
elif format == "markdown":
lines = ["# Memory Export\n"]
current_cat = None
for m in memories:
cat = m.get("category", "/")
if cat != current_cat:
lines.append(f"\n## {cat}/\n")
current_cat = cat
lines.append(f"### {m.get('title', 'Untitled')}\n")
lines.append(f"**ID:** `{m.get('id', '')}` **Tags:** {', '.join(m.get('tags', []))}\n\n")
return "".join(lines)
elif format == "csv":
lines = ["id,title,tags,category,created,updated,content\n"]
for m in memories:
tags = "|".join(m.get('tags', []))
content = (m.get('content', '') or '').replace('"', '""').replace('\n', ' ')
lines.append(f'"{m.get("id", "")}","{m.get("title", "")}","{tags}","{m.get("category", "")}","{m.get("created", "")}","{m.get("updated", "")}","{content}"\n')
return "".join(lines)
return json.dumps(memories)
# === CLI Interface ===
def cli():
parser = argparse.ArgumentParser(description="Memory Store CLI")
parser.add_argument("--path", type=Path, default=Path(__file__).parent.parent / "memories")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Add
add_p = subparsers.add_parser("add", help="Add memory")
add_p.add_argument("-t", "--title", required=True)
add_p.add_argument("-c", "--content", required=True)
add_p.add_argument("--tags", nargs="*", default=[])
add_p.add_argument("--category", "-C", default="/")
# Get
get_p = subparsers.add_parser("get", help="Get memory")
get_p.add_argument("id")
get_p.add_argument("--category", "-C")
# List
list_p = subparsers.add_parser("list", help="List memories")
list_p.add_argument("--category", "-C")
list_p.add_argument("--no-recursive", action="store_true")
# Search
search_p = subparsers.add_parser("search", help="Search")
search_p.add_argument("query")
search_p.add_argument("--category", "-C")
# Update
update_p = subparsers.add_parser("update", help="Update")
update_p.add_argument("id")
update_p.add_argument("--category", "-C")
update_p.add_argument("-t", "--title")
update_p.add_argument("-c", "--content")
update_p.add_argument("--tags", nargs="*")
update_p.add_argument("--move", "-M", dest="new_category")
# Delete
delete_p = subparsers.add_parser("delete", help="Delete")
delete_p.add_argument("id")
delete_p.add_argument("--category", "-C")
# ls (tree)
ls_p = subparsers.add_parser("ls", help="List structure")
ls_p.add_argument("--category", "-C", default="/")
# mkdir
mkdir_p = subparsers.add_parser("mkdir", help="Create category")
mkdir_p.add_argument("category")
# rmdir
rmdir_p = subparsers.add_parser("rmdir", help="Remove empty category")
rmdir_p.add_argument("category")
# Export
export_p = subparsers.add_parser("export", help="Export")
export_p.add_argument("--format", choices=["json", "markdown", "csv"], default="json")
export_p.add_argument("-o", "--output", type=Path)
export_p.add_argument("--category", "-C")
args = parser.parse_args()
store = MemoryStore(args.path)
if args.command == "add":
mid = store.add(args.title, args.content, args.category, args.tags)
print(f"Added: {mid} -> {args.category}")
elif args.command == "get":
m = store.get(args.id, args.category)
if m:
print(f"# {m['title']}\n")
print(f"ID: {m['id']} Category: {m.get('category', '/')} Tags: {', '.join(m.get('tags', []))}")
print(f"Created: {m['created']} Updated: {m['updated']}\n")
print("---")
print(m['content'])
else:
print(f"Not found: {args.id}", file=sys.stderr)
elif args.command == "list":
for m in store.list(args.category, not args.no_recursive):
cat = m.get('category', '/')
tags = ", ".join(m.get('tags', [])[:2])
print(f"{m['id']:<10} [{cat:<15}] {m['title']:<30} [{tags}]")
elif args.command == "search":
for m in store.search(args.query, args.category):
print(f"- [{m.get('category', '/')}] {m['id']} {m['title']} ({', '.join(m.get('tags', []))})")
elif args.command == "update":
ok = store.update(args.id, args.category, args.title, args.content, args.tags, args.new_category)
print("Updated" if ok else "Not found")
elif args.command == "delete":
ok = store.delete(args.id, args.category)
print("Deleted" if ok else "Not found")
elif args.command == "ls":
result = store.ls(args.category)
print(f"📁 {result['path']}/")
if result['categories']:
for c in result['categories']:
print(f" 📂 {c}/")
for m in result['memories']:
print(f" 📄 {m['id']} - {m['title']}")
elif args.command == "mkdir":
ok = store.mkdir(args.category)
print("Created" if ok else "Already exists")
elif args.command == "rmdir":
ok = store.rmdir(args.category)
print("Removed" if ok else "Failed (not empty or not found)")
elif args.command == "export":
output = store.export(args.format, args.category)
if args.output:
args.output.write_text(output, encoding="utf-8")
print(f"Exported to {args.output}")
else:
print(output)
else:
parser.print_help()
# === MCP JSON-RPC Server ===
def mcp_server():
"""Run as MCP JSON-RPC server (stdio)."""
store = MemoryStore(Path(__file__).parent.parent / "memories")
for line in sys.stdin:
try:
request = json.loads(line.strip())
method = request.get("method", "")
params = request.get("params", {})
req_id = request.get("id")
result = None
error = None
try:
if method == "add":
result = store.add(
params.get("title", ""),
params.get("content", ""),
params.get("category"),
params.get("tags", [])
)
elif method == "get":
result = store.get(params.get("id", ""), params.get("category"))
elif method == "list":
result = store.list(params.get("category"), params.get("recursive", True))
elif method == "search":
result = store.search(params.get("query", ""), params.get("category"))
elif method == "update":
result = store.update(
params.get("id", ""),
params.get("category"),
params.get("title"),
params.get("content"),
params.get("tags"),
params.get("new_category")
)
elif method == "delete":
result = store.delete(params.get("id", ""), params.get("category"))
elif method == "ls":
result = store.ls(params.get("category"))
elif method == "mkdir":
result = store.mkdir(params.get("category", ""))
elif method == "rmdir":
result = store.rmdir(params.get("category", ""))
elif method == "export":
result = store.export(params.get("format", "json"), params.get("category"))
else:
error = {"code": -32601, "message": f"Unknown method: {method}"}
except Exception as e:
error = {"code": -32603, "message": str(e)}
response = {"jsonrpc": "2.0", "id": req_id}
if error:
response["error"] = error
else:
response["result"] = result
print(json.dumps(response), flush=True)
except json.JSONDecodeError:
continue
except Exception:
continue
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--mcp":
mcp_server()
else:
cli()