alma-memory 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alma/__init__.py +75 -0
- alma/config/__init__.py +5 -0
- alma/config/loader.py +156 -0
- alma/core.py +322 -0
- alma/harness/__init__.py +35 -0
- alma/harness/base.py +377 -0
- alma/harness/domains.py +689 -0
- alma/integration/__init__.py +62 -0
- alma/integration/claude_agents.py +432 -0
- alma/integration/helena.py +413 -0
- alma/integration/victor.py +447 -0
- alma/learning/__init__.py +86 -0
- alma/learning/forgetting.py +1396 -0
- alma/learning/heuristic_extractor.py +374 -0
- alma/learning/protocols.py +326 -0
- alma/learning/validation.py +341 -0
- alma/mcp/__init__.py +45 -0
- alma/mcp/__main__.py +155 -0
- alma/mcp/resources.py +121 -0
- alma/mcp/server.py +533 -0
- alma/mcp/tools.py +374 -0
- alma/retrieval/__init__.py +53 -0
- alma/retrieval/cache.py +1062 -0
- alma/retrieval/embeddings.py +202 -0
- alma/retrieval/engine.py +287 -0
- alma/retrieval/scoring.py +334 -0
- alma/storage/__init__.py +20 -0
- alma/storage/azure_cosmos.py +972 -0
- alma/storage/base.py +372 -0
- alma/storage/file_based.py +583 -0
- alma/storage/sqlite_local.py +912 -0
- alma/types.py +216 -0
- alma_memory-0.2.0.dist-info/METADATA +327 -0
- alma_memory-0.2.0.dist-info/RECORD +36 -0
- alma_memory-0.2.0.dist-info/WHEEL +5 -0
- alma_memory-0.2.0.dist-info/top_level.txt +1 -0
alma/mcp/server.py
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
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 Dict, Any, Optional, List, Callable
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
from alma import ALMA
|
|
16
|
+
from alma.mcp.tools import (
|
|
17
|
+
alma_retrieve,
|
|
18
|
+
alma_learn,
|
|
19
|
+
alma_add_preference,
|
|
20
|
+
alma_add_knowledge,
|
|
21
|
+
alma_forget,
|
|
22
|
+
alma_stats,
|
|
23
|
+
alma_health,
|
|
24
|
+
)
|
|
25
|
+
from alma.mcp.resources import (
|
|
26
|
+
get_config_resource,
|
|
27
|
+
get_agents_resource,
|
|
28
|
+
list_resources,
|
|
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
|
+
|
|
240
|
+
async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
241
|
+
"""
|
|
242
|
+
Handle an incoming MCP request.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
request: The MCP request
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
MCP response
|
|
249
|
+
"""
|
|
250
|
+
method = request.get("method", "")
|
|
251
|
+
params = request.get("params", {})
|
|
252
|
+
request_id = request.get("id")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
if method == "initialize":
|
|
256
|
+
return self._handle_initialize(request_id, params)
|
|
257
|
+
elif method == "tools/list":
|
|
258
|
+
return self._handle_tools_list(request_id)
|
|
259
|
+
elif method == "tools/call":
|
|
260
|
+
return await self._handle_tool_call(request_id, params)
|
|
261
|
+
elif method == "resources/list":
|
|
262
|
+
return self._handle_resources_list(request_id)
|
|
263
|
+
elif method == "resources/read":
|
|
264
|
+
return self._handle_resource_read(request_id, params)
|
|
265
|
+
elif method == "ping":
|
|
266
|
+
return self._success_response(request_id, {})
|
|
267
|
+
else:
|
|
268
|
+
return self._error_response(
|
|
269
|
+
request_id,
|
|
270
|
+
-32601,
|
|
271
|
+
f"Method not found: {method}",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.exception(f"Error handling request: {e}")
|
|
276
|
+
return self._error_response(request_id, -32603, str(e))
|
|
277
|
+
|
|
278
|
+
def _handle_initialize(
|
|
279
|
+
self,
|
|
280
|
+
request_id: Optional[int],
|
|
281
|
+
params: Dict[str, Any],
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""Handle initialize request."""
|
|
284
|
+
return self._success_response(request_id, {
|
|
285
|
+
"protocolVersion": "2024-11-05",
|
|
286
|
+
"serverInfo": {
|
|
287
|
+
"name": self.server_name,
|
|
288
|
+
"version": self.server_version,
|
|
289
|
+
},
|
|
290
|
+
"capabilities": {
|
|
291
|
+
"tools": {},
|
|
292
|
+
"resources": {},
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
def _handle_tools_list(self, request_id: Optional[int]) -> Dict[str, Any]:
|
|
297
|
+
"""Handle tools/list request."""
|
|
298
|
+
return self._success_response(request_id, {"tools": self.tools})
|
|
299
|
+
|
|
300
|
+
async def _handle_tool_call(
|
|
301
|
+
self,
|
|
302
|
+
request_id: Optional[int],
|
|
303
|
+
params: Dict[str, Any],
|
|
304
|
+
) -> Dict[str, Any]:
|
|
305
|
+
"""Handle tools/call request."""
|
|
306
|
+
tool_name = params.get("name", "")
|
|
307
|
+
arguments = params.get("arguments", {})
|
|
308
|
+
|
|
309
|
+
# Map tool names to functions
|
|
310
|
+
tool_handlers = {
|
|
311
|
+
"alma_retrieve": lambda: alma_retrieve(
|
|
312
|
+
self.alma,
|
|
313
|
+
task=arguments.get("task", ""),
|
|
314
|
+
agent=arguments.get("agent", ""),
|
|
315
|
+
user_id=arguments.get("user_id"),
|
|
316
|
+
top_k=arguments.get("top_k", 5),
|
|
317
|
+
),
|
|
318
|
+
"alma_learn": lambda: alma_learn(
|
|
319
|
+
self.alma,
|
|
320
|
+
agent=arguments.get("agent", ""),
|
|
321
|
+
task=arguments.get("task", ""),
|
|
322
|
+
outcome=arguments.get("outcome", ""),
|
|
323
|
+
strategy_used=arguments.get("strategy_used", ""),
|
|
324
|
+
task_type=arguments.get("task_type"),
|
|
325
|
+
duration_ms=arguments.get("duration_ms"),
|
|
326
|
+
error_message=arguments.get("error_message"),
|
|
327
|
+
feedback=arguments.get("feedback"),
|
|
328
|
+
),
|
|
329
|
+
"alma_add_preference": lambda: alma_add_preference(
|
|
330
|
+
self.alma,
|
|
331
|
+
user_id=arguments.get("user_id", ""),
|
|
332
|
+
category=arguments.get("category", ""),
|
|
333
|
+
preference=arguments.get("preference", ""),
|
|
334
|
+
source=arguments.get("source", "explicit_instruction"),
|
|
335
|
+
),
|
|
336
|
+
"alma_add_knowledge": lambda: alma_add_knowledge(
|
|
337
|
+
self.alma,
|
|
338
|
+
agent=arguments.get("agent", ""),
|
|
339
|
+
domain=arguments.get("domain", ""),
|
|
340
|
+
fact=arguments.get("fact", ""),
|
|
341
|
+
source=arguments.get("source", "user_stated"),
|
|
342
|
+
),
|
|
343
|
+
"alma_forget": lambda: alma_forget(
|
|
344
|
+
self.alma,
|
|
345
|
+
agent=arguments.get("agent"),
|
|
346
|
+
older_than_days=arguments.get("older_than_days", 90),
|
|
347
|
+
below_confidence=arguments.get("below_confidence", 0.3),
|
|
348
|
+
),
|
|
349
|
+
"alma_stats": lambda: alma_stats(
|
|
350
|
+
self.alma,
|
|
351
|
+
agent=arguments.get("agent"),
|
|
352
|
+
),
|
|
353
|
+
"alma_health": lambda: alma_health(self.alma),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if tool_name not in tool_handlers:
|
|
357
|
+
return self._error_response(
|
|
358
|
+
request_id,
|
|
359
|
+
-32602,
|
|
360
|
+
f"Unknown tool: {tool_name}",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
result = tool_handlers[tool_name]()
|
|
364
|
+
|
|
365
|
+
return self._success_response(request_id, {
|
|
366
|
+
"content": [
|
|
367
|
+
{
|
|
368
|
+
"type": "text",
|
|
369
|
+
"text": json.dumps(result, indent=2),
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
def _handle_resources_list(
|
|
375
|
+
self,
|
|
376
|
+
request_id: Optional[int],
|
|
377
|
+
) -> Dict[str, Any]:
|
|
378
|
+
"""Handle resources/list request."""
|
|
379
|
+
return self._success_response(request_id, {"resources": self.resources})
|
|
380
|
+
|
|
381
|
+
def _handle_resource_read(
|
|
382
|
+
self,
|
|
383
|
+
request_id: Optional[int],
|
|
384
|
+
params: Dict[str, Any],
|
|
385
|
+
) -> Dict[str, Any]:
|
|
386
|
+
"""Handle resources/read request."""
|
|
387
|
+
uri = params.get("uri", "")
|
|
388
|
+
|
|
389
|
+
if uri == "alma://config":
|
|
390
|
+
resource = get_config_resource(self.alma)
|
|
391
|
+
elif uri == "alma://agents":
|
|
392
|
+
resource = get_agents_resource(self.alma)
|
|
393
|
+
else:
|
|
394
|
+
return self._error_response(
|
|
395
|
+
request_id,
|
|
396
|
+
-32602,
|
|
397
|
+
f"Unknown resource: {uri}",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return self._success_response(request_id, {
|
|
401
|
+
"contents": [
|
|
402
|
+
{
|
|
403
|
+
"uri": resource["uri"],
|
|
404
|
+
"mimeType": resource["mimeType"],
|
|
405
|
+
"text": json.dumps(resource["content"], indent=2),
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
def _success_response(
|
|
411
|
+
self,
|
|
412
|
+
request_id: Optional[int],
|
|
413
|
+
result: Any,
|
|
414
|
+
) -> Dict[str, Any]:
|
|
415
|
+
"""Create a success response."""
|
|
416
|
+
return {
|
|
417
|
+
"jsonrpc": "2.0",
|
|
418
|
+
"id": request_id,
|
|
419
|
+
"result": result,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
def _error_response(
|
|
423
|
+
self,
|
|
424
|
+
request_id: Optional[int],
|
|
425
|
+
code: int,
|
|
426
|
+
message: str,
|
|
427
|
+
) -> Dict[str, Any]:
|
|
428
|
+
"""Create an error response."""
|
|
429
|
+
return {
|
|
430
|
+
"jsonrpc": "2.0",
|
|
431
|
+
"id": request_id,
|
|
432
|
+
"error": {
|
|
433
|
+
"code": code,
|
|
434
|
+
"message": message,
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async def run_stdio(self):
|
|
439
|
+
"""Run the server in stdio mode for Claude Code integration."""
|
|
440
|
+
logger.info(f"Starting ALMA MCP Server (stdio mode)")
|
|
441
|
+
|
|
442
|
+
reader = asyncio.StreamReader()
|
|
443
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
444
|
+
await asyncio.get_event_loop().connect_read_pipe(
|
|
445
|
+
lambda: protocol, sys.stdin
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
writer_transport, writer_protocol = await asyncio.get_event_loop().connect_write_pipe(
|
|
449
|
+
asyncio.streams.FlowControlMixin, sys.stdout
|
|
450
|
+
)
|
|
451
|
+
writer = asyncio.StreamWriter(
|
|
452
|
+
writer_transport, writer_protocol, None, asyncio.get_event_loop()
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
while True:
|
|
456
|
+
try:
|
|
457
|
+
# Read Content-Length header
|
|
458
|
+
header_line = await reader.readline()
|
|
459
|
+
if not header_line:
|
|
460
|
+
break
|
|
461
|
+
|
|
462
|
+
header = header_line.decode().strip()
|
|
463
|
+
if not header.startswith("Content-Length:"):
|
|
464
|
+
continue
|
|
465
|
+
|
|
466
|
+
content_length = int(header.split(":")[1].strip())
|
|
467
|
+
|
|
468
|
+
# Read empty line
|
|
469
|
+
await reader.readline()
|
|
470
|
+
|
|
471
|
+
# Read content
|
|
472
|
+
content = await reader.read(content_length)
|
|
473
|
+
request = json.loads(content.decode())
|
|
474
|
+
|
|
475
|
+
# Handle request
|
|
476
|
+
response = await self.handle_request(request)
|
|
477
|
+
|
|
478
|
+
# Send response
|
|
479
|
+
response_str = json.dumps(response)
|
|
480
|
+
response_bytes = response_str.encode()
|
|
481
|
+
header_bytes = f"Content-Length: {len(response_bytes)}\r\n\r\n".encode()
|
|
482
|
+
|
|
483
|
+
writer.write(header_bytes + response_bytes)
|
|
484
|
+
await writer.drain()
|
|
485
|
+
|
|
486
|
+
except asyncio.CancelledError:
|
|
487
|
+
break
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.exception(f"Error in stdio loop: {e}")
|
|
490
|
+
|
|
491
|
+
async def run_http(self, host: str = "0.0.0.0", port: int = 8765):
|
|
492
|
+
"""
|
|
493
|
+
Run the server in HTTP mode for remote access.
|
|
494
|
+
|
|
495
|
+
Note: Requires aiohttp (optional dependency).
|
|
496
|
+
"""
|
|
497
|
+
try:
|
|
498
|
+
from aiohttp import web
|
|
499
|
+
except ImportError:
|
|
500
|
+
logger.error("aiohttp required for HTTP mode. Install with: pip install aiohttp")
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
async def handle_post(request: web.Request) -> web.Response:
|
|
504
|
+
"""Handle HTTP POST requests."""
|
|
505
|
+
try:
|
|
506
|
+
data = await request.json()
|
|
507
|
+
response = await self.handle_request(data)
|
|
508
|
+
return web.json_response(response)
|
|
509
|
+
except Exception as e:
|
|
510
|
+
return web.json_response(
|
|
511
|
+
{"error": str(e)},
|
|
512
|
+
status=500,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
async def handle_health(request: web.Request) -> web.Response:
|
|
516
|
+
"""Handle health check endpoint."""
|
|
517
|
+
result = alma_health(self.alma)
|
|
518
|
+
return web.json_response(result)
|
|
519
|
+
|
|
520
|
+
app = web.Application()
|
|
521
|
+
app.router.add_post("/", handle_post)
|
|
522
|
+
app.router.add_get("/health", handle_health)
|
|
523
|
+
|
|
524
|
+
runner = web.AppRunner(app)
|
|
525
|
+
await runner.setup()
|
|
526
|
+
site = web.TCPSite(runner, host, port)
|
|
527
|
+
await site.start()
|
|
528
|
+
|
|
529
|
+
logger.info(f"ALMA MCP Server running on http://{host}:{port}")
|
|
530
|
+
|
|
531
|
+
# Keep running
|
|
532
|
+
while True:
|
|
533
|
+
await asyncio.sleep(3600)
|