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.
@@ -77,6 +77,6 @@ blog/
77
77
  sales/
78
78
  videos/
79
79
  local-instance/
80
-
81
- # doc-claims verifier scratch output
82
- tools/verifier_out*/
80
+
81
+ # doc-claims verifier scratch output
82
+ tools/verifier_out*/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexdb-mcp
3
- Version: 0.4.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. |
@@ -1,3 +1,3 @@
1
1
  """CortexDB MCP Server -- expose CortexDB memory operations to AI agents via MCP."""
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.5.0"
@@ -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,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cortexdb-mcp"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "MCP Server for CortexDB — expose memory operations to AI agents"
9
9
  requires-python = ">=3.10"
10
10
  license = "MIT"
@@ -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