mem-brain-mcp 1.0.6__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.
@@ -0,0 +1,4 @@
1
+ """Mem-Brain MCP Server - Exposes Mem-Brain API as MCP tools."""
2
+
3
+ __version__ = "1.0.6"
4
+
@@ -0,0 +1,29 @@
1
+ """Entry point for Mem-Brain MCP Server."""
2
+
3
+ import logging
4
+ import sys
5
+
6
+ from mem_brain_mcp.config import settings
7
+ from mem_brain_mcp.server import run_server
8
+
9
+ def main():
10
+ """Main entry point for the CLI."""
11
+ # Setup logging
12
+ logging.basicConfig(
13
+ level=getattr(logging, settings.log_level.upper()),
14
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
15
+ stream=sys.stderr
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ try:
21
+ run_server()
22
+ except KeyboardInterrupt:
23
+ logger.info("Server stopped by user")
24
+ except Exception as e:
25
+ logger.error(f"Server error: {e}", exc_info=True)
26
+ sys.exit(1)
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,260 @@
1
+ """HTTP client for Mem-Brain API."""
2
+
3
+ import logging
4
+ from typing import List, Optional, Dict, Any
5
+ import httpx
6
+
7
+ from mem_brain_mcp.config import settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class APIClient:
13
+ """HTTP client for interacting with Mem-Brain API."""
14
+
15
+ def __init__(self, api_url: Optional[str] = None, api_key: Optional[str] = None):
16
+ """Initialize API client.
17
+
18
+ Args:
19
+ api_url: Base URL for the API (defaults to settings.api_url)
20
+ api_key: API key for authentication (defaults to settings.api_key)
21
+ """
22
+ self.api_url = api_url or settings.api_url
23
+ self.api_key = api_key or settings.api_key
24
+ self.base_url = f"{self.api_url}/api/v1"
25
+
26
+ def _get_headers(self) -> Dict[str, str]:
27
+ """Get request headers with authentication."""
28
+ headers = {"Content-Type": "application/json"}
29
+ if self.api_key:
30
+ # api_key parameter now holds JWT token - use Bearer authentication
31
+ headers["Authorization"] = f"Bearer {self.api_key}"
32
+ logger.debug(f"Using JWT token authentication: {self.api_key[:20]}...")
33
+ else:
34
+ logger.debug("No authentication token configured for this client instance")
35
+ return headers
36
+
37
+ async def _request(
38
+ self,
39
+ method: str,
40
+ endpoint: str,
41
+ **kwargs
42
+ ) -> Dict[str, Any]:
43
+ """Make HTTP request to API.
44
+
45
+ Args:
46
+ method: HTTP method (GET, POST, PUT, DELETE)
47
+ endpoint: API endpoint path (without /api/v1 prefix)
48
+ **kwargs: Additional arguments for httpx request
49
+
50
+ Returns:
51
+ Response JSON data
52
+
53
+ Raises:
54
+ httpx.HTTPError: If request fails
55
+ """
56
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
57
+ headers = self._get_headers()
58
+ headers.update(kwargs.pop("headers", {}))
59
+
60
+ # Debug logging for request details
61
+ logger.debug(f"Making {method} request to: {url}")
62
+ logger.debug(f"Headers: {dict((k, v[:20] + '...' if k == 'Authorization' and len(v) > 20 else v) for k, v in headers.items())}")
63
+ if kwargs.get("json"):
64
+ logger.debug(f"Request body: {kwargs.get('json')}")
65
+ if kwargs.get("params"):
66
+ logger.debug(f"Request params: {kwargs.get('params')}")
67
+
68
+ async with httpx.AsyncClient(timeout=30.0) as client:
69
+ try:
70
+ response = await client.request(
71
+ method=method,
72
+ url=url,
73
+ headers=headers,
74
+ **kwargs
75
+ )
76
+ logger.debug(f"Response status: {response.status_code}")
77
+ logger.debug(f"Response headers: {dict(response.headers)}")
78
+
79
+ try:
80
+ response.raise_for_status()
81
+ result = response.json()
82
+ logger.debug(f"Response data keys: {list(result.keys()) if isinstance(result, dict) else 'N/A'}")
83
+ return result
84
+ except httpx.HTTPStatusError as e:
85
+ error_detail = e.response.text if e.response else "No response body"
86
+ logger.error(f"API request failed: {e.request.method} {e.request.url} - {e.response.status_code}: {error_detail}")
87
+ raise
88
+ except httpx.RequestError as e:
89
+ logger.error(f"Request error: {type(e).__name__}: {str(e)}")
90
+ raise
91
+
92
+ async def add_memory(
93
+ self,
94
+ content: str,
95
+ tags: Optional[List[str]] = None,
96
+ category: Optional[str] = None
97
+ ) -> Dict[str, Any]:
98
+ """Add a new memory.
99
+
100
+ Args:
101
+ content: Memory content
102
+ tags: Optional list of tags
103
+ category: Optional category
104
+
105
+ Returns:
106
+ Response with memory_id and memory data
107
+ """
108
+ data = {"content": content}
109
+ if tags:
110
+ data["tags"] = tags
111
+ if category:
112
+ data["category"] = category
113
+
114
+ return await self._request("POST", "/memories", json=data)
115
+
116
+ async def search_memories(
117
+ self,
118
+ query: str,
119
+ k: int = 5
120
+ ) -> Dict[str, Any]:
121
+ """Search memories using semantic similarity.
122
+
123
+ Args:
124
+ query: Search query string
125
+ k: Number of results to return (1-100)
126
+
127
+ Returns:
128
+ Search results
129
+ """
130
+ data = {"query": query, "k": k}
131
+ return await self._request("POST", "/memories/search", json=data)
132
+
133
+ async def get_memories(
134
+ self,
135
+ memory_ids: List[str]
136
+ ) -> Dict[str, Any]:
137
+ """Retrieve multiple memories by ID.
138
+
139
+ Args:
140
+ memory_ids: List of memory IDs to retrieve
141
+
142
+ Returns:
143
+ Response with memories
144
+ """
145
+ data = {"memory_ids": memory_ids}
146
+ return await self._request("POST", "/memories/batch", json=data)
147
+
148
+ async def update_memory(
149
+ self,
150
+ memory_id: str,
151
+ content: Optional[str] = None,
152
+ tags: Optional[List[str]] = None
153
+ ) -> Dict[str, Any]:
154
+ """Update an existing memory.
155
+
156
+ Args:
157
+ memory_id: Memory ID to update
158
+ content: New content (optional)
159
+ tags: New tags (optional)
160
+
161
+ Returns:
162
+ Updated memory data
163
+ """
164
+ data = {}
165
+ if content is not None:
166
+ data["content"] = content
167
+ if tags is not None:
168
+ data["tags"] = tags
169
+
170
+ return await self._request("PUT", f"/memories/{memory_id}", json=data)
171
+
172
+ async def delete_memories(
173
+ self,
174
+ memory_id: Optional[str] = None,
175
+ tags: Optional[str] = None,
176
+ category: Optional[str] = None
177
+ ) -> Dict[str, Any]:
178
+ """Delete memories by ID or filter.
179
+
180
+ Args:
181
+ memory_id: Specific memory ID to delete (takes precedence)
182
+ tags: Comma-separated tags for filter-based deletion
183
+ category: Category for filter-based deletion
184
+
185
+ Returns:
186
+ Deletion response
187
+ """
188
+ params = {}
189
+ if memory_id:
190
+ params["memory_id"] = memory_id
191
+ if tags:
192
+ params["tags"] = tags
193
+ if category:
194
+ params["category"] = category
195
+
196
+ return await self._request("DELETE", "/memories/bulk", params=params)
197
+
198
+ async def unlink_memories(
199
+ self,
200
+ memory_id_1: str,
201
+ memory_id_2: str
202
+ ) -> Dict[str, Any]:
203
+ """Remove link between two memories.
204
+
205
+ Args:
206
+ memory_id_1: First memory ID
207
+ memory_id_2: Second memory ID
208
+
209
+ Returns:
210
+ Unlink response
211
+ """
212
+ data = {
213
+ "memory_id_1": memory_id_1,
214
+ "memory_id_2": memory_id_2
215
+ }
216
+ return await self._request("POST", "/memories/unlink", json=data)
217
+
218
+ async def get_stats(self) -> Dict[str, Any]:
219
+ """Get memory system statistics.
220
+
221
+ Returns:
222
+ Statistics response
223
+ """
224
+ logger.debug("get_stats() called - making request to /stats endpoint")
225
+ return await self._request("GET", "/stats")
226
+
227
+ async def find_path(
228
+ self,
229
+ from_id: str,
230
+ to_id: str
231
+ ) -> Dict[str, Any]:
232
+ """Find shortest path between two memories.
233
+
234
+ Args:
235
+ from_id: Source memory ID
236
+ to_id: Target memory ID
237
+
238
+ Returns:
239
+ Path response
240
+ """
241
+ params = {"from_id": from_id, "to_id": to_id}
242
+ return await self._request("GET", "/graph/path", params=params)
243
+
244
+ async def get_neighborhood(
245
+ self,
246
+ memory_id: str,
247
+ hops: int = 2
248
+ ) -> Dict[str, Any]:
249
+ """Get all memories within N hops of a given memory.
250
+
251
+ Args:
252
+ memory_id: Center memory ID
253
+ hops: Number of hops (1-5)
254
+
255
+ Returns:
256
+ Neighborhood response
257
+ """
258
+ params = {"memory_id": memory_id, "hops": hops}
259
+ return await self._request("GET", "/graph/neighborhood", params=params)
260
+
@@ -0,0 +1,45 @@
1
+ """Configuration management for Mem-Brain MCP Server."""
2
+
3
+ import os
4
+ from typing import Optional
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ """Application settings loaded from environment variables."""
10
+
11
+ model_config = SettingsConfigDict(
12
+ env_file=".env",
13
+ env_file_encoding="utf-8",
14
+ case_sensitive=False,
15
+ extra="ignore"
16
+ )
17
+
18
+ # API Configuration
19
+ api_base_url: str = "http://membrain-api-alb-1094729422.ap-south-1.elb.amazonaws.com"
20
+ membrain_api_key: Optional[str] = None
21
+ # NOTE: default_user_id is deprecated and unused.
22
+ # Per-user API keys are extracted from request headers for proper isolation.
23
+ # Each MCP client should configure their own API key via headers.
24
+
25
+ @property
26
+ def api_key(self) -> Optional[str]:
27
+ """Backward compatibility property for api_key."""
28
+ return self.membrain_api_key
29
+
30
+ # MCP Server Configuration
31
+ mcp_server_host: str = "0.0.0.0"
32
+ mcp_server_port: int = 8100
33
+
34
+ # Logging
35
+ log_level: str = "INFO"
36
+
37
+ @property
38
+ def api_url(self) -> str:
39
+ """Get the full API base URL."""
40
+ return self.api_base_url.rstrip("/")
41
+
42
+
43
+ # Global settings instance
44
+ settings = Settings()
45
+