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.
- mem_brain_mcp/__init__.py +4 -0
- mem_brain_mcp/__main__.py +29 -0
- mem_brain_mcp/client.py +242 -0
- mem_brain_mcp/config.py +40 -0
- mem_brain_mcp/server.py +939 -0
- mem_brain_mcp-1.0.0.dist-info/METADATA +176 -0
- mem_brain_mcp-1.0.0.dist-info/RECORD +9 -0
- mem_brain_mcp-1.0.0.dist-info/WHEEL +4 -0
- mem_brain_mcp-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -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()
|
mem_brain_mcp/client.py
ADDED
|
@@ -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
|
+
|
mem_brain_mcp/config.py
ADDED
|
@@ -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
|
+
|