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,300 @@
1
+ """Request tracing and timing utilities for Spatial Memory MCP Server.
2
+
3
+ This module provides request context tracking and timing utilities to support
4
+ observability and debugging. It uses contextvars for safe async propagation.
5
+
6
+ Usage:
7
+ from spatial_memory.core.tracing import (
8
+ RequestContext,
9
+ TimingContext,
10
+ get_current_context,
11
+ set_context,
12
+ clear_context,
13
+ )
14
+
15
+ # Set request context
16
+ ctx = RequestContext(
17
+ request_id="abc123def456",
18
+ agent_id="agent-1",
19
+ tool_name="recall",
20
+ started_at=utc_now(),
21
+ namespace="default",
22
+ )
23
+ token = set_context(ctx)
24
+ try:
25
+ # ... do work
26
+ pass
27
+ finally:
28
+ clear_context(token)
29
+
30
+ # Measure operation timings
31
+ timing = TimingContext()
32
+ with timing.measure("embedding"):
33
+ # ... generate embedding
34
+ pass
35
+ with timing.measure("search"):
36
+ # ... perform search
37
+ pass
38
+ print(f"Total: {timing.total_ms():.2f}ms")
39
+ print(f"Timings: {timing.timings}")
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import contextvars
45
+ import time
46
+ import uuid
47
+ from collections.abc import Generator
48
+ from contextlib import contextmanager
49
+ from dataclasses import dataclass, field
50
+ from datetime import datetime
51
+ from typing import TYPE_CHECKING
52
+
53
+ if TYPE_CHECKING:
54
+ from contextvars import Token
55
+
56
+ from spatial_memory.core.utils import utc_now
57
+
58
+
59
+ @dataclass
60
+ class RequestContext:
61
+ """Context information for a request.
62
+
63
+ Attributes:
64
+ request_id: Unique identifier for the request (first 12 chars of UUID).
65
+ agent_id: Optional identifier for the calling agent.
66
+ tool_name: Name of the MCP tool being called.
67
+ started_at: When the request started.
68
+ namespace: Optional namespace being operated on.
69
+ """
70
+
71
+ request_id: str
72
+ agent_id: str | None
73
+ tool_name: str
74
+ started_at: datetime
75
+ namespace: str | None = None
76
+
77
+ @classmethod
78
+ def create(
79
+ cls,
80
+ tool_name: str,
81
+ agent_id: str | None = None,
82
+ namespace: str | None = None,
83
+ ) -> RequestContext:
84
+ """Create a new request context with auto-generated ID.
85
+
86
+ Args:
87
+ tool_name: Name of the MCP tool being called.
88
+ agent_id: Optional identifier for the calling agent.
89
+ namespace: Optional namespace being operated on.
90
+
91
+ Returns:
92
+ A new RequestContext with generated request_id and started_at.
93
+
94
+ Example:
95
+ ctx = RequestContext.create("recall", agent_id="agent-1")
96
+ """
97
+ return cls(
98
+ request_id=uuid.uuid4().hex[:12],
99
+ agent_id=agent_id,
100
+ tool_name=tool_name,
101
+ started_at=utc_now(),
102
+ namespace=namespace,
103
+ )
104
+
105
+ def elapsed_ms(self) -> float:
106
+ """Calculate elapsed time since request started.
107
+
108
+ Returns:
109
+ Elapsed time in milliseconds.
110
+ """
111
+ return (utc_now() - self.started_at).total_seconds() * 1000
112
+
113
+
114
+ # Context variable for request tracking
115
+ _context: contextvars.ContextVar[RequestContext | None] = contextvars.ContextVar(
116
+ "request_context", default=None
117
+ )
118
+
119
+
120
+ def get_current_context() -> RequestContext | None:
121
+ """Get the current request context.
122
+
123
+ Returns:
124
+ The current RequestContext, or None if not in a request context.
125
+
126
+ Example:
127
+ ctx = get_current_context()
128
+ if ctx:
129
+ print(f"Request {ctx.request_id} for tool {ctx.tool_name}")
130
+ """
131
+ return _context.get()
132
+
133
+
134
+ def set_context(ctx: RequestContext) -> Token[RequestContext | None]:
135
+ """Set the current request context.
136
+
137
+ Args:
138
+ ctx: The request context to set.
139
+
140
+ Returns:
141
+ A token that can be used to reset the context.
142
+
143
+ Example:
144
+ ctx = RequestContext.create("recall")
145
+ token = set_context(ctx)
146
+ try:
147
+ # ... do work
148
+ pass
149
+ finally:
150
+ clear_context(token)
151
+ """
152
+ return _context.set(ctx)
153
+
154
+
155
+ def clear_context(token: Token[RequestContext | None]) -> None:
156
+ """Reset the context to its previous value.
157
+
158
+ Args:
159
+ token: The token returned from set_context().
160
+
161
+ Example:
162
+ token = set_context(ctx)
163
+ try:
164
+ # ... do work
165
+ pass
166
+ finally:
167
+ clear_context(token)
168
+ """
169
+ _context.reset(token)
170
+
171
+
172
+ @contextmanager
173
+ def request_context(
174
+ tool_name: str,
175
+ agent_id: str | None = None,
176
+ namespace: str | None = None,
177
+ ) -> Generator[RequestContext, None, None]:
178
+ """Context manager for request tracing.
179
+
180
+ Creates a RequestContext, sets it as current, and clears it on exit.
181
+
182
+ Args:
183
+ tool_name: Name of the MCP tool being called.
184
+ agent_id: Optional identifier for the calling agent.
185
+ namespace: Optional namespace being operated on.
186
+
187
+ Yields:
188
+ The created RequestContext.
189
+
190
+ Example:
191
+ with request_context("recall", agent_id="agent-1") as ctx:
192
+ print(f"Request {ctx.request_id}")
193
+ # ... do work
194
+ """
195
+ ctx = RequestContext.create(tool_name, agent_id=agent_id, namespace=namespace)
196
+ token = set_context(ctx)
197
+ try:
198
+ yield ctx
199
+ finally:
200
+ clear_context(token)
201
+
202
+
203
+ @dataclass
204
+ class TimingContext:
205
+ """Context for measuring operation timings.
206
+
207
+ Tracks timing of multiple named operations within a request.
208
+ Uses perf_counter for high-precision timing.
209
+
210
+ Attributes:
211
+ timings: Dictionary mapping operation names to durations in milliseconds.
212
+ start: Start time of the context (perf_counter value).
213
+
214
+ Example:
215
+ timing = TimingContext()
216
+ with timing.measure("embedding"):
217
+ embed = generate_embedding(text)
218
+ with timing.measure("search"):
219
+ results = search(embed)
220
+ print(f"Total: {timing.total_ms():.2f}ms")
221
+ print(f"Embedding: {timing.timings['embedding']:.2f}ms")
222
+ """
223
+
224
+ timings: dict[str, float] = field(default_factory=dict)
225
+ start: float = field(default_factory=time.perf_counter)
226
+
227
+ @contextmanager
228
+ def measure(self, name: str) -> Generator[None, None, None]:
229
+ """Measure the duration of a named operation.
230
+
231
+ Args:
232
+ name: Name of the operation being measured.
233
+
234
+ Yields:
235
+ None
236
+
237
+ Example:
238
+ with timing.measure("database_query"):
239
+ results = db.query(...)
240
+ """
241
+ t0 = time.perf_counter()
242
+ try:
243
+ yield
244
+ finally:
245
+ self.timings[name] = (time.perf_counter() - t0) * 1000
246
+
247
+ def total_ms(self) -> float:
248
+ """Calculate total elapsed time since context creation.
249
+
250
+ Returns:
251
+ Total elapsed time in milliseconds.
252
+
253
+ Example:
254
+ timing = TimingContext()
255
+ # ... do some work
256
+ print(f"Total time: {timing.total_ms():.2f}ms")
257
+ """
258
+ return (time.perf_counter() - self.start) * 1000
259
+
260
+ def summary(self) -> dict[str, float]:
261
+ """Get a summary of all timings including total.
262
+
263
+ Returns:
264
+ Dictionary with all named timings plus 'total_ms' and 'other_ms'.
265
+ 'other_ms' is time not accounted for by named operations.
266
+
267
+ Example:
268
+ timing = TimingContext()
269
+ with timing.measure("op1"):
270
+ time.sleep(0.01)
271
+ summary = timing.summary()
272
+ # {'op1': 10.0, 'total_ms': 10.5, 'other_ms': 0.5}
273
+ """
274
+ total = self.total_ms()
275
+ measured = sum(self.timings.values())
276
+ return {
277
+ **self.timings,
278
+ "total_ms": total,
279
+ "other_ms": max(0.0, total - measured),
280
+ }
281
+
282
+
283
+ def format_context_prefix() -> str:
284
+ """Format the current context as a log prefix.
285
+
286
+ Returns:
287
+ A string like "[req=abc123][agent=agent-1]" or "" if no context.
288
+
289
+ Example:
290
+ prefix = format_context_prefix()
291
+ logger.info(f"{prefix}Processing request...")
292
+ """
293
+ ctx = get_current_context()
294
+ if ctx is None:
295
+ return ""
296
+
297
+ parts = [f"[req={ctx.request_id}]"]
298
+ if ctx.agent_id:
299
+ parts.append(f"[agent={ctx.agent_id}]")
300
+ return "".join(parts)
@@ -0,0 +1,110 @@
1
+ """Shared utility functions for Spatial Memory MCP Server.
2
+
3
+ This module provides timezone-aware datetime utilities that handle the
4
+ impedance mismatch between Python's timezone-aware datetimes and LanceDB's
5
+ naive datetime storage.
6
+
7
+ Design Principles:
8
+ - Use timezone-aware datetimes for business logic and API responses
9
+ - Use naive UTC datetimes for database operations (LanceDB compatibility)
10
+ - Centralize all timezone conversion logic here for consistency
11
+ """
12
+
13
+ from datetime import datetime, timezone
14
+
15
+
16
+ def utc_now() -> datetime:
17
+ """Get current UTC datetime (timezone-aware).
18
+
19
+ Use this for business logic, API responses, and when timezone
20
+ information should be preserved.
21
+
22
+ Returns:
23
+ A timezone-aware datetime object representing the current time in UTC.
24
+
25
+ Example:
26
+ >>> from spatial_memory.core.utils import utc_now
27
+ >>> now = utc_now()
28
+ >>> now.tzinfo is not None
29
+ True
30
+ >>> print(now.isoformat()) # 2024-01-15T10:30:00+00:00
31
+ """
32
+ return datetime.now(timezone.utc)
33
+
34
+
35
+ def utc_now_naive() -> datetime:
36
+ """Get current UTC datetime as naive (no timezone info).
37
+
38
+ Use this for database comparisons where LanceDB stores naive timestamps.
39
+ This replaces the deprecated datetime.utcnow() function.
40
+
41
+ Returns:
42
+ A naive datetime object representing the current time in UTC.
43
+
44
+ Example:
45
+ >>> from spatial_memory.core.utils import utc_now_naive
46
+ >>> now = utc_now_naive()
47
+ >>> now.tzinfo is None
48
+ True
49
+ >>> print(now.isoformat()) # 2024-01-15T10:30:00
50
+ """
51
+ return datetime.now(timezone.utc).replace(tzinfo=None)
52
+
53
+
54
+ def to_naive_utc(dt: datetime) -> datetime:
55
+ """Convert any datetime to naive UTC for database operations.
56
+
57
+ This handles the conversion safely regardless of input type:
58
+ - If timezone-aware: converts to UTC, then strips tzinfo
59
+ - If naive: assumes already UTC, returns as-is
60
+
61
+ Args:
62
+ dt: A datetime object (naive or timezone-aware).
63
+
64
+ Returns:
65
+ A naive datetime object representing the time in UTC.
66
+
67
+ Example:
68
+ >>> from datetime import datetime, timezone
69
+ >>> from spatial_memory.core.utils import to_naive_utc
70
+ >>> aware = datetime(2024, 1, 15, 10, 30, tzinfo=timezone.utc)
71
+ >>> naive = to_naive_utc(aware)
72
+ >>> naive.tzinfo is None
73
+ True
74
+ >>> naive.hour
75
+ 10
76
+ """
77
+ if dt.tzinfo is not None:
78
+ # Convert to UTC first (handles non-UTC timezones), then strip tzinfo
79
+ dt = dt.astimezone(timezone.utc)
80
+ return dt.replace(tzinfo=None)
81
+ return dt
82
+
83
+
84
+ def to_aware_utc(dt: datetime) -> datetime:
85
+ """Convert any datetime to timezone-aware UTC.
86
+
87
+ This handles the conversion safely regardless of input type:
88
+ - If naive: assumes UTC, adds tzinfo
89
+ - If aware: converts to UTC
90
+
91
+ Args:
92
+ dt: A datetime object (naive or timezone-aware).
93
+
94
+ Returns:
95
+ A timezone-aware datetime object in UTC.
96
+
97
+ Example:
98
+ >>> from datetime import datetime
99
+ >>> from spatial_memory.core.utils import to_aware_utc
100
+ >>> naive = datetime(2024, 1, 15, 10, 30)
101
+ >>> aware = to_aware_utc(naive)
102
+ >>> aware.tzinfo is not None
103
+ True
104
+ >>> aware.hour
105
+ 10
106
+ """
107
+ if dt.tzinfo is None:
108
+ # Assume naive datetime is already UTC
109
+ return dt.replace(tzinfo=timezone.utc)
110
+ return dt.astimezone(timezone.utc)