@luisarg/memory-auto 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -8
- package/dist/index.js.map +1 -1
- package/package.json +35 -7
- package/server/__init__.py +1 -0
- package/server/cli.py +27 -0
- package/server/launcher.mjs +69 -0
- package/server/pyproject.toml +14 -0
- package/server/registry.py +141 -0
- package/server/requirements.txt +4 -0
- package/server/server.py +399 -0
- package/server/store.py +827 -0
- package/server/uv.lock +783 -0
- package/vault/README.md +38 -0
- package/vault/tag-vocabulary.json +175 -0
- package/vault/templates/context.md +20 -0
- package/vault/templates/convention.md +13 -0
- package/vault/templates/decision.md +23 -0
- package/vault/templates/fact.md +14 -0
- package/vault/templates/idea.md +28 -0
- package/vault/templates/learning.md +23 -0
- package/vault/templates/profile.md +13 -0
- package/vault/templates/source.md +21 -0
- package/vault/type-registry.yaml +75 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Type registry loader — reads memory/type-registry.yaml and provides
|
|
2
|
+
derived maps for store, server, and digest_session modules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegistryValidationError(Exception):
|
|
13
|
+
"""Raised when the type registry fails validation."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_REQUIRED_KEYS = ("name", "directory", "singular", "tool", "template", "extraction_hint")
|
|
17
|
+
_RESERVED_DIRECTORIES = {"index.md", "log.md", ".obsidian"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_registry(types: list[dict[str, Any]]) -> None:
|
|
21
|
+
"""Validate a parsed registry. Raises RegistryValidationError on failure."""
|
|
22
|
+
# Required keys
|
|
23
|
+
for t in types:
|
|
24
|
+
for key in _REQUIRED_KEYS:
|
|
25
|
+
if key not in t:
|
|
26
|
+
raise RegistryValidationError(
|
|
27
|
+
f"missing required key {key!r} in type {t.get('name', '<unnamed>')}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Duplicate name
|
|
31
|
+
seen_names: set[str] = set()
|
|
32
|
+
for t in types:
|
|
33
|
+
n: str = t["name"]
|
|
34
|
+
if n in seen_names:
|
|
35
|
+
raise RegistryValidationError(f"duplicate name: {n!r}")
|
|
36
|
+
seen_names.add(n)
|
|
37
|
+
|
|
38
|
+
# Duplicate directory across project-attached types
|
|
39
|
+
seen_project_dirs: set[str] = set()
|
|
40
|
+
for t in types:
|
|
41
|
+
if t.get("project_attached", True) is True:
|
|
42
|
+
d: str = t["directory"]
|
|
43
|
+
if d in seen_project_dirs:
|
|
44
|
+
raise RegistryValidationError(
|
|
45
|
+
f"duplicate directory for project-attached types: {d!r}"
|
|
46
|
+
)
|
|
47
|
+
seen_project_dirs.add(d)
|
|
48
|
+
|
|
49
|
+
# Reserved directory collision
|
|
50
|
+
for t in types:
|
|
51
|
+
d = t.get("directory", "")
|
|
52
|
+
if d in _RESERVED_DIRECTORIES:
|
|
53
|
+
raise RegistryValidationError(
|
|
54
|
+
f"directory {d!r} collides with reserved path"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Empty extraction_hint on project-attached types
|
|
58
|
+
for t in types:
|
|
59
|
+
if t.get("project_attached", True) is True:
|
|
60
|
+
hint = t.get("extraction_hint")
|
|
61
|
+
if hint is None or (isinstance(hint, str) and hint == ""):
|
|
62
|
+
raise RegistryValidationError(
|
|
63
|
+
f"empty extraction_hint on project-attached type {t.get('name')!r}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=1)
|
|
68
|
+
def load_type_registry(bundle_root: Path) -> list[dict[str, Any]]:
|
|
69
|
+
"""Load and validate the type registry from bundle_root/type-registry.yaml.
|
|
70
|
+
|
|
71
|
+
Returns the list of type dicts. Raises RegistryValidationError on
|
|
72
|
+
validation failure. Uses @lru_cache so the file is read only once.
|
|
73
|
+
"""
|
|
74
|
+
import yaml # lazy import: only needed when loading the registry
|
|
75
|
+
|
|
76
|
+
registry_path = bundle_root / "type-registry.yaml"
|
|
77
|
+
if not registry_path.is_file():
|
|
78
|
+
raise RegistryValidationError(
|
|
79
|
+
f"registry file not found: {registry_path}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
raw = registry_path.read_text(encoding="utf-8")
|
|
83
|
+
try:
|
|
84
|
+
data = yaml.safe_load(raw)
|
|
85
|
+
except yaml.YAMLError as exc:
|
|
86
|
+
raise RegistryValidationError(f"YAML parse error: {exc}") from exc
|
|
87
|
+
|
|
88
|
+
if not isinstance(data, dict) or "types" not in data:
|
|
89
|
+
raise RegistryValidationError(
|
|
90
|
+
"registry must contain a top-level 'types' key"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
types = data["types"]
|
|
94
|
+
if not isinstance(types, list) or len(types) == 0:
|
|
95
|
+
raise RegistryValidationError("'types' must be a non-empty list")
|
|
96
|
+
|
|
97
|
+
_validate_registry(types)
|
|
98
|
+
return types
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_type_maps(
|
|
102
|
+
types: list[dict[str, Any]],
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
"""Derive the standard maps from a registry type list.
|
|
105
|
+
|
|
106
|
+
Returns a dict with keys:
|
|
107
|
+
valid_types: set of singular names
|
|
108
|
+
type_dir_map: singular -> directory
|
|
109
|
+
type_label_map: directory -> name (label)
|
|
110
|
+
dir_to_singular: directory -> singular
|
|
111
|
+
type_order: list of directory names in registry order (project-attached only)
|
|
112
|
+
extraction_types: list of dicts for types with non-null extraction_hint
|
|
113
|
+
"""
|
|
114
|
+
valid_types: set[str] = set()
|
|
115
|
+
type_dir_map: dict[str, str] = {}
|
|
116
|
+
type_label_map: dict[str, str] = {}
|
|
117
|
+
dir_to_singular: dict[str, str] = {}
|
|
118
|
+
type_order: list[str] = []
|
|
119
|
+
extraction_types: list[dict[str, Any]] = []
|
|
120
|
+
|
|
121
|
+
for t in types:
|
|
122
|
+
singular = t["singular"]
|
|
123
|
+
directory = t["directory"]
|
|
124
|
+
name = t["name"]
|
|
125
|
+
valid_types.add(singular)
|
|
126
|
+
type_dir_map[singular] = directory
|
|
127
|
+
type_label_map[directory] = name
|
|
128
|
+
dir_to_singular[directory] = singular
|
|
129
|
+
if t.get("project_attached", True):
|
|
130
|
+
type_order.append(directory)
|
|
131
|
+
if t.get("extraction_hint") is not None:
|
|
132
|
+
extraction_types.append(t)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"valid_types": valid_types,
|
|
136
|
+
"type_dir_map": type_dir_map,
|
|
137
|
+
"type_label_map": type_label_map,
|
|
138
|
+
"dir_to_singular": dir_to_singular,
|
|
139
|
+
"type_order": type_order,
|
|
140
|
+
"extraction_types": extraction_types,
|
|
141
|
+
}
|
package/server/server.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""MCP server exposing memory store tools via the Model Context Protocol.
|
|
2
|
+
|
|
3
|
+
Eight tools (per openspec/specs/memory-mcp-server/spec.md):
|
|
4
|
+
search_memory, store_decision, store_fact, store_learning,
|
|
5
|
+
store_convention, store_profile, export_memories, get_profile, ping.
|
|
6
|
+
|
|
7
|
+
All reads are explicit (no background polling). Server validates storage
|
|
8
|
+
accessibility at startup.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from mcp.server import Server
|
|
19
|
+
from mcp.server.lowlevel.server import ServerRequestContext
|
|
20
|
+
from mcp.server.stdio import stdio_server
|
|
21
|
+
from mcp.types import (
|
|
22
|
+
CallToolRequestParams,
|
|
23
|
+
CallToolResult,
|
|
24
|
+
ListToolsResult,
|
|
25
|
+
PaginatedRequestParams,
|
|
26
|
+
TextContent,
|
|
27
|
+
Tool,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from store import (
|
|
31
|
+
VALID_TYPES,
|
|
32
|
+
MemoryStore,
|
|
33
|
+
_first_non_heading_line,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Observability log: append-only JSONL. Path is derived from MEMORY_PATH so the
|
|
37
|
+
# log lives next to the bundle, never blocking tool behavior.
|
|
38
|
+
_LOG_PATH: Path | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _log_path() -> Path:
|
|
42
|
+
global _LOG_PATH
|
|
43
|
+
if _LOG_PATH is not None:
|
|
44
|
+
return _LOG_PATH
|
|
45
|
+
from cli import get_memory_path
|
|
46
|
+
|
|
47
|
+
base = get_memory_path()
|
|
48
|
+
_LOG_PATH = base / "tool-calls.log"
|
|
49
|
+
return _LOG_PATH
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _log_tool_call(tool_name: str, params: dict) -> None:
|
|
53
|
+
"""Append a JSONL entry for each tool call. Silently ignore write errors."""
|
|
54
|
+
try:
|
|
55
|
+
entry = {
|
|
56
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
57
|
+
"tool": tool_name,
|
|
58
|
+
"project": params.get("project"),
|
|
59
|
+
"entry_type": params.get("entry_type"),
|
|
60
|
+
}
|
|
61
|
+
path = _log_path()
|
|
62
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with open(path, "a") as f:
|
|
64
|
+
f.write(json.dumps(entry, default=str) + "\n")
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _require(params: dict, key: str) -> str:
|
|
70
|
+
val = params.get(key)
|
|
71
|
+
if not val:
|
|
72
|
+
raise ValueError(f"'{key}' is required and must not be empty")
|
|
73
|
+
return val
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _derive_description(content: str) -> str:
|
|
77
|
+
"""Derive a one-sentence description from the first non-heading line."""
|
|
78
|
+
return _first_non_heading_line(content) or content[:120]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Tool handlers (sync, return JSON strings) ──────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def handle_search_memory(store: MemoryStore, params: dict) -> str:
|
|
85
|
+
project = params.get("project") or None
|
|
86
|
+
entry_type = params.get("entry_type") or None
|
|
87
|
+
tags = params.get("tags") or None
|
|
88
|
+
query = params.get("query") or None
|
|
89
|
+
results = store.search_entries(
|
|
90
|
+
project=project, entry_type=entry_type, tags=tags, query=query
|
|
91
|
+
)
|
|
92
|
+
return json.dumps(results)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _handle_store_typed(
|
|
96
|
+
store: MemoryStore, params: dict, entry_type: str
|
|
97
|
+
) -> str:
|
|
98
|
+
project = _require(params, "project")
|
|
99
|
+
content = _require(params, "content")
|
|
100
|
+
tags = params.get("tags") or []
|
|
101
|
+
description = params.get("description")
|
|
102
|
+
if not description or not str(description).strip():
|
|
103
|
+
description = _derive_description(content)
|
|
104
|
+
openspec_change_id = params.get("openspec_change_id") or None
|
|
105
|
+
confidence = params.get("confidence", 1.0)
|
|
106
|
+
if confidence is None:
|
|
107
|
+
confidence = 1.0
|
|
108
|
+
entry = store.upsert_entry(
|
|
109
|
+
entry_type=entry_type,
|
|
110
|
+
project=project,
|
|
111
|
+
content=content,
|
|
112
|
+
tags=tags,
|
|
113
|
+
description=str(description).strip(),
|
|
114
|
+
confidence=float(confidence),
|
|
115
|
+
openspec_change_id=openspec_change_id,
|
|
116
|
+
)
|
|
117
|
+
return json.dumps(entry)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def handle_store_decision(store: MemoryStore, params: dict) -> str:
|
|
121
|
+
return _handle_store_typed(store, params, "decision")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def handle_store_fact(store: MemoryStore, params: dict) -> str:
|
|
125
|
+
return _handle_store_typed(store, params, "fact")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def handle_store_learning(store: MemoryStore, params: dict) -> str:
|
|
129
|
+
return _handle_store_typed(store, params, "learning")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def handle_store_convention(store: MemoryStore, params: dict) -> str:
|
|
133
|
+
return _handle_store_typed(store, params, "convention")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def handle_store_profile(store: MemoryStore, params: dict) -> str:
|
|
137
|
+
project = _require(params, "project")
|
|
138
|
+
content = _require(params, "content")
|
|
139
|
+
tags = params.get("tags") or []
|
|
140
|
+
entry = store.upsert_profile(project=project, content=content, tags=tags)
|
|
141
|
+
return json.dumps(entry)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def handle_export_memories(store: MemoryStore, params: dict) -> str:
|
|
145
|
+
project = _require(params, "project")
|
|
146
|
+
entry_type = params.get("entry_type") or None
|
|
147
|
+
results = store.export_entries(project=project, entry_type=entry_type)
|
|
148
|
+
return json.dumps(results)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def handle_get_profile(store: MemoryStore, params: dict) -> str:
|
|
152
|
+
project = _require(params, "project")
|
|
153
|
+
entry_type = params.get("entry_type") or None
|
|
154
|
+
results = store.get_profile(project=project, entry_type=entry_type)
|
|
155
|
+
return json.dumps(results)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def handle_ping(store: MemoryStore, params: dict) -> str:
|
|
159
|
+
return json.dumps(
|
|
160
|
+
{"status": "ok", "timestamp": datetime.now(timezone.utc).isoformat()}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def handle_store_source(store: MemoryStore, params: dict) -> str:
|
|
165
|
+
url = _require(params, "url")
|
|
166
|
+
title = _require(params, "title")
|
|
167
|
+
description = _require(params, "description")
|
|
168
|
+
source_kind = _require(params, "source_kind")
|
|
169
|
+
tags = params.get("tags") or []
|
|
170
|
+
content = params.get("content") or None
|
|
171
|
+
supersedes = params.get("supersedes") or None
|
|
172
|
+
result = store.store_source(
|
|
173
|
+
url=url,
|
|
174
|
+
title=title,
|
|
175
|
+
description=description,
|
|
176
|
+
source_kind=source_kind,
|
|
177
|
+
tags=tags,
|
|
178
|
+
content=content,
|
|
179
|
+
supersedes=supersedes,
|
|
180
|
+
)
|
|
181
|
+
return json.dumps(result)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── Tool definitions (shared between list_tools and the wire-up) ──────────
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _tool_definitions() -> list[Tool]:
|
|
188
|
+
"""Static tool definitions for the memory-server MCP surface."""
|
|
189
|
+
valid_types = sorted(VALID_TYPES)
|
|
190
|
+
return [
|
|
191
|
+
Tool(
|
|
192
|
+
name="search_memory",
|
|
193
|
+
description="Search memory entries across projects. Omit project to search all projects.",
|
|
194
|
+
inputSchema={
|
|
195
|
+
"type": "object",
|
|
196
|
+
"properties": {
|
|
197
|
+
"project": {"type": "string"},
|
|
198
|
+
"entry_type": {"type": "string", "enum": valid_types},
|
|
199
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
200
|
+
"query": {"type": "string"},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
Tool(
|
|
205
|
+
name="store_decision",
|
|
206
|
+
description="Store a decision entry",
|
|
207
|
+
inputSchema={
|
|
208
|
+
"type": "object",
|
|
209
|
+
"required": ["project", "content"],
|
|
210
|
+
"properties": {
|
|
211
|
+
"project": {"type": "string"},
|
|
212
|
+
"content": {"type": "string"},
|
|
213
|
+
"description": {"type": "string"},
|
|
214
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
215
|
+
"openspec_change_id": {"type": "string"},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
),
|
|
219
|
+
Tool(
|
|
220
|
+
name="store_fact",
|
|
221
|
+
description="Store a fact entry",
|
|
222
|
+
inputSchema={
|
|
223
|
+
"type": "object",
|
|
224
|
+
"required": ["project", "content"],
|
|
225
|
+
"properties": {
|
|
226
|
+
"project": {"type": "string"},
|
|
227
|
+
"content": {"type": "string"},
|
|
228
|
+
"description": {"type": "string"},
|
|
229
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
230
|
+
"confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
),
|
|
234
|
+
Tool(
|
|
235
|
+
name="store_learning",
|
|
236
|
+
description="Store a learning entry",
|
|
237
|
+
inputSchema={
|
|
238
|
+
"type": "object",
|
|
239
|
+
"required": ["project", "content"],
|
|
240
|
+
"properties": {
|
|
241
|
+
"project": {"type": "string"},
|
|
242
|
+
"content": {"type": "string"},
|
|
243
|
+
"description": {"type": "string"},
|
|
244
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
),
|
|
248
|
+
Tool(
|
|
249
|
+
name="store_convention",
|
|
250
|
+
description="Store a convention entry",
|
|
251
|
+
inputSchema={
|
|
252
|
+
"type": "object",
|
|
253
|
+
"required": ["project", "content"],
|
|
254
|
+
"properties": {
|
|
255
|
+
"project": {"type": "string"},
|
|
256
|
+
"content": {"type": "string"},
|
|
257
|
+
"description": {"type": "string"},
|
|
258
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
),
|
|
262
|
+
Tool(
|
|
263
|
+
name="store_profile",
|
|
264
|
+
description="Store or update a user profile entry for a project",
|
|
265
|
+
inputSchema={
|
|
266
|
+
"type": "object",
|
|
267
|
+
"required": ["project", "content"],
|
|
268
|
+
"properties": {
|
|
269
|
+
"project": {"type": "string"},
|
|
270
|
+
"content": {"type": "string"},
|
|
271
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
Tool(
|
|
276
|
+
name="store_source",
|
|
277
|
+
description="Store a source reference (article, transcript, PDF, video, link)",
|
|
278
|
+
inputSchema={
|
|
279
|
+
"type": "object",
|
|
280
|
+
"required": ["url", "title", "description", "source_kind"],
|
|
281
|
+
"properties": {
|
|
282
|
+
"url": {"type": "string"},
|
|
283
|
+
"title": {"type": "string"},
|
|
284
|
+
"description": {"type": "string"},
|
|
285
|
+
"source_kind": {
|
|
286
|
+
"type": "string",
|
|
287
|
+
"enum": ["article", "transcript", "pdf", "video", "link", "other"],
|
|
288
|
+
},
|
|
289
|
+
"tags": {"type": "array", "items": {"type": "string"}},
|
|
290
|
+
"content": {"type": "string"},
|
|
291
|
+
"supersedes": {"type": "string"},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
),
|
|
295
|
+
Tool(
|
|
296
|
+
name="export_memories",
|
|
297
|
+
description="Export all memory entries for a project (no limit)",
|
|
298
|
+
inputSchema={
|
|
299
|
+
"type": "object",
|
|
300
|
+
"required": ["project"],
|
|
301
|
+
"properties": {
|
|
302
|
+
"project": {"type": "string"},
|
|
303
|
+
"entry_type": {"type": "string", "enum": valid_types},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
),
|
|
307
|
+
Tool(
|
|
308
|
+
name="get_profile",
|
|
309
|
+
description="Retrieve the global tech profile for a project",
|
|
310
|
+
inputSchema={
|
|
311
|
+
"type": "object",
|
|
312
|
+
"required": ["project"],
|
|
313
|
+
"properties": {
|
|
314
|
+
"project": {"type": "string"},
|
|
315
|
+
"entry_type": {"type": "string", "enum": valid_types},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
),
|
|
319
|
+
Tool(
|
|
320
|
+
name="ping",
|
|
321
|
+
description="Health check — returns ok and current timestamp",
|
|
322
|
+
inputSchema={"type": "object", "properties": {}},
|
|
323
|
+
),
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ── MCP server factory (mcp 1.26.0 keyword-arg API) ───────────────────────
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def create_app(store: MemoryStore) -> Server:
|
|
331
|
+
"""Create and return a configured MCP server instance.
|
|
332
|
+
|
|
333
|
+
Uses the keyword-argument API of `mcp.server.Server` (>= 1.0). The handlers
|
|
334
|
+
run synchronously (the underlying MCP layer awaits them in an executor).
|
|
335
|
+
"""
|
|
336
|
+
tool_defs = _tool_definitions()
|
|
337
|
+
handlers = {
|
|
338
|
+
"search_memory": handle_search_memory,
|
|
339
|
+
"store_decision": handle_store_decision,
|
|
340
|
+
"store_fact": handle_store_fact,
|
|
341
|
+
"store_learning": handle_store_learning,
|
|
342
|
+
"store_convention": handle_store_convention,
|
|
343
|
+
"store_profile": handle_store_profile,
|
|
344
|
+
"store_source": handle_store_source,
|
|
345
|
+
"export_memories": handle_export_memories,
|
|
346
|
+
"get_profile": handle_get_profile,
|
|
347
|
+
"ping": handle_ping,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async def on_list_tools(
|
|
351
|
+
ctx: ServerRequestContext, params: PaginatedRequestParams | None
|
|
352
|
+
) -> ListToolsResult:
|
|
353
|
+
return ListToolsResult(tools=tool_defs)
|
|
354
|
+
|
|
355
|
+
async def on_call_tool(
|
|
356
|
+
ctx: ServerRequestContext, params: CallToolRequestParams
|
|
357
|
+
) -> CallToolResult:
|
|
358
|
+
name = params.name
|
|
359
|
+
arguments: dict[str, Any] = dict(params.arguments or {})
|
|
360
|
+
if name not in handlers:
|
|
361
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
362
|
+
text = handlers[name](store, arguments)
|
|
363
|
+
_log_tool_call(name, arguments)
|
|
364
|
+
return CallToolResult(
|
|
365
|
+
content=[TextContent(type="text", text=text)],
|
|
366
|
+
isError=False,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return Server(
|
|
370
|
+
"memory-server",
|
|
371
|
+
on_list_tools=on_list_tools,
|
|
372
|
+
on_call_tool=on_call_tool,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ── Entry point for `memory-server` console script ──────────────────────────
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def run() -> None:
|
|
380
|
+
"""Validate storage, build store, start stdio MCP server."""
|
|
381
|
+
from cli import get_memory_path, validate_storage_path
|
|
382
|
+
|
|
383
|
+
memory_path = get_memory_path()
|
|
384
|
+
validate_storage_path(str(memory_path))
|
|
385
|
+
store = MemoryStore(storage_path=memory_path)
|
|
386
|
+
store.initialize()
|
|
387
|
+
app = create_app(store)
|
|
388
|
+
async with stdio_server() as (r, w):
|
|
389
|
+
await app.run(r, w, app.create_initialization_options())
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def main() -> None:
|
|
393
|
+
import asyncio
|
|
394
|
+
|
|
395
|
+
asyncio.run(run())
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
main()
|