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.
- spatial_memory/__init__.py +97 -0
- spatial_memory/__main__.py +270 -0
- spatial_memory/adapters/__init__.py +7 -0
- spatial_memory/adapters/lancedb_repository.py +878 -0
- spatial_memory/config.py +728 -0
- spatial_memory/core/__init__.py +118 -0
- spatial_memory/core/cache.py +317 -0
- spatial_memory/core/circuit_breaker.py +297 -0
- spatial_memory/core/connection_pool.py +220 -0
- spatial_memory/core/consolidation_strategies.py +402 -0
- spatial_memory/core/database.py +3069 -0
- 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 +557 -0
- spatial_memory/core/errors.py +317 -0
- spatial_memory/core/file_security.py +702 -0
- spatial_memory/core/filesystem.py +178 -0
- spatial_memory/core/health.py +289 -0
- spatial_memory/core/helpers.py +79 -0
- spatial_memory/core/import_security.py +432 -0
- spatial_memory/core/lifecycle_ops.py +1067 -0
- spatial_memory/core/logging.py +194 -0
- spatial_memory/core/metrics.py +192 -0
- spatial_memory/core/models.py +628 -0
- spatial_memory/core/rate_limiter.py +326 -0
- spatial_memory/core/response_types.py +497 -0
- spatial_memory/core/security.py +588 -0
- spatial_memory/core/spatial_ops.py +426 -0
- spatial_memory/core/tracing.py +300 -0
- spatial_memory/core/utils.py +110 -0
- spatial_memory/core/validation.py +403 -0
- spatial_memory/factory.py +407 -0
- spatial_memory/migrations/__init__.py +40 -0
- spatial_memory/ports/__init__.py +11 -0
- spatial_memory/ports/repositories.py +631 -0
- spatial_memory/py.typed +0 -0
- spatial_memory/server.py +1141 -0
- spatial_memory/services/__init__.py +70 -0
- spatial_memory/services/export_import.py +1023 -0
- spatial_memory/services/lifecycle.py +1120 -0
- spatial_memory/services/memory.py +412 -0
- spatial_memory/services/spatial.py +1147 -0
- spatial_memory/services/utility.py +409 -0
- spatial_memory/tools/__init__.py +5 -0
- spatial_memory/tools/definitions.py +695 -0
- spatial_memory/verify.py +140 -0
- spatial_memory_mcp-1.6.1.dist-info/METADATA +499 -0
- spatial_memory_mcp-1.6.1.dist-info/RECORD +54 -0
- spatial_memory_mcp-1.6.1.dist-info/WHEEL +4 -0
- spatial_memory_mcp-1.6.1.dist-info/entry_points.txt +2 -0
- 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)
|