spatial-memory-mcp 1.5.3__py3-none-any.whl → 1.6.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.

Potentially problematic release.


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

Files changed (34) hide show
  1. spatial_memory/__init__.py +1 -1
  2. spatial_memory/__main__.py +241 -2
  3. spatial_memory/adapters/lancedb_repository.py +74 -5
  4. spatial_memory/config.py +10 -2
  5. spatial_memory/core/__init__.py +9 -0
  6. spatial_memory/core/connection_pool.py +41 -3
  7. spatial_memory/core/consolidation_strategies.py +402 -0
  8. spatial_memory/core/database.py +774 -918
  9. spatial_memory/core/db_idempotency.py +242 -0
  10. spatial_memory/core/db_indexes.py +575 -0
  11. spatial_memory/core/db_migrations.py +584 -0
  12. spatial_memory/core/db_search.py +509 -0
  13. spatial_memory/core/db_versioning.py +177 -0
  14. spatial_memory/core/embeddings.py +65 -18
  15. spatial_memory/core/errors.py +75 -3
  16. spatial_memory/core/filesystem.py +178 -0
  17. spatial_memory/core/models.py +4 -0
  18. spatial_memory/core/rate_limiter.py +26 -9
  19. spatial_memory/core/response_types.py +497 -0
  20. spatial_memory/core/validation.py +86 -2
  21. spatial_memory/factory.py +407 -0
  22. spatial_memory/migrations/__init__.py +40 -0
  23. spatial_memory/ports/repositories.py +52 -2
  24. spatial_memory/server.py +131 -189
  25. spatial_memory/services/export_import.py +61 -43
  26. spatial_memory/services/lifecycle.py +397 -122
  27. spatial_memory/services/memory.py +2 -2
  28. spatial_memory/services/spatial.py +129 -46
  29. {spatial_memory_mcp-1.5.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/METADATA +83 -3
  30. spatial_memory_mcp-1.6.0.dist-info/RECORD +54 -0
  31. spatial_memory_mcp-1.5.3.dist-info/RECORD +0 -44
  32. {spatial_memory_mcp-1.5.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/WHEEL +0 -0
  33. {spatial_memory_mcp-1.5.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/entry_points.txt +0 -0
  34. {spatial_memory_mcp-1.5.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/licenses/LICENSE +0 -0
spatial_memory/server.py CHANGED
@@ -14,7 +14,9 @@ import signal
14
14
  import sys
15
15
  import uuid
16
16
  from collections.abc import Callable
17
+ from concurrent.futures import ThreadPoolExecutor
17
18
  from dataclasses import asdict
19
+ from functools import partial
18
20
  from typing import TYPE_CHECKING, Any
19
21
 
20
22
  from mcp.server import Server
@@ -22,15 +24,12 @@ from mcp.server.stdio import stdio_server
22
24
  from mcp.types import TextContent, Tool
23
25
 
24
26
  from spatial_memory import __version__
25
- from spatial_memory.adapters.lancedb_repository import LanceDBMemoryRepository
26
27
  from spatial_memory.config import ConfigurationError, get_settings, validate_startup
27
- from spatial_memory.core.cache import ResponseCache
28
+ from spatial_memory.factory import ServiceFactory
28
29
  from spatial_memory.core.database import (
29
- Database,
30
30
  clear_connection_cache,
31
31
  set_connection_pool_max_size,
32
32
  )
33
- from spatial_memory.core.embeddings import EmbeddingService
34
33
  from spatial_memory.core.errors import (
35
34
  ConsolidationError,
36
35
  DecayError,
@@ -47,21 +46,40 @@ from spatial_memory.core.errors import (
47
46
  SpatialMemoryError,
48
47
  ValidationError,
49
48
  )
49
+ from spatial_memory.core.response_types import (
50
+ ConsolidateResponse,
51
+ DecayResponse,
52
+ DeleteNamespaceResponse,
53
+ ExportResponse,
54
+ ExtractResponse,
55
+ ForgetBatchResponse,
56
+ ForgetResponse,
57
+ HandlerResponse,
58
+ HealthResponse,
59
+ HybridRecallResponse,
60
+ ImportResponse,
61
+ JourneyResponse,
62
+ NamespacesResponse,
63
+ NearbyResponse,
64
+ RecallResponse,
65
+ RegionsResponse,
66
+ ReinforceResponse,
67
+ RememberBatchResponse,
68
+ RememberResponse,
69
+ RenameNamespaceResponse,
70
+ StatsResponse,
71
+ VisualizeResponse,
72
+ WanderResponse,
73
+ )
50
74
  from spatial_memory.core.health import HealthChecker
51
75
  from spatial_memory.core.logging import configure_logging
52
76
  from spatial_memory.core.metrics import is_available as metrics_available
53
77
  from spatial_memory.core.metrics import record_request
54
- from spatial_memory.core.rate_limiter import AgentAwareRateLimiter, RateLimiter
55
78
  from spatial_memory.core.tracing import (
56
79
  RequestContext,
57
80
  TimingContext,
58
81
  request_context,
59
82
  )
60
- from spatial_memory.services.export_import import ExportImportConfig, ExportImportService
61
- from spatial_memory.services.lifecycle import LifecycleConfig, LifecycleService
62
- from spatial_memory.services.memory import MemoryService
63
- from spatial_memory.services.spatial import SpatialConfig, SpatialService
64
- from spatial_memory.services.utility import UtilityConfig, UtilityService
65
83
  from spatial_memory.tools import TOOLS
66
84
 
67
85
  if TYPE_CHECKING:
@@ -149,163 +167,45 @@ class SpatialMemoryServer:
149
167
  embeddings: Optional embedding service (uses local model if not provided).
150
168
  """
151
169
  self._settings = get_settings()
152
- self._db: Database | None = None
153
170
 
154
171
  # Configure connection pool size from settings
155
172
  set_connection_pool_max_size(self._settings.connection_pool_max_size)
156
173
 
157
- # Set up dependencies
158
- if repository is None or embeddings is None:
159
- # Create embedding service FIRST to auto-detect dimensions
160
- if embeddings is None:
161
- embeddings = EmbeddingService(
162
- model_name=self._settings.embedding_model,
163
- openai_api_key=self._settings.openai_api_key,
164
- backend=self._settings.embedding_backend, # type: ignore[arg-type]
165
- )
166
-
167
- # Auto-detect embedding dimensions from the model
168
- embedding_dim = embeddings.dimensions
169
- logger.info(f"Auto-detected embedding dimensions: {embedding_dim}")
170
- logger.info(f"Embedding backend: {embeddings.backend}")
171
-
172
- # Create database with all config values wired
173
- self._db = Database(
174
- storage_path=self._settings.memory_path,
175
- embedding_dim=embedding_dim,
176
- auto_create_indexes=self._settings.auto_create_indexes,
177
- vector_index_threshold=self._settings.vector_index_threshold,
178
- enable_fts=self._settings.enable_fts_index,
179
- index_nprobes=self._settings.index_nprobes,
180
- index_refine_factor=self._settings.index_refine_factor,
181
- max_retry_attempts=self._settings.max_retry_attempts,
182
- retry_backoff_seconds=self._settings.retry_backoff_seconds,
183
- read_consistency_interval_ms=self._settings.read_consistency_interval_ms,
184
- index_wait_timeout_seconds=self._settings.index_wait_timeout_seconds,
185
- fts_stem=self._settings.fts_stem,
186
- fts_remove_stop_words=self._settings.fts_remove_stop_words,
187
- fts_language=self._settings.fts_language,
188
- index_type=self._settings.index_type,
189
- hnsw_m=self._settings.hnsw_m,
190
- hnsw_ef_construction=self._settings.hnsw_ef_construction,
191
- enable_memory_expiration=self._settings.enable_memory_expiration,
192
- default_memory_ttl_days=self._settings.default_memory_ttl_days,
193
- )
194
- self._db.connect()
195
-
196
- if repository is None:
197
- repository = LanceDBMemoryRepository(self._db)
198
-
199
- self._memory_service = MemoryService(
174
+ # Use ServiceFactory for dependency injection
175
+ factory = ServiceFactory(
176
+ settings=self._settings,
200
177
  repository=repository,
201
178
  embeddings=embeddings,
202
179
  )
203
-
204
- # Create spatial service for exploration operations
205
- self._spatial_service = SpatialService(
206
- repository=repository,
207
- embeddings=embeddings,
208
- config=SpatialConfig(
209
- journey_max_steps=self._settings.max_journey_steps,
210
- wander_max_steps=self._settings.max_wander_steps,
211
- regions_max_memories=self._settings.regions_max_memories,
212
- visualize_max_memories=self._settings.max_visualize_memories,
213
- visualize_n_neighbors=self._settings.umap_n_neighbors,
214
- visualize_min_dist=self._settings.umap_min_dist,
215
- visualize_similarity_threshold=self._settings.visualize_similarity_threshold,
216
- ),
217
- )
218
-
219
- # Create lifecycle service for memory lifecycle management
220
- self._lifecycle_service = LifecycleService(
221
- repository=repository,
222
- embeddings=embeddings,
223
- config=LifecycleConfig(
224
- decay_default_half_life_days=self._settings.decay_default_half_life_days,
225
- decay_default_function=self._settings.decay_default_function,
226
- decay_min_importance_floor=self._settings.decay_min_importance_floor,
227
- decay_batch_size=self._settings.decay_batch_size,
228
- reinforce_default_boost=self._settings.reinforce_default_boost,
229
- reinforce_max_importance=self._settings.reinforce_max_importance,
230
- extract_max_text_length=self._settings.extract_max_text_length,
231
- extract_max_candidates=self._settings.extract_max_candidates,
232
- extract_default_importance=self._settings.extract_default_importance,
233
- extract_default_namespace=self._settings.extract_default_namespace,
234
- consolidate_min_threshold=self._settings.consolidate_min_threshold,
235
- consolidate_content_weight=self._settings.consolidate_content_weight,
236
- consolidate_max_batch=self._settings.consolidate_max_batch,
237
- ),
180
+ services = factory.create_all()
181
+
182
+ # Store service references
183
+ self._db = services.database
184
+ self._embeddings = services.embeddings
185
+ self._memory_service = services.memory
186
+ self._spatial_service = services.spatial
187
+ self._lifecycle_service = services.lifecycle
188
+ self._utility_service = services.utility
189
+ self._export_import_service = services.export_import
190
+
191
+ # Rate limiting
192
+ self._per_agent_rate_limiting = services.per_agent_rate_limiting
193
+ self._rate_limiter = services.rate_limiter
194
+ self._agent_rate_limiter = services.agent_rate_limiter
195
+
196
+ # Response cache
197
+ self._cache_enabled = services.cache_enabled
198
+ self._cache = services.cache
199
+ self._regions_cache_ttl = services.regions_cache_ttl
200
+
201
+ # ThreadPoolExecutor for non-blocking embedding operations
202
+ self._executor = ThreadPoolExecutor(
203
+ max_workers=2,
204
+ thread_name_prefix="embed-",
238
205
  )
239
206
 
240
- # Create utility service for stats, namespaces, and hybrid search
241
- self._utility_service = UtilityService(
242
- repository=repository,
243
- embeddings=embeddings,
244
- config=UtilityConfig(
245
- hybrid_default_alpha=self._settings.hybrid_default_alpha,
246
- hybrid_min_alpha=self._settings.hybrid_min_alpha,
247
- hybrid_max_alpha=self._settings.hybrid_max_alpha,
248
- stats_include_index_details=True,
249
- namespace_batch_size=self._settings.namespace_batch_size,
250
- delete_namespace_require_confirmation=self._settings.destructive_require_namespace_confirmation,
251
- ),
252
- )
253
-
254
- # Create export/import service for data portability
255
- self._export_import_service = ExportImportService(
256
- repository=repository,
257
- embeddings=embeddings,
258
- config=ExportImportConfig(
259
- default_export_format=self._settings.export_default_format,
260
- export_batch_size=self._settings.export_batch_size,
261
- import_batch_size=self._settings.import_batch_size,
262
- import_deduplicate=self._settings.import_deduplicate_default,
263
- import_dedup_threshold=self._settings.import_dedup_threshold,
264
- validate_on_import=self._settings.import_validate_vectors,
265
- parquet_compression="zstd",
266
- max_import_records=self._settings.import_max_records,
267
- csv_include_vectors=self._settings.csv_include_vectors,
268
- max_export_records=self._settings.max_export_records,
269
- ),
270
- allowed_export_paths=self._settings.export_allowed_paths,
271
- allowed_import_paths=self._settings.import_allowed_paths,
272
- allow_symlinks=self._settings.export_allow_symlinks,
273
- max_import_size_bytes=int(self._settings.import_max_file_size_mb * 1024 * 1024),
274
- )
275
-
276
- # Store embeddings and database for health checks
277
- self._embeddings = embeddings
278
-
279
- # Rate limiting for resource protection
280
- # Use per-agent rate limiter if enabled, otherwise fall back to simple rate limiter
281
- self._per_agent_rate_limiting = self._settings.rate_limit_per_agent_enabled
282
- self._agent_rate_limiter: AgentAwareRateLimiter | None = None
283
- self._rate_limiter: RateLimiter | None = None
284
- if self._per_agent_rate_limiting:
285
- self._agent_rate_limiter = AgentAwareRateLimiter(
286
- global_rate=self._settings.embedding_rate_limit,
287
- per_agent_rate=self._settings.rate_limit_per_agent_rate,
288
- max_agents=self._settings.rate_limit_max_tracked_agents,
289
- )
290
- else:
291
- self._rate_limiter = RateLimiter(
292
- rate=self._settings.embedding_rate_limit,
293
- capacity=int(self._settings.embedding_rate_limit * 2)
294
- )
295
-
296
- # Response cache for read-only operations
297
- self._cache_enabled = self._settings.response_cache_enabled
298
- self._cache: ResponseCache | None = None
299
- self._regions_cache_ttl = 0.0
300
- if self._cache_enabled:
301
- self._cache = ResponseCache(
302
- max_size=self._settings.response_cache_max_size,
303
- default_ttl=self._settings.response_cache_default_ttl,
304
- )
305
- self._regions_cache_ttl = self._settings.response_cache_regions_ttl
306
-
307
207
  # Tool handler registry for dispatch pattern
308
- self._tool_handlers: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
208
+ self._tool_handlers: dict[str, Callable[[dict[str, Any]], HandlerResponse]] = {
309
209
  "remember": self._handle_remember,
310
210
  "remember_batch": self._handle_remember_batch,
311
211
  "recall": self._handle_recall,
@@ -344,6 +244,42 @@ class SpatialMemoryServer:
344
244
  )
345
245
  self._setup_handlers()
346
246
 
247
+ async def _run_in_executor(self, func: Callable[..., Any], *args: Any) -> Any:
248
+ """Run a synchronous function in the thread pool executor.
249
+
250
+ This allows CPU-bound or blocking operations (like embedding generation)
251
+ to run without blocking the asyncio event loop.
252
+
253
+ Args:
254
+ func: The synchronous function to run.
255
+ *args: Arguments to pass to the function.
256
+
257
+ Returns:
258
+ The result of the function call.
259
+ """
260
+ loop = asyncio.get_running_loop()
261
+ return await loop.run_in_executor(self._executor, partial(func, *args))
262
+
263
+ async def _handle_tool_async(
264
+ self, name: str, arguments: dict[str, Any]
265
+ ) -> HandlerResponse:
266
+ """Handle tool call asynchronously by running handler in executor.
267
+
268
+ This wraps synchronous handlers to run in a thread pool, preventing
269
+ blocking operations from stalling the event loop.
270
+
271
+ Args:
272
+ name: Tool name.
273
+ arguments: Tool arguments.
274
+
275
+ Returns:
276
+ Tool result as typed dictionary.
277
+
278
+ Raises:
279
+ ValidationError: If tool name is unknown.
280
+ """
281
+ return await self._run_in_executor(self._handle_tool, name, arguments)
282
+
347
283
  def _setup_handlers(self) -> None:
348
284
  """Set up MCP tool handlers."""
349
285
 
@@ -397,13 +333,15 @@ class SpatialMemoryServer:
397
333
  result = cached_result
398
334
  else:
399
335
  with timing.measure("handler"):
400
- result = self._handle_tool(name, arguments)
336
+ # Run handler in executor to avoid blocking event loop
337
+ result = await self._handle_tool_async(name, arguments)
401
338
  # Cache the result with appropriate TTL
402
339
  ttl = self._regions_cache_ttl if name == "regions" else None
403
340
  self._cache.set(cache_key, result, ttl=ttl)
404
341
  else:
405
342
  with timing.measure("handler"):
406
- result = self._handle_tool(name, arguments)
343
+ # Run handler in executor to avoid blocking event loop
344
+ result = await self._handle_tool_async(name, arguments)
407
345
 
408
346
  # Invalidate cache on mutations
409
347
  if self._cache_enabled and self._cache is not None:
@@ -466,7 +404,7 @@ class SpatialMemoryServer:
466
404
  # Tool Handler Methods
467
405
  # =========================================================================
468
406
 
469
- def _handle_remember(self, arguments: dict[str, Any]) -> dict[str, Any]:
407
+ def _handle_remember(self, arguments: dict[str, Any]) -> RememberResponse:
470
408
  """Handle remember tool call."""
471
409
  remember_result = self._memory_service.remember(
472
410
  content=arguments["content"],
@@ -475,16 +413,16 @@ class SpatialMemoryServer:
475
413
  importance=arguments.get("importance", 0.5),
476
414
  metadata=arguments.get("metadata"),
477
415
  )
478
- return asdict(remember_result)
416
+ return asdict(remember_result) # type: ignore[return-value]
479
417
 
480
- def _handle_remember_batch(self, arguments: dict[str, Any]) -> dict[str, Any]:
418
+ def _handle_remember_batch(self, arguments: dict[str, Any]) -> RememberBatchResponse:
481
419
  """Handle remember_batch tool call."""
482
420
  batch_result = self._memory_service.remember_batch(
483
421
  memories=arguments["memories"],
484
422
  )
485
- return asdict(batch_result)
423
+ return asdict(batch_result) # type: ignore[return-value]
486
424
 
487
- def _handle_recall(self, arguments: dict[str, Any]) -> dict[str, Any]:
425
+ def _handle_recall(self, arguments: dict[str, Any]) -> RecallResponse:
488
426
  """Handle recall tool call."""
489
427
  recall_result = self._memory_service.recall(
490
428
  query=arguments["query"],
@@ -509,7 +447,7 @@ class SpatialMemoryServer:
509
447
  "total": recall_result.total,
510
448
  }
511
449
 
512
- def _handle_nearby(self, arguments: dict[str, Any]) -> dict[str, Any]:
450
+ def _handle_nearby(self, arguments: dict[str, Any]) -> NearbyResponse:
513
451
  """Handle nearby tool call."""
514
452
  nearby_result = self._memory_service.nearby(
515
453
  memory_id=arguments["memory_id"],
@@ -533,21 +471,21 @@ class SpatialMemoryServer:
533
471
  ],
534
472
  }
535
473
 
536
- def _handle_forget(self, arguments: dict[str, Any]) -> dict[str, Any]:
474
+ def _handle_forget(self, arguments: dict[str, Any]) -> ForgetResponse:
537
475
  """Handle forget tool call."""
538
476
  forget_result = self._memory_service.forget(
539
477
  memory_id=arguments["memory_id"],
540
478
  )
541
- return asdict(forget_result)
479
+ return asdict(forget_result) # type: ignore[return-value]
542
480
 
543
- def _handle_forget_batch(self, arguments: dict[str, Any]) -> dict[str, Any]:
481
+ def _handle_forget_batch(self, arguments: dict[str, Any]) -> ForgetBatchResponse:
544
482
  """Handle forget_batch tool call."""
545
483
  forget_batch_result = self._memory_service.forget_batch(
546
484
  memory_ids=arguments["memory_ids"],
547
485
  )
548
- return asdict(forget_batch_result)
486
+ return asdict(forget_batch_result) # type: ignore[return-value]
549
487
 
550
- def _handle_health(self, arguments: dict[str, Any]) -> dict[str, Any]:
488
+ def _handle_health(self, arguments: dict[str, Any]) -> HealthResponse:
551
489
  """Handle health tool call."""
552
490
  verbose = arguments.get("verbose", False)
553
491
 
@@ -559,7 +497,7 @@ class SpatialMemoryServer:
559
497
 
560
498
  report = health_checker.get_health_report()
561
499
 
562
- result: dict[str, Any] = {
500
+ result: HealthResponse = {
563
501
  "version": __version__,
564
502
  "status": report.status.value,
565
503
  "timestamp": report.timestamp.isoformat(),
@@ -580,7 +518,7 @@ class SpatialMemoryServer:
580
518
 
581
519
  return result
582
520
 
583
- def _handle_journey(self, arguments: dict[str, Any]) -> dict[str, Any]:
521
+ def _handle_journey(self, arguments: dict[str, Any]) -> JourneyResponse:
584
522
  """Handle journey tool call."""
585
523
  journey_result = self._spatial_service.journey(
586
524
  start_id=arguments["start_id"],
@@ -610,7 +548,7 @@ class SpatialMemoryServer:
610
548
  "path_coverage": journey_result.path_coverage,
611
549
  }
612
550
 
613
- def _handle_wander(self, arguments: dict[str, Any]) -> dict[str, Any]:
551
+ def _handle_wander(self, arguments: dict[str, Any]) -> WanderResponse:
614
552
  """Handle wander tool call."""
615
553
  start_id = arguments.get("start_id")
616
554
  if start_id is None:
@@ -649,7 +587,7 @@ class SpatialMemoryServer:
649
587
  "total_distance": wander_result.total_distance,
650
588
  }
651
589
 
652
- def _handle_regions(self, arguments: dict[str, Any]) -> dict[str, Any]:
590
+ def _handle_regions(self, arguments: dict[str, Any]) -> RegionsResponse:
653
591
  """Handle regions tool call."""
654
592
  regions_result = self._spatial_service.regions(
655
593
  namespace=arguments.get("namespace"),
@@ -683,7 +621,7 @@ class SpatialMemoryServer:
683
621
  "clustering_quality": regions_result.clustering_quality,
684
622
  }
685
623
 
686
- def _handle_visualize(self, arguments: dict[str, Any]) -> dict[str, Any]:
624
+ def _handle_visualize(self, arguments: dict[str, Any]) -> VisualizeResponse:
687
625
  """Handle visualize tool call."""
688
626
  visualize_result = self._spatial_service.visualize(
689
627
  memory_ids=arguments.get("memory_ids"),
@@ -723,7 +661,7 @@ class SpatialMemoryServer:
723
661
  "format": visualize_result.format,
724
662
  }
725
663
 
726
- def _handle_decay(self, arguments: dict[str, Any]) -> dict[str, Any]:
664
+ def _handle_decay(self, arguments: dict[str, Any]) -> DecayResponse:
727
665
  """Handle decay tool call."""
728
666
  decay_result = self._lifecycle_service.decay(
729
667
  namespace=arguments.get("namespace"),
@@ -752,7 +690,7 @@ class SpatialMemoryServer:
752
690
  "dry_run": decay_result.dry_run,
753
691
  }
754
692
 
755
- def _handle_reinforce(self, arguments: dict[str, Any]) -> dict[str, Any]:
693
+ def _handle_reinforce(self, arguments: dict[str, Any]) -> ReinforceResponse:
756
694
  """Handle reinforce tool call."""
757
695
  reinforce_result = self._lifecycle_service.reinforce(
758
696
  memory_ids=arguments["memory_ids"],
@@ -776,7 +714,7 @@ class SpatialMemoryServer:
776
714
  "not_found": reinforce_result.not_found,
777
715
  }
778
716
 
779
- def _handle_extract(self, arguments: dict[str, Any]) -> dict[str, Any]:
717
+ def _handle_extract(self, arguments: dict[str, Any]) -> ExtractResponse:
780
718
  """Handle extract tool call."""
781
719
  extract_result = self._lifecycle_service.extract(
782
720
  text=arguments["text"],
@@ -803,7 +741,7 @@ class SpatialMemoryServer:
803
741
  ],
804
742
  }
805
743
 
806
- def _handle_consolidate(self, arguments: dict[str, Any]) -> dict[str, Any]:
744
+ def _handle_consolidate(self, arguments: dict[str, Any]) -> ConsolidateResponse:
807
745
  """Handle consolidate tool call."""
808
746
  consolidate_result = self._lifecycle_service.consolidate(
809
747
  namespace=arguments["namespace"],
@@ -828,7 +766,7 @@ class SpatialMemoryServer:
828
766
  "dry_run": consolidate_result.dry_run,
829
767
  }
830
768
 
831
- def _handle_stats(self, arguments: dict[str, Any]) -> dict[str, Any]:
769
+ def _handle_stats(self, arguments: dict[str, Any]) -> StatsResponse:
832
770
  """Handle stats tool call."""
833
771
  stats_result = self._utility_service.stats(
834
772
  namespace=arguments.get("namespace"),
@@ -866,7 +804,7 @@ class SpatialMemoryServer:
866
804
  "avg_content_length": stats_result.avg_content_length,
867
805
  }
868
806
 
869
- def _handle_namespaces(self, arguments: dict[str, Any]) -> dict[str, Any]:
807
+ def _handle_namespaces(self, arguments: dict[str, Any]) -> NamespacesResponse:
870
808
  """Handle namespaces tool call."""
871
809
  namespaces_result = self._utility_service.namespaces(
872
810
  include_stats=arguments.get("include_stats", True),
@@ -889,7 +827,7 @@ class SpatialMemoryServer:
889
827
  "total_memories": namespaces_result.total_memories,
890
828
  }
891
829
 
892
- def _handle_delete_namespace(self, arguments: dict[str, Any]) -> dict[str, Any]:
830
+ def _handle_delete_namespace(self, arguments: dict[str, Any]) -> DeleteNamespaceResponse:
893
831
  """Handle delete_namespace tool call."""
894
832
  delete_result = self._utility_service.delete_namespace(
895
833
  namespace=arguments["namespace"],
@@ -904,7 +842,7 @@ class SpatialMemoryServer:
904
842
  "dry_run": delete_result.dry_run,
905
843
  }
906
844
 
907
- def _handle_rename_namespace(self, arguments: dict[str, Any]) -> dict[str, Any]:
845
+ def _handle_rename_namespace(self, arguments: dict[str, Any]) -> RenameNamespaceResponse:
908
846
  """Handle rename_namespace tool call."""
909
847
  rename_result = self._utility_service.rename_namespace(
910
848
  old_namespace=arguments["old_namespace"],
@@ -918,7 +856,7 @@ class SpatialMemoryServer:
918
856
  "message": rename_result.message,
919
857
  }
920
858
 
921
- def _handle_export_memories(self, arguments: dict[str, Any]) -> dict[str, Any]:
859
+ def _handle_export_memories(self, arguments: dict[str, Any]) -> ExportResponse:
922
860
  """Handle export_memories tool call."""
923
861
  export_result = self._export_import_service.export_memories(
924
862
  output_path=arguments["output_path"],
@@ -937,7 +875,7 @@ class SpatialMemoryServer:
937
875
  "compression": export_result.compression,
938
876
  }
939
877
 
940
- def _handle_import_memories(self, arguments: dict[str, Any]) -> dict[str, Any]:
878
+ def _handle_import_memories(self, arguments: dict[str, Any]) -> ImportResponse:
941
879
  """Handle import_memories tool call."""
942
880
  dry_run = arguments.get("dry_run", True)
943
881
  import_result = self._export_import_service.import_memories(
@@ -979,7 +917,7 @@ class SpatialMemoryServer:
979
917
  ] if import_result.imported_memories else [],
980
918
  }
981
919
 
982
- def _handle_hybrid_recall(self, arguments: dict[str, Any]) -> dict[str, Any]:
920
+ def _handle_hybrid_recall(self, arguments: dict[str, Any]) -> HybridRecallResponse:
983
921
  """Handle hybrid_recall tool call."""
984
922
  hybrid_result = self._utility_service.hybrid_recall(
985
923
  query=arguments["query"],
@@ -1016,7 +954,7 @@ class SpatialMemoryServer:
1016
954
  # Tool Routing
1017
955
  # =========================================================================
1018
956
 
1019
- def _handle_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
957
+ def _handle_tool(self, name: str, arguments: dict[str, Any]) -> HandlerResponse:
1020
958
  """Route tool call to appropriate handler.
1021
959
 
1022
960
  Args:
@@ -1024,7 +962,7 @@ class SpatialMemoryServer:
1024
962
  arguments: Tool arguments.
1025
963
 
1026
964
  Returns:
1027
- Tool result as dictionary.
965
+ Tool result as typed dictionary.
1028
966
 
1029
967
  Raises:
1030
968
  ValidationError: If tool name is unknown.
@@ -1033,7 +971,7 @@ class SpatialMemoryServer:
1033
971
  with record_request(name, "success"):
1034
972
  return self._handle_tool_impl(name, arguments)
1035
973
 
1036
- def _handle_tool_impl(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
974
+ def _handle_tool_impl(self, name: str, arguments: dict[str, Any]) -> HandlerResponse:
1037
975
  """Implementation of tool routing using dispatch pattern.
1038
976
 
1039
977
  Args:
@@ -1116,6 +1054,10 @@ Then use `extract` to automatically capture important information.
1116
1054
 
1117
1055
  def close(self) -> None:
1118
1056
  """Clean up resources."""
1057
+ # Shutdown the thread pool executor
1058
+ if hasattr(self, "_executor"):
1059
+ self._executor.shutdown(wait=False)
1060
+
1119
1061
  if self._db is not None:
1120
1062
  self._db.close()
1121
1063