mem-brain-mcp 1.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.
@@ -0,0 +1,4 @@
1
+ """Mem-Brain MCP Server - Exposes Mem-Brain API as MCP tools."""
2
+
3
+ __version__ = "1.0.0"
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,242 @@
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
+ async with httpx.AsyncClient(timeout=30.0) as client:
61
+ response = await client.request(
62
+ method=method,
63
+ url=url,
64
+ headers=headers,
65
+ **kwargs
66
+ )
67
+ try:
68
+ response.raise_for_status()
69
+ return response.json()
70
+ except httpx.HTTPStatusError as e:
71
+ error_detail = e.response.text if e.response else "No response body"
72
+ logger.error(f"API request failed: {e.request.method} {e.request.url} - {e.response.status_code}: {error_detail}")
73
+ raise
74
+
75
+ async def add_memory(
76
+ self,
77
+ content: str,
78
+ tags: Optional[List[str]] = None,
79
+ category: Optional[str] = None
80
+ ) -> Dict[str, Any]:
81
+ """Add a new memory.
82
+
83
+ Args:
84
+ content: Memory content
85
+ tags: Optional list of tags
86
+ category: Optional category
87
+
88
+ Returns:
89
+ Response with memory_id and memory data
90
+ """
91
+ data = {"content": content}
92
+ if tags:
93
+ data["tags"] = tags
94
+ if category:
95
+ data["category"] = category
96
+
97
+ return await self._request("POST", "/memories", json=data)
98
+
99
+ async def search_memories(
100
+ self,
101
+ query: str,
102
+ k: int = 5
103
+ ) -> Dict[str, Any]:
104
+ """Search memories using semantic similarity.
105
+
106
+ Args:
107
+ query: Search query string
108
+ k: Number of results to return (1-100)
109
+
110
+ Returns:
111
+ Search results
112
+ """
113
+ data = {"query": query, "k": k}
114
+ return await self._request("POST", "/memories/search", json=data)
115
+
116
+ async def get_memories(
117
+ self,
118
+ memory_ids: List[str]
119
+ ) -> Dict[str, Any]:
120
+ """Retrieve multiple memories by ID.
121
+
122
+ Args:
123
+ memory_ids: List of memory IDs to retrieve
124
+
125
+ Returns:
126
+ Response with memories
127
+ """
128
+ data = {"memory_ids": memory_ids}
129
+ return await self._request("POST", "/memories/batch", json=data)
130
+
131
+ async def update_memory(
132
+ self,
133
+ memory_id: str,
134
+ content: Optional[str] = None,
135
+ tags: Optional[List[str]] = None
136
+ ) -> Dict[str, Any]:
137
+ """Update an existing memory.
138
+
139
+ Args:
140
+ memory_id: Memory ID to update
141
+ content: New content (optional)
142
+ tags: New tags (optional)
143
+
144
+ Returns:
145
+ Updated memory data
146
+ """
147
+ data = {}
148
+ if content is not None:
149
+ data["content"] = content
150
+ if tags is not None:
151
+ data["tags"] = tags
152
+
153
+ return await self._request("PUT", f"/memories/{memory_id}", json=data)
154
+
155
+ async def delete_memories(
156
+ self,
157
+ memory_id: Optional[str] = None,
158
+ tags: Optional[str] = None,
159
+ category: Optional[str] = None
160
+ ) -> Dict[str, Any]:
161
+ """Delete memories by ID or filter.
162
+
163
+ Args:
164
+ memory_id: Specific memory ID to delete (takes precedence)
165
+ tags: Comma-separated tags for filter-based deletion
166
+ category: Category for filter-based deletion
167
+
168
+ Returns:
169
+ Deletion response
170
+ """
171
+ params = {}
172
+ if memory_id:
173
+ params["memory_id"] = memory_id
174
+ if tags:
175
+ params["tags"] = tags
176
+ if category:
177
+ params["category"] = category
178
+
179
+ return await self._request("DELETE", "/memories/bulk", params=params)
180
+
181
+ async def unlink_memories(
182
+ self,
183
+ memory_id_1: str,
184
+ memory_id_2: str
185
+ ) -> Dict[str, Any]:
186
+ """Remove link between two memories.
187
+
188
+ Args:
189
+ memory_id_1: First memory ID
190
+ memory_id_2: Second memory ID
191
+
192
+ Returns:
193
+ Unlink response
194
+ """
195
+ data = {
196
+ "memory_id_1": memory_id_1,
197
+ "memory_id_2": memory_id_2
198
+ }
199
+ return await self._request("POST", "/memories/unlink", json=data)
200
+
201
+ async def get_stats(self) -> Dict[str, Any]:
202
+ """Get memory system statistics.
203
+
204
+ Returns:
205
+ Statistics response
206
+ """
207
+ return await self._request("GET", "/stats")
208
+
209
+ async def find_path(
210
+ self,
211
+ from_id: str,
212
+ to_id: str
213
+ ) -> Dict[str, Any]:
214
+ """Find shortest path between two memories.
215
+
216
+ Args:
217
+ from_id: Source memory ID
218
+ to_id: Target memory ID
219
+
220
+ Returns:
221
+ Path response
222
+ """
223
+ params = {"from_id": from_id, "to_id": to_id}
224
+ return await self._request("GET", "/graph/path", params=params)
225
+
226
+ async def get_neighborhood(
227
+ self,
228
+ memory_id: str,
229
+ hops: int = 2
230
+ ) -> Dict[str, Any]:
231
+ """Get all memories within N hops of a given memory.
232
+
233
+ Args:
234
+ memory_id: Center memory ID
235
+ hops: Number of hops (1-5)
236
+
237
+ Returns:
238
+ Neighborhood response
239
+ """
240
+ params = {"memory_id": memory_id, "hops": hops}
241
+ return await self._request("GET", "/graph/neighborhood", params=params)
242
+
@@ -0,0 +1,40 @@
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://localhost:8000"
20
+ 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
+ # MCP Server Configuration
26
+ mcp_server_host: str = "0.0.0.0"
27
+ mcp_server_port: int = 8100
28
+
29
+ # Logging
30
+ log_level: str = "INFO"
31
+
32
+ @property
33
+ def api_url(self) -> str:
34
+ """Get the full API base URL."""
35
+ return self.api_base_url.rstrip("/")
36
+
37
+
38
+ # Global settings instance
39
+ settings = Settings()
40
+