spatial-memory-mcp 1.6.1__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.

Potentially problematic release.


This version of spatial-memory-mcp might be problematic. Click here for more details.

Files changed (54) hide show
  1. spatial_memory/__init__.py +97 -0
  2. spatial_memory/__main__.py +270 -0
  3. spatial_memory/adapters/__init__.py +7 -0
  4. spatial_memory/adapters/lancedb_repository.py +878 -0
  5. spatial_memory/config.py +728 -0
  6. spatial_memory/core/__init__.py +118 -0
  7. spatial_memory/core/cache.py +317 -0
  8. spatial_memory/core/circuit_breaker.py +297 -0
  9. spatial_memory/core/connection_pool.py +220 -0
  10. spatial_memory/core/consolidation_strategies.py +402 -0
  11. spatial_memory/core/database.py +3069 -0
  12. spatial_memory/core/db_idempotency.py +242 -0
  13. spatial_memory/core/db_indexes.py +575 -0
  14. spatial_memory/core/db_migrations.py +584 -0
  15. spatial_memory/core/db_search.py +509 -0
  16. spatial_memory/core/db_versioning.py +177 -0
  17. spatial_memory/core/embeddings.py +557 -0
  18. spatial_memory/core/errors.py +317 -0
  19. spatial_memory/core/file_security.py +702 -0
  20. spatial_memory/core/filesystem.py +178 -0
  21. spatial_memory/core/health.py +289 -0
  22. spatial_memory/core/helpers.py +79 -0
  23. spatial_memory/core/import_security.py +432 -0
  24. spatial_memory/core/lifecycle_ops.py +1067 -0
  25. spatial_memory/core/logging.py +194 -0
  26. spatial_memory/core/metrics.py +192 -0
  27. spatial_memory/core/models.py +628 -0
  28. spatial_memory/core/rate_limiter.py +326 -0
  29. spatial_memory/core/response_types.py +497 -0
  30. spatial_memory/core/security.py +588 -0
  31. spatial_memory/core/spatial_ops.py +426 -0
  32. spatial_memory/core/tracing.py +300 -0
  33. spatial_memory/core/utils.py +110 -0
  34. spatial_memory/core/validation.py +403 -0
  35. spatial_memory/factory.py +407 -0
  36. spatial_memory/migrations/__init__.py +40 -0
  37. spatial_memory/ports/__init__.py +11 -0
  38. spatial_memory/ports/repositories.py +631 -0
  39. spatial_memory/py.typed +0 -0
  40. spatial_memory/server.py +1141 -0
  41. spatial_memory/services/__init__.py +70 -0
  42. spatial_memory/services/export_import.py +1023 -0
  43. spatial_memory/services/lifecycle.py +1120 -0
  44. spatial_memory/services/memory.py +412 -0
  45. spatial_memory/services/spatial.py +1147 -0
  46. spatial_memory/services/utility.py +409 -0
  47. spatial_memory/tools/__init__.py +5 -0
  48. spatial_memory/tools/definitions.py +695 -0
  49. spatial_memory/verify.py +140 -0
  50. spatial_memory_mcp-1.6.1.dist-info/METADATA +499 -0
  51. spatial_memory_mcp-1.6.1.dist-info/RECORD +54 -0
  52. spatial_memory_mcp-1.6.1.dist-info/WHEEL +4 -0
  53. spatial_memory_mcp-1.6.1.dist-info/entry_points.txt +2 -0
  54. spatial_memory_mcp-1.6.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,409 @@
1
+ """Utility service for database management operations.
2
+
3
+ This service provides the application layer for utility operations:
4
+ - stats: Get database statistics and health metrics
5
+ - namespaces: List namespaces with memory counts
6
+ - delete_namespace: Delete all memories in a namespace
7
+ - rename_namespace: Rename namespace (move all memories)
8
+ - hybrid_recall: Combined vector + FTS search
9
+
10
+ The service uses dependency injection for repository and embedding services.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import TYPE_CHECKING
17
+
18
+ from spatial_memory.core.errors import (
19
+ NamespaceNotFoundError,
20
+ NamespaceOperationError,
21
+ ValidationError,
22
+ )
23
+ from spatial_memory.core.models import (
24
+ DeleteNamespaceResult,
25
+ HybridMemoryMatch,
26
+ HybridRecallResult,
27
+ IndexInfo,
28
+ NamespaceInfo,
29
+ NamespacesResult,
30
+ RenameNamespaceResult,
31
+ StatsResult,
32
+ UtilityConfig,
33
+ )
34
+ from spatial_memory.core.validation import validate_namespace
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ if TYPE_CHECKING:
39
+ from spatial_memory.ports.repositories import (
40
+ EmbeddingServiceProtocol,
41
+ MemoryRepositoryProtocol,
42
+ )
43
+
44
+
45
+ class UtilityService:
46
+ """Service for database utility operations.
47
+
48
+ Uses Clean Architecture - depends on protocol interfaces, not implementations.
49
+ Provides database statistics, namespace management, and hybrid search.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ repository: MemoryRepositoryProtocol,
55
+ embeddings: EmbeddingServiceProtocol,
56
+ config: UtilityConfig | None = None,
57
+ ) -> None:
58
+ """Initialize the utility service.
59
+
60
+ Args:
61
+ repository: Repository for memory storage.
62
+ embeddings: Service for generating embeddings.
63
+ config: Optional configuration (uses defaults if not provided).
64
+ """
65
+ self._repo = repository
66
+ self._embeddings = embeddings
67
+ self._config = config or UtilityConfig()
68
+
69
+ def stats(
70
+ self,
71
+ namespace: str | None = None,
72
+ include_index_details: bool = True,
73
+ ) -> StatsResult:
74
+ """Get comprehensive database statistics.
75
+
76
+ Retrieves statistics about the memory database including total counts,
77
+ storage size, index information, and health metrics.
78
+
79
+ Args:
80
+ namespace: Filter statistics to a specific namespace.
81
+ If None, returns statistics for all namespaces.
82
+ include_index_details: Include detailed index statistics.
83
+
84
+ Returns:
85
+ StatsResult with database information.
86
+
87
+ Raises:
88
+ ValidationError: If namespace is invalid.
89
+ NamespaceOperationError: If stats retrieval fails.
90
+ """
91
+ # Validate namespace if provided
92
+ if namespace is not None:
93
+ namespace = validate_namespace(namespace)
94
+
95
+ try:
96
+ # Get stats from repository
97
+ raw_stats = self._repo.get_stats(namespace=namespace)
98
+
99
+ # Transform indices to IndexInfo objects
100
+ indices: list[IndexInfo] = []
101
+ if include_index_details:
102
+ for idx_data in raw_stats.get("indices", []):
103
+ indices.append(
104
+ IndexInfo(
105
+ name=idx_data.get("name", "unknown"),
106
+ index_type=idx_data.get("index_type", "unknown"),
107
+ column=idx_data.get("column", "unknown"),
108
+ num_indexed_rows=idx_data.get("num_indexed_rows", 0),
109
+ status=idx_data.get("status", "unknown"),
110
+ )
111
+ )
112
+
113
+ # Get namespace breakdown from raw_stats
114
+ memories_by_namespace: dict[str, int] = raw_stats.get("namespaces", {})
115
+
116
+ # Calculate estimated vector bytes (dims * 4 bytes per float * num memories)
117
+ total_memories = raw_stats.get("total_memories", 0)
118
+ embedding_dims = self._embeddings.dimensions
119
+ estimated_vector_bytes = total_memories * embedding_dims * 4
120
+
121
+ return StatsResult(
122
+ total_memories=total_memories,
123
+ memories_by_namespace=memories_by_namespace,
124
+ storage_bytes=raw_stats.get("storage_bytes", 0),
125
+ storage_mb=raw_stats.get("storage_mb", 0.0),
126
+ estimated_vector_bytes=estimated_vector_bytes,
127
+ has_vector_index=raw_stats.get("has_vector_index", False),
128
+ has_fts_index=raw_stats.get("has_fts_index", False),
129
+ indices=indices,
130
+ num_fragments=raw_stats.get("num_fragments", 0),
131
+ needs_compaction=raw_stats.get("needs_compaction", False),
132
+ table_version=raw_stats.get("table_version", 1),
133
+ oldest_memory_date=raw_stats.get("oldest_memory_date"),
134
+ newest_memory_date=raw_stats.get("newest_memory_date"),
135
+ avg_content_length=raw_stats.get("avg_content_length"),
136
+ )
137
+
138
+ except ValidationError:
139
+ raise
140
+ except Exception as e:
141
+ raise NamespaceOperationError(f"Failed to get stats: {e}") from e
142
+
143
+ def namespaces(
144
+ self,
145
+ include_stats: bool = True,
146
+ ) -> NamespacesResult:
147
+ """List all namespaces with optional statistics.
148
+
149
+ Args:
150
+ include_stats: Include memory counts and date ranges per namespace.
151
+
152
+ Returns:
153
+ NamespacesResult with namespace list and totals.
154
+
155
+ Raises:
156
+ NamespaceOperationError: If namespace listing fails.
157
+ """
158
+ try:
159
+ # Get list of namespaces from repository
160
+ namespace_names = self._repo.get_namespaces()
161
+
162
+ namespaces: list[NamespaceInfo] = []
163
+ total_memories = 0
164
+
165
+ for ns_name in namespace_names:
166
+ if include_stats:
167
+ # Get detailed stats for each namespace
168
+ try:
169
+ ns_stats = self._repo.get_namespace_stats(ns_name)
170
+ memory_count = ns_stats.get("memory_count", 0)
171
+ namespaces.append(
172
+ NamespaceInfo(
173
+ name=ns_name,
174
+ memory_count=memory_count,
175
+ oldest_memory=ns_stats.get("oldest_memory"),
176
+ newest_memory=ns_stats.get("newest_memory"),
177
+ )
178
+ )
179
+ total_memories += memory_count
180
+ except Exception as e:
181
+ logger.warning(f"Failed to get stats for namespace {ns_name}: {e}")
182
+ # Still include namespace with zero count
183
+ namespaces.append(
184
+ NamespaceInfo(
185
+ name=ns_name,
186
+ memory_count=0,
187
+ )
188
+ )
189
+ else:
190
+ # Just include name without stats
191
+ namespaces.append(
192
+ NamespaceInfo(
193
+ name=ns_name,
194
+ memory_count=0,
195
+ )
196
+ )
197
+
198
+ # If we didn't include stats, get total from count
199
+ if not include_stats:
200
+ total_memories = self._repo.count()
201
+
202
+ return NamespacesResult(
203
+ namespaces=namespaces,
204
+ total_namespaces=len(namespaces),
205
+ total_memories=total_memories,
206
+ )
207
+
208
+ except Exception as e:
209
+ raise NamespaceOperationError(f"Failed to list namespaces: {e}") from e
210
+
211
+ def delete_namespace(
212
+ self,
213
+ namespace: str,
214
+ confirm: bool = False,
215
+ dry_run: bool = True,
216
+ ) -> DeleteNamespaceResult:
217
+ """Delete all memories in a namespace.
218
+
219
+ DESTRUCTIVE OPERATION - requires explicit confirmation unless dry_run.
220
+
221
+ Args:
222
+ namespace: Namespace to delete.
223
+ confirm: Set to True to confirm deletion (required for non-dry-run).
224
+ dry_run: Preview deletion without executing (default True).
225
+
226
+ Returns:
227
+ DeleteNamespaceResult with deletion details.
228
+
229
+ Raises:
230
+ ValidationError: If namespace is invalid or confirmation missing.
231
+ NamespaceOperationError: If deletion fails.
232
+ """
233
+ # Validate namespace
234
+ namespace = validate_namespace(namespace)
235
+
236
+ try:
237
+ # Get count of memories that would be deleted
238
+ memory_count = self._repo.count(namespace=namespace)
239
+
240
+ # If dry run, just return preview
241
+ if dry_run:
242
+ return DeleteNamespaceResult(
243
+ namespace=namespace,
244
+ memories_deleted=memory_count,
245
+ success=True,
246
+ message=f"DRY RUN: Would delete {memory_count} memories from namespace '{namespace}'",
247
+ dry_run=True,
248
+ )
249
+
250
+ # Not a dry run - require confirmation
251
+ if not confirm:
252
+ raise ValidationError(
253
+ f"Deletion of {memory_count} memories in namespace '{namespace}' "
254
+ "requires confirm=True. Use dry_run=True to preview first."
255
+ )
256
+
257
+ # Perform actual deletion
258
+ deleted_count = self._repo.delete_by_namespace(namespace)
259
+
260
+ return DeleteNamespaceResult(
261
+ namespace=namespace,
262
+ memories_deleted=deleted_count,
263
+ success=True,
264
+ message=f"Successfully deleted {deleted_count} memories from namespace '{namespace}'",
265
+ dry_run=False,
266
+ )
267
+
268
+ except ValidationError:
269
+ raise
270
+ except Exception as e:
271
+ raise NamespaceOperationError(
272
+ f"Failed to delete namespace '{namespace}': {e}"
273
+ ) from e
274
+
275
+ def rename_namespace(
276
+ self,
277
+ old_namespace: str,
278
+ new_namespace: str,
279
+ ) -> RenameNamespaceResult:
280
+ """Rename all memories from one namespace to another.
281
+
282
+ Atomically updates the namespace field for all memories belonging
283
+ to the source namespace.
284
+
285
+ Args:
286
+ old_namespace: The current namespace name (source).
287
+ new_namespace: The new namespace name (target).
288
+
289
+ Returns:
290
+ RenameNamespaceResult with rename details.
291
+
292
+ Raises:
293
+ ValidationError: If namespace names are invalid or the same.
294
+ NamespaceNotFoundError: If old_namespace doesn't exist.
295
+ NamespaceOperationError: If rename fails.
296
+ """
297
+ # Validate both namespaces
298
+ old_namespace = validate_namespace(old_namespace)
299
+ new_namespace = validate_namespace(new_namespace)
300
+
301
+ # Check they are different
302
+ if old_namespace == new_namespace:
303
+ raise ValidationError(
304
+ f"Cannot rename namespace to same name: '{old_namespace}'"
305
+ )
306
+
307
+ try:
308
+ # Call repository to perform rename
309
+ renamed_count = self._repo.rename_namespace(old_namespace, new_namespace)
310
+
311
+ return RenameNamespaceResult(
312
+ old_namespace=old_namespace,
313
+ new_namespace=new_namespace,
314
+ memories_renamed=renamed_count,
315
+ success=True,
316
+ message=f"Successfully renamed {renamed_count} memories from '{old_namespace}' to '{new_namespace}'",
317
+ )
318
+
319
+ except NamespaceNotFoundError:
320
+ raise
321
+ except ValidationError:
322
+ raise
323
+ except Exception as e:
324
+ raise NamespaceOperationError(
325
+ f"Failed to rename namespace '{old_namespace}' to '{new_namespace}': {e}"
326
+ ) from e
327
+
328
+ def hybrid_recall(
329
+ self,
330
+ query: str,
331
+ alpha: float = 0.5,
332
+ limit: int = 5,
333
+ namespace: str | None = None,
334
+ min_similarity: float = 0.0,
335
+ ) -> HybridRecallResult:
336
+ """Search using combined vector similarity and keyword matching.
337
+
338
+ Performs hybrid search combining semantic similarity (vector search)
339
+ and keyword matching (full-text search). Alpha parameter controls
340
+ the balance: 1.0 = pure vector, 0.0 = pure keyword, 0.5 = balanced.
341
+
342
+ Args:
343
+ query: Search query text.
344
+ alpha: Balance between vector (1.0) and keyword (0.0) search.
345
+ limit: Maximum number of results.
346
+ namespace: Filter to specific namespace.
347
+ min_similarity: Minimum similarity threshold (0-1).
348
+
349
+ Returns:
350
+ HybridRecallResult with matching memories.
351
+
352
+ Raises:
353
+ ValidationError: If input validation fails.
354
+ """
355
+ # Validate query
356
+ if not query or not query.strip():
357
+ raise ValidationError("Query cannot be empty")
358
+
359
+ # Validate alpha
360
+ if alpha < self._config.hybrid_min_alpha or alpha > self._config.hybrid_max_alpha:
361
+ raise ValidationError(
362
+ f"Alpha must be between {self._config.hybrid_min_alpha} "
363
+ f"and {self._config.hybrid_max_alpha}, got {alpha}"
364
+ )
365
+
366
+ # Validate namespace if provided
367
+ if namespace is not None:
368
+ namespace = validate_namespace(namespace)
369
+
370
+ # Generate query embedding
371
+ query_vector = self._embeddings.embed(query)
372
+
373
+ # Perform hybrid search
374
+ results = self._repo.hybrid_search(
375
+ query_vector=query_vector,
376
+ query_text=query,
377
+ limit=limit,
378
+ namespace=namespace,
379
+ alpha=alpha,
380
+ )
381
+
382
+ # Transform results to HybridMemoryMatch and filter by min_similarity
383
+ memories: list[HybridMemoryMatch] = []
384
+ for result in results:
385
+ if result.similarity >= min_similarity:
386
+ memories.append(
387
+ HybridMemoryMatch(
388
+ id=result.id,
389
+ content=result.content,
390
+ similarity=result.similarity,
391
+ namespace=result.namespace,
392
+ tags=list(result.tags),
393
+ importance=result.importance,
394
+ created_at=result.created_at,
395
+ metadata=dict(result.metadata),
396
+ # These may be populated if repository provides them
397
+ vector_score=getattr(result, "vector_score", None),
398
+ fts_score=getattr(result, "fts_score", None),
399
+ combined_score=result.similarity,
400
+ )
401
+ )
402
+
403
+ return HybridRecallResult(
404
+ query=query,
405
+ alpha=alpha,
406
+ memories=memories,
407
+ total=len(memories),
408
+ search_type="hybrid",
409
+ )
@@ -0,0 +1,5 @@
1
+ """MCP Tool definitions for Spatial Memory Server."""
2
+
3
+ from spatial_memory.tools.definitions import TOOLS
4
+
5
+ __all__ = ["TOOLS"]