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.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. 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()