memplex 3.2.0__py3-none-any.whl
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.
- memnex/__init__.py +31 -0
- memnex/__main__.py +6 -0
- memnex/_plugin/.claude-plugin/plugin.json +24 -0
- memnex/_plugin/.mcp.json +9 -0
- memnex/_plugin/__init__.py +0 -0
- memnex/_plugin/hooks/hooks.json +43 -0
- memnex/_plugin/scripts/hook-runner.py +166 -0
- memnex/_plugin/skills/mem-explore/SKILL.md +83 -0
- memnex/_plugin/skills/mem-manage/SKILL.md +92 -0
- memnex/_plugin/skills/mem-search/SKILL.md +85 -0
- memnex/_plugin/skills/mem-write/SKILL.md +78 -0
- memnex/adapters/__init__.py +14 -0
- memnex/adapters/claude_skill.py +169 -0
- memnex/adapters/cli.py +525 -0
- memnex/adapters/http_api.py +314 -0
- memnex/adapters/mcp_server.py +448 -0
- memnex/compaction.py +563 -0
- memnex/config.py +366 -0
- memnex/core/__init__.py +13 -0
- memnex/core/associator/__init__.py +8 -0
- memnex/core/associator/domain_classifier.py +75 -0
- memnex/core/associator/entity_aligner.py +127 -0
- memnex/core/associator/ref_linker.py +197 -0
- memnex/core/associator/term_mapper.py +77 -0
- memnex/core/dictionaries/__init__.py +50 -0
- memnex/core/engine.py +667 -0
- memnex/core/extractors/__init__.py +15 -0
- memnex/core/extractors/docx.py +97 -0
- memnex/core/extractors/image.py +233 -0
- memnex/core/extractors/markdown.py +139 -0
- memnex/core/extractors/pdf.py +133 -0
- memnex/core/extractors/vision_mapper.py +131 -0
- memnex/core/handlers/__init__.py +7 -0
- memnex/core/handlers/clipboard.py +40 -0
- memnex/core/handlers/file_handler.py +62 -0
- memnex/core/handlers/url_handler.py +132 -0
- memnex/llm/__init__.py +25 -0
- memnex/llm/enhancer.py +226 -0
- memnex/llm/fallback_chain.py +87 -0
- memnex/llm/injection_guard.py +178 -0
- memnex/llm/provider.py +130 -0
- memnex/llm/providers/__init__.py +22 -0
- memnex/llm/providers/anthropic.py +135 -0
- memnex/llm/providers/local.py +135 -0
- memnex/llm/providers/rule_based.py +68 -0
- memnex/llm/sanitizer.py +67 -0
- memnex/models/__init__.py +68 -0
- memnex/models/feedback.py +42 -0
- memnex/models/graph.py +33 -0
- memnex/models/memory.py +102 -0
- memnex/models/misc.py +185 -0
- memnex/models/paragraph.py +45 -0
- memnex/models/search.py +51 -0
- memnex/models/source.py +23 -0
- memnex/models/task.py +62 -0
- memnex/processing/__init__.py +1 -0
- memnex/processing/graph_builder.py +278 -0
- memnex/processing/merger/__init__.py +6 -0
- memnex/processing/merger/confidence_calculator.py +127 -0
- memnex/processing/merger/conflict_resolver.py +116 -0
- memnex/retrieval/__init__.py +1 -0
- memnex/retrieval/dedup.py +386 -0
- memnex/retrieval/embedding.py +289 -0
- memnex/retrieval/reranker.py +299 -0
- memnex/service.py +902 -0
- memnex/storage/__init__.py +65 -0
- memnex/storage/base.py +132 -0
- memnex/storage/changelog.py +106 -0
- memnex/storage/feedback.py +486 -0
- memnex/storage/lite/__init__.py +5 -0
- memnex/storage/lite/store.py +606 -0
- memnex/storage/vector.py +265 -0
- memnex/wiki/__init__.py +11 -0
- memnex/wiki/community.py +221 -0
- memnex/wiki/compiler.py +545 -0
- memnex/wiki/generator.py +270 -0
- memnex/wiki/search.py +282 -0
- memnex/worker.py +412 -0
- memplex-3.2.0.dist-info/METADATA +37 -0
- memplex-3.2.0.dist-info/RECORD +83 -0
- memplex-3.2.0.dist-info/WHEEL +5 -0
- memplex-3.2.0.dist-info/entry_points.txt +2 -0
- memplex-3.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""MemNex MCP Server -- Model Context Protocol over stdio JSON-RPC.
|
|
2
|
+
|
|
3
|
+
Implements a lightweight MCP server that communicates via stdin/stdout
|
|
4
|
+
using JSON-RPC 2.0. No external MCP SDK dependency -- pure Python.
|
|
5
|
+
|
|
6
|
+
Protocol::
|
|
7
|
+
|
|
8
|
+
Request: {"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "...", "arguments": {...}}, "id": 1}
|
|
9
|
+
Response: {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
|
10
|
+
|
|
11
|
+
Usage::
|
|
12
|
+
|
|
13
|
+
from memnex.adapters.mcp_server import MCPServer
|
|
14
|
+
|
|
15
|
+
server = MCPServer()
|
|
16
|
+
server.run() # reads from stdin, writes to stdout
|
|
17
|
+
|
|
18
|
+
Or as a module::
|
|
19
|
+
|
|
20
|
+
python -m memnex.adapters.mcp_server
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import sys
|
|
28
|
+
import traceback
|
|
29
|
+
from dataclasses import asdict
|
|
30
|
+
from typing import Any, Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Helpers ─────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _dataclass_to_dict(obj) -> Any:
|
|
39
|
+
"""Recursively convert dataclasses to plain dicts."""
|
|
40
|
+
if hasattr(obj, "__dataclass_fields__"):
|
|
41
|
+
return asdict(obj)
|
|
42
|
+
if isinstance(obj, list):
|
|
43
|
+
return [_dataclass_to_dict(item) for item in obj]
|
|
44
|
+
if isinstance(obj, dict):
|
|
45
|
+
return {k: _dataclass_to_dict(v) for k, v in obj.items()}
|
|
46
|
+
return obj
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Tool definitions ────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
_TOOL_DEFINITIONS = [
|
|
52
|
+
{
|
|
53
|
+
"name": "memory_search",
|
|
54
|
+
"description": "Search MemNex knowledge graph. Returns index with IDs, names, relevance scores. ALWAYS use this before memory_get to filter results (10x token savings).",
|
|
55
|
+
"inputSchema": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"query": {"type": "string", "description": "Natural language search query"},
|
|
59
|
+
"top_k": {"type": "integer", "description": "Max results (default 10, max 100)", "default": 10},
|
|
60
|
+
},
|
|
61
|
+
"required": ["query"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "memory_add",
|
|
66
|
+
"description": "Add a new memory from text content.",
|
|
67
|
+
"inputSchema": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"content": {"type": "string", "description": "Text content to store"},
|
|
71
|
+
"source_type": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Source type: text | file | url (default: text)",
|
|
74
|
+
"default": "text",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"required": ["content"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "memory_get",
|
|
82
|
+
"description": "Retrieve full details for a specific memory. Use AFTER memory_search to get details only for filtered IDs (~500-1000 tokens each).",
|
|
83
|
+
"inputSchema": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"memory_id": {"type": "string", "description": "Memory ID from search results"},
|
|
87
|
+
},
|
|
88
|
+
"required": ["memory_id"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "memory_update",
|
|
93
|
+
"description": "Update a field value of an existing memory (self-editing).",
|
|
94
|
+
"inputSchema": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"memory_id": {"type": "string", "description": "Memory ID"},
|
|
98
|
+
"role": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Field role: trigger | condition | action | benefit",
|
|
101
|
+
},
|
|
102
|
+
"new_value": {"type": "string", "description": "New field value text"},
|
|
103
|
+
},
|
|
104
|
+
"required": ["memory_id", "role", "new_value"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "memory_delete",
|
|
109
|
+
"description": "Delete a memory by ID.",
|
|
110
|
+
"inputSchema": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": {
|
|
113
|
+
"memory_id": {"type": "string", "description": "Memory ID"},
|
|
114
|
+
},
|
|
115
|
+
"required": ["memory_id"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "memory_feedback",
|
|
120
|
+
"description": "Submit feedback on a memory field value.",
|
|
121
|
+
"inputSchema": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"memory_id": {"type": "string", "description": "Memory ID"},
|
|
125
|
+
"role": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Field role: trigger | action | condition | benefit",
|
|
128
|
+
},
|
|
129
|
+
"index": {"type": "integer", "description": "Value index within the field"},
|
|
130
|
+
"verdict": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"description": "Verdict: correct | wrong",
|
|
133
|
+
},
|
|
134
|
+
"reason": {"type": "string", "description": "Optional explanation"},
|
|
135
|
+
},
|
|
136
|
+
"required": ["memory_id", "role", "index", "verdict"],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"name": "memory_pending_reviews",
|
|
141
|
+
"description": "List pending feedback reviews that need resolution.",
|
|
142
|
+
"inputSchema": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"properties": {
|
|
145
|
+
"owner": {"type": "string", "description": "Filter by owner (optional)"},
|
|
146
|
+
"limit": {"type": "integer", "description": "Max results (default 100)", "default": 100},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "memory_resolve",
|
|
152
|
+
"description": "Apply a resolution to a pending review.",
|
|
153
|
+
"inputSchema": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"memory_id": {"type": "string", "description": "Memory ID"},
|
|
157
|
+
"field_role": {"type": "string", "description": "Field role under review"},
|
|
158
|
+
"action": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": "Resolution action: accept | reject | merge",
|
|
161
|
+
},
|
|
162
|
+
"new_value": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"description": "Replacement value (required when action=merge)",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"required": ["memory_id", "field_role", "action"],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "memory_health",
|
|
172
|
+
"description": "Check MemNex service health status.",
|
|
173
|
+
"inputSchema": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": {},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── MCPServer ───────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MCPServer:
|
|
185
|
+
"""MCP Server for MemNex, communicating over stdio JSON-RPC.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
config:
|
|
190
|
+
Optional :class:`MemNexConfig`. When ``None``, loaded via
|
|
191
|
+
:func:`load_config`.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(self, config=None) -> None:
|
|
195
|
+
self._config = config
|
|
196
|
+
self._service = None
|
|
197
|
+
|
|
198
|
+
# ── Lifecycle ────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def _ensure_service(self):
|
|
201
|
+
"""Lazy-initialize the MemNexService."""
|
|
202
|
+
if self._service is not None:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
from memnex.config import load_config
|
|
206
|
+
from memnex.service import MemNexService
|
|
207
|
+
|
|
208
|
+
cfg = self._config or load_config()
|
|
209
|
+
self._service = MemNexService(config=cfg)
|
|
210
|
+
self._service.start()
|
|
211
|
+
|
|
212
|
+
# ── JSON-RPC I/O ─────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
def _read_request(self) -> Optional[dict]:
|
|
215
|
+
"""Read a single JSON-RPC request from stdin."""
|
|
216
|
+
line = sys.stdin.readline()
|
|
217
|
+
if not line:
|
|
218
|
+
return None
|
|
219
|
+
line = line.strip()
|
|
220
|
+
if not line:
|
|
221
|
+
return None
|
|
222
|
+
try:
|
|
223
|
+
return json.loads(line)
|
|
224
|
+
except json.JSONDecodeError as exc:
|
|
225
|
+
logger.warning("Invalid JSON from stdin: %s", exc)
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def _write_response(self, response: dict) -> None:
|
|
229
|
+
"""Write a JSON-RPC response to stdout."""
|
|
230
|
+
sys.stdout.write(json.dumps(response, default=str, ensure_ascii=False) + "\n")
|
|
231
|
+
sys.stdout.flush()
|
|
232
|
+
|
|
233
|
+
def _make_result(self, result: Any, req_id: Any) -> dict:
|
|
234
|
+
"""Build a JSON-RPC success response."""
|
|
235
|
+
return {"jsonrpc": "2.0", "result": result, "id": req_id}
|
|
236
|
+
|
|
237
|
+
def _make_error(self, code: int, message: str, req_id: Any = None) -> dict:
|
|
238
|
+
"""Build a JSON-RPC error response."""
|
|
239
|
+
return {
|
|
240
|
+
"jsonrpc": "2.0",
|
|
241
|
+
"error": {"code": code, "message": message},
|
|
242
|
+
"id": req_id,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# ── Method dispatch ──────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
def _handle_initialize(self, params: dict) -> dict:
|
|
248
|
+
"""Handle ``initialize`` request."""
|
|
249
|
+
return {
|
|
250
|
+
"protocolVersion": "2024-11-05",
|
|
251
|
+
"capabilities": {
|
|
252
|
+
"tools": {},
|
|
253
|
+
},
|
|
254
|
+
"serverInfo": {
|
|
255
|
+
"name": "memnex",
|
|
256
|
+
"version": "3.2.0",
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def _handle_tools_list(self, params: dict) -> dict:
|
|
261
|
+
"""Handle ``tools/list`` request."""
|
|
262
|
+
return {"tools": _TOOL_DEFINITIONS}
|
|
263
|
+
|
|
264
|
+
def _handle_tools_call(self, params: dict) -> dict:
|
|
265
|
+
"""Handle ``tools/call`` request."""
|
|
266
|
+
tool_name = params.get("name", "")
|
|
267
|
+
arguments = params.get("arguments", {})
|
|
268
|
+
|
|
269
|
+
handler = self._tool_handlers.get(tool_name)
|
|
270
|
+
if handler is None:
|
|
271
|
+
raise ValueError(f"Unknown tool: {tool_name!r}")
|
|
272
|
+
|
|
273
|
+
result = handler(self, arguments)
|
|
274
|
+
return {
|
|
275
|
+
"content": [
|
|
276
|
+
{
|
|
277
|
+
"type": "text",
|
|
278
|
+
"text": json.dumps(result, default=str, ensure_ascii=False, indent=2),
|
|
279
|
+
}
|
|
280
|
+
],
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
def _handle_request(self, request: dict) -> Optional[dict]:
|
|
284
|
+
"""Route a JSON-RPC request to the correct handler."""
|
|
285
|
+
method = request.get("method", "")
|
|
286
|
+
params = request.get("params", {})
|
|
287
|
+
req_id = request.get("id")
|
|
288
|
+
|
|
289
|
+
# Notifications (no id) do not expect a response
|
|
290
|
+
if req_id is None and method.endswith("/notification"):
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
if method == "initialize":
|
|
295
|
+
result = self._handle_initialize(params)
|
|
296
|
+
elif method == "tools/list":
|
|
297
|
+
result = self._handle_tools_list(params)
|
|
298
|
+
elif method == "tools/call":
|
|
299
|
+
self._ensure_service()
|
|
300
|
+
result = self._handle_tools_call(params)
|
|
301
|
+
elif method == "ping":
|
|
302
|
+
result = {}
|
|
303
|
+
else:
|
|
304
|
+
return self._make_error(-32601, f"Method not found: {method}", req_id)
|
|
305
|
+
|
|
306
|
+
return self._make_result(result, req_id)
|
|
307
|
+
|
|
308
|
+
except Exception as exc:
|
|
309
|
+
logger.error("Error handling %s: %s", method, exc)
|
|
310
|
+
traceback.print_exc(file=sys.stderr)
|
|
311
|
+
return self._make_error(-32603, str(exc), req_id)
|
|
312
|
+
|
|
313
|
+
# ── Tool implementations ─────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
def _tool_memory_search(self, args: dict) -> dict:
|
|
316
|
+
"""Search memories."""
|
|
317
|
+
result = self._service.query(
|
|
318
|
+
text=args["query"],
|
|
319
|
+
top_k=args.get("top_k", 10),
|
|
320
|
+
)
|
|
321
|
+
return {
|
|
322
|
+
"total": len(result.results),
|
|
323
|
+
"scope": result.scope.value if hasattr(result.scope, "value") else str(result.scope),
|
|
324
|
+
"latency_ms": result.latency_ms,
|
|
325
|
+
"results": [
|
|
326
|
+
{
|
|
327
|
+
"id": r.func_id,
|
|
328
|
+
"name": r.name,
|
|
329
|
+
"relevance": round(r.relevance_score, 4),
|
|
330
|
+
"summary": r.summary,
|
|
331
|
+
"domain": r.domain,
|
|
332
|
+
}
|
|
333
|
+
for r in result.results
|
|
334
|
+
],
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def _tool_memory_add(self, args: dict) -> dict:
|
|
338
|
+
"""Add a new memory."""
|
|
339
|
+
content = args["content"]
|
|
340
|
+
source_type = args.get("source_type", "text")
|
|
341
|
+
result = self._service.write_text(text=content, source_type=source_type)
|
|
342
|
+
return {
|
|
343
|
+
"functions_extracted": len(result.functions),
|
|
344
|
+
"edges": len(result.graph.edges),
|
|
345
|
+
"function_ids": [f.id for f in result.functions],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
def _tool_memory_get(self, args: dict) -> dict:
|
|
349
|
+
"""Get a memory by ID."""
|
|
350
|
+
func = self._service.get(args["memory_id"])
|
|
351
|
+
if func is None:
|
|
352
|
+
return {"error": "Memory not found", "memory_id": args["memory_id"]}
|
|
353
|
+
return _dataclass_to_dict(func)
|
|
354
|
+
|
|
355
|
+
def _tool_memory_update(self, args: dict) -> dict:
|
|
356
|
+
"""Update a memory field."""
|
|
357
|
+
result = self._service.update_memory(
|
|
358
|
+
memory_id=args["memory_id"],
|
|
359
|
+
role=args["role"],
|
|
360
|
+
new_value=args["new_value"],
|
|
361
|
+
)
|
|
362
|
+
return _dataclass_to_dict(result)
|
|
363
|
+
|
|
364
|
+
def _tool_memory_delete(self, args: dict) -> dict:
|
|
365
|
+
"""Delete a memory."""
|
|
366
|
+
self._service.delete(args["memory_id"])
|
|
367
|
+
return {"status": "deleted", "id": args["memory_id"]}
|
|
368
|
+
|
|
369
|
+
def _tool_memory_feedback(self, args: dict) -> dict:
|
|
370
|
+
"""Submit feedback."""
|
|
371
|
+
self._service.submit_feedback(
|
|
372
|
+
memory_id=args["memory_id"],
|
|
373
|
+
field_role=args["role"],
|
|
374
|
+
value_index=args["index"],
|
|
375
|
+
verdict=args["verdict"],
|
|
376
|
+
reason=args.get("reason"),
|
|
377
|
+
)
|
|
378
|
+
return {"status": "recorded"}
|
|
379
|
+
|
|
380
|
+
def _tool_memory_pending_reviews(self, args: dict) -> dict:
|
|
381
|
+
"""List pending reviews."""
|
|
382
|
+
reviews = self._service.get_pending_reviews(
|
|
383
|
+
owner=args.get("owner"),
|
|
384
|
+
limit=args.get("limit", 100),
|
|
385
|
+
)
|
|
386
|
+
return {
|
|
387
|
+
"total": len(reviews),
|
|
388
|
+
"reviews": _dataclass_to_dict(reviews),
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
def _tool_memory_resolve(self, args: dict) -> dict:
|
|
392
|
+
"""Resolve a pending review."""
|
|
393
|
+
return self._service.apply_resolution(
|
|
394
|
+
memory_id=args["memory_id"],
|
|
395
|
+
field_role=args["field_role"],
|
|
396
|
+
action=args["action"],
|
|
397
|
+
new_value=args.get("new_value"),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def _tool_memory_health(self, args: dict) -> dict:
|
|
401
|
+
"""Health check."""
|
|
402
|
+
self._ensure_service()
|
|
403
|
+
return self._service.health()
|
|
404
|
+
|
|
405
|
+
# Map tool names to handler methods
|
|
406
|
+
_tool_handlers: Dict[str, Any] = {
|
|
407
|
+
"memory_search": _tool_memory_search,
|
|
408
|
+
"memory_add": _tool_memory_add,
|
|
409
|
+
"memory_get": _tool_memory_get,
|
|
410
|
+
"memory_update": _tool_memory_update,
|
|
411
|
+
"memory_delete": _tool_memory_delete,
|
|
412
|
+
"memory_feedback": _tool_memory_feedback,
|
|
413
|
+
"memory_pending_reviews": _tool_memory_pending_reviews,
|
|
414
|
+
"memory_resolve": _tool_memory_resolve,
|
|
415
|
+
"memory_health": _tool_memory_health,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# ── Main loop ────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
def run(self) -> None:
|
|
421
|
+
"""Run the MCP server, reading requests from stdin.
|
|
422
|
+
|
|
423
|
+
Blocks until stdin is closed (EOF) or a fatal error occurs.
|
|
424
|
+
"""
|
|
425
|
+
logger.info("MemNex MCP Server starting (stdio JSON-RPC)")
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
while True:
|
|
429
|
+
request = self._read_request()
|
|
430
|
+
if request is None:
|
|
431
|
+
break # EOF
|
|
432
|
+
|
|
433
|
+
response = self._handle_request(request)
|
|
434
|
+
if response is not None:
|
|
435
|
+
self._write_response(response)
|
|
436
|
+
except KeyboardInterrupt:
|
|
437
|
+
pass
|
|
438
|
+
finally:
|
|
439
|
+
if self._service is not None:
|
|
440
|
+
self._service.stop()
|
|
441
|
+
logger.info("MemNex MCP Server stopped")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ── CLI entry point ─────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
|
|
448
|
+
MCPServer().run()
|