spatial-memory-mcp 1.0.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.
- spatial_memory/__init__.py +97 -97
- spatial_memory/__main__.py +241 -2
- spatial_memory/adapters/lancedb_repository.py +74 -5
- spatial_memory/config.py +115 -2
- spatial_memory/core/__init__.py +35 -0
- spatial_memory/core/cache.py +317 -0
- spatial_memory/core/circuit_breaker.py +297 -0
- spatial_memory/core/connection_pool.py +41 -3
- spatial_memory/core/consolidation_strategies.py +402 -0
- spatial_memory/core/database.py +791 -769
- spatial_memory/core/db_idempotency.py +242 -0
- spatial_memory/core/db_indexes.py +575 -0
- spatial_memory/core/db_migrations.py +584 -0
- spatial_memory/core/db_search.py +509 -0
- spatial_memory/core/db_versioning.py +177 -0
- spatial_memory/core/embeddings.py +156 -19
- spatial_memory/core/errors.py +75 -3
- spatial_memory/core/filesystem.py +178 -0
- spatial_memory/core/logging.py +194 -103
- spatial_memory/core/models.py +4 -0
- spatial_memory/core/rate_limiter.py +326 -105
- spatial_memory/core/response_types.py +497 -0
- spatial_memory/core/tracing.py +300 -0
- spatial_memory/core/validation.py +403 -319
- spatial_memory/factory.py +407 -0
- spatial_memory/migrations/__init__.py +40 -0
- spatial_memory/ports/repositories.py +52 -2
- spatial_memory/server.py +329 -188
- spatial_memory/services/export_import.py +61 -43
- spatial_memory/services/lifecycle.py +397 -122
- spatial_memory/services/memory.py +81 -4
- spatial_memory/services/spatial.py +129 -46
- spatial_memory/tools/definitions.py +695 -671
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/METADATA +83 -3
- spatial_memory_mcp-1.6.0.dist-info/RECORD +54 -0
- spatial_memory_mcp-1.0.3.dist-info/RECORD +0 -41
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/WHEEL +0 -0
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/entry_points.txt +0 -0
- {spatial_memory_mcp-1.0.3.dist-info → spatial_memory_mcp-1.6.0.dist-info}/licenses/LICENSE +0 -0
spatial_memory/server.py
CHANGED
|
@@ -13,22 +13,23 @@ import logging
|
|
|
13
13
|
import signal
|
|
14
14
|
import sys
|
|
15
15
|
import uuid
|
|
16
|
-
from dataclasses import asdict
|
|
17
16
|
from collections.abc import Callable
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
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
|
|
21
23
|
from mcp.server.stdio import stdio_server
|
|
22
24
|
from mcp.types import TextContent, Tool
|
|
23
25
|
|
|
24
|
-
from spatial_memory
|
|
26
|
+
from spatial_memory import __version__
|
|
25
27
|
from spatial_memory.config import ConfigurationError, get_settings, validate_startup
|
|
28
|
+
from spatial_memory.factory import ServiceFactory
|
|
26
29
|
from spatial_memory.core.database import (
|
|
27
|
-
Database,
|
|
28
30
|
clear_connection_cache,
|
|
29
31
|
set_connection_pool_max_size,
|
|
30
32
|
)
|
|
31
|
-
from spatial_memory.core.embeddings import EmbeddingService
|
|
32
33
|
from spatial_memory.core.errors import (
|
|
33
34
|
ConsolidationError,
|
|
34
35
|
DecayError,
|
|
@@ -45,17 +46,40 @@ from spatial_memory.core.errors import (
|
|
|
45
46
|
SpatialMemoryError,
|
|
46
47
|
ValidationError,
|
|
47
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
|
+
)
|
|
48
74
|
from spatial_memory.core.health import HealthChecker
|
|
49
75
|
from spatial_memory.core.logging import configure_logging
|
|
50
76
|
from spatial_memory.core.metrics import is_available as metrics_available
|
|
51
77
|
from spatial_memory.core.metrics import record_request
|
|
52
|
-
from spatial_memory.core.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
from spatial_memory.services.utility import UtilityConfig, UtilityService
|
|
58
|
-
from spatial_memory import __version__
|
|
78
|
+
from spatial_memory.core.tracing import (
|
|
79
|
+
RequestContext,
|
|
80
|
+
TimingContext,
|
|
81
|
+
request_context,
|
|
82
|
+
)
|
|
59
83
|
from spatial_memory.tools import TOOLS
|
|
60
84
|
|
|
61
85
|
if TYPE_CHECKING:
|
|
@@ -66,6 +90,33 @@ if TYPE_CHECKING:
|
|
|
66
90
|
|
|
67
91
|
logger = logging.getLogger(__name__)
|
|
68
92
|
|
|
93
|
+
# Tools that can be cached (read-only operations)
|
|
94
|
+
CACHEABLE_TOOLS = frozenset({"recall", "nearby", "hybrid_recall", "regions"})
|
|
95
|
+
|
|
96
|
+
# Tools that invalidate cache by namespace
|
|
97
|
+
NAMESPACE_INVALIDATING_TOOLS = frozenset({"remember", "forget", "forget_batch"})
|
|
98
|
+
|
|
99
|
+
# Tools that invalidate entire cache
|
|
100
|
+
FULL_INVALIDATING_TOOLS = frozenset({"decay", "reinforce", "consolidate"})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _generate_cache_key(tool_name: str, arguments: dict[str, Any]) -> str:
|
|
104
|
+
"""Generate a cache key from tool name and arguments.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
tool_name: Name of the tool.
|
|
108
|
+
arguments: Tool arguments (excluding _agent_id).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A string cache key suitable for response caching.
|
|
112
|
+
"""
|
|
113
|
+
# Remove _agent_id from cache key computation (same query from different agents = same result)
|
|
114
|
+
cache_args = {k: v for k, v in sorted(arguments.items()) if k != "_agent_id"}
|
|
115
|
+
# Create a stable string representation
|
|
116
|
+
args_str = json.dumps(cache_args, sort_keys=True, default=str)
|
|
117
|
+
return f"{tool_name}:{hash(args_str)}"
|
|
118
|
+
|
|
119
|
+
|
|
69
120
|
# Error type to response name mapping for standardized error responses
|
|
70
121
|
ERROR_MAPPINGS: dict[type[Exception], str] = {
|
|
71
122
|
MemoryNotFoundError: "MemoryNotFound",
|
|
@@ -116,141 +167,45 @@ class SpatialMemoryServer:
|
|
|
116
167
|
embeddings: Optional embedding service (uses local model if not provided).
|
|
117
168
|
"""
|
|
118
169
|
self._settings = get_settings()
|
|
119
|
-
self._db: Database | None = None
|
|
120
170
|
|
|
121
171
|
# Configure connection pool size from settings
|
|
122
172
|
set_connection_pool_max_size(self._settings.connection_pool_max_size)
|
|
123
173
|
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if embeddings is None:
|
|
128
|
-
embeddings = EmbeddingService(
|
|
129
|
-
model_name=self._settings.embedding_model,
|
|
130
|
-
openai_api_key=self._settings.openai_api_key,
|
|
131
|
-
backend=self._settings.embedding_backend, # type: ignore[arg-type]
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
# Auto-detect embedding dimensions from the model
|
|
135
|
-
embedding_dim = embeddings.dimensions
|
|
136
|
-
logger.info(f"Auto-detected embedding dimensions: {embedding_dim}")
|
|
137
|
-
logger.info(f"Embedding backend: {embeddings.backend}")
|
|
138
|
-
|
|
139
|
-
# Create database with all config values wired
|
|
140
|
-
self._db = Database(
|
|
141
|
-
storage_path=self._settings.memory_path,
|
|
142
|
-
embedding_dim=embedding_dim,
|
|
143
|
-
auto_create_indexes=self._settings.auto_create_indexes,
|
|
144
|
-
vector_index_threshold=self._settings.vector_index_threshold,
|
|
145
|
-
enable_fts=self._settings.enable_fts_index,
|
|
146
|
-
index_nprobes=self._settings.index_nprobes,
|
|
147
|
-
index_refine_factor=self._settings.index_refine_factor,
|
|
148
|
-
max_retry_attempts=self._settings.max_retry_attempts,
|
|
149
|
-
retry_backoff_seconds=self._settings.retry_backoff_seconds,
|
|
150
|
-
read_consistency_interval_ms=self._settings.read_consistency_interval_ms,
|
|
151
|
-
index_wait_timeout_seconds=self._settings.index_wait_timeout_seconds,
|
|
152
|
-
fts_stem=self._settings.fts_stem,
|
|
153
|
-
fts_remove_stop_words=self._settings.fts_remove_stop_words,
|
|
154
|
-
fts_language=self._settings.fts_language,
|
|
155
|
-
index_type=self._settings.index_type,
|
|
156
|
-
hnsw_m=self._settings.hnsw_m,
|
|
157
|
-
hnsw_ef_construction=self._settings.hnsw_ef_construction,
|
|
158
|
-
enable_memory_expiration=self._settings.enable_memory_expiration,
|
|
159
|
-
default_memory_ttl_days=self._settings.default_memory_ttl_days,
|
|
160
|
-
)
|
|
161
|
-
self._db.connect()
|
|
162
|
-
|
|
163
|
-
if repository is None:
|
|
164
|
-
repository = LanceDBMemoryRepository(self._db)
|
|
165
|
-
|
|
166
|
-
self._memory_service = MemoryService(
|
|
167
|
-
repository=repository,
|
|
168
|
-
embeddings=embeddings,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# Create spatial service for exploration operations
|
|
172
|
-
self._spatial_service = SpatialService(
|
|
174
|
+
# Use ServiceFactory for dependency injection
|
|
175
|
+
factory = ServiceFactory(
|
|
176
|
+
settings=self._settings,
|
|
173
177
|
repository=repository,
|
|
174
178
|
embeddings=embeddings,
|
|
175
|
-
config=SpatialConfig(
|
|
176
|
-
journey_max_steps=self._settings.max_journey_steps,
|
|
177
|
-
wander_max_steps=self._settings.max_wander_steps,
|
|
178
|
-
regions_max_memories=self._settings.regions_max_memories,
|
|
179
|
-
visualize_max_memories=self._settings.max_visualize_memories,
|
|
180
|
-
visualize_n_neighbors=self._settings.umap_n_neighbors,
|
|
181
|
-
visualize_min_dist=self._settings.umap_min_dist,
|
|
182
|
-
visualize_similarity_threshold=self._settings.visualize_similarity_threshold,
|
|
183
|
-
),
|
|
184
179
|
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
embeddings=embeddings,
|
|
211
|
-
config=UtilityConfig(
|
|
212
|
-
hybrid_default_alpha=self._settings.hybrid_default_alpha,
|
|
213
|
-
hybrid_min_alpha=self._settings.hybrid_min_alpha,
|
|
214
|
-
hybrid_max_alpha=self._settings.hybrid_max_alpha,
|
|
215
|
-
stats_include_index_details=True,
|
|
216
|
-
namespace_batch_size=self._settings.namespace_batch_size,
|
|
217
|
-
delete_namespace_require_confirmation=self._settings.destructive_require_namespace_confirmation,
|
|
218
|
-
),
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
# Create export/import service for data portability
|
|
222
|
-
self._export_import_service = ExportImportService(
|
|
223
|
-
repository=repository,
|
|
224
|
-
embeddings=embeddings,
|
|
225
|
-
config=ExportImportConfig(
|
|
226
|
-
default_export_format=self._settings.export_default_format,
|
|
227
|
-
export_batch_size=self._settings.export_batch_size,
|
|
228
|
-
import_batch_size=self._settings.import_batch_size,
|
|
229
|
-
import_deduplicate=self._settings.import_deduplicate_default,
|
|
230
|
-
import_dedup_threshold=self._settings.import_dedup_threshold,
|
|
231
|
-
validate_on_import=self._settings.import_validate_vectors,
|
|
232
|
-
parquet_compression="zstd",
|
|
233
|
-
max_import_records=self._settings.import_max_records,
|
|
234
|
-
csv_include_vectors=self._settings.csv_include_vectors,
|
|
235
|
-
max_export_records=self._settings.max_export_records,
|
|
236
|
-
),
|
|
237
|
-
allowed_export_paths=self._settings.export_allowed_paths,
|
|
238
|
-
allowed_import_paths=self._settings.import_allowed_paths,
|
|
239
|
-
allow_symlinks=self._settings.export_allow_symlinks,
|
|
240
|
-
max_import_size_bytes=int(self._settings.import_max_file_size_mb * 1024 * 1024),
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
# Store embeddings and database for health checks
|
|
244
|
-
self._embeddings = embeddings
|
|
245
|
-
|
|
246
|
-
# Rate limiting for resource protection
|
|
247
|
-
self._rate_limiter = RateLimiter(
|
|
248
|
-
rate=self._settings.embedding_rate_limit,
|
|
249
|
-
capacity=int(self._settings.embedding_rate_limit * 2)
|
|
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-",
|
|
250
205
|
)
|
|
251
206
|
|
|
252
207
|
# Tool handler registry for dispatch pattern
|
|
253
|
-
self._tool_handlers: dict[str, Callable[[dict],
|
|
208
|
+
self._tool_handlers: dict[str, Callable[[dict[str, Any]], HandlerResponse]] = {
|
|
254
209
|
"remember": self._handle_remember,
|
|
255
210
|
"remember_batch": self._handle_remember_batch,
|
|
256
211
|
"recall": self._handle_recall,
|
|
@@ -281,10 +236,50 @@ class SpatialMemoryServer:
|
|
|
281
236
|
else:
|
|
282
237
|
logger.info("Prometheus metrics disabled (prometheus_client not installed)")
|
|
283
238
|
|
|
284
|
-
# Create MCP server
|
|
285
|
-
self._server = Server(
|
|
239
|
+
# Create MCP server with behavioral instructions
|
|
240
|
+
self._server = Server(
|
|
241
|
+
name="spatial-memory",
|
|
242
|
+
version=__version__,
|
|
243
|
+
instructions=self._get_server_instructions(),
|
|
244
|
+
)
|
|
286
245
|
self._setup_handlers()
|
|
287
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
|
+
|
|
288
283
|
def _setup_handlers(self) -> None:
|
|
289
284
|
"""Set up MCP tool handlers."""
|
|
290
285
|
|
|
@@ -295,33 +290,121 @@ class SpatialMemoryServer:
|
|
|
295
290
|
|
|
296
291
|
@self._server.call_tool()
|
|
297
292
|
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
298
|
-
"""Handle tool calls."""
|
|
293
|
+
"""Handle tool calls with tracing, caching, and rate limiting."""
|
|
294
|
+
# Extract _agent_id for tracing and rate limiting (don't pass to handler)
|
|
295
|
+
agent_id = arguments.pop("_agent_id", None)
|
|
296
|
+
|
|
299
297
|
# Apply rate limiting
|
|
300
|
-
if
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
298
|
+
if self._per_agent_rate_limiting and self._agent_rate_limiter is not None:
|
|
299
|
+
if not self._agent_rate_limiter.wait(agent_id=agent_id, timeout=30.0):
|
|
300
|
+
return [TextContent(
|
|
301
|
+
type="text",
|
|
302
|
+
text=json.dumps({
|
|
303
|
+
"error": "RateLimitExceeded",
|
|
304
|
+
"message": "Too many requests. Please wait and try again.",
|
|
305
|
+
"isError": True,
|
|
306
|
+
})
|
|
307
|
+
)]
|
|
308
|
+
elif self._rate_limiter is not None:
|
|
309
|
+
if not self._rate_limiter.wait(timeout=30.0):
|
|
310
|
+
return [TextContent(
|
|
311
|
+
type="text",
|
|
312
|
+
text=json.dumps({
|
|
313
|
+
"error": "RateLimitExceeded",
|
|
314
|
+
"message": "Too many requests. Please wait and try again.",
|
|
315
|
+
"isError": True,
|
|
316
|
+
})
|
|
317
|
+
)]
|
|
318
|
+
|
|
319
|
+
# Use request context for tracing
|
|
320
|
+
namespace = arguments.get("namespace")
|
|
321
|
+
with request_context(tool_name=name, agent_id=agent_id, namespace=namespace) as ctx:
|
|
322
|
+
timing = TimingContext()
|
|
323
|
+
cache_hit = False
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Check cache for cacheable tools
|
|
327
|
+
if self._cache_enabled and self._cache is not None and name in CACHEABLE_TOOLS:
|
|
328
|
+
cache_key = _generate_cache_key(name, arguments)
|
|
329
|
+
with timing.measure("cache_lookup"):
|
|
330
|
+
cached_result = self._cache.get(cache_key)
|
|
331
|
+
if cached_result is not None:
|
|
332
|
+
cache_hit = True
|
|
333
|
+
result = cached_result
|
|
334
|
+
else:
|
|
335
|
+
with timing.measure("handler"):
|
|
336
|
+
# Run handler in executor to avoid blocking event loop
|
|
337
|
+
result = await self._handle_tool_async(name, arguments)
|
|
338
|
+
# Cache the result with appropriate TTL
|
|
339
|
+
ttl = self._regions_cache_ttl if name == "regions" else None
|
|
340
|
+
self._cache.set(cache_key, result, ttl=ttl)
|
|
341
|
+
else:
|
|
342
|
+
with timing.measure("handler"):
|
|
343
|
+
# Run handler in executor to avoid blocking event loop
|
|
344
|
+
result = await self._handle_tool_async(name, arguments)
|
|
345
|
+
|
|
346
|
+
# Invalidate cache on mutations
|
|
347
|
+
if self._cache_enabled and self._cache is not None:
|
|
348
|
+
self._invalidate_cache_for_tool(name, arguments)
|
|
349
|
+
|
|
350
|
+
# Add _meta to response if enabled
|
|
351
|
+
if self._settings.include_request_meta:
|
|
352
|
+
result["_meta"] = self._build_meta(ctx, timing, cache_hit)
|
|
353
|
+
|
|
354
|
+
return [TextContent(type="text", text=json.dumps(result, default=str))]
|
|
355
|
+
except tuple(ERROR_MAPPINGS.keys()) as e:
|
|
356
|
+
return _create_error_response(e)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
error_id = str(uuid.uuid4())[:8]
|
|
359
|
+
logger.error(f"Unexpected error [{error_id}] in {name}: {e}", exc_info=True)
|
|
360
|
+
return _create_error_response(e, error_id)
|
|
361
|
+
|
|
362
|
+
def _build_meta(
|
|
363
|
+
self,
|
|
364
|
+
ctx: RequestContext,
|
|
365
|
+
timing: TimingContext,
|
|
366
|
+
cache_hit: bool,
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
"""Build the _meta object for response.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
ctx: The request context.
|
|
372
|
+
timing: The timing context.
|
|
373
|
+
cache_hit: Whether this was a cache hit.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Dictionary with request metadata.
|
|
377
|
+
"""
|
|
378
|
+
meta: dict[str, Any] = {
|
|
379
|
+
"request_id": ctx.request_id,
|
|
380
|
+
"agent_id": ctx.agent_id,
|
|
381
|
+
"cache_hit": cache_hit,
|
|
382
|
+
}
|
|
383
|
+
if self._settings.include_timing_breakdown:
|
|
384
|
+
meta["timing_ms"] = timing.summary()
|
|
385
|
+
return meta
|
|
386
|
+
|
|
387
|
+
def _invalidate_cache_for_tool(self, name: str, arguments: dict[str, Any]) -> None:
|
|
388
|
+
"""Invalidate cache entries based on the tool that was called.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
name: Tool name.
|
|
392
|
+
arguments: Tool arguments.
|
|
393
|
+
"""
|
|
394
|
+
if self._cache is None:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
if name in FULL_INVALIDATING_TOOLS:
|
|
398
|
+
self._cache.invalidate_all()
|
|
399
|
+
elif name in NAMESPACE_INVALIDATING_TOOLS:
|
|
400
|
+
namespace = arguments.get("namespace", "default")
|
|
401
|
+
self._cache.invalidate_namespace(namespace)
|
|
319
402
|
|
|
320
403
|
# =========================================================================
|
|
321
404
|
# Tool Handler Methods
|
|
322
405
|
# =========================================================================
|
|
323
406
|
|
|
324
|
-
def _handle_remember(self, arguments: dict[str, Any]) ->
|
|
407
|
+
def _handle_remember(self, arguments: dict[str, Any]) -> RememberResponse:
|
|
325
408
|
"""Handle remember tool call."""
|
|
326
409
|
remember_result = self._memory_service.remember(
|
|
327
410
|
content=arguments["content"],
|
|
@@ -330,16 +413,16 @@ class SpatialMemoryServer:
|
|
|
330
413
|
importance=arguments.get("importance", 0.5),
|
|
331
414
|
metadata=arguments.get("metadata"),
|
|
332
415
|
)
|
|
333
|
-
return asdict(remember_result)
|
|
416
|
+
return asdict(remember_result) # type: ignore[return-value]
|
|
334
417
|
|
|
335
|
-
def _handle_remember_batch(self, arguments: dict[str, Any]) ->
|
|
418
|
+
def _handle_remember_batch(self, arguments: dict[str, Any]) -> RememberBatchResponse:
|
|
336
419
|
"""Handle remember_batch tool call."""
|
|
337
420
|
batch_result = self._memory_service.remember_batch(
|
|
338
421
|
memories=arguments["memories"],
|
|
339
422
|
)
|
|
340
|
-
return asdict(batch_result)
|
|
423
|
+
return asdict(batch_result) # type: ignore[return-value]
|
|
341
424
|
|
|
342
|
-
def _handle_recall(self, arguments: dict[str, Any]) ->
|
|
425
|
+
def _handle_recall(self, arguments: dict[str, Any]) -> RecallResponse:
|
|
343
426
|
"""Handle recall tool call."""
|
|
344
427
|
recall_result = self._memory_service.recall(
|
|
345
428
|
query=arguments["query"],
|
|
@@ -364,7 +447,7 @@ class SpatialMemoryServer:
|
|
|
364
447
|
"total": recall_result.total,
|
|
365
448
|
}
|
|
366
449
|
|
|
367
|
-
def _handle_nearby(self, arguments: dict[str, Any]) ->
|
|
450
|
+
def _handle_nearby(self, arguments: dict[str, Any]) -> NearbyResponse:
|
|
368
451
|
"""Handle nearby tool call."""
|
|
369
452
|
nearby_result = self._memory_service.nearby(
|
|
370
453
|
memory_id=arguments["memory_id"],
|
|
@@ -388,21 +471,21 @@ class SpatialMemoryServer:
|
|
|
388
471
|
],
|
|
389
472
|
}
|
|
390
473
|
|
|
391
|
-
def _handle_forget(self, arguments: dict[str, Any]) ->
|
|
474
|
+
def _handle_forget(self, arguments: dict[str, Any]) -> ForgetResponse:
|
|
392
475
|
"""Handle forget tool call."""
|
|
393
476
|
forget_result = self._memory_service.forget(
|
|
394
477
|
memory_id=arguments["memory_id"],
|
|
395
478
|
)
|
|
396
|
-
return asdict(forget_result)
|
|
479
|
+
return asdict(forget_result) # type: ignore[return-value]
|
|
397
480
|
|
|
398
|
-
def _handle_forget_batch(self, arguments: dict[str, Any]) ->
|
|
481
|
+
def _handle_forget_batch(self, arguments: dict[str, Any]) -> ForgetBatchResponse:
|
|
399
482
|
"""Handle forget_batch tool call."""
|
|
400
483
|
forget_batch_result = self._memory_service.forget_batch(
|
|
401
484
|
memory_ids=arguments["memory_ids"],
|
|
402
485
|
)
|
|
403
|
-
return asdict(forget_batch_result)
|
|
486
|
+
return asdict(forget_batch_result) # type: ignore[return-value]
|
|
404
487
|
|
|
405
|
-
def _handle_health(self, arguments: dict[str, Any]) ->
|
|
488
|
+
def _handle_health(self, arguments: dict[str, Any]) -> HealthResponse:
|
|
406
489
|
"""Handle health tool call."""
|
|
407
490
|
verbose = arguments.get("verbose", False)
|
|
408
491
|
|
|
@@ -414,7 +497,7 @@ class SpatialMemoryServer:
|
|
|
414
497
|
|
|
415
498
|
report = health_checker.get_health_report()
|
|
416
499
|
|
|
417
|
-
result:
|
|
500
|
+
result: HealthResponse = {
|
|
418
501
|
"version": __version__,
|
|
419
502
|
"status": report.status.value,
|
|
420
503
|
"timestamp": report.timestamp.isoformat(),
|
|
@@ -435,7 +518,7 @@ class SpatialMemoryServer:
|
|
|
435
518
|
|
|
436
519
|
return result
|
|
437
520
|
|
|
438
|
-
def _handle_journey(self, arguments: dict[str, Any]) ->
|
|
521
|
+
def _handle_journey(self, arguments: dict[str, Any]) -> JourneyResponse:
|
|
439
522
|
"""Handle journey tool call."""
|
|
440
523
|
journey_result = self._spatial_service.journey(
|
|
441
524
|
start_id=arguments["start_id"],
|
|
@@ -465,7 +548,7 @@ class SpatialMemoryServer:
|
|
|
465
548
|
"path_coverage": journey_result.path_coverage,
|
|
466
549
|
}
|
|
467
550
|
|
|
468
|
-
def _handle_wander(self, arguments: dict[str, Any]) ->
|
|
551
|
+
def _handle_wander(self, arguments: dict[str, Any]) -> WanderResponse:
|
|
469
552
|
"""Handle wander tool call."""
|
|
470
553
|
start_id = arguments.get("start_id")
|
|
471
554
|
if start_id is None:
|
|
@@ -504,7 +587,7 @@ class SpatialMemoryServer:
|
|
|
504
587
|
"total_distance": wander_result.total_distance,
|
|
505
588
|
}
|
|
506
589
|
|
|
507
|
-
def _handle_regions(self, arguments: dict[str, Any]) ->
|
|
590
|
+
def _handle_regions(self, arguments: dict[str, Any]) -> RegionsResponse:
|
|
508
591
|
"""Handle regions tool call."""
|
|
509
592
|
regions_result = self._spatial_service.regions(
|
|
510
593
|
namespace=arguments.get("namespace"),
|
|
@@ -538,7 +621,7 @@ class SpatialMemoryServer:
|
|
|
538
621
|
"clustering_quality": regions_result.clustering_quality,
|
|
539
622
|
}
|
|
540
623
|
|
|
541
|
-
def _handle_visualize(self, arguments: dict[str, Any]) ->
|
|
624
|
+
def _handle_visualize(self, arguments: dict[str, Any]) -> VisualizeResponse:
|
|
542
625
|
"""Handle visualize tool call."""
|
|
543
626
|
visualize_result = self._spatial_service.visualize(
|
|
544
627
|
memory_ids=arguments.get("memory_ids"),
|
|
@@ -578,7 +661,7 @@ class SpatialMemoryServer:
|
|
|
578
661
|
"format": visualize_result.format,
|
|
579
662
|
}
|
|
580
663
|
|
|
581
|
-
def _handle_decay(self, arguments: dict[str, Any]) ->
|
|
664
|
+
def _handle_decay(self, arguments: dict[str, Any]) -> DecayResponse:
|
|
582
665
|
"""Handle decay tool call."""
|
|
583
666
|
decay_result = self._lifecycle_service.decay(
|
|
584
667
|
namespace=arguments.get("namespace"),
|
|
@@ -607,7 +690,7 @@ class SpatialMemoryServer:
|
|
|
607
690
|
"dry_run": decay_result.dry_run,
|
|
608
691
|
}
|
|
609
692
|
|
|
610
|
-
def _handle_reinforce(self, arguments: dict[str, Any]) ->
|
|
693
|
+
def _handle_reinforce(self, arguments: dict[str, Any]) -> ReinforceResponse:
|
|
611
694
|
"""Handle reinforce tool call."""
|
|
612
695
|
reinforce_result = self._lifecycle_service.reinforce(
|
|
613
696
|
memory_ids=arguments["memory_ids"],
|
|
@@ -631,7 +714,7 @@ class SpatialMemoryServer:
|
|
|
631
714
|
"not_found": reinforce_result.not_found,
|
|
632
715
|
}
|
|
633
716
|
|
|
634
|
-
def _handle_extract(self, arguments: dict[str, Any]) ->
|
|
717
|
+
def _handle_extract(self, arguments: dict[str, Any]) -> ExtractResponse:
|
|
635
718
|
"""Handle extract tool call."""
|
|
636
719
|
extract_result = self._lifecycle_service.extract(
|
|
637
720
|
text=arguments["text"],
|
|
@@ -658,7 +741,7 @@ class SpatialMemoryServer:
|
|
|
658
741
|
],
|
|
659
742
|
}
|
|
660
743
|
|
|
661
|
-
def _handle_consolidate(self, arguments: dict[str, Any]) ->
|
|
744
|
+
def _handle_consolidate(self, arguments: dict[str, Any]) -> ConsolidateResponse:
|
|
662
745
|
"""Handle consolidate tool call."""
|
|
663
746
|
consolidate_result = self._lifecycle_service.consolidate(
|
|
664
747
|
namespace=arguments["namespace"],
|
|
@@ -683,7 +766,7 @@ class SpatialMemoryServer:
|
|
|
683
766
|
"dry_run": consolidate_result.dry_run,
|
|
684
767
|
}
|
|
685
768
|
|
|
686
|
-
def _handle_stats(self, arguments: dict[str, Any]) ->
|
|
769
|
+
def _handle_stats(self, arguments: dict[str, Any]) -> StatsResponse:
|
|
687
770
|
"""Handle stats tool call."""
|
|
688
771
|
stats_result = self._utility_service.stats(
|
|
689
772
|
namespace=arguments.get("namespace"),
|
|
@@ -721,7 +804,7 @@ class SpatialMemoryServer:
|
|
|
721
804
|
"avg_content_length": stats_result.avg_content_length,
|
|
722
805
|
}
|
|
723
806
|
|
|
724
|
-
def _handle_namespaces(self, arguments: dict[str, Any]) ->
|
|
807
|
+
def _handle_namespaces(self, arguments: dict[str, Any]) -> NamespacesResponse:
|
|
725
808
|
"""Handle namespaces tool call."""
|
|
726
809
|
namespaces_result = self._utility_service.namespaces(
|
|
727
810
|
include_stats=arguments.get("include_stats", True),
|
|
@@ -744,7 +827,7 @@ class SpatialMemoryServer:
|
|
|
744
827
|
"total_memories": namespaces_result.total_memories,
|
|
745
828
|
}
|
|
746
829
|
|
|
747
|
-
def _handle_delete_namespace(self, arguments: dict[str, Any]) ->
|
|
830
|
+
def _handle_delete_namespace(self, arguments: dict[str, Any]) -> DeleteNamespaceResponse:
|
|
748
831
|
"""Handle delete_namespace tool call."""
|
|
749
832
|
delete_result = self._utility_service.delete_namespace(
|
|
750
833
|
namespace=arguments["namespace"],
|
|
@@ -759,7 +842,7 @@ class SpatialMemoryServer:
|
|
|
759
842
|
"dry_run": delete_result.dry_run,
|
|
760
843
|
}
|
|
761
844
|
|
|
762
|
-
def _handle_rename_namespace(self, arguments: dict[str, Any]) ->
|
|
845
|
+
def _handle_rename_namespace(self, arguments: dict[str, Any]) -> RenameNamespaceResponse:
|
|
763
846
|
"""Handle rename_namespace tool call."""
|
|
764
847
|
rename_result = self._utility_service.rename_namespace(
|
|
765
848
|
old_namespace=arguments["old_namespace"],
|
|
@@ -773,7 +856,7 @@ class SpatialMemoryServer:
|
|
|
773
856
|
"message": rename_result.message,
|
|
774
857
|
}
|
|
775
858
|
|
|
776
|
-
def _handle_export_memories(self, arguments: dict[str, Any]) ->
|
|
859
|
+
def _handle_export_memories(self, arguments: dict[str, Any]) -> ExportResponse:
|
|
777
860
|
"""Handle export_memories tool call."""
|
|
778
861
|
export_result = self._export_import_service.export_memories(
|
|
779
862
|
output_path=arguments["output_path"],
|
|
@@ -792,7 +875,7 @@ class SpatialMemoryServer:
|
|
|
792
875
|
"compression": export_result.compression,
|
|
793
876
|
}
|
|
794
877
|
|
|
795
|
-
def _handle_import_memories(self, arguments: dict[str, Any]) ->
|
|
878
|
+
def _handle_import_memories(self, arguments: dict[str, Any]) -> ImportResponse:
|
|
796
879
|
"""Handle import_memories tool call."""
|
|
797
880
|
dry_run = arguments.get("dry_run", True)
|
|
798
881
|
import_result = self._export_import_service.import_memories(
|
|
@@ -834,7 +917,7 @@ class SpatialMemoryServer:
|
|
|
834
917
|
] if import_result.imported_memories else [],
|
|
835
918
|
}
|
|
836
919
|
|
|
837
|
-
def _handle_hybrid_recall(self, arguments: dict[str, Any]) ->
|
|
920
|
+
def _handle_hybrid_recall(self, arguments: dict[str, Any]) -> HybridRecallResponse:
|
|
838
921
|
"""Handle hybrid_recall tool call."""
|
|
839
922
|
hybrid_result = self._utility_service.hybrid_recall(
|
|
840
923
|
query=arguments["query"],
|
|
@@ -871,7 +954,7 @@ class SpatialMemoryServer:
|
|
|
871
954
|
# Tool Routing
|
|
872
955
|
# =========================================================================
|
|
873
956
|
|
|
874
|
-
def _handle_tool(self, name: str, arguments: dict[str, Any]) ->
|
|
957
|
+
def _handle_tool(self, name: str, arguments: dict[str, Any]) -> HandlerResponse:
|
|
875
958
|
"""Route tool call to appropriate handler.
|
|
876
959
|
|
|
877
960
|
Args:
|
|
@@ -879,7 +962,7 @@ class SpatialMemoryServer:
|
|
|
879
962
|
arguments: Tool arguments.
|
|
880
963
|
|
|
881
964
|
Returns:
|
|
882
|
-
Tool result as dictionary.
|
|
965
|
+
Tool result as typed dictionary.
|
|
883
966
|
|
|
884
967
|
Raises:
|
|
885
968
|
ValidationError: If tool name is unknown.
|
|
@@ -888,7 +971,7 @@ class SpatialMemoryServer:
|
|
|
888
971
|
with record_request(name, "success"):
|
|
889
972
|
return self._handle_tool_impl(name, arguments)
|
|
890
973
|
|
|
891
|
-
def _handle_tool_impl(self, name: str, arguments: dict[str, Any]) ->
|
|
974
|
+
def _handle_tool_impl(self, name: str, arguments: dict[str, Any]) -> HandlerResponse:
|
|
892
975
|
"""Implementation of tool routing using dispatch pattern.
|
|
893
976
|
|
|
894
977
|
Args:
|
|
@@ -906,6 +989,60 @@ class SpatialMemoryServer:
|
|
|
906
989
|
raise ValidationError(f"Unknown tool: {name}")
|
|
907
990
|
return handler(arguments)
|
|
908
991
|
|
|
992
|
+
@staticmethod
|
|
993
|
+
def _get_server_instructions() -> str:
|
|
994
|
+
"""Return behavioral instructions for Claude when using spatial-memory.
|
|
995
|
+
|
|
996
|
+
These instructions are automatically injected into Claude's system prompt
|
|
997
|
+
when the MCP server connects, enabling proactive memory management without
|
|
998
|
+
requiring user configuration.
|
|
999
|
+
"""
|
|
1000
|
+
return '''## Spatial Memory System
|
|
1001
|
+
|
|
1002
|
+
You have access to a persistent semantic memory system. Use it proactively to build cumulative knowledge across sessions.
|
|
1003
|
+
|
|
1004
|
+
### Session Start
|
|
1005
|
+
At conversation start, call `recall` with the user's apparent task/context to load relevant memories. Present insights naturally:
|
|
1006
|
+
- Good: "Based on previous work, you decided to use PostgreSQL because..."
|
|
1007
|
+
- Bad: "The database returned: [{id: '...', content: '...'}]"
|
|
1008
|
+
|
|
1009
|
+
### Recognizing Memory-Worthy Moments
|
|
1010
|
+
After these events, ask briefly "Save this? y/n" (minimal friction):
|
|
1011
|
+
- **Decisions**: "Let's use X...", "We decided...", "The approach is..."
|
|
1012
|
+
- **Solutions**: "The fix was...", "It failed because...", "The error was..."
|
|
1013
|
+
- **Patterns**: "This pattern works...", "The trick is...", "Always do X when..."
|
|
1014
|
+
- **Discoveries**: "I found that...", "Important:...", "TIL..."
|
|
1015
|
+
|
|
1016
|
+
Do NOT ask for trivial information. Only prompt for insights that would help future sessions.
|
|
1017
|
+
|
|
1018
|
+
### Saving Memories
|
|
1019
|
+
When user confirms, save with:
|
|
1020
|
+
- **Detailed content**: Include full context, reasoning, and specifics. Future agents need complete information.
|
|
1021
|
+
- **Contextual namespace**: Use project name, or categories like "decisions", "errors", "patterns"
|
|
1022
|
+
- **Descriptive tags**: Technologies, concepts, error types involved
|
|
1023
|
+
- **High importance (0.8-1.0)**: For decisions and critical fixes
|
|
1024
|
+
- **Medium importance (0.5-0.7)**: For patterns and learnings
|
|
1025
|
+
|
|
1026
|
+
### Synthesizing Answers
|
|
1027
|
+
When using `recall` or `hybrid_recall`, present results as natural knowledge:
|
|
1028
|
+
- Integrate memories into your response conversationally
|
|
1029
|
+
- Reference prior decisions: "You previously decided X because Y"
|
|
1030
|
+
- Don't expose raw JSON or tool mechanics to the user
|
|
1031
|
+
|
|
1032
|
+
### Auto-Extract for Long Sessions
|
|
1033
|
+
For significant problem-solving conversations (debugging sessions, architecture discussions), offer:
|
|
1034
|
+
"This session had good learnings. Extract key memories? y/n"
|
|
1035
|
+
Then use `extract` to automatically capture important information.
|
|
1036
|
+
|
|
1037
|
+
### Tool Selection Guide
|
|
1038
|
+
- `remember`: Store a single memory with full context
|
|
1039
|
+
- `recall`: Semantic search for relevant memories
|
|
1040
|
+
- `hybrid_recall`: Combined keyword + semantic search (better for specific terms)
|
|
1041
|
+
- `extract`: Auto-extract memories from conversation text
|
|
1042
|
+
- `nearby`: Find memories similar to a known memory
|
|
1043
|
+
- `regions`: Discover topic clusters in memory space
|
|
1044
|
+
- `journey`: Navigate conceptual path between two memories'''
|
|
1045
|
+
|
|
909
1046
|
async def run(self) -> None:
|
|
910
1047
|
"""Run the MCP server using stdio transport."""
|
|
911
1048
|
async with stdio_server() as (read_stream, write_stream):
|
|
@@ -917,6 +1054,10 @@ class SpatialMemoryServer:
|
|
|
917
1054
|
|
|
918
1055
|
def close(self) -> None:
|
|
919
1056
|
"""Clean up resources."""
|
|
1057
|
+
# Shutdown the thread pool executor
|
|
1058
|
+
if hasattr(self, "_executor"):
|
|
1059
|
+
self._executor.shutdown(wait=False)
|
|
1060
|
+
|
|
920
1061
|
if self._db is not None:
|
|
921
1062
|
self._db.close()
|
|
922
1063
|
|