cortexdb-mcp 0.4.0__tar.gz → 0.5.0__tar.gz
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.
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/.gitignore +3 -3
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/PKG-INFO +2 -1
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/README.md +1 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/__init__.py +1 -1
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/server.py +51 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/pyproject.toml +1 -1
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/tests/test_integration.py +67 -67
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/tests/test_server.py +50 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/Dockerfile +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/__main__.py +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/api.py +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/config.py +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/insights.py +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/cortexdb_mcp/render.py +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/tests/__init__.py +0 -0
- {cortexdb_mcp-0.4.0 → cortexdb_mcp-0.5.0}/tests/test_insights.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexdb-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: MCP Server for CortexDB — expose memory operations to AI agents
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -157,6 +157,7 @@ On Windows, MCP clients sometimes need the absolute path:
|
|
|
157
157
|
| Tool | Maps to | Description |
|
|
158
158
|
|------|---------|-------------|
|
|
159
159
|
| `memory_store` | `POST /v1/experience` | Store a new memory. Source/tags/type become labels. |
|
|
160
|
+
| `memory_status` | `GET /v1/experience/status` | Did a store land? Check by event ID or idempotency key after a timeout — "not found"/"failed" both mean re-storing is safe. |
|
|
160
161
|
| `memory_search` | `POST /v1/recall` | Search memories using natural language. |
|
|
161
162
|
| `memory_forget` | `POST /v1/forget` | Delete memories. With `query`, narrows by subject. |
|
|
162
163
|
| `get_context` | `POST /v1/recall` (holistic) | Deep context with facts + beliefs. |
|
|
@@ -144,6 +144,7 @@ On Windows, MCP clients sometimes need the absolute path:
|
|
|
144
144
|
| Tool | Maps to | Description |
|
|
145
145
|
|------|---------|-------------|
|
|
146
146
|
| `memory_store` | `POST /v1/experience` | Store a new memory. Source/tags/type become labels. |
|
|
147
|
+
| `memory_status` | `GET /v1/experience/status` | Did a store land? Check by event ID or idempotency key after a timeout — "not found"/"failed" both mean re-storing is safe. |
|
|
147
148
|
| `memory_search` | `POST /v1/recall` | Search memories using natural language. |
|
|
148
149
|
| `memory_forget` | `POST /v1/forget` | Delete memories. With `query`, narrows by subject. |
|
|
149
150
|
| `get_context` | `POST /v1/recall` (holistic) | Deep context with facts + beliefs. |
|
|
@@ -253,6 +253,57 @@ async def memory_store(
|
|
|
253
253
|
return f"Stored successfully. Event ID: {event_id}{warning_suffix}"
|
|
254
254
|
|
|
255
255
|
|
|
256
|
+
@mcp.tool()
|
|
257
|
+
async def memory_status(
|
|
258
|
+
event_id: str | None = None,
|
|
259
|
+
idempotency_key: str | None = None,
|
|
260
|
+
) -> str:
|
|
261
|
+
"""Check whether a stored memory landed and how far it has progressed,
|
|
262
|
+
via GET /v1/experience/status. Use this after a memory_store call timed
|
|
263
|
+
out or errored to decide whether to retry: "not found" or "failed" both
|
|
264
|
+
mean re-storing the same content is safe (it re-processes, never
|
|
265
|
+
duplicates).
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
event_id:
|
|
270
|
+
The ``Event ID`` returned by ``memory_store``.
|
|
271
|
+
idempotency_key:
|
|
272
|
+
Alternatively, the idempotency key of the original write
|
|
273
|
+
(keys are kept for 24 hours).
|
|
274
|
+
"""
|
|
275
|
+
if not event_id and not idempotency_key:
|
|
276
|
+
return "Pass event_id (from memory_store) or idempotency_key."
|
|
277
|
+
params: dict[str, Any] = {}
|
|
278
|
+
if event_id:
|
|
279
|
+
params["event_id"] = event_id
|
|
280
|
+
if idempotency_key:
|
|
281
|
+
params["idempotency_key"] = idempotency_key
|
|
282
|
+
result = await _request("GET", "/v1/experience/status", params=params)
|
|
283
|
+
if not result.get("found"):
|
|
284
|
+
return (
|
|
285
|
+
"Not found: never stored, expired (>24h), or failed and evicted. "
|
|
286
|
+
"Re-storing the same content is safe."
|
|
287
|
+
)
|
|
288
|
+
status = result.get("status", "unknown")
|
|
289
|
+
stages = ", ".join(result.get("stages_completed", [])) or "none"
|
|
290
|
+
line = (
|
|
291
|
+
f"Status: {status}. Event ID: {result.get('event_id', 'unknown')}. "
|
|
292
|
+
f"Stages completed: {stages}."
|
|
293
|
+
)
|
|
294
|
+
if status == "failed":
|
|
295
|
+
reason = result.get("failure_reason", "unknown")
|
|
296
|
+
line += (
|
|
297
|
+
f" Indexing failed ({reason}) — the memory is NOT searchable; "
|
|
298
|
+
"re-store the same content to re-process it."
|
|
299
|
+
)
|
|
300
|
+
elif status == "indexed":
|
|
301
|
+
line += " The memory is stored and searchable."
|
|
302
|
+
else:
|
|
303
|
+
line += " Accepted and durable; indexing still in progress."
|
|
304
|
+
return line
|
|
305
|
+
|
|
306
|
+
|
|
256
307
|
@mcp.tool()
|
|
257
308
|
async def memory_search(
|
|
258
309
|
query: str,
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
"""Live-instance smoke test for the v1 MCP server.
|
|
2
|
-
|
|
3
|
-
Hits a real CortexDB v1 deployment. Skipped by default — set
|
|
4
|
-
``CORTEXDB_LIVE=1`` to run.
|
|
5
|
-
|
|
6
|
-
Defaults to ``https://api-v1.cortexdb.ai`` and anonymous signup so the
|
|
7
|
-
test needs no pre-existing API key. Override with the usual env vars
|
|
8
|
-
(``CORTEXDB_URL``, ``CORTEXDB_API_KEY``, ``CORTEXDB_ACTOR``,
|
|
9
|
-
``CORTEXDB_SCOPE``) when targeting a private deployment.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import os
|
|
15
|
-
import time
|
|
16
|
-
|
|
17
|
-
import pytest
|
|
18
|
-
|
|
19
|
-
# Hard gate — never runs in normal CI.
|
|
20
|
-
if not os.environ.get("CORTEXDB_LIVE"):
|
|
21
|
-
pytest.skip("set CORTEXDB_LIVE=1 to run", allow_module_level=True)
|
|
22
|
-
|
|
23
|
-
# Stash a real API URL into env BEFORE importing the server module, since
|
|
24
|
-
# `_config` is built at import time.
|
|
25
|
-
os.environ.setdefault("CORTEXDB_URL", "https://api-v1.cortexdb.ai")
|
|
26
|
-
|
|
27
|
-
from cortexdb_mcp.server import ( # noqa: E402
|
|
28
|
-
health_check,
|
|
29
|
-
memory_list,
|
|
30
|
-
memory_search,
|
|
31
|
-
memory_store,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@pytest.mark.asyncio
|
|
36
|
-
async def test_health_check_returns_caller():
|
|
37
|
-
"""Health check proves the bearer works AND the deployment is reachable."""
|
|
38
|
-
out = await health_check()
|
|
39
|
-
assert "caller" in out
|
|
40
|
-
assert "deployment_preset" in out
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@pytest.mark.asyncio
|
|
44
|
-
async def test_store_then_search_roundtrip():
|
|
45
|
-
"""Full happy path: store a memory, wait for indexing, search it back."""
|
|
46
|
-
marker = f"mcp-live-{int(time.time())}"
|
|
47
|
-
store_out = await memory_store(
|
|
48
|
-
content=f"MCP integration test event {marker}",
|
|
49
|
-
source="mcp-integration-test",
|
|
50
|
-
)
|
|
51
|
-
assert "Stored successfully" in store_out
|
|
52
|
-
|
|
53
|
-
# Give the v1 pipeline a beat to index. Async enrichment finishes
|
|
54
|
-
# in ~4-10s; 15s margin keeps the test stable.
|
|
55
|
-
time.sleep(15)
|
|
56
|
-
|
|
57
|
-
search_out = await memory_search(query=marker, max_results=5)
|
|
58
|
-
# The marker text should round-trip through recall.
|
|
59
|
-
assert marker in search_out or "No results" in search_out
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@pytest.mark.asyncio
|
|
63
|
-
async def test_memory_list_returns_recent():
|
|
64
|
-
"""List endpoint surfaces events the caller can see."""
|
|
65
|
-
out = await memory_list(limit=3)
|
|
66
|
-
# Either some events or the empty-list message — both are healthy.
|
|
67
|
-
assert isinstance(out, str)
|
|
1
|
+
"""Live-instance smoke test for the v1 MCP server.
|
|
2
|
+
|
|
3
|
+
Hits a real CortexDB v1 deployment. Skipped by default — set
|
|
4
|
+
``CORTEXDB_LIVE=1`` to run.
|
|
5
|
+
|
|
6
|
+
Defaults to ``https://api-v1.cortexdb.ai`` and anonymous signup so the
|
|
7
|
+
test needs no pre-existing API key. Override with the usual env vars
|
|
8
|
+
(``CORTEXDB_URL``, ``CORTEXDB_API_KEY``, ``CORTEXDB_ACTOR``,
|
|
9
|
+
``CORTEXDB_SCOPE``) when targeting a private deployment.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
# Hard gate — never runs in normal CI.
|
|
20
|
+
if not os.environ.get("CORTEXDB_LIVE"):
|
|
21
|
+
pytest.skip("set CORTEXDB_LIVE=1 to run", allow_module_level=True)
|
|
22
|
+
|
|
23
|
+
# Stash a real API URL into env BEFORE importing the server module, since
|
|
24
|
+
# `_config` is built at import time.
|
|
25
|
+
os.environ.setdefault("CORTEXDB_URL", "https://api-v1.cortexdb.ai")
|
|
26
|
+
|
|
27
|
+
from cortexdb_mcp.server import ( # noqa: E402
|
|
28
|
+
health_check,
|
|
29
|
+
memory_list,
|
|
30
|
+
memory_search,
|
|
31
|
+
memory_store,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_health_check_returns_caller():
|
|
37
|
+
"""Health check proves the bearer works AND the deployment is reachable."""
|
|
38
|
+
out = await health_check()
|
|
39
|
+
assert "caller" in out
|
|
40
|
+
assert "deployment_preset" in out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_store_then_search_roundtrip():
|
|
45
|
+
"""Full happy path: store a memory, wait for indexing, search it back."""
|
|
46
|
+
marker = f"mcp-live-{int(time.time())}"
|
|
47
|
+
store_out = await memory_store(
|
|
48
|
+
content=f"MCP integration test event {marker}",
|
|
49
|
+
source="mcp-integration-test",
|
|
50
|
+
)
|
|
51
|
+
assert "Stored successfully" in store_out
|
|
52
|
+
|
|
53
|
+
# Give the v1 pipeline a beat to index. Async enrichment finishes
|
|
54
|
+
# in ~4-10s; 15s margin keeps the test stable.
|
|
55
|
+
time.sleep(15)
|
|
56
|
+
|
|
57
|
+
search_out = await memory_search(query=marker, max_results=5)
|
|
58
|
+
# The marker text should round-trip through recall.
|
|
59
|
+
assert marker in search_out or "No results" in search_out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
async def test_memory_list_returns_recent():
|
|
64
|
+
"""List endpoint surfaces events the caller can see."""
|
|
65
|
+
out = await memory_list(limit=3)
|
|
66
|
+
# Either some events or the empty-list message — both are healthy.
|
|
67
|
+
assert isinstance(out, str)
|
|
@@ -41,6 +41,7 @@ from cortexdb_mcp.server import (
|
|
|
41
41
|
memory_get,
|
|
42
42
|
memory_list,
|
|
43
43
|
memory_search,
|
|
44
|
+
memory_status,
|
|
44
45
|
memory_store,
|
|
45
46
|
resolve_conflict,
|
|
46
47
|
)
|
|
@@ -155,6 +156,55 @@ class TestMemoryStore:
|
|
|
155
156
|
assert "observed_at" in body["context"]
|
|
156
157
|
assert body["idempotency_key"].startswith("mcp-")
|
|
157
158
|
|
|
159
|
+
@pytest.mark.asyncio
|
|
160
|
+
async def test_memory_status_by_event_id(self):
|
|
161
|
+
patcher, mock = _mock_request(
|
|
162
|
+
{
|
|
163
|
+
"found": True,
|
|
164
|
+
"event_id": "evt_123",
|
|
165
|
+
"status": "indexed",
|
|
166
|
+
"stages_completed": ["captured", "indexed"],
|
|
167
|
+
"note": "stored and queryable",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
with patcher:
|
|
171
|
+
result = await memory_status(event_id="evt_123")
|
|
172
|
+
assert "indexed" in result
|
|
173
|
+
assert "evt_123" in result
|
|
174
|
+
method, path = mock.call_args[0][:2]
|
|
175
|
+
assert method == "GET"
|
|
176
|
+
assert path == "/v1/experience/status"
|
|
177
|
+
assert mock.call_args[1]["params"] == {"event_id": "evt_123"}
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_memory_status_failed_says_restore_is_safe(self):
|
|
181
|
+
patcher, _ = _mock_request(
|
|
182
|
+
{
|
|
183
|
+
"found": True,
|
|
184
|
+
"event_id": "evt_bad",
|
|
185
|
+
"status": "failed",
|
|
186
|
+
"stages_completed": ["captured"],
|
|
187
|
+
"failure_reason": "embedding provider unavailable",
|
|
188
|
+
"note": "…",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
with patcher:
|
|
192
|
+
result = await memory_status(idempotency_key="k-1")
|
|
193
|
+
assert "failed" in result
|
|
194
|
+
assert "re-store" in result.lower()
|
|
195
|
+
|
|
196
|
+
@pytest.mark.asyncio
|
|
197
|
+
async def test_memory_status_not_found_is_safe_to_retry(self):
|
|
198
|
+
patcher, _ = _mock_request({"found": False, "note": "…"})
|
|
199
|
+
with patcher:
|
|
200
|
+
result = await memory_status(idempotency_key="never-sent")
|
|
201
|
+
assert "safe" in result.lower()
|
|
202
|
+
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
async def test_memory_status_requires_an_argument(self):
|
|
205
|
+
result = await memory_status()
|
|
206
|
+
assert "event_id" in result
|
|
207
|
+
|
|
158
208
|
@pytest.mark.asyncio
|
|
159
209
|
async def test_source_and_tags_become_labels(self):
|
|
160
210
|
patcher, mock = _mock_request({"event_id": "evt_456"})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|