alma-memory 0.5.0__py3-none-any.whl → 0.7.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 (111) hide show
  1. alma/__init__.py +296 -194
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -322
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -264
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -81
  26. alma/graph/backends/__init__.py +32 -18
  27. alma/graph/backends/kuzu.py +624 -0
  28. alma/graph/backends/memgraph.py +432 -0
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -432
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -511
  55. alma/observability/__init__.py +91 -0
  56. alma/observability/config.py +302 -0
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -0
  59. alma/observability/metrics.py +583 -0
  60. alma/observability/tracing.py +440 -0
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -366
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -61
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1048
  80. alma/storage/base.py +1083 -525
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -0
  83. alma/storage/file_based.py +614 -619
  84. alma/storage/migrations/__init__.py +21 -0
  85. alma/storage/migrations/base.py +321 -0
  86. alma/storage/migrations/runner.py +323 -0
  87. alma/storage/migrations/version_stores.py +337 -0
  88. alma/storage/migrations/versions/__init__.py +11 -0
  89. alma/storage/migrations/versions/v1_0_0.py +373 -0
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1452
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1358
  95. alma/testing/__init__.py +46 -0
  96. alma/testing/factories.py +301 -0
  97. alma/testing/mocks.py +389 -0
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/METADATA +244 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.0.dist-info/RECORD +0 -76
  110. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.0.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
alma/mcp/server.py CHANGED
@@ -1,591 +1,955 @@
1
- """
2
- ALMA MCP Server Implementation.
3
-
4
- Provides the main server class that handles MCP protocol communication.
5
- Supports both stdio (for Claude Code) and HTTP modes.
6
- """
7
-
8
- import asyncio
9
- import json
10
- import logging
11
- import sys
12
- from typing import Any, Dict, List, Optional
13
-
14
- from alma import ALMA
15
- from alma.mcp.resources import (
16
- get_agents_resource,
17
- get_config_resource,
18
- list_resources,
19
- )
20
- from alma.mcp.tools import (
21
- alma_add_knowledge,
22
- alma_add_preference,
23
- alma_consolidate,
24
- alma_forget,
25
- alma_health,
26
- alma_learn,
27
- alma_retrieve,
28
- alma_stats,
29
- )
30
-
31
- logger = logging.getLogger(__name__)
32
-
33
-
34
- class ALMAMCPServer:
35
- """
36
- MCP Server for ALMA.
37
-
38
- Exposes ALMA functionality via the Model Context Protocol,
39
- allowing any MCP-compatible client (like Claude Code) to
40
- interact with the memory system.
41
- """
42
-
43
- def __init__(
44
- self,
45
- alma: ALMA,
46
- server_name: str = "alma-memory",
47
- server_version: str = "0.2.0",
48
- ):
49
- """
50
- Initialize the MCP server.
51
-
52
- Args:
53
- alma: Configured ALMA instance
54
- server_name: Server identifier
55
- server_version: Server version
56
- """
57
- self.alma = alma
58
- self.server_name = server_name
59
- self.server_version = server_version
60
-
61
- # Register tools
62
- self.tools = self._register_tools()
63
-
64
- # Register resources
65
- self.resources = list_resources()
66
-
67
- def _register_tools(self) -> List[Dict[str, Any]]:
68
- """Register available MCP tools."""
69
- return [
70
- {
71
- "name": "alma_retrieve",
72
- "description": "Retrieve relevant memories for a task. Returns heuristics, domain knowledge, anti-patterns, and user preferences.",
73
- "inputSchema": {
74
- "type": "object",
75
- "properties": {
76
- "task": {
77
- "type": "string",
78
- "description": "Description of the task to perform",
79
- },
80
- "agent": {
81
- "type": "string",
82
- "description": "Name of the agent requesting memories (e.g., 'helena', 'victor')",
83
- },
84
- "user_id": {
85
- "type": "string",
86
- "description": "Optional user ID for preference retrieval",
87
- },
88
- "top_k": {
89
- "type": "integer",
90
- "description": "Maximum items per memory type (default: 5)",
91
- "default": 5,
92
- },
93
- },
94
- "required": ["task", "agent"],
95
- },
96
- },
97
- {
98
- "name": "alma_learn",
99
- "description": "Record a task outcome for learning. Use after completing a task to help improve future performance.",
100
- "inputSchema": {
101
- "type": "object",
102
- "properties": {
103
- "agent": {
104
- "type": "string",
105
- "description": "Name of the agent that executed the task",
106
- },
107
- "task": {
108
- "type": "string",
109
- "description": "Description of the task",
110
- },
111
- "outcome": {
112
- "type": "string",
113
- "enum": ["success", "failure"],
114
- "description": "Whether the task succeeded or failed",
115
- },
116
- "strategy_used": {
117
- "type": "string",
118
- "description": "What approach was taken",
119
- },
120
- "task_type": {
121
- "type": "string",
122
- "description": "Category of task (for grouping)",
123
- },
124
- "duration_ms": {
125
- "type": "integer",
126
- "description": "How long the task took in milliseconds",
127
- },
128
- "error_message": {
129
- "type": "string",
130
- "description": "Error details if failed",
131
- },
132
- "feedback": {
133
- "type": "string",
134
- "description": "User feedback if provided",
135
- },
136
- },
137
- "required": ["agent", "task", "outcome", "strategy_used"],
138
- },
139
- },
140
- {
141
- "name": "alma_add_preference",
142
- "description": "Add a user preference to memory. Preferences persist across sessions.",
143
- "inputSchema": {
144
- "type": "object",
145
- "properties": {
146
- "user_id": {
147
- "type": "string",
148
- "description": "User identifier",
149
- },
150
- "category": {
151
- "type": "string",
152
- "description": "Category (communication, code_style, workflow)",
153
- },
154
- "preference": {
155
- "type": "string",
156
- "description": "The preference text",
157
- },
158
- "source": {
159
- "type": "string",
160
- "description": "How this was learned (default: explicit_instruction)",
161
- "default": "explicit_instruction",
162
- },
163
- },
164
- "required": ["user_id", "category", "preference"],
165
- },
166
- },
167
- {
168
- "name": "alma_add_knowledge",
169
- "description": "Add domain knowledge within agent's scope. Knowledge is facts, not strategies.",
170
- "inputSchema": {
171
- "type": "object",
172
- "properties": {
173
- "agent": {
174
- "type": "string",
175
- "description": "Agent this knowledge belongs to",
176
- },
177
- "domain": {
178
- "type": "string",
179
- "description": "Knowledge domain",
180
- },
181
- "fact": {
182
- "type": "string",
183
- "description": "The fact to remember",
184
- },
185
- "source": {
186
- "type": "string",
187
- "description": "How this was learned (default: user_stated)",
188
- "default": "user_stated",
189
- },
190
- },
191
- "required": ["agent", "domain", "fact"],
192
- },
193
- },
194
- {
195
- "name": "alma_forget",
196
- "description": "Prune stale or low-confidence memories to keep the system clean.",
197
- "inputSchema": {
198
- "type": "object",
199
- "properties": {
200
- "agent": {
201
- "type": "string",
202
- "description": "Specific agent to prune, or omit for all",
203
- },
204
- "older_than_days": {
205
- "type": "integer",
206
- "description": "Remove outcomes older than this (default: 90)",
207
- "default": 90,
208
- },
209
- "below_confidence": {
210
- "type": "number",
211
- "description": "Remove heuristics below this confidence (default: 0.3)",
212
- "default": 0.3,
213
- },
214
- },
215
- },
216
- },
217
- {
218
- "name": "alma_stats",
219
- "description": "Get memory statistics for monitoring and debugging.",
220
- "inputSchema": {
221
- "type": "object",
222
- "properties": {
223
- "agent": {
224
- "type": "string",
225
- "description": "Specific agent or omit for all",
226
- },
227
- },
228
- },
229
- },
230
- {
231
- "name": "alma_health",
232
- "description": "Health check for the ALMA server.",
233
- "inputSchema": {
234
- "type": "object",
235
- "properties": {},
236
- },
237
- },
238
- {
239
- "name": "alma_consolidate",
240
- "description": "Consolidate similar memories to reduce redundancy. Merges near-duplicate memories based on semantic similarity. Use dry_run=true first to preview what would be merged.",
241
- "inputSchema": {
242
- "type": "object",
243
- "properties": {
244
- "agent": {
245
- "type": "string",
246
- "description": "Agent whose memories to consolidate",
247
- },
248
- "memory_type": {
249
- "type": "string",
250
- "enum": [
251
- "heuristics",
252
- "outcomes",
253
- "domain_knowledge",
254
- "anti_patterns",
255
- ],
256
- "description": "Type of memory to consolidate (default: heuristics)",
257
- "default": "heuristics",
258
- },
259
- "similarity_threshold": {
260
- "type": "number",
261
- "description": "Minimum cosine similarity to group memories (0.0-1.0, default: 0.85). Higher values are more conservative.",
262
- "default": 0.85,
263
- },
264
- "dry_run": {
265
- "type": "boolean",
266
- "description": "If true, preview what would be merged without modifying storage (default: true)",
267
- "default": True,
268
- },
269
- },
270
- "required": ["agent"],
271
- },
272
- },
273
- ]
274
-
275
- async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
276
- """
277
- Handle an incoming MCP request.
278
-
279
- Args:
280
- request: The MCP request
281
-
282
- Returns:
283
- MCP response
284
- """
285
- method = request.get("method", "")
286
- params = request.get("params", {})
287
- request_id = request.get("id")
288
-
289
- try:
290
- if method == "initialize":
291
- return self._handle_initialize(request_id, params)
292
- elif method == "tools/list":
293
- return self._handle_tools_list(request_id)
294
- elif method == "tools/call":
295
- return await self._handle_tool_call(request_id, params)
296
- elif method == "resources/list":
297
- return self._handle_resources_list(request_id)
298
- elif method == "resources/read":
299
- return self._handle_resource_read(request_id, params)
300
- elif method == "ping":
301
- return self._success_response(request_id, {})
302
- else:
303
- return self._error_response(
304
- request_id,
305
- -32601,
306
- f"Method not found: {method}",
307
- )
308
-
309
- except Exception as e:
310
- logger.exception(f"Error handling request: {e}")
311
- return self._error_response(request_id, -32603, str(e))
312
-
313
- def _handle_initialize(
314
- self,
315
- request_id: Optional[int],
316
- params: Dict[str, Any],
317
- ) -> Dict[str, Any]:
318
- """Handle initialize request."""
319
- return self._success_response(
320
- request_id,
321
- {
322
- "protocolVersion": "2024-11-05",
323
- "serverInfo": {
324
- "name": self.server_name,
325
- "version": self.server_version,
326
- },
327
- "capabilities": {
328
- "tools": {},
329
- "resources": {},
330
- },
331
- },
332
- )
333
-
334
- def _handle_tools_list(self, request_id: Optional[int]) -> Dict[str, Any]:
335
- """Handle tools/list request."""
336
- return self._success_response(request_id, {"tools": self.tools})
337
-
338
- async def _handle_tool_call(
339
- self,
340
- request_id: Optional[int],
341
- params: Dict[str, Any],
342
- ) -> Dict[str, Any]:
343
- """Handle tools/call request."""
344
- tool_name = params.get("name", "")
345
- arguments = params.get("arguments", {})
346
-
347
- # Map tool names to functions
348
- tool_handlers = {
349
- "alma_retrieve": lambda: alma_retrieve(
350
- self.alma,
351
- task=arguments.get("task", ""),
352
- agent=arguments.get("agent", ""),
353
- user_id=arguments.get("user_id"),
354
- top_k=arguments.get("top_k", 5),
355
- ),
356
- "alma_learn": lambda: alma_learn(
357
- self.alma,
358
- agent=arguments.get("agent", ""),
359
- task=arguments.get("task", ""),
360
- outcome=arguments.get("outcome", ""),
361
- strategy_used=arguments.get("strategy_used", ""),
362
- task_type=arguments.get("task_type"),
363
- duration_ms=arguments.get("duration_ms"),
364
- error_message=arguments.get("error_message"),
365
- feedback=arguments.get("feedback"),
366
- ),
367
- "alma_add_preference": lambda: alma_add_preference(
368
- self.alma,
369
- user_id=arguments.get("user_id", ""),
370
- category=arguments.get("category", ""),
371
- preference=arguments.get("preference", ""),
372
- source=arguments.get("source", "explicit_instruction"),
373
- ),
374
- "alma_add_knowledge": lambda: alma_add_knowledge(
375
- self.alma,
376
- agent=arguments.get("agent", ""),
377
- domain=arguments.get("domain", ""),
378
- fact=arguments.get("fact", ""),
379
- source=arguments.get("source", "user_stated"),
380
- ),
381
- "alma_forget": lambda: alma_forget(
382
- self.alma,
383
- agent=arguments.get("agent"),
384
- older_than_days=arguments.get("older_than_days", 90),
385
- below_confidence=arguments.get("below_confidence", 0.3),
386
- ),
387
- "alma_stats": lambda: alma_stats(
388
- self.alma,
389
- agent=arguments.get("agent"),
390
- ),
391
- "alma_health": lambda: alma_health(self.alma),
392
- "alma_consolidate": lambda: alma_consolidate(
393
- self.alma,
394
- agent=arguments.get("agent", ""),
395
- memory_type=arguments.get("memory_type", "heuristics"),
396
- similarity_threshold=arguments.get("similarity_threshold", 0.85),
397
- dry_run=arguments.get("dry_run", True),
398
- ),
399
- }
400
-
401
- if tool_name not in tool_handlers:
402
- return self._error_response(
403
- request_id,
404
- -32602,
405
- f"Unknown tool: {tool_name}",
406
- )
407
-
408
- result = tool_handlers[tool_name]()
409
-
410
- # Handle async functions (like alma_consolidate)
411
- if asyncio.iscoroutine(result):
412
- result = await result
413
-
414
- return self._success_response(
415
- request_id,
416
- {
417
- "content": [
418
- {
419
- "type": "text",
420
- "text": json.dumps(result, indent=2),
421
- }
422
- ],
423
- },
424
- )
425
-
426
- def _handle_resources_list(
427
- self,
428
- request_id: Optional[int],
429
- ) -> Dict[str, Any]:
430
- """Handle resources/list request."""
431
- return self._success_response(request_id, {"resources": self.resources})
432
-
433
- def _handle_resource_read(
434
- self,
435
- request_id: Optional[int],
436
- params: Dict[str, Any],
437
- ) -> Dict[str, Any]:
438
- """Handle resources/read request."""
439
- uri = params.get("uri", "")
440
-
441
- if uri == "alma://config":
442
- resource = get_config_resource(self.alma)
443
- elif uri == "alma://agents":
444
- resource = get_agents_resource(self.alma)
445
- else:
446
- return self._error_response(
447
- request_id,
448
- -32602,
449
- f"Unknown resource: {uri}",
450
- )
451
-
452
- return self._success_response(
453
- request_id,
454
- {
455
- "contents": [
456
- {
457
- "uri": resource["uri"],
458
- "mimeType": resource["mimeType"],
459
- "text": json.dumps(resource["content"], indent=2),
460
- }
461
- ],
462
- },
463
- )
464
-
465
- def _success_response(
466
- self,
467
- request_id: Optional[int],
468
- result: Any,
469
- ) -> Dict[str, Any]:
470
- """Create a success response."""
471
- return {
472
- "jsonrpc": "2.0",
473
- "id": request_id,
474
- "result": result,
475
- }
476
-
477
- def _error_response(
478
- self,
479
- request_id: Optional[int],
480
- code: int,
481
- message: str,
482
- ) -> Dict[str, Any]:
483
- """Create an error response."""
484
- return {
485
- "jsonrpc": "2.0",
486
- "id": request_id,
487
- "error": {
488
- "code": code,
489
- "message": message,
490
- },
491
- }
492
-
493
- async def run_stdio(self):
494
- """Run the server in stdio mode for Claude Code integration."""
495
- logger.info("Starting ALMA MCP Server (stdio mode)")
496
-
497
- reader = asyncio.StreamReader()
498
- protocol = asyncio.StreamReaderProtocol(reader)
499
- await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
500
-
501
- (
502
- writer_transport,
503
- writer_protocol,
504
- ) = await asyncio.get_event_loop().connect_write_pipe(
505
- asyncio.streams.FlowControlMixin, sys.stdout
506
- )
507
- writer = asyncio.StreamWriter(
508
- writer_transport, writer_protocol, None, asyncio.get_event_loop()
509
- )
510
-
511
- while True:
512
- try:
513
- # Read Content-Length header
514
- header_line = await reader.readline()
515
- if not header_line:
516
- break
517
-
518
- header = header_line.decode().strip()
519
- if not header.startswith("Content-Length:"):
520
- continue
521
-
522
- content_length = int(header.split(":")[1].strip())
523
-
524
- # Read empty line
525
- await reader.readline()
526
-
527
- # Read content
528
- content = await reader.read(content_length)
529
- request = json.loads(content.decode())
530
-
531
- # Handle request
532
- response = await self.handle_request(request)
533
-
534
- # Send response
535
- response_str = json.dumps(response)
536
- response_bytes = response_str.encode()
537
- header_bytes = f"Content-Length: {len(response_bytes)}\r\n\r\n".encode()
538
-
539
- writer.write(header_bytes + response_bytes)
540
- await writer.drain()
541
-
542
- except asyncio.CancelledError:
543
- break
544
- except Exception as e:
545
- logger.exception(f"Error in stdio loop: {e}")
546
-
547
- async def run_http(self, host: str = "0.0.0.0", port: int = 8765):
548
- """
549
- Run the server in HTTP mode for remote access.
550
-
551
- Note: Requires aiohttp (optional dependency).
552
- """
553
- try:
554
- from aiohttp import web
555
- except ImportError:
556
- logger.error(
557
- "aiohttp required for HTTP mode. Install with: pip install aiohttp"
558
- )
559
- return
560
-
561
- async def handle_post(request: web.Request) -> web.Response:
562
- """Handle HTTP POST requests."""
563
- try:
564
- data = await request.json()
565
- response = await self.handle_request(data)
566
- return web.json_response(response)
567
- except Exception as e:
568
- return web.json_response(
569
- {"error": str(e)},
570
- status=500,
571
- )
572
-
573
- async def handle_health(request: web.Request) -> web.Response:
574
- """Handle health check endpoint."""
575
- result = alma_health(self.alma)
576
- return web.json_response(result)
577
-
578
- app = web.Application()
579
- app.router.add_post("/", handle_post)
580
- app.router.add_get("/health", handle_health)
581
-
582
- runner = web.AppRunner(app)
583
- await runner.setup()
584
- site = web.TCPSite(runner, host, port)
585
- await site.start()
586
-
587
- logger.info(f"ALMA MCP Server running on http://{host}:{port}")
588
-
589
- # Keep running
590
- while True:
591
- await asyncio.sleep(3600)
1
+ """
2
+ ALMA MCP Server Implementation.
3
+
4
+ Provides the main server class that handles MCP protocol communication.
5
+ Supports both stdio (for Claude Code) and HTTP modes.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import logging
11
+ import sys
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from alma import ALMA
15
+ from alma.mcp.resources import (
16
+ get_agents_resource,
17
+ get_config_resource,
18
+ list_resources,
19
+ )
20
+ from alma.mcp.tools import (
21
+ alma_cleanup_checkpoints,
22
+ alma_consolidate,
23
+ alma_get_artifacts,
24
+ alma_health,
25
+ alma_merge_states,
26
+ async_alma_add_knowledge,
27
+ async_alma_add_preference,
28
+ # Async workflow variants
29
+ async_alma_checkpoint,
30
+ async_alma_forget,
31
+ async_alma_health,
32
+ async_alma_learn,
33
+ async_alma_link_artifact,
34
+ async_alma_resume,
35
+ async_alma_retrieve,
36
+ async_alma_retrieve_scoped,
37
+ async_alma_stats,
38
+ async_alma_workflow_learn,
39
+ )
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class ALMAMCPServer:
45
+ """
46
+ MCP Server for ALMA.
47
+
48
+ Exposes ALMA functionality via the Model Context Protocol,
49
+ allowing any MCP-compatible client (like Claude Code) to
50
+ interact with the memory system.
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ alma: ALMA,
56
+ server_name: str = "alma-memory",
57
+ server_version: str = "0.6.0",
58
+ ):
59
+ """
60
+ Initialize the MCP server.
61
+
62
+ Args:
63
+ alma: Configured ALMA instance
64
+ server_name: Server identifier
65
+ server_version: Server version
66
+ """
67
+ self.alma = alma
68
+ self.server_name = server_name
69
+ self.server_version = server_version
70
+
71
+ # Register tools
72
+ self.tools = self._register_tools()
73
+
74
+ # Register resources
75
+ self.resources = list_resources()
76
+
77
+ def _register_tools(self) -> List[Dict[str, Any]]:
78
+ """Register available MCP tools."""
79
+ return [
80
+ {
81
+ "name": "alma_retrieve",
82
+ "description": "Retrieve relevant memories for a task. Returns heuristics, domain knowledge, anti-patterns, and user preferences.",
83
+ "inputSchema": {
84
+ "type": "object",
85
+ "properties": {
86
+ "task": {
87
+ "type": "string",
88
+ "description": "Description of the task to perform",
89
+ },
90
+ "agent": {
91
+ "type": "string",
92
+ "description": "Name of the agent requesting memories (e.g., 'helena', 'victor')",
93
+ },
94
+ "user_id": {
95
+ "type": "string",
96
+ "description": "Optional user ID for preference retrieval",
97
+ },
98
+ "top_k": {
99
+ "type": "integer",
100
+ "description": "Maximum items per memory type (default: 5)",
101
+ "default": 5,
102
+ },
103
+ },
104
+ "required": ["task", "agent"],
105
+ },
106
+ },
107
+ {
108
+ "name": "alma_learn",
109
+ "description": "Record a task outcome for learning. Use after completing a task to help improve future performance.",
110
+ "inputSchema": {
111
+ "type": "object",
112
+ "properties": {
113
+ "agent": {
114
+ "type": "string",
115
+ "description": "Name of the agent that executed the task",
116
+ },
117
+ "task": {
118
+ "type": "string",
119
+ "description": "Description of the task",
120
+ },
121
+ "outcome": {
122
+ "type": "string",
123
+ "enum": ["success", "failure"],
124
+ "description": "Whether the task succeeded or failed",
125
+ },
126
+ "strategy_used": {
127
+ "type": "string",
128
+ "description": "What approach was taken",
129
+ },
130
+ "task_type": {
131
+ "type": "string",
132
+ "description": "Category of task (for grouping)",
133
+ },
134
+ "duration_ms": {
135
+ "type": "integer",
136
+ "description": "How long the task took in milliseconds",
137
+ },
138
+ "error_message": {
139
+ "type": "string",
140
+ "description": "Error details if failed",
141
+ },
142
+ "feedback": {
143
+ "type": "string",
144
+ "description": "User feedback if provided",
145
+ },
146
+ },
147
+ "required": ["agent", "task", "outcome", "strategy_used"],
148
+ },
149
+ },
150
+ {
151
+ "name": "alma_add_preference",
152
+ "description": "Add a user preference to memory. Preferences persist across sessions.",
153
+ "inputSchema": {
154
+ "type": "object",
155
+ "properties": {
156
+ "user_id": {
157
+ "type": "string",
158
+ "description": "User identifier",
159
+ },
160
+ "category": {
161
+ "type": "string",
162
+ "description": "Category (communication, code_style, workflow)",
163
+ },
164
+ "preference": {
165
+ "type": "string",
166
+ "description": "The preference text",
167
+ },
168
+ "source": {
169
+ "type": "string",
170
+ "description": "How this was learned (default: explicit_instruction)",
171
+ "default": "explicit_instruction",
172
+ },
173
+ },
174
+ "required": ["user_id", "category", "preference"],
175
+ },
176
+ },
177
+ {
178
+ "name": "alma_add_knowledge",
179
+ "description": "Add domain knowledge within agent's scope. Knowledge is facts, not strategies.",
180
+ "inputSchema": {
181
+ "type": "object",
182
+ "properties": {
183
+ "agent": {
184
+ "type": "string",
185
+ "description": "Agent this knowledge belongs to",
186
+ },
187
+ "domain": {
188
+ "type": "string",
189
+ "description": "Knowledge domain",
190
+ },
191
+ "fact": {
192
+ "type": "string",
193
+ "description": "The fact to remember",
194
+ },
195
+ "source": {
196
+ "type": "string",
197
+ "description": "How this was learned (default: user_stated)",
198
+ "default": "user_stated",
199
+ },
200
+ },
201
+ "required": ["agent", "domain", "fact"],
202
+ },
203
+ },
204
+ {
205
+ "name": "alma_forget",
206
+ "description": "Prune stale or low-confidence memories to keep the system clean.",
207
+ "inputSchema": {
208
+ "type": "object",
209
+ "properties": {
210
+ "agent": {
211
+ "type": "string",
212
+ "description": "Specific agent to prune, or omit for all",
213
+ },
214
+ "older_than_days": {
215
+ "type": "integer",
216
+ "description": "Remove outcomes older than this (default: 90)",
217
+ "default": 90,
218
+ },
219
+ "below_confidence": {
220
+ "type": "number",
221
+ "description": "Remove heuristics below this confidence (default: 0.3)",
222
+ "default": 0.3,
223
+ },
224
+ },
225
+ },
226
+ },
227
+ {
228
+ "name": "alma_stats",
229
+ "description": "Get memory statistics for monitoring and debugging.",
230
+ "inputSchema": {
231
+ "type": "object",
232
+ "properties": {
233
+ "agent": {
234
+ "type": "string",
235
+ "description": "Specific agent or omit for all",
236
+ },
237
+ },
238
+ },
239
+ },
240
+ {
241
+ "name": "alma_health",
242
+ "description": "Health check for the ALMA server.",
243
+ "inputSchema": {
244
+ "type": "object",
245
+ "properties": {},
246
+ },
247
+ },
248
+ {
249
+ "name": "alma_consolidate",
250
+ "description": "Consolidate similar memories to reduce redundancy. Merges near-duplicate memories based on semantic similarity. Use dry_run=true first to preview what would be merged.",
251
+ "inputSchema": {
252
+ "type": "object",
253
+ "properties": {
254
+ "agent": {
255
+ "type": "string",
256
+ "description": "Agent whose memories to consolidate",
257
+ },
258
+ "memory_type": {
259
+ "type": "string",
260
+ "enum": [
261
+ "heuristics",
262
+ "outcomes",
263
+ "domain_knowledge",
264
+ "anti_patterns",
265
+ ],
266
+ "description": "Type of memory to consolidate (default: heuristics)",
267
+ "default": "heuristics",
268
+ },
269
+ "similarity_threshold": {
270
+ "type": "number",
271
+ "description": "Minimum cosine similarity to group memories (0.0-1.0, default: 0.85). Higher values are more conservative.",
272
+ "default": 0.85,
273
+ },
274
+ "dry_run": {
275
+ "type": "boolean",
276
+ "description": "If true, preview what would be merged without modifying storage (default: true)",
277
+ "default": True,
278
+ },
279
+ },
280
+ "required": ["agent"],
281
+ },
282
+ },
283
+ # ==================== WORKFLOW TOOLS (v0.6.0) ====================
284
+ {
285
+ "name": "alma_checkpoint",
286
+ "description": "Create a checkpoint for crash recovery. Persists workflow state at key execution points.",
287
+ "inputSchema": {
288
+ "type": "object",
289
+ "properties": {
290
+ "run_id": {
291
+ "type": "string",
292
+ "description": "The workflow run identifier",
293
+ },
294
+ "node_id": {
295
+ "type": "string",
296
+ "description": "The node creating this checkpoint",
297
+ },
298
+ "state": {
299
+ "type": "object",
300
+ "description": "The state to persist",
301
+ },
302
+ "branch_id": {
303
+ "type": "string",
304
+ "description": "Optional branch identifier for parallel execution",
305
+ },
306
+ "parent_checkpoint_id": {
307
+ "type": "string",
308
+ "description": "Previous checkpoint in the chain",
309
+ },
310
+ "metadata": {
311
+ "type": "object",
312
+ "description": "Additional checkpoint metadata",
313
+ },
314
+ "skip_if_unchanged": {
315
+ "type": "boolean",
316
+ "description": "Skip if state hasn't changed (default: true)",
317
+ "default": True,
318
+ },
319
+ },
320
+ "required": ["run_id", "node_id", "state"],
321
+ },
322
+ },
323
+ {
324
+ "name": "alma_resume",
325
+ "description": "Get the checkpoint to resume from after a crash.",
326
+ "inputSchema": {
327
+ "type": "object",
328
+ "properties": {
329
+ "run_id": {
330
+ "type": "string",
331
+ "description": "The workflow run identifier",
332
+ },
333
+ "branch_id": {
334
+ "type": "string",
335
+ "description": "Optional branch to filter by",
336
+ },
337
+ },
338
+ "required": ["run_id"],
339
+ },
340
+ },
341
+ {
342
+ "name": "alma_merge_states",
343
+ "description": "Merge multiple branch states after parallel execution using configurable reducers.",
344
+ "inputSchema": {
345
+ "type": "object",
346
+ "properties": {
347
+ "states": {
348
+ "type": "array",
349
+ "items": {"type": "object"},
350
+ "description": "List of state dicts from parallel branches",
351
+ },
352
+ "reducer_config": {
353
+ "type": "object",
354
+ "description": "Mapping of key -> reducer (append, merge_dict, last_value, first_value, sum, max, min, union)",
355
+ },
356
+ },
357
+ "required": ["states"],
358
+ },
359
+ },
360
+ {
361
+ "name": "alma_workflow_learn",
362
+ "description": "Record learnings from a completed workflow execution.",
363
+ "inputSchema": {
364
+ "type": "object",
365
+ "properties": {
366
+ "agent": {
367
+ "type": "string",
368
+ "description": "The agent that executed the workflow",
369
+ },
370
+ "workflow_id": {
371
+ "type": "string",
372
+ "description": "The workflow definition identifier",
373
+ },
374
+ "run_id": {
375
+ "type": "string",
376
+ "description": "The specific run identifier",
377
+ },
378
+ "result": {
379
+ "type": "string",
380
+ "enum": [
381
+ "success",
382
+ "failure",
383
+ "partial",
384
+ "cancelled",
385
+ "timeout",
386
+ ],
387
+ "description": "Result status",
388
+ },
389
+ "summary": {
390
+ "type": "string",
391
+ "description": "Human-readable summary of what happened",
392
+ },
393
+ "strategies_used": {
394
+ "type": "array",
395
+ "items": {"type": "string"},
396
+ "description": "List of strategies attempted",
397
+ },
398
+ "successful_patterns": {
399
+ "type": "array",
400
+ "items": {"type": "string"},
401
+ "description": "Patterns that worked well",
402
+ },
403
+ "failed_patterns": {
404
+ "type": "array",
405
+ "items": {"type": "string"},
406
+ "description": "Patterns that didn't work",
407
+ },
408
+ "duration_seconds": {
409
+ "type": "number",
410
+ "description": "How long the workflow took",
411
+ },
412
+ "node_count": {
413
+ "type": "integer",
414
+ "description": "Number of nodes executed",
415
+ },
416
+ "error_message": {
417
+ "type": "string",
418
+ "description": "Error details if failed",
419
+ },
420
+ "tenant_id": {
421
+ "type": "string",
422
+ "description": "Multi-tenant isolation identifier",
423
+ },
424
+ "metadata": {
425
+ "type": "object",
426
+ "description": "Additional outcome metadata",
427
+ },
428
+ },
429
+ "required": ["agent", "workflow_id", "run_id", "result", "summary"],
430
+ },
431
+ },
432
+ {
433
+ "name": "alma_link_artifact",
434
+ "description": "Link an external artifact (file, screenshot, log) to a memory.",
435
+ "inputSchema": {
436
+ "type": "object",
437
+ "properties": {
438
+ "memory_id": {
439
+ "type": "string",
440
+ "description": "The memory to link the artifact to",
441
+ },
442
+ "artifact_type": {
443
+ "type": "string",
444
+ "description": "Type (screenshot, log, report, file, document, image, etc.)",
445
+ },
446
+ "storage_url": {
447
+ "type": "string",
448
+ "description": "URL or path to the artifact in storage",
449
+ },
450
+ "filename": {
451
+ "type": "string",
452
+ "description": "Original filename",
453
+ },
454
+ "mime_type": {
455
+ "type": "string",
456
+ "description": "MIME type",
457
+ },
458
+ "size_bytes": {
459
+ "type": "integer",
460
+ "description": "Size in bytes",
461
+ },
462
+ "checksum": {
463
+ "type": "string",
464
+ "description": "SHA256 checksum for integrity",
465
+ },
466
+ "metadata": {
467
+ "type": "object",
468
+ "description": "Additional artifact metadata",
469
+ },
470
+ },
471
+ "required": ["memory_id", "artifact_type", "storage_url"],
472
+ },
473
+ },
474
+ {
475
+ "name": "alma_get_artifacts",
476
+ "description": "Get all artifacts linked to a memory.",
477
+ "inputSchema": {
478
+ "type": "object",
479
+ "properties": {
480
+ "memory_id": {
481
+ "type": "string",
482
+ "description": "The memory to get artifacts for",
483
+ },
484
+ },
485
+ "required": ["memory_id"],
486
+ },
487
+ },
488
+ {
489
+ "name": "alma_cleanup_checkpoints",
490
+ "description": "Clean up old checkpoints for a completed workflow run.",
491
+ "inputSchema": {
492
+ "type": "object",
493
+ "properties": {
494
+ "run_id": {
495
+ "type": "string",
496
+ "description": "The workflow run identifier",
497
+ },
498
+ "keep_latest": {
499
+ "type": "integer",
500
+ "description": "Number of latest checkpoints to keep (default: 1)",
501
+ "default": 1,
502
+ },
503
+ },
504
+ "required": ["run_id"],
505
+ },
506
+ },
507
+ {
508
+ "name": "alma_retrieve_scoped",
509
+ "description": "Retrieve memories with workflow scope filtering. Supports hierarchical scoping: node -> run -> workflow -> agent -> tenant -> global.",
510
+ "inputSchema": {
511
+ "type": "object",
512
+ "properties": {
513
+ "task": {
514
+ "type": "string",
515
+ "description": "Description of the task to perform",
516
+ },
517
+ "agent": {
518
+ "type": "string",
519
+ "description": "Name of the agent requesting memories",
520
+ },
521
+ "scope": {
522
+ "type": "string",
523
+ "enum": [
524
+ "node",
525
+ "run",
526
+ "workflow",
527
+ "agent",
528
+ "tenant",
529
+ "global",
530
+ ],
531
+ "description": "Scope level for filtering (default: agent)",
532
+ "default": "agent",
533
+ },
534
+ "tenant_id": {
535
+ "type": "string",
536
+ "description": "Tenant identifier for multi-tenant",
537
+ },
538
+ "workflow_id": {
539
+ "type": "string",
540
+ "description": "Workflow definition identifier",
541
+ },
542
+ "run_id": {
543
+ "type": "string",
544
+ "description": "Specific run identifier",
545
+ },
546
+ "node_id": {
547
+ "type": "string",
548
+ "description": "Current node identifier",
549
+ },
550
+ "user_id": {
551
+ "type": "string",
552
+ "description": "Optional user ID for preferences",
553
+ },
554
+ "top_k": {
555
+ "type": "integer",
556
+ "description": "Maximum items per memory type (default: 5)",
557
+ "default": 5,
558
+ },
559
+ },
560
+ "required": ["task", "agent"],
561
+ },
562
+ },
563
+ ]
564
+
565
+ async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
566
+ """
567
+ Handle an incoming MCP request.
568
+
569
+ Args:
570
+ request: The MCP request
571
+
572
+ Returns:
573
+ MCP response
574
+ """
575
+ method = request.get("method", "")
576
+ params = request.get("params", {})
577
+ request_id = request.get("id")
578
+
579
+ try:
580
+ if method == "initialize":
581
+ return self._handle_initialize(request_id, params)
582
+ elif method == "tools/list":
583
+ return self._handle_tools_list(request_id)
584
+ elif method == "tools/call":
585
+ return await self._handle_tool_call(request_id, params)
586
+ elif method == "resources/list":
587
+ return self._handle_resources_list(request_id)
588
+ elif method == "resources/read":
589
+ return self._handle_resource_read(request_id, params)
590
+ elif method == "ping":
591
+ return self._success_response(request_id, {})
592
+ else:
593
+ return self._error_response(
594
+ request_id,
595
+ -32601,
596
+ f"Method not found: {method}",
597
+ )
598
+
599
+ except Exception as e:
600
+ logger.exception(f"Error handling request: {e}")
601
+ return self._error_response(request_id, -32603, str(e))
602
+
603
+ def _handle_initialize(
604
+ self,
605
+ request_id: Optional[int],
606
+ params: Dict[str, Any],
607
+ ) -> Dict[str, Any]:
608
+ """Handle initialize request."""
609
+ return self._success_response(
610
+ request_id,
611
+ {
612
+ "protocolVersion": "2024-11-05",
613
+ "serverInfo": {
614
+ "name": self.server_name,
615
+ "version": self.server_version,
616
+ },
617
+ "capabilities": {
618
+ "tools": {},
619
+ "resources": {},
620
+ },
621
+ },
622
+ )
623
+
624
+ def _handle_tools_list(self, request_id: Optional[int]) -> Dict[str, Any]:
625
+ """Handle tools/list request."""
626
+ return self._success_response(request_id, {"tools": self.tools})
627
+
628
+ async def _handle_tool_call(
629
+ self,
630
+ request_id: Optional[int],
631
+ params: Dict[str, Any],
632
+ ) -> Dict[str, Any]:
633
+ """
634
+ Handle tools/call request.
635
+
636
+ Uses async tool variants for better concurrency in the async server.
637
+ """
638
+ tool_name = params.get("name", "")
639
+ arguments = params.get("arguments", {})
640
+
641
+ # Map tool names to async functions for non-blocking execution
642
+ # All tools now use async variants for consistency in async server
643
+ async_tool_handlers = {
644
+ "alma_retrieve": lambda: async_alma_retrieve(
645
+ self.alma,
646
+ task=arguments.get("task", ""),
647
+ agent=arguments.get("agent", ""),
648
+ user_id=arguments.get("user_id"),
649
+ top_k=arguments.get("top_k", 5),
650
+ ),
651
+ "alma_learn": lambda: async_alma_learn(
652
+ self.alma,
653
+ agent=arguments.get("agent", ""),
654
+ task=arguments.get("task", ""),
655
+ outcome=arguments.get("outcome", ""),
656
+ strategy_used=arguments.get("strategy_used", ""),
657
+ task_type=arguments.get("task_type"),
658
+ duration_ms=arguments.get("duration_ms"),
659
+ error_message=arguments.get("error_message"),
660
+ feedback=arguments.get("feedback"),
661
+ ),
662
+ "alma_add_preference": lambda: async_alma_add_preference(
663
+ self.alma,
664
+ user_id=arguments.get("user_id", ""),
665
+ category=arguments.get("category", ""),
666
+ preference=arguments.get("preference", ""),
667
+ source=arguments.get("source", "explicit_instruction"),
668
+ ),
669
+ "alma_add_knowledge": lambda: async_alma_add_knowledge(
670
+ self.alma,
671
+ agent=arguments.get("agent", ""),
672
+ domain=arguments.get("domain", ""),
673
+ fact=arguments.get("fact", ""),
674
+ source=arguments.get("source", "user_stated"),
675
+ ),
676
+ "alma_forget": lambda: async_alma_forget(
677
+ self.alma,
678
+ agent=arguments.get("agent"),
679
+ older_than_days=arguments.get("older_than_days", 90),
680
+ below_confidence=arguments.get("below_confidence", 0.3),
681
+ ),
682
+ "alma_stats": lambda: async_alma_stats(
683
+ self.alma,
684
+ agent=arguments.get("agent"),
685
+ ),
686
+ "alma_health": lambda: async_alma_health(self.alma),
687
+ "alma_consolidate": lambda: alma_consolidate(
688
+ self.alma,
689
+ agent=arguments.get("agent", ""),
690
+ memory_type=arguments.get("memory_type", "heuristics"),
691
+ similarity_threshold=arguments.get("similarity_threshold", 0.85),
692
+ dry_run=arguments.get("dry_run", True),
693
+ ),
694
+ # Workflow tools (v0.6.0)
695
+ "alma_checkpoint": lambda: async_alma_checkpoint(
696
+ self.alma,
697
+ run_id=arguments.get("run_id", ""),
698
+ node_id=arguments.get("node_id", ""),
699
+ state=arguments.get("state", {}),
700
+ branch_id=arguments.get("branch_id"),
701
+ parent_checkpoint_id=arguments.get("parent_checkpoint_id"),
702
+ metadata=arguments.get("metadata"),
703
+ skip_if_unchanged=arguments.get("skip_if_unchanged", True),
704
+ ),
705
+ "alma_resume": lambda: async_alma_resume(
706
+ self.alma,
707
+ run_id=arguments.get("run_id", ""),
708
+ branch_id=arguments.get("branch_id"),
709
+ ),
710
+ "alma_merge_states": lambda: alma_merge_states(
711
+ self.alma,
712
+ states=arguments.get("states", []),
713
+ reducer_config=arguments.get("reducer_config"),
714
+ ),
715
+ "alma_workflow_learn": lambda: async_alma_workflow_learn(
716
+ self.alma,
717
+ agent=arguments.get("agent", ""),
718
+ workflow_id=arguments.get("workflow_id", ""),
719
+ run_id=arguments.get("run_id", ""),
720
+ result=arguments.get("result", ""),
721
+ summary=arguments.get("summary", ""),
722
+ strategies_used=arguments.get("strategies_used"),
723
+ successful_patterns=arguments.get("successful_patterns"),
724
+ failed_patterns=arguments.get("failed_patterns"),
725
+ duration_seconds=arguments.get("duration_seconds"),
726
+ node_count=arguments.get("node_count"),
727
+ error_message=arguments.get("error_message"),
728
+ tenant_id=arguments.get("tenant_id"),
729
+ metadata=arguments.get("metadata"),
730
+ ),
731
+ "alma_link_artifact": lambda: async_alma_link_artifact(
732
+ self.alma,
733
+ memory_id=arguments.get("memory_id", ""),
734
+ artifact_type=arguments.get("artifact_type", ""),
735
+ storage_url=arguments.get("storage_url", ""),
736
+ filename=arguments.get("filename"),
737
+ mime_type=arguments.get("mime_type"),
738
+ size_bytes=arguments.get("size_bytes"),
739
+ checksum=arguments.get("checksum"),
740
+ metadata=arguments.get("metadata"),
741
+ ),
742
+ "alma_get_artifacts": lambda: alma_get_artifacts(
743
+ self.alma,
744
+ memory_id=arguments.get("memory_id", ""),
745
+ ),
746
+ "alma_cleanup_checkpoints": lambda: alma_cleanup_checkpoints(
747
+ self.alma,
748
+ run_id=arguments.get("run_id", ""),
749
+ keep_latest=arguments.get("keep_latest", 1),
750
+ ),
751
+ "alma_retrieve_scoped": lambda: async_alma_retrieve_scoped(
752
+ self.alma,
753
+ task=arguments.get("task", ""),
754
+ agent=arguments.get("agent", ""),
755
+ scope=arguments.get("scope", "agent"),
756
+ tenant_id=arguments.get("tenant_id"),
757
+ workflow_id=arguments.get("workflow_id"),
758
+ run_id=arguments.get("run_id"),
759
+ node_id=arguments.get("node_id"),
760
+ user_id=arguments.get("user_id"),
761
+ top_k=arguments.get("top_k", 5),
762
+ ),
763
+ }
764
+
765
+ if tool_name not in async_tool_handlers:
766
+ return self._error_response(
767
+ request_id,
768
+ -32602,
769
+ f"Unknown tool: {tool_name}",
770
+ )
771
+
772
+ result = async_tool_handlers[tool_name]()
773
+
774
+ # All handlers now return coroutines
775
+ if asyncio.iscoroutine(result):
776
+ result = await result
777
+
778
+ return self._success_response(
779
+ request_id,
780
+ {
781
+ "content": [
782
+ {
783
+ "type": "text",
784
+ "text": json.dumps(result, indent=2),
785
+ }
786
+ ],
787
+ },
788
+ )
789
+
790
+ def _handle_resources_list(
791
+ self,
792
+ request_id: Optional[int],
793
+ ) -> Dict[str, Any]:
794
+ """Handle resources/list request."""
795
+ return self._success_response(request_id, {"resources": self.resources})
796
+
797
+ def _handle_resource_read(
798
+ self,
799
+ request_id: Optional[int],
800
+ params: Dict[str, Any],
801
+ ) -> Dict[str, Any]:
802
+ """Handle resources/read request."""
803
+ uri = params.get("uri", "")
804
+
805
+ if uri == "alma://config":
806
+ resource = get_config_resource(self.alma)
807
+ elif uri == "alma://agents":
808
+ resource = get_agents_resource(self.alma)
809
+ else:
810
+ return self._error_response(
811
+ request_id,
812
+ -32602,
813
+ f"Unknown resource: {uri}",
814
+ )
815
+
816
+ return self._success_response(
817
+ request_id,
818
+ {
819
+ "contents": [
820
+ {
821
+ "uri": resource["uri"],
822
+ "mimeType": resource["mimeType"],
823
+ "text": json.dumps(resource["content"], indent=2),
824
+ }
825
+ ],
826
+ },
827
+ )
828
+
829
+ def _success_response(
830
+ self,
831
+ request_id: Optional[int],
832
+ result: Any,
833
+ ) -> Dict[str, Any]:
834
+ """Create a success response."""
835
+ return {
836
+ "jsonrpc": "2.0",
837
+ "id": request_id,
838
+ "result": result,
839
+ }
840
+
841
+ def _error_response(
842
+ self,
843
+ request_id: Optional[int],
844
+ code: int,
845
+ message: str,
846
+ ) -> Dict[str, Any]:
847
+ """Create an error response."""
848
+ return {
849
+ "jsonrpc": "2.0",
850
+ "id": request_id,
851
+ "error": {
852
+ "code": code,
853
+ "message": message,
854
+ },
855
+ }
856
+
857
+ async def run_stdio(self):
858
+ """Run the server in stdio mode for Claude Code integration."""
859
+ logger.info("Starting ALMA MCP Server (stdio mode)")
860
+
861
+ reader = asyncio.StreamReader()
862
+ protocol = asyncio.StreamReaderProtocol(reader)
863
+ await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
864
+
865
+ (
866
+ writer_transport,
867
+ writer_protocol,
868
+ ) = await asyncio.get_event_loop().connect_write_pipe(
869
+ asyncio.streams.FlowControlMixin, sys.stdout
870
+ )
871
+ writer = asyncio.StreamWriter(
872
+ writer_transport, writer_protocol, None, asyncio.get_event_loop()
873
+ )
874
+
875
+ while True:
876
+ try:
877
+ # Read Content-Length header
878
+ header_line = await reader.readline()
879
+ if not header_line:
880
+ break
881
+
882
+ header = header_line.decode().strip()
883
+ if not header.startswith("Content-Length:"):
884
+ continue
885
+
886
+ content_length = int(header.split(":")[1].strip())
887
+
888
+ # Read empty line
889
+ await reader.readline()
890
+
891
+ # Read content
892
+ content = await reader.read(content_length)
893
+ request = json.loads(content.decode())
894
+
895
+ # Handle request
896
+ response = await self.handle_request(request)
897
+
898
+ # Send response
899
+ response_str = json.dumps(response)
900
+ response_bytes = response_str.encode()
901
+ header_bytes = f"Content-Length: {len(response_bytes)}\r\n\r\n".encode()
902
+
903
+ writer.write(header_bytes + response_bytes)
904
+ await writer.drain()
905
+
906
+ except asyncio.CancelledError:
907
+ break
908
+ except Exception as e:
909
+ logger.exception(f"Error in stdio loop: {e}")
910
+
911
+ async def run_http(self, host: str = "0.0.0.0", port: int = 8765):
912
+ """
913
+ Run the server in HTTP mode for remote access.
914
+
915
+ Note: Requires aiohttp (optional dependency).
916
+ """
917
+ try:
918
+ from aiohttp import web
919
+ except ImportError:
920
+ logger.error(
921
+ "aiohttp required for HTTP mode. Install with: pip install aiohttp"
922
+ )
923
+ return
924
+
925
+ async def handle_post(request: web.Request) -> web.Response:
926
+ """Handle HTTP POST requests."""
927
+ try:
928
+ data = await request.json()
929
+ response = await self.handle_request(data)
930
+ return web.json_response(response)
931
+ except Exception as e:
932
+ return web.json_response(
933
+ {"error": str(e)},
934
+ status=500,
935
+ )
936
+
937
+ async def handle_health(request: web.Request) -> web.Response:
938
+ """Handle health check endpoint."""
939
+ result = alma_health(self.alma)
940
+ return web.json_response(result)
941
+
942
+ app = web.Application()
943
+ app.router.add_post("/", handle_post)
944
+ app.router.add_get("/health", handle_health)
945
+
946
+ runner = web.AppRunner(app)
947
+ await runner.setup()
948
+ site = web.TCPSite(runner, host, port)
949
+ await site.start()
950
+
951
+ logger.info(f"ALMA MCP Server running on http://{host}:{port}")
952
+
953
+ # Keep running
954
+ while True:
955
+ await asyncio.sleep(3600)