runtime-memory 3.0.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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1574 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) Server for Runtime Memory.
|
|
2
|
+
|
|
3
|
+
This module implements an MCP server that exposes memory operations
|
|
4
|
+
as tools for AI agents. It uses JSON-RPC over stdio for communication.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
mem serve --mcp
|
|
8
|
+
|
|
9
|
+
Protocol:
|
|
10
|
+
The server communicates via JSON-RPC 2.0 over stdin/stdout.
|
|
11
|
+
Each message is a newline-delimited JSON object.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import Any, Callable, Coroutine, Optional
|
|
25
|
+
|
|
26
|
+
from runtime_memory import __version__
|
|
27
|
+
from runtime_memory.core.engine import MemoryEngine
|
|
28
|
+
from runtime_memory.core.logging import get_logger
|
|
29
|
+
from runtime_memory.core.models import (
|
|
30
|
+
Memory,
|
|
31
|
+
MemoryCategory,
|
|
32
|
+
MemoryScope,
|
|
33
|
+
MemorySource,
|
|
34
|
+
Outcome,
|
|
35
|
+
)
|
|
36
|
+
from runtime_memory.core.paths import default_db_path
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# MCP Protocol Types
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MCPErrorCode(int, Enum):
|
|
47
|
+
"""JSON-RPC error codes for MCP."""
|
|
48
|
+
|
|
49
|
+
PARSE_ERROR = -32700
|
|
50
|
+
INVALID_REQUEST = -32600
|
|
51
|
+
METHOD_NOT_FOUND = -32601
|
|
52
|
+
INVALID_PARAMS = -32602
|
|
53
|
+
INTERNAL_ERROR = -32603
|
|
54
|
+
|
|
55
|
+
# Custom error codes
|
|
56
|
+
TOOL_NOT_FOUND = -32001
|
|
57
|
+
VALIDATION_ERROR = -32002
|
|
58
|
+
RATE_LIMITED = -32003
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class MCPError:
|
|
63
|
+
"""MCP error response."""
|
|
64
|
+
|
|
65
|
+
code: int
|
|
66
|
+
message: str
|
|
67
|
+
data: Optional[Any] = None
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
"""Convert to dictionary."""
|
|
71
|
+
result = {"code": self.code, "message": self.message}
|
|
72
|
+
if self.data is not None:
|
|
73
|
+
result["data"] = self.data
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class MCPRequest:
|
|
79
|
+
"""MCP JSON-RPC request."""
|
|
80
|
+
|
|
81
|
+
jsonrpc: str
|
|
82
|
+
method: str
|
|
83
|
+
id: Optional[str | int] = None
|
|
84
|
+
params: Optional[dict[str, Any]] = None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, data: dict[str, Any]) -> MCPRequest:
|
|
88
|
+
"""Create from dictionary."""
|
|
89
|
+
return cls(
|
|
90
|
+
jsonrpc=data.get("jsonrpc", "2.0"),
|
|
91
|
+
method=data["method"],
|
|
92
|
+
id=data.get("id"),
|
|
93
|
+
params=data.get("params"),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class MCPResponse:
|
|
99
|
+
"""MCP JSON-RPC response."""
|
|
100
|
+
|
|
101
|
+
jsonrpc: str = "2.0"
|
|
102
|
+
id: Optional[str | int] = None
|
|
103
|
+
result: Optional[Any] = None
|
|
104
|
+
error: Optional[MCPError] = None
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> dict[str, Any]:
|
|
107
|
+
"""Convert to dictionary."""
|
|
108
|
+
response: dict[str, Any] = {"jsonrpc": self.jsonrpc}
|
|
109
|
+
if self.id is not None:
|
|
110
|
+
response["id"] = self.id
|
|
111
|
+
if self.error is not None:
|
|
112
|
+
response["error"] = self.error.to_dict()
|
|
113
|
+
else:
|
|
114
|
+
response["result"] = self.result
|
|
115
|
+
return response
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class MCPToolSchema:
|
|
120
|
+
"""Schema definition for an MCP tool."""
|
|
121
|
+
|
|
122
|
+
name: str
|
|
123
|
+
description: str
|
|
124
|
+
input_schema: dict[str, Any]
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict[str, Any]:
|
|
127
|
+
"""Convert to dictionary."""
|
|
128
|
+
return {
|
|
129
|
+
"name": self.name,
|
|
130
|
+
"description": self.description,
|
|
131
|
+
"inputSchema": self.input_schema,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# Rate Limiter
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class RateLimiter:
|
|
142
|
+
"""Simple rate limiter for MCP requests."""
|
|
143
|
+
|
|
144
|
+
max_requests: int = 100
|
|
145
|
+
window_seconds: float = 60.0
|
|
146
|
+
_requests: dict[str, list[float]] = field(default_factory=lambda: defaultdict(list))
|
|
147
|
+
|
|
148
|
+
def is_allowed(self, client_id: str = "default") -> bool:
|
|
149
|
+
"""Check if a request is allowed."""
|
|
150
|
+
now = time.time()
|
|
151
|
+
window_start = now - self.window_seconds
|
|
152
|
+
|
|
153
|
+
# Clean old requests
|
|
154
|
+
self._requests[client_id] = [
|
|
155
|
+
t for t in self._requests[client_id] if t > window_start
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
# Check limit
|
|
159
|
+
if len(self._requests[client_id]) >= self.max_requests:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
# Record request
|
|
163
|
+
self._requests[client_id].append(now)
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def reset(self, client_id: str = "default") -> None:
|
|
167
|
+
"""Reset rate limit for a client."""
|
|
168
|
+
self._requests[client_id] = []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# =============================================================================
|
|
172
|
+
# Tool Schemas
|
|
173
|
+
# =============================================================================
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
TOOL_SCHEMAS: list[MCPToolSchema] = [
|
|
177
|
+
MCPToolSchema(
|
|
178
|
+
name="search_memories",
|
|
179
|
+
description="Search for relevant memories by query. Returns memories ranked by relevance and outcome score.",
|
|
180
|
+
input_schema={
|
|
181
|
+
"type": "object",
|
|
182
|
+
"properties": {
|
|
183
|
+
"query": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"description": "Search query text",
|
|
186
|
+
"minLength": 1,
|
|
187
|
+
"maxLength": 1000,
|
|
188
|
+
},
|
|
189
|
+
"limit": {
|
|
190
|
+
"type": "integer",
|
|
191
|
+
"description": "Maximum number of results (1-100)",
|
|
192
|
+
"minimum": 1,
|
|
193
|
+
"maximum": 100,
|
|
194
|
+
"default": 10,
|
|
195
|
+
},
|
|
196
|
+
"categories": {
|
|
197
|
+
"type": "array",
|
|
198
|
+
"description": "Filter by categories",
|
|
199
|
+
"items": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"enum": [c.value for c in MemoryCategory],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
"project": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "Filter by project name",
|
|
207
|
+
},
|
|
208
|
+
"min_score": {
|
|
209
|
+
"type": "number",
|
|
210
|
+
"description": "Minimum outcome score (-1.0 to 1.0)",
|
|
211
|
+
"minimum": -1.0,
|
|
212
|
+
"maximum": 1.0,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
"required": ["query"],
|
|
216
|
+
},
|
|
217
|
+
),
|
|
218
|
+
MCPToolSchema(
|
|
219
|
+
name="add_memory",
|
|
220
|
+
description="Store a new memory with category. Categories: architecture, convention, decision, pattern, gotcha, workaround, troubleshooting, command, preference, dependency, environment, coding_style, tool_preference, context, todo, general.",
|
|
221
|
+
input_schema={
|
|
222
|
+
"type": "object",
|
|
223
|
+
"properties": {
|
|
224
|
+
"content": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": "Memory content text",
|
|
227
|
+
"minLength": 1,
|
|
228
|
+
"maxLength": 10000,
|
|
229
|
+
},
|
|
230
|
+
"category": {
|
|
231
|
+
"type": "string",
|
|
232
|
+
"description": "Memory category",
|
|
233
|
+
"enum": [c.value for c in MemoryCategory],
|
|
234
|
+
"default": "general",
|
|
235
|
+
},
|
|
236
|
+
"project": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Project name for scoping",
|
|
239
|
+
},
|
|
240
|
+
"tags": {
|
|
241
|
+
"type": "array",
|
|
242
|
+
"description": "Optional tags",
|
|
243
|
+
"items": {"type": "string"},
|
|
244
|
+
"maxItems": 20,
|
|
245
|
+
},
|
|
246
|
+
"importance": {
|
|
247
|
+
"type": "number",
|
|
248
|
+
"description": "Importance weight (0.0 to 1.0)",
|
|
249
|
+
"minimum": 0.0,
|
|
250
|
+
"maximum": 1.0,
|
|
251
|
+
"default": 0.5,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
"required": ["content"],
|
|
255
|
+
},
|
|
256
|
+
),
|
|
257
|
+
MCPToolSchema(
|
|
258
|
+
name="record_outcome",
|
|
259
|
+
description="Record worked/failed/partial feedback for memories. This adjusts the outcome score: worked +0.2, failed -0.3, partial +0.05.",
|
|
260
|
+
input_schema={
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"memory_ids": {
|
|
264
|
+
"type": "array",
|
|
265
|
+
"description": "IDs of memories to apply outcome to",
|
|
266
|
+
"items": {"type": "string"},
|
|
267
|
+
"minItems": 1,
|
|
268
|
+
"maxItems": 50,
|
|
269
|
+
},
|
|
270
|
+
"outcome": {
|
|
271
|
+
"type": "string",
|
|
272
|
+
"description": "Outcome result",
|
|
273
|
+
"enum": ["worked", "failed", "partial"],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
"required": ["memory_ids", "outcome"],
|
|
277
|
+
},
|
|
278
|
+
),
|
|
279
|
+
MCPToolSchema(
|
|
280
|
+
name="get_context",
|
|
281
|
+
description="Get memories relevant to the current project context, formatted for injection into the conversation.",
|
|
282
|
+
input_schema={
|
|
283
|
+
"type": "object",
|
|
284
|
+
"properties": {
|
|
285
|
+
"project": {
|
|
286
|
+
"type": "string",
|
|
287
|
+
"description": "Project name",
|
|
288
|
+
},
|
|
289
|
+
"limit": {
|
|
290
|
+
"type": "integer",
|
|
291
|
+
"description": "Maximum memories to include",
|
|
292
|
+
"minimum": 1,
|
|
293
|
+
"maximum": 50,
|
|
294
|
+
"default": 10,
|
|
295
|
+
},
|
|
296
|
+
"format": {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"description": "Output format",
|
|
299
|
+
"enum": ["brief", "detailed", "markdown"],
|
|
300
|
+
"default": "markdown",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
),
|
|
305
|
+
MCPToolSchema(
|
|
306
|
+
name="update_memory",
|
|
307
|
+
description="Update an existing memory's content or category.",
|
|
308
|
+
input_schema={
|
|
309
|
+
"type": "object",
|
|
310
|
+
"properties": {
|
|
311
|
+
"id": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"description": "Memory ID to update",
|
|
314
|
+
},
|
|
315
|
+
"content": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"description": "New content (optional)",
|
|
318
|
+
"minLength": 1,
|
|
319
|
+
"maxLength": 10000,
|
|
320
|
+
},
|
|
321
|
+
"category": {
|
|
322
|
+
"type": "string",
|
|
323
|
+
"description": "New category (optional)",
|
|
324
|
+
"enum": [c.value for c in MemoryCategory],
|
|
325
|
+
},
|
|
326
|
+
"tags": {
|
|
327
|
+
"type": "array",
|
|
328
|
+
"description": "New tags (optional)",
|
|
329
|
+
"items": {"type": "string"},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
"required": ["id"],
|
|
333
|
+
},
|
|
334
|
+
),
|
|
335
|
+
MCPToolSchema(
|
|
336
|
+
name="delete_memory",
|
|
337
|
+
description="Archive (soft delete) a memory by ID.",
|
|
338
|
+
input_schema={
|
|
339
|
+
"type": "object",
|
|
340
|
+
"properties": {
|
|
341
|
+
"id": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": "Memory ID to delete",
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
"required": ["id"],
|
|
347
|
+
},
|
|
348
|
+
),
|
|
349
|
+
MCPToolSchema(
|
|
350
|
+
name="list_memories",
|
|
351
|
+
description="List memories with optional filters by category, project, or minimum score.",
|
|
352
|
+
input_schema={
|
|
353
|
+
"type": "object",
|
|
354
|
+
"properties": {
|
|
355
|
+
"category": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"description": "Filter by category",
|
|
358
|
+
"enum": [c.value for c in MemoryCategory],
|
|
359
|
+
},
|
|
360
|
+
"project": {
|
|
361
|
+
"type": "string",
|
|
362
|
+
"description": "Filter by project",
|
|
363
|
+
},
|
|
364
|
+
"limit": {
|
|
365
|
+
"type": "integer",
|
|
366
|
+
"description": "Maximum results",
|
|
367
|
+
"minimum": 1,
|
|
368
|
+
"maximum": 100,
|
|
369
|
+
"default": 20,
|
|
370
|
+
},
|
|
371
|
+
"include_archived": {
|
|
372
|
+
"type": "boolean",
|
|
373
|
+
"description": "Include archived memories",
|
|
374
|
+
"default": False,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
),
|
|
379
|
+
MCPToolSchema(
|
|
380
|
+
name="get_stats",
|
|
381
|
+
description="Get memory statistics including total count, category distribution, and average outcome scores.",
|
|
382
|
+
input_schema={
|
|
383
|
+
"type": "object",
|
|
384
|
+
"properties": {
|
|
385
|
+
"project": {
|
|
386
|
+
"type": "string",
|
|
387
|
+
"description": "Filter stats by project",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
),
|
|
392
|
+
# Beads Integration Tools
|
|
393
|
+
MCPToolSchema(
|
|
394
|
+
name="beads_sync",
|
|
395
|
+
description="Sync outcomes for completed Beads tasks. When a task completes, memories that helped solve it get their outcome scores boosted.",
|
|
396
|
+
input_schema={
|
|
397
|
+
"type": "object",
|
|
398
|
+
"properties": {
|
|
399
|
+
"task_id": {
|
|
400
|
+
"type": "string",
|
|
401
|
+
"description": "Sync specific task ID only (optional, syncs all if not provided)",
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
),
|
|
406
|
+
MCPToolSchema(
|
|
407
|
+
name="beads_context",
|
|
408
|
+
description="Get unified context combining Beads task info and relevant memories for the current or specified task.",
|
|
409
|
+
input_schema={
|
|
410
|
+
"type": "object",
|
|
411
|
+
"properties": {
|
|
412
|
+
"task_id": {
|
|
413
|
+
"type": "string",
|
|
414
|
+
"description": "Task ID to get context for (optional, uses current task if not provided)",
|
|
415
|
+
},
|
|
416
|
+
"limit": {
|
|
417
|
+
"type": "integer",
|
|
418
|
+
"description": "Maximum number of memories to include (default: 10)",
|
|
419
|
+
"default": 10,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
),
|
|
424
|
+
MCPToolSchema(
|
|
425
|
+
name="beads_link",
|
|
426
|
+
description="Link a memory to a Beads task for outcome tracking. When the task completes, the memory's outcome will be recorded.",
|
|
427
|
+
input_schema={
|
|
428
|
+
"type": "object",
|
|
429
|
+
"properties": {
|
|
430
|
+
"memory_id": {
|
|
431
|
+
"type": "string",
|
|
432
|
+
"description": "Memory ID to link",
|
|
433
|
+
},
|
|
434
|
+
"task_id": {
|
|
435
|
+
"type": "string",
|
|
436
|
+
"description": "Task ID to link to (optional, uses current task if not provided)",
|
|
437
|
+
},
|
|
438
|
+
"context": {
|
|
439
|
+
"type": "string",
|
|
440
|
+
"description": "Optional context about how the memory is used",
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
"required": ["memory_id"],
|
|
444
|
+
},
|
|
445
|
+
),
|
|
446
|
+
MCPToolSchema(
|
|
447
|
+
name="beads_tasks",
|
|
448
|
+
description="List Beads tasks with optional status filter.",
|
|
449
|
+
input_schema={
|
|
450
|
+
"type": "object",
|
|
451
|
+
"properties": {
|
|
452
|
+
"status": {
|
|
453
|
+
"type": "string",
|
|
454
|
+
"description": "Filter by status: pending, in_progress, done, blocked, cancelled",
|
|
455
|
+
"enum": ["pending", "in_progress", "done", "blocked", "cancelled"],
|
|
456
|
+
},
|
|
457
|
+
"limit": {
|
|
458
|
+
"type": "integer",
|
|
459
|
+
"description": "Maximum number of tasks to return (default: 20)",
|
|
460
|
+
"default": 20,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
),
|
|
465
|
+
# Unified Tasks Tools (Phase 7 - Claude Code Tasks Adapter)
|
|
466
|
+
MCPToolSchema(
|
|
467
|
+
name="tasks_list",
|
|
468
|
+
description="List tasks from all available sources (Beads and Claude Code). Provides unified access to tasks.",
|
|
469
|
+
input_schema={
|
|
470
|
+
"type": "object",
|
|
471
|
+
"properties": {
|
|
472
|
+
"source": {
|
|
473
|
+
"type": "string",
|
|
474
|
+
"description": "Filter by source: beads, claude_code (optional, includes all if not provided)",
|
|
475
|
+
"enum": ["beads", "claude_code"],
|
|
476
|
+
},
|
|
477
|
+
"status": {
|
|
478
|
+
"type": "string",
|
|
479
|
+
"description": "Filter by status: pending, in_progress, done, completed",
|
|
480
|
+
"enum": ["pending", "in_progress", "done", "completed"],
|
|
481
|
+
},
|
|
482
|
+
"limit": {
|
|
483
|
+
"type": "integer",
|
|
484
|
+
"description": "Maximum number of tasks to return (default: 50)",
|
|
485
|
+
"default": 50,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
),
|
|
490
|
+
MCPToolSchema(
|
|
491
|
+
name="tasks_sync",
|
|
492
|
+
description="Sync outcomes for completed tasks from all sources. When a task completes, memories that helped solve it get their outcome scores boosted.",
|
|
493
|
+
input_schema={
|
|
494
|
+
"type": "object",
|
|
495
|
+
"properties": {
|
|
496
|
+
"source": {
|
|
497
|
+
"type": "string",
|
|
498
|
+
"description": "Sync specific source only: beads, claude_code (optional, syncs all if not provided)",
|
|
499
|
+
"enum": ["beads", "claude_code"],
|
|
500
|
+
},
|
|
501
|
+
"task_id": {
|
|
502
|
+
"type": "string",
|
|
503
|
+
"description": "Sync specific task ID only (optional, syncs all completed if not provided)",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
),
|
|
508
|
+
MCPToolSchema(
|
|
509
|
+
name="tasks_context",
|
|
510
|
+
description="Get unified context combining task info and relevant memories from any source.",
|
|
511
|
+
input_schema={
|
|
512
|
+
"type": "object",
|
|
513
|
+
"properties": {
|
|
514
|
+
"task_id": {
|
|
515
|
+
"type": "string",
|
|
516
|
+
"description": "Task ID to get context for (optional, uses current task if not provided)",
|
|
517
|
+
},
|
|
518
|
+
"source": {
|
|
519
|
+
"type": "string",
|
|
520
|
+
"description": "Source to use: beads, claude_code (optional, auto-detects from task ID)",
|
|
521
|
+
"enum": ["beads", "claude_code"],
|
|
522
|
+
},
|
|
523
|
+
"limit": {
|
|
524
|
+
"type": "integer",
|
|
525
|
+
"description": "Maximum number of memories to include (default: 10)",
|
|
526
|
+
"default": 10,
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
),
|
|
531
|
+
MCPToolSchema(
|
|
532
|
+
name="tasks_stats",
|
|
533
|
+
description="Get statistics for all task integrations including available sources and task counts.",
|
|
534
|
+
input_schema={
|
|
535
|
+
"type": "object",
|
|
536
|
+
"properties": {},
|
|
537
|
+
},
|
|
538
|
+
),
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# =============================================================================
|
|
543
|
+
# Input Validators
|
|
544
|
+
# =============================================================================
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def validate_string(
|
|
548
|
+
value: Any,
|
|
549
|
+
name: str,
|
|
550
|
+
min_length: int = 0,
|
|
551
|
+
max_length: int = 10000,
|
|
552
|
+
required: bool = True,
|
|
553
|
+
) -> Optional[str]:
|
|
554
|
+
"""Validate a string parameter."""
|
|
555
|
+
if value is None:
|
|
556
|
+
if required:
|
|
557
|
+
raise ValueError(f"{name} is required")
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
if not isinstance(value, str):
|
|
561
|
+
raise ValueError(f"{name} must be a string")
|
|
562
|
+
|
|
563
|
+
if len(value) < min_length:
|
|
564
|
+
raise ValueError(f"{name} must be at least {min_length} characters")
|
|
565
|
+
|
|
566
|
+
if len(value) > max_length:
|
|
567
|
+
raise ValueError(f"{name} must be at most {max_length} characters")
|
|
568
|
+
|
|
569
|
+
return value
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def validate_integer(
|
|
573
|
+
value: Any,
|
|
574
|
+
name: str,
|
|
575
|
+
minimum: int = 0,
|
|
576
|
+
maximum: int = 100,
|
|
577
|
+
default: Optional[int] = None,
|
|
578
|
+
) -> Optional[int]:
|
|
579
|
+
"""Validate an integer parameter."""
|
|
580
|
+
if value is None:
|
|
581
|
+
return default
|
|
582
|
+
|
|
583
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
584
|
+
raise ValueError(f"{name} must be an integer")
|
|
585
|
+
|
|
586
|
+
if value < minimum:
|
|
587
|
+
raise ValueError(f"{name} must be at least {minimum}")
|
|
588
|
+
|
|
589
|
+
if value > maximum:
|
|
590
|
+
raise ValueError(f"{name} must be at most {maximum}")
|
|
591
|
+
|
|
592
|
+
return value
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def validate_float(
|
|
596
|
+
value: Any,
|
|
597
|
+
name: str,
|
|
598
|
+
minimum: float = 0.0,
|
|
599
|
+
maximum: float = 1.0,
|
|
600
|
+
default: Optional[float] = None,
|
|
601
|
+
) -> Optional[float]:
|
|
602
|
+
"""Validate a float parameter."""
|
|
603
|
+
if value is None:
|
|
604
|
+
return default
|
|
605
|
+
|
|
606
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
607
|
+
raise ValueError(f"{name} must be a number")
|
|
608
|
+
|
|
609
|
+
if value < minimum:
|
|
610
|
+
raise ValueError(f"{name} must be at least {minimum}")
|
|
611
|
+
|
|
612
|
+
if value > maximum:
|
|
613
|
+
raise ValueError(f"{name} must be at most {maximum}")
|
|
614
|
+
|
|
615
|
+
return float(value)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def validate_enum(
|
|
619
|
+
value: Any,
|
|
620
|
+
name: str,
|
|
621
|
+
enum_class: type[Enum],
|
|
622
|
+
required: bool = True,
|
|
623
|
+
default: Optional[Enum] = None,
|
|
624
|
+
) -> Optional[Enum]:
|
|
625
|
+
"""Validate an enum parameter."""
|
|
626
|
+
if value is None:
|
|
627
|
+
if required and default is None:
|
|
628
|
+
raise ValueError(f"{name} is required")
|
|
629
|
+
return default
|
|
630
|
+
|
|
631
|
+
if not isinstance(value, str):
|
|
632
|
+
raise ValueError(f"{name} must be a string")
|
|
633
|
+
|
|
634
|
+
try:
|
|
635
|
+
return enum_class(value)
|
|
636
|
+
except ValueError:
|
|
637
|
+
valid_values = [e.value for e in enum_class]
|
|
638
|
+
raise ValueError(f"{name} must be one of: {valid_values}")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def validate_list(
|
|
642
|
+
value: Any,
|
|
643
|
+
name: str,
|
|
644
|
+
item_type: type = str,
|
|
645
|
+
min_items: int = 0,
|
|
646
|
+
max_items: int = 100,
|
|
647
|
+
required: bool = False,
|
|
648
|
+
) -> Optional[list]:
|
|
649
|
+
"""Validate a list parameter."""
|
|
650
|
+
if value is None:
|
|
651
|
+
if required:
|
|
652
|
+
raise ValueError(f"{name} is required")
|
|
653
|
+
return None
|
|
654
|
+
|
|
655
|
+
if not isinstance(value, list):
|
|
656
|
+
raise ValueError(f"{name} must be a list")
|
|
657
|
+
|
|
658
|
+
if len(value) < min_items:
|
|
659
|
+
raise ValueError(f"{name} must have at least {min_items} items")
|
|
660
|
+
|
|
661
|
+
if len(value) > max_items:
|
|
662
|
+
raise ValueError(f"{name} must have at most {max_items} items")
|
|
663
|
+
|
|
664
|
+
for i, item in enumerate(value):
|
|
665
|
+
if not isinstance(item, item_type):
|
|
666
|
+
raise ValueError(f"{name}[{i}] must be a {item_type.__name__}")
|
|
667
|
+
|
|
668
|
+
return value
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# =============================================================================
|
|
672
|
+
# MCP Server
|
|
673
|
+
# =============================================================================
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class MCPServer:
|
|
677
|
+
"""MCP Server for Runtime Memory.
|
|
678
|
+
|
|
679
|
+
Exposes memory operations as MCP tools via JSON-RPC over stdio.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def __init__(
|
|
683
|
+
self,
|
|
684
|
+
engine: Optional[MemoryEngine] = None,
|
|
685
|
+
rate_limit: int = 100,
|
|
686
|
+
rate_window: float = 60.0,
|
|
687
|
+
):
|
|
688
|
+
"""Initialize the MCP server.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
engine: MemoryEngine instance (created if not provided)
|
|
692
|
+
rate_limit: Maximum requests per window
|
|
693
|
+
rate_window: Rate limit window in seconds
|
|
694
|
+
"""
|
|
695
|
+
self.engine = engine
|
|
696
|
+
self._rate_limiter = RateLimiter(
|
|
697
|
+
max_requests=rate_limit,
|
|
698
|
+
window_seconds=rate_window,
|
|
699
|
+
)
|
|
700
|
+
self._running = False
|
|
701
|
+
self._handlers: dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
|
702
|
+
"initialize": self._handle_initialize,
|
|
703
|
+
"tools/list": self._handle_list_tools,
|
|
704
|
+
"tools/call": self._handle_call_tool,
|
|
705
|
+
"notifications/initialized": self._handle_initialized,
|
|
706
|
+
}
|
|
707
|
+
self._tool_handlers: dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
|
708
|
+
"search_memories": self._handle_search_memories,
|
|
709
|
+
"add_memory": self._handle_add_memory,
|
|
710
|
+
"record_outcome": self._handle_record_outcome,
|
|
711
|
+
"get_context": self._handle_get_context,
|
|
712
|
+
"update_memory": self._handle_update_memory,
|
|
713
|
+
"delete_memory": self._handle_delete_memory,
|
|
714
|
+
"list_memories": self._handle_list_memories,
|
|
715
|
+
"get_stats": self._handle_get_stats,
|
|
716
|
+
# Beads integration
|
|
717
|
+
"beads_sync": self._handle_beads_sync,
|
|
718
|
+
"beads_context": self._handle_beads_context,
|
|
719
|
+
"beads_link": self._handle_beads_link,
|
|
720
|
+
"beads_tasks": self._handle_beads_tasks,
|
|
721
|
+
# Unified Tasks integration (Phase 7)
|
|
722
|
+
"tasks_list": self._handle_tasks_list,
|
|
723
|
+
"tasks_sync": self._handle_tasks_sync,
|
|
724
|
+
"tasks_context": self._handle_tasks_context,
|
|
725
|
+
"tasks_stats": self._handle_tasks_stats,
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async def _ensure_engine(self) -> MemoryEngine:
|
|
729
|
+
"""Get or create (and initialize) the engine."""
|
|
730
|
+
if self.engine is None:
|
|
731
|
+
import os
|
|
732
|
+
from pathlib import Path
|
|
733
|
+
|
|
734
|
+
from runtime_memory.core.engine import EngineConfig
|
|
735
|
+
|
|
736
|
+
db_path = os.environ.get(
|
|
737
|
+
"RUNTIME_MEMORY_DB",
|
|
738
|
+
str(default_db_path()),
|
|
739
|
+
)
|
|
740
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
741
|
+
self.engine = MemoryEngine(config=EngineConfig(db_path=db_path))
|
|
742
|
+
await self.engine.initialize()
|
|
743
|
+
return self.engine
|
|
744
|
+
|
|
745
|
+
async def handle_request(self, request: MCPRequest) -> MCPResponse:
|
|
746
|
+
"""Handle an MCP request.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
request: The MCP request to handle
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
MCP response
|
|
753
|
+
"""
|
|
754
|
+
# Check rate limit
|
|
755
|
+
if not self._rate_limiter.is_allowed():
|
|
756
|
+
return MCPResponse(
|
|
757
|
+
id=request.id,
|
|
758
|
+
error=MCPError(
|
|
759
|
+
code=MCPErrorCode.RATE_LIMITED,
|
|
760
|
+
message="Rate limit exceeded",
|
|
761
|
+
),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# Find handler
|
|
765
|
+
handler = self._handlers.get(request.method)
|
|
766
|
+
if handler is None:
|
|
767
|
+
return MCPResponse(
|
|
768
|
+
id=request.id,
|
|
769
|
+
error=MCPError(
|
|
770
|
+
code=MCPErrorCode.METHOD_NOT_FOUND,
|
|
771
|
+
message=f"Method not found: {request.method}",
|
|
772
|
+
),
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
try:
|
|
776
|
+
result = await handler(request.params or {})
|
|
777
|
+
return MCPResponse(id=request.id, result=result)
|
|
778
|
+
except ValueError as e:
|
|
779
|
+
return MCPResponse(
|
|
780
|
+
id=request.id,
|
|
781
|
+
error=MCPError(
|
|
782
|
+
code=MCPErrorCode.VALIDATION_ERROR,
|
|
783
|
+
message=str(e),
|
|
784
|
+
),
|
|
785
|
+
)
|
|
786
|
+
except Exception as e:
|
|
787
|
+
logger.error(f"Error handling request: {e}")
|
|
788
|
+
return MCPResponse(
|
|
789
|
+
id=request.id,
|
|
790
|
+
error=MCPError(
|
|
791
|
+
code=MCPErrorCode.INTERNAL_ERROR,
|
|
792
|
+
message=str(e),
|
|
793
|
+
),
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# -------------------------------------------------------------------------
|
|
797
|
+
# Protocol Handlers
|
|
798
|
+
# -------------------------------------------------------------------------
|
|
799
|
+
|
|
800
|
+
async def _handle_initialize(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
801
|
+
"""Handle initialize request."""
|
|
802
|
+
return {
|
|
803
|
+
"protocolVersion": "2024-11-05",
|
|
804
|
+
"capabilities": {
|
|
805
|
+
"tools": {},
|
|
806
|
+
},
|
|
807
|
+
"serverInfo": {
|
|
808
|
+
"name": "runtime-memory",
|
|
809
|
+
"version": __version__,
|
|
810
|
+
},
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async def _handle_initialized(self, params: dict[str, Any]) -> None:
|
|
814
|
+
"""Handle initialized notification."""
|
|
815
|
+
logger.info("MCP client initialized")
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
async def _handle_list_tools(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
819
|
+
"""Handle tools/list request."""
|
|
820
|
+
return {
|
|
821
|
+
"tools": [schema.to_dict() for schema in TOOL_SCHEMAS],
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async def _handle_call_tool(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
825
|
+
"""Handle tools/call request."""
|
|
826
|
+
tool_name = params.get("name")
|
|
827
|
+
if not tool_name:
|
|
828
|
+
raise ValueError("Tool name is required")
|
|
829
|
+
|
|
830
|
+
tool_handler = self._tool_handlers.get(tool_name)
|
|
831
|
+
if tool_handler is None:
|
|
832
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
|
833
|
+
|
|
834
|
+
arguments = params.get("arguments", {})
|
|
835
|
+
result = await tool_handler(arguments)
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
"content": [
|
|
839
|
+
{
|
|
840
|
+
"type": "text",
|
|
841
|
+
"text": json.dumps(result, indent=2, default=str),
|
|
842
|
+
}
|
|
843
|
+
],
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
# -------------------------------------------------------------------------
|
|
847
|
+
# Tool Handlers
|
|
848
|
+
# -------------------------------------------------------------------------
|
|
849
|
+
|
|
850
|
+
async def _handle_search_memories(
|
|
851
|
+
self, args: dict[str, Any]
|
|
852
|
+
) -> dict[str, Any]:
|
|
853
|
+
"""Handle search_memories tool."""
|
|
854
|
+
engine = await self._ensure_engine()
|
|
855
|
+
|
|
856
|
+
# Validate inputs
|
|
857
|
+
query = validate_string(args.get("query"), "query", min_length=1)
|
|
858
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 100, default=10)
|
|
859
|
+
min_score = validate_float(
|
|
860
|
+
args.get("min_score"), "min_score", -1.0, 1.0, default=-1.0
|
|
861
|
+
)
|
|
862
|
+
project = validate_string(
|
|
863
|
+
args.get("project"), "project", required=False
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
# Parse categories
|
|
867
|
+
categories = None
|
|
868
|
+
if args.get("categories"):
|
|
869
|
+
cat_list = validate_list(
|
|
870
|
+
args.get("categories"), "categories", str, max_items=16
|
|
871
|
+
)
|
|
872
|
+
if cat_list:
|
|
873
|
+
categories = [MemoryCategory(c) for c in cat_list]
|
|
874
|
+
|
|
875
|
+
# Execute search - engine only supports single category
|
|
876
|
+
category = categories[0] if categories else None
|
|
877
|
+
results = await engine.search(
|
|
878
|
+
query=query,
|
|
879
|
+
limit=limit,
|
|
880
|
+
category=category,
|
|
881
|
+
project=project,
|
|
882
|
+
min_score=min_score,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
"count": len(results),
|
|
887
|
+
"results": [
|
|
888
|
+
{
|
|
889
|
+
"id": r.memory.id,
|
|
890
|
+
"content": r.memory.content,
|
|
891
|
+
"category": r.memory.category.value,
|
|
892
|
+
"score": r.score,
|
|
893
|
+
"outcome_score": r.memory.outcome_score,
|
|
894
|
+
"use_count": r.memory.use_count,
|
|
895
|
+
}
|
|
896
|
+
for r in results
|
|
897
|
+
],
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async def _handle_add_memory(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
901
|
+
"""Handle add_memory tool."""
|
|
902
|
+
engine = await self._ensure_engine()
|
|
903
|
+
|
|
904
|
+
# Validate inputs
|
|
905
|
+
content = validate_string(args.get("content"), "content", min_length=1)
|
|
906
|
+
category = validate_enum(
|
|
907
|
+
args.get("category"),
|
|
908
|
+
"category",
|
|
909
|
+
MemoryCategory,
|
|
910
|
+
required=False,
|
|
911
|
+
default=MemoryCategory.GENERAL,
|
|
912
|
+
)
|
|
913
|
+
project = validate_string(args.get("project"), "project", required=False)
|
|
914
|
+
tags = validate_list(args.get("tags"), "tags", str, max_items=20) or []
|
|
915
|
+
importance = validate_float(
|
|
916
|
+
args.get("importance"), "importance", 0.0, 1.0, default=0.5
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
# Add memory
|
|
920
|
+
memory = await engine.add(
|
|
921
|
+
content=content,
|
|
922
|
+
category=category,
|
|
923
|
+
project=project,
|
|
924
|
+
tags=tags,
|
|
925
|
+
importance=importance,
|
|
926
|
+
source=MemorySource.EXPLICIT,
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
"id": memory.id,
|
|
931
|
+
"content": memory.content,
|
|
932
|
+
"category": memory.category.value,
|
|
933
|
+
"created_at": memory.created_at.isoformat(),
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async def _handle_record_outcome(
|
|
937
|
+
self, args: dict[str, Any]
|
|
938
|
+
) -> dict[str, Any]:
|
|
939
|
+
"""Handle record_outcome tool."""
|
|
940
|
+
engine = await self._ensure_engine()
|
|
941
|
+
|
|
942
|
+
# Validate inputs
|
|
943
|
+
memory_ids = validate_list(
|
|
944
|
+
args.get("memory_ids"), "memory_ids", str, min_items=1, max_items=50
|
|
945
|
+
)
|
|
946
|
+
outcome = validate_enum(args.get("outcome"), "outcome", Outcome)
|
|
947
|
+
|
|
948
|
+
# Record outcome
|
|
949
|
+
success = await engine.record_outcome(
|
|
950
|
+
memory_ids=memory_ids,
|
|
951
|
+
outcome=outcome,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
adjustment = {
|
|
955
|
+
Outcome.WORKED: "+0.2",
|
|
956
|
+
Outcome.FAILED: "-0.3",
|
|
957
|
+
Outcome.PARTIAL: "+0.05",
|
|
958
|
+
}[outcome]
|
|
959
|
+
|
|
960
|
+
return {
|
|
961
|
+
"success": success,
|
|
962
|
+
"memory_ids": memory_ids,
|
|
963
|
+
"outcome": outcome.value,
|
|
964
|
+
"adjustment": adjustment,
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async def _handle_get_context(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
968
|
+
"""Handle get_context tool."""
|
|
969
|
+
engine = await self._ensure_engine()
|
|
970
|
+
|
|
971
|
+
# Validate inputs
|
|
972
|
+
project = validate_string(args.get("project"), "project", required=False)
|
|
973
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 50, default=10)
|
|
974
|
+
format_style = args.get("format", "markdown")
|
|
975
|
+
|
|
976
|
+
# Get context
|
|
977
|
+
context = await engine.get_context(project=project, max_memories=limit)
|
|
978
|
+
|
|
979
|
+
# Format output
|
|
980
|
+
from runtime_memory.plugin import ContextFormatter
|
|
981
|
+
|
|
982
|
+
formatted = ContextFormatter.format_for_injection(
|
|
983
|
+
context.memories,
|
|
984
|
+
style=format_style if format_style in ["brief", "detailed", "markdown"] else "markdown",
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
"project": project,
|
|
989
|
+
"total_count": context.total_count,
|
|
990
|
+
"included_count": context.included_count,
|
|
991
|
+
"formatted": formatted,
|
|
992
|
+
"memories": [
|
|
993
|
+
{
|
|
994
|
+
"id": m.id,
|
|
995
|
+
"content": m.content,
|
|
996
|
+
"category": m.category.value,
|
|
997
|
+
"outcome_score": m.outcome_score,
|
|
998
|
+
}
|
|
999
|
+
for m in context.memories
|
|
1000
|
+
],
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async def _handle_update_memory(
|
|
1004
|
+
self, args: dict[str, Any]
|
|
1005
|
+
) -> dict[str, Any]:
|
|
1006
|
+
"""Handle update_memory tool."""
|
|
1007
|
+
engine = await self._ensure_engine()
|
|
1008
|
+
|
|
1009
|
+
# Validate inputs
|
|
1010
|
+
memory_id = validate_string(args.get("id"), "id", min_length=1)
|
|
1011
|
+
content = validate_string(
|
|
1012
|
+
args.get("content"), "content", required=False
|
|
1013
|
+
)
|
|
1014
|
+
category = validate_enum(
|
|
1015
|
+
args.get("category"),
|
|
1016
|
+
"category",
|
|
1017
|
+
MemoryCategory,
|
|
1018
|
+
required=False,
|
|
1019
|
+
)
|
|
1020
|
+
tags = validate_list(args.get("tags"), "tags", str, max_items=20)
|
|
1021
|
+
|
|
1022
|
+
# Check memory exists
|
|
1023
|
+
existing = await engine.get(memory_id)
|
|
1024
|
+
if existing is None:
|
|
1025
|
+
raise ValueError(f"Memory not found: {memory_id}")
|
|
1026
|
+
|
|
1027
|
+
# Update memory with provided fields
|
|
1028
|
+
updated = await engine.update(
|
|
1029
|
+
memory_id=memory_id,
|
|
1030
|
+
content=content,
|
|
1031
|
+
category=category,
|
|
1032
|
+
tags=tags,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
"id": updated.id,
|
|
1037
|
+
"content": updated.content,
|
|
1038
|
+
"category": updated.category.value,
|
|
1039
|
+
"updated_at": updated.updated_at.isoformat(),
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async def _handle_delete_memory(
|
|
1043
|
+
self, args: dict[str, Any]
|
|
1044
|
+
) -> dict[str, Any]:
|
|
1045
|
+
"""Handle delete_memory tool."""
|
|
1046
|
+
from runtime_memory.core.engine import MemoryNotFoundError
|
|
1047
|
+
|
|
1048
|
+
engine = await self._ensure_engine()
|
|
1049
|
+
|
|
1050
|
+
# Validate inputs
|
|
1051
|
+
memory_id = validate_string(args.get("id"), "id", min_length=1)
|
|
1052
|
+
|
|
1053
|
+
# Delete (archive) memory
|
|
1054
|
+
try:
|
|
1055
|
+
await engine.delete(memory_id)
|
|
1056
|
+
except MemoryNotFoundError:
|
|
1057
|
+
raise ValueError(f"Memory not found: {memory_id}")
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
"success": True,
|
|
1061
|
+
"id": memory_id,
|
|
1062
|
+
"message": "Memory archived",
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async def _handle_list_memories(
|
|
1066
|
+
self, args: dict[str, Any]
|
|
1067
|
+
) -> dict[str, Any]:
|
|
1068
|
+
"""Handle list_memories tool."""
|
|
1069
|
+
engine = await self._ensure_engine()
|
|
1070
|
+
|
|
1071
|
+
# Validate inputs
|
|
1072
|
+
category = validate_enum(
|
|
1073
|
+
args.get("category"),
|
|
1074
|
+
"category",
|
|
1075
|
+
MemoryCategory,
|
|
1076
|
+
required=False,
|
|
1077
|
+
)
|
|
1078
|
+
project = validate_string(args.get("project"), "project", required=False)
|
|
1079
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 100, default=20)
|
|
1080
|
+
include_archived = args.get("include_archived", False)
|
|
1081
|
+
|
|
1082
|
+
# List memories
|
|
1083
|
+
memories = await engine.list(
|
|
1084
|
+
category=category,
|
|
1085
|
+
project=project,
|
|
1086
|
+
limit=limit,
|
|
1087
|
+
include_archived=include_archived,
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
"count": len(memories),
|
|
1092
|
+
"memories": [
|
|
1093
|
+
{
|
|
1094
|
+
"id": m.id,
|
|
1095
|
+
"content": m.content[:100] + "..." if len(m.content) > 100 else m.content,
|
|
1096
|
+
"category": m.category.value,
|
|
1097
|
+
"outcome_score": m.outcome_score,
|
|
1098
|
+
"use_count": m.use_count,
|
|
1099
|
+
"archived": m.archived,
|
|
1100
|
+
}
|
|
1101
|
+
for m in memories
|
|
1102
|
+
],
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async def _handle_get_stats(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1106
|
+
"""Handle get_stats tool."""
|
|
1107
|
+
engine = await self._ensure_engine()
|
|
1108
|
+
|
|
1109
|
+
# Validate inputs
|
|
1110
|
+
project = validate_string(args.get("project"), "project", required=False)
|
|
1111
|
+
|
|
1112
|
+
# Get stats
|
|
1113
|
+
stats = await engine.stats(project=project)
|
|
1114
|
+
|
|
1115
|
+
return stats
|
|
1116
|
+
|
|
1117
|
+
# -------------------------------------------------------------------------
|
|
1118
|
+
# Beads Integration Handlers
|
|
1119
|
+
# -------------------------------------------------------------------------
|
|
1120
|
+
|
|
1121
|
+
async def _get_beads_adapter(self):
|
|
1122
|
+
"""Get or create the Beads adapter."""
|
|
1123
|
+
if not hasattr(self, "_beads_adapter"):
|
|
1124
|
+
from runtime_memory.tasks import BeadsAdapter
|
|
1125
|
+
engine = await self._ensure_engine()
|
|
1126
|
+
self._beads_adapter = BeadsAdapter(engine)
|
|
1127
|
+
return self._beads_adapter
|
|
1128
|
+
|
|
1129
|
+
async def _ensure_beads_initialized(self):
|
|
1130
|
+
"""Ensure Beads adapter is initialized."""
|
|
1131
|
+
adapter = await self._get_beads_adapter()
|
|
1132
|
+
if not adapter._initialized:
|
|
1133
|
+
await adapter.initialize()
|
|
1134
|
+
return adapter
|
|
1135
|
+
|
|
1136
|
+
async def _handle_beads_sync(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1137
|
+
"""Handle beads_sync tool."""
|
|
1138
|
+
adapter = await self._ensure_beads_initialized()
|
|
1139
|
+
|
|
1140
|
+
if not adapter.is_available:
|
|
1141
|
+
return {
|
|
1142
|
+
"success": False,
|
|
1143
|
+
"error": "Beads not available (no .beads/ directory found)",
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
task_id = validate_string(args.get("task_id"), "task_id", required=False)
|
|
1147
|
+
|
|
1148
|
+
if task_id:
|
|
1149
|
+
# Sync specific task
|
|
1150
|
+
from runtime_memory.tasks import BeadsTaskStatus
|
|
1151
|
+
task = adapter.get_task(task_id)
|
|
1152
|
+
if not task:
|
|
1153
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
1154
|
+
|
|
1155
|
+
if task.status == BeadsTaskStatus.DONE:
|
|
1156
|
+
count = await adapter.on_task_done(task_id)
|
|
1157
|
+
elif task.status == BeadsTaskStatus.CANCELLED:
|
|
1158
|
+
count = await adapter.on_task_cancelled(task_id)
|
|
1159
|
+
elif task.status == BeadsTaskStatus.BLOCKED:
|
|
1160
|
+
count = await adapter.on_task_blocked(task_id)
|
|
1161
|
+
else:
|
|
1162
|
+
count = 0
|
|
1163
|
+
|
|
1164
|
+
return {
|
|
1165
|
+
"success": True,
|
|
1166
|
+
"task_id": task_id,
|
|
1167
|
+
"task_status": task.status.value,
|
|
1168
|
+
"outcomes_recorded": count,
|
|
1169
|
+
}
|
|
1170
|
+
else:
|
|
1171
|
+
# Sync all completed tasks
|
|
1172
|
+
result = await adapter.sync()
|
|
1173
|
+
return {
|
|
1174
|
+
"success": result.success,
|
|
1175
|
+
"tasks_found": result.tasks_found,
|
|
1176
|
+
"tasks_synced": result.tasks_synced,
|
|
1177
|
+
"outcomes_recorded": result.outcomes_recorded,
|
|
1178
|
+
"errors": result.errors,
|
|
1179
|
+
"warnings": result.warnings,
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async def _handle_beads_context(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1183
|
+
"""Handle beads_context tool."""
|
|
1184
|
+
adapter = await self._ensure_beads_initialized()
|
|
1185
|
+
|
|
1186
|
+
if not adapter.is_available:
|
|
1187
|
+
return {
|
|
1188
|
+
"success": False,
|
|
1189
|
+
"error": "Beads not available (no .beads/ directory found)",
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
task_id = validate_string(args.get("task_id"), "task_id", required=False)
|
|
1193
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 50, default=10)
|
|
1194
|
+
|
|
1195
|
+
context = await adapter.get_unified_context(task_id, limit)
|
|
1196
|
+
|
|
1197
|
+
if not context:
|
|
1198
|
+
return {
|
|
1199
|
+
"success": False,
|
|
1200
|
+
"error": "No task found",
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return {
|
|
1204
|
+
"success": True,
|
|
1205
|
+
"task_id": context.task.id,
|
|
1206
|
+
"task_title": context.task.title,
|
|
1207
|
+
"task_status": context.task.status.value,
|
|
1208
|
+
"task_description": context.task.description,
|
|
1209
|
+
"memories_count": len(context.memories),
|
|
1210
|
+
"formatted": context.formatted,
|
|
1211
|
+
"memories": [
|
|
1212
|
+
{
|
|
1213
|
+
"id": getattr(m, "id", ""),
|
|
1214
|
+
"content": getattr(m, "content", str(m))[:200],
|
|
1215
|
+
"category": getattr(getattr(m, "category", None), "value", "unknown"),
|
|
1216
|
+
}
|
|
1217
|
+
for m in context.memories
|
|
1218
|
+
],
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async def _handle_beads_link(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1222
|
+
"""Handle beads_link tool."""
|
|
1223
|
+
adapter = await self._ensure_beads_initialized()
|
|
1224
|
+
|
|
1225
|
+
if not adapter.is_available:
|
|
1226
|
+
return {
|
|
1227
|
+
"success": False,
|
|
1228
|
+
"error": "Beads not available (no .beads/ directory found)",
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
memory_id = validate_string(args.get("memory_id"), "memory_id", min_length=1)
|
|
1232
|
+
task_id = validate_string(args.get("task_id"), "task_id", required=False)
|
|
1233
|
+
context = validate_string(args.get("context"), "context", required=False)
|
|
1234
|
+
|
|
1235
|
+
# Get task ID
|
|
1236
|
+
if task_id:
|
|
1237
|
+
task = adapter.get_task(task_id)
|
|
1238
|
+
if not task:
|
|
1239
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
1240
|
+
else:
|
|
1241
|
+
task = adapter.get_current_task()
|
|
1242
|
+
if not task:
|
|
1243
|
+
return {"success": False, "error": "No current task found"}
|
|
1244
|
+
task_id = task.id
|
|
1245
|
+
|
|
1246
|
+
# Verify memory exists
|
|
1247
|
+
engine = await self._ensure_engine()
|
|
1248
|
+
try:
|
|
1249
|
+
await engine.get(memory_id)
|
|
1250
|
+
except Exception:
|
|
1251
|
+
return {"success": False, "error": f"Memory {memory_id} not found"}
|
|
1252
|
+
|
|
1253
|
+
# Create link
|
|
1254
|
+
await adapter.link_memory_to_task(task_id, memory_id, context)
|
|
1255
|
+
|
|
1256
|
+
return {
|
|
1257
|
+
"success": True,
|
|
1258
|
+
"memory_id": memory_id,
|
|
1259
|
+
"task_id": task_id,
|
|
1260
|
+
"context": context,
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async def _handle_beads_tasks(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1264
|
+
"""Handle beads_tasks tool."""
|
|
1265
|
+
adapter = await self._ensure_beads_initialized()
|
|
1266
|
+
|
|
1267
|
+
if not adapter.is_available:
|
|
1268
|
+
return {
|
|
1269
|
+
"success": False,
|
|
1270
|
+
"error": "Beads not available (no .beads/ directory found)",
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
status_str = validate_string(args.get("status"), "status", required=False)
|
|
1274
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 100, default=20)
|
|
1275
|
+
|
|
1276
|
+
# Parse status filter
|
|
1277
|
+
status = None
|
|
1278
|
+
if status_str:
|
|
1279
|
+
from runtime_memory.tasks import BeadsTaskStatus
|
|
1280
|
+
try:
|
|
1281
|
+
status = BeadsTaskStatus(status_str)
|
|
1282
|
+
except ValueError:
|
|
1283
|
+
return {"success": False, "error": f"Invalid status: {status_str}"}
|
|
1284
|
+
|
|
1285
|
+
tasks = adapter.list_tasks(status=status)[:limit]
|
|
1286
|
+
|
|
1287
|
+
return {
|
|
1288
|
+
"success": True,
|
|
1289
|
+
"count": len(tasks),
|
|
1290
|
+
"tasks": [
|
|
1291
|
+
{
|
|
1292
|
+
"id": t.id,
|
|
1293
|
+
"title": t.title,
|
|
1294
|
+
"status": t.status.value,
|
|
1295
|
+
"description": t.description[:100] if t.description else "",
|
|
1296
|
+
"is_ready": t.is_ready,
|
|
1297
|
+
"is_completed": t.is_completed,
|
|
1298
|
+
}
|
|
1299
|
+
for t in tasks
|
|
1300
|
+
],
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
# -------------------------------------------------------------------------
|
|
1304
|
+
# Unified Tasks Handlers (Phase 7 - Claude Code Tasks Adapter)
|
|
1305
|
+
# -------------------------------------------------------------------------
|
|
1306
|
+
|
|
1307
|
+
async def _get_unified_adapter(self):
|
|
1308
|
+
"""Get or create the unified task adapter."""
|
|
1309
|
+
if not hasattr(self, "_unified_adapter"):
|
|
1310
|
+
from runtime_memory.tasks import UnifiedTaskAdapter
|
|
1311
|
+
engine = await self._ensure_engine()
|
|
1312
|
+
self._unified_adapter = UnifiedTaskAdapter(engine)
|
|
1313
|
+
return self._unified_adapter
|
|
1314
|
+
|
|
1315
|
+
async def _ensure_unified_initialized(self):
|
|
1316
|
+
"""Ensure unified adapter is initialized."""
|
|
1317
|
+
adapter = await self._get_unified_adapter()
|
|
1318
|
+
if not adapter._initialized:
|
|
1319
|
+
await adapter.initialize()
|
|
1320
|
+
return adapter
|
|
1321
|
+
|
|
1322
|
+
async def _handle_tasks_list(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1323
|
+
"""Handle tasks_list tool."""
|
|
1324
|
+
adapter = await self._ensure_unified_initialized()
|
|
1325
|
+
|
|
1326
|
+
source_str = validate_string(args.get("source"), "source", required=False)
|
|
1327
|
+
status_str = validate_string(args.get("status"), "status", required=False)
|
|
1328
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 200, default=50)
|
|
1329
|
+
|
|
1330
|
+
# Parse source filter
|
|
1331
|
+
source = None
|
|
1332
|
+
if source_str:
|
|
1333
|
+
from runtime_memory.tasks import TaskSource
|
|
1334
|
+
try:
|
|
1335
|
+
source = TaskSource(source_str)
|
|
1336
|
+
except ValueError:
|
|
1337
|
+
return {"success": False, "error": f"Invalid source: {source_str}"}
|
|
1338
|
+
|
|
1339
|
+
tasks = adapter.list_tasks(source=source, status=status_str)[:limit]
|
|
1340
|
+
|
|
1341
|
+
return {
|
|
1342
|
+
"success": True,
|
|
1343
|
+
"count": len(tasks),
|
|
1344
|
+
"sources": [s.value for s in adapter.available_sources],
|
|
1345
|
+
"tasks": [
|
|
1346
|
+
{
|
|
1347
|
+
"id": t.id,
|
|
1348
|
+
"title": t.title,
|
|
1349
|
+
"status": t.status,
|
|
1350
|
+
"description": t.description[:100] if t.description else "",
|
|
1351
|
+
"source": t.source.value,
|
|
1352
|
+
"is_ready": t.is_ready,
|
|
1353
|
+
"is_completed": t.is_completed,
|
|
1354
|
+
}
|
|
1355
|
+
for t in tasks
|
|
1356
|
+
],
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
async def _handle_tasks_sync(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1360
|
+
"""Handle tasks_sync tool."""
|
|
1361
|
+
adapter = await self._ensure_unified_initialized()
|
|
1362
|
+
|
|
1363
|
+
source_str = validate_string(args.get("source"), "source", required=False)
|
|
1364
|
+
task_id = validate_string(args.get("task_id"), "task_id", required=False)
|
|
1365
|
+
|
|
1366
|
+
if task_id:
|
|
1367
|
+
# Sync specific task
|
|
1368
|
+
count = await adapter.on_task_completed(task_id)
|
|
1369
|
+
return {
|
|
1370
|
+
"success": True,
|
|
1371
|
+
"task_id": task_id,
|
|
1372
|
+
"outcomes_recorded": count,
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
# Parse source filter
|
|
1376
|
+
source = None
|
|
1377
|
+
if source_str:
|
|
1378
|
+
from runtime_memory.tasks import TaskSource
|
|
1379
|
+
try:
|
|
1380
|
+
source = TaskSource(source_str)
|
|
1381
|
+
except ValueError:
|
|
1382
|
+
return {"success": False, "error": f"Invalid source: {source_str}"}
|
|
1383
|
+
|
|
1384
|
+
result = await adapter.sync(source=source)
|
|
1385
|
+
|
|
1386
|
+
if hasattr(result, 'results'):
|
|
1387
|
+
# UnifiedSyncResult
|
|
1388
|
+
return {
|
|
1389
|
+
"success": result.success,
|
|
1390
|
+
"total_tasks_found": result.total_tasks_found,
|
|
1391
|
+
"total_tasks_synced": result.total_tasks_synced,
|
|
1392
|
+
"total_outcomes_recorded": result.total_outcomes_recorded,
|
|
1393
|
+
"results": {k.value: v.to_dict() for k, v in result.results.items()},
|
|
1394
|
+
"errors": result.errors,
|
|
1395
|
+
}
|
|
1396
|
+
else:
|
|
1397
|
+
# Single TaskSyncResult
|
|
1398
|
+
return {
|
|
1399
|
+
"success": result.success,
|
|
1400
|
+
"source": result.source.value,
|
|
1401
|
+
"tasks_found": result.tasks_found,
|
|
1402
|
+
"tasks_synced": result.tasks_synced,
|
|
1403
|
+
"outcomes_recorded": result.outcomes_recorded,
|
|
1404
|
+
"errors": result.errors,
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
async def _handle_tasks_context(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1408
|
+
"""Handle tasks_context tool."""
|
|
1409
|
+
adapter = await self._ensure_unified_initialized()
|
|
1410
|
+
|
|
1411
|
+
task_id = validate_string(args.get("task_id"), "task_id", required=False)
|
|
1412
|
+
source_str = validate_string(args.get("source"), "source", required=False)
|
|
1413
|
+
limit = validate_integer(args.get("limit"), "limit", 1, 50, default=10)
|
|
1414
|
+
|
|
1415
|
+
# Parse source filter
|
|
1416
|
+
source = None
|
|
1417
|
+
if source_str:
|
|
1418
|
+
from runtime_memory.tasks import TaskSource
|
|
1419
|
+
try:
|
|
1420
|
+
source = TaskSource(source_str)
|
|
1421
|
+
except ValueError:
|
|
1422
|
+
return {"success": False, "error": f"Invalid source: {source_str}"}
|
|
1423
|
+
|
|
1424
|
+
context = await adapter.get_unified_context(task_id, source, limit)
|
|
1425
|
+
|
|
1426
|
+
if not context:
|
|
1427
|
+
return {
|
|
1428
|
+
"success": False,
|
|
1429
|
+
"error": "No task found",
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return {
|
|
1433
|
+
"success": True,
|
|
1434
|
+
"task_id": context.task.id,
|
|
1435
|
+
"task_title": context.task.title,
|
|
1436
|
+
"task_status": context.task.status.value,
|
|
1437
|
+
"source": context.source.value,
|
|
1438
|
+
"memories_count": len(context.memories),
|
|
1439
|
+
"formatted": context.formatted,
|
|
1440
|
+
"memories": [
|
|
1441
|
+
{
|
|
1442
|
+
"id": getattr(m, "id", ""),
|
|
1443
|
+
"content": getattr(m, "content", str(m))[:200],
|
|
1444
|
+
"category": getattr(getattr(m, "category", None), "value", "unknown"),
|
|
1445
|
+
}
|
|
1446
|
+
for m in context.memories
|
|
1447
|
+
],
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async def _handle_tasks_stats(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
1451
|
+
"""Handle tasks_stats tool."""
|
|
1452
|
+
adapter = await self._ensure_unified_initialized()
|
|
1453
|
+
|
|
1454
|
+
stats = await adapter.get_stats()
|
|
1455
|
+
|
|
1456
|
+
return {
|
|
1457
|
+
"success": True,
|
|
1458
|
+
"available_sources": stats.get("available_sources", []),
|
|
1459
|
+
"beads": stats.get("beads", {}),
|
|
1460
|
+
"claude_code": stats.get("claude_code", {}),
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
# -------------------------------------------------------------------------
|
|
1464
|
+
# Transport
|
|
1465
|
+
# -------------------------------------------------------------------------
|
|
1466
|
+
|
|
1467
|
+
async def run_stdio(self) -> None:
|
|
1468
|
+
"""Run the server using stdio transport."""
|
|
1469
|
+
self._running = True
|
|
1470
|
+
logger.info("MCP server starting on stdio")
|
|
1471
|
+
|
|
1472
|
+
reader = asyncio.StreamReader()
|
|
1473
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
1474
|
+
|
|
1475
|
+
loop = asyncio.get_event_loop()
|
|
1476
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
1477
|
+
|
|
1478
|
+
writer_transport, writer_protocol = await loop.connect_write_pipe(
|
|
1479
|
+
asyncio.streams.FlowControlMixin, sys.stdout
|
|
1480
|
+
)
|
|
1481
|
+
writer = asyncio.StreamWriter(
|
|
1482
|
+
writer_transport, writer_protocol, reader, loop
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
try:
|
|
1486
|
+
while self._running:
|
|
1487
|
+
try:
|
|
1488
|
+
line = await reader.readline()
|
|
1489
|
+
if not line:
|
|
1490
|
+
break
|
|
1491
|
+
|
|
1492
|
+
line_str = line.decode("utf-8").strip()
|
|
1493
|
+
if not line_str:
|
|
1494
|
+
continue
|
|
1495
|
+
|
|
1496
|
+
# Parse request
|
|
1497
|
+
try:
|
|
1498
|
+
data = json.loads(line_str)
|
|
1499
|
+
request = MCPRequest.from_dict(data)
|
|
1500
|
+
except json.JSONDecodeError as e:
|
|
1501
|
+
response = MCPResponse(
|
|
1502
|
+
error=MCPError(
|
|
1503
|
+
code=MCPErrorCode.PARSE_ERROR,
|
|
1504
|
+
message=f"Parse error: {e}",
|
|
1505
|
+
)
|
|
1506
|
+
)
|
|
1507
|
+
await self._write_response(writer, response)
|
|
1508
|
+
continue
|
|
1509
|
+
except KeyError as e:
|
|
1510
|
+
response = MCPResponse(
|
|
1511
|
+
error=MCPError(
|
|
1512
|
+
code=MCPErrorCode.INVALID_REQUEST,
|
|
1513
|
+
message=f"Invalid request: missing {e}",
|
|
1514
|
+
)
|
|
1515
|
+
)
|
|
1516
|
+
await self._write_response(writer, response)
|
|
1517
|
+
continue
|
|
1518
|
+
|
|
1519
|
+
# Handle request
|
|
1520
|
+
response = await self.handle_request(request)
|
|
1521
|
+
|
|
1522
|
+
# Send response (only for requests with id)
|
|
1523
|
+
if request.id is not None:
|
|
1524
|
+
await self._write_response(writer, response)
|
|
1525
|
+
|
|
1526
|
+
except asyncio.CancelledError:
|
|
1527
|
+
break
|
|
1528
|
+
except Exception as e:
|
|
1529
|
+
logger.error(f"Error in stdio loop: {e}")
|
|
1530
|
+
|
|
1531
|
+
finally:
|
|
1532
|
+
self._running = False
|
|
1533
|
+
writer.close()
|
|
1534
|
+
logger.info("MCP server stopped")
|
|
1535
|
+
|
|
1536
|
+
async def _write_response(
|
|
1537
|
+
self, writer: asyncio.StreamWriter, response: MCPResponse
|
|
1538
|
+
) -> None:
|
|
1539
|
+
"""Write a response to the output stream."""
|
|
1540
|
+
data = json.dumps(response.to_dict()) + "\n"
|
|
1541
|
+
writer.write(data.encode("utf-8"))
|
|
1542
|
+
await writer.drain()
|
|
1543
|
+
|
|
1544
|
+
def stop(self) -> None:
|
|
1545
|
+
"""Stop the server."""
|
|
1546
|
+
self._running = False
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
# =============================================================================
|
|
1550
|
+
# Entry Point
|
|
1551
|
+
# =============================================================================
|
|
1552
|
+
|
|
1553
|
+
|
|
1554
|
+
async def run_mcp_server(
|
|
1555
|
+
engine: Optional[MemoryEngine] = None,
|
|
1556
|
+
rate_limit: int = 100,
|
|
1557
|
+
) -> None:
|
|
1558
|
+
"""Run the MCP server.
|
|
1559
|
+
|
|
1560
|
+
Args:
|
|
1561
|
+
engine: Optional MemoryEngine instance
|
|
1562
|
+
rate_limit: Maximum requests per minute
|
|
1563
|
+
"""
|
|
1564
|
+
server = MCPServer(engine=engine, rate_limit=rate_limit)
|
|
1565
|
+
await server.run_stdio()
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def main() -> None:
|
|
1569
|
+
"""Main entry point for MCP server."""
|
|
1570
|
+
asyncio.run(run_mcp_server())
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
if __name__ == "__main__":
|
|
1574
|
+
main()
|