cortexdb-mcp 0.7.2__tar.gz → 0.7.4__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.
@@ -1,101 +1,109 @@
1
- # Rust
2
- /target
3
- **/*.rs.bk
4
-
5
- # Environment / secrets
6
- .env
7
- .env.local
8
- .env*.local
9
- *.pem
10
- *.key
11
- .npmrc
12
-
13
- # SQLite database
14
- *.sqlite
15
- *.sqlite-wal
16
- *.sqlite-shm
17
-
18
- # OS
19
- .DS_Store
20
- Thumbs.db
21
- desktop.ini
22
-
23
- # IDE
24
- .idea/
25
- .vscode/
26
- *.swp
27
- *.swo
28
-
29
- # Data directories
30
- cortexdb_data*/
31
- /data/
32
- # Per-bench tenant stores (RocksDB + Tantivy + HNSW state; regeneratable per run)
33
- /data_*/
34
- # Experimental per-branch stores (not tracked on this branch but left gitignored
35
- # so checkout from other branches doesn't surface them in git status)
36
- /event_memory_store/
37
- /llm_cache/
38
-
39
- # Benchmark inputs and per-run outputs (kept local, regenerated each run)
40
- benchmarks/longmemeval/data/
41
- benchmarks/longmemeval/server_results/
42
- benchmarks/longmemeval/fast_results/
43
- benchmarks/longmemeval/micro_results/
44
- benchmarks/longmemeval/server_logs/
45
- benchmarks/longmemeval/*.log
46
- benchmarks/locomo/locomo_results*.json
47
- benchmarks/locomo/server_results/
48
- benchmarks/locomo/*.log
49
- /answer_out.json
50
-
51
- # Local Claude Code state
52
- .claude/
53
- .tmp/
54
-
55
- # Python
56
- __pycache__/
57
- *.pyc
58
- .venv/
59
- venv/
60
-
61
- # Node
62
- node_modules/
63
- dist/
64
- .next/
65
-
66
- # Egg info
67
- *.egg-info/
68
-
69
- # Scratch/debug text files at root
70
- /*.txt
71
- /*.log
72
-
73
- # Local debug / marketing / private content (not for repo)
74
- harness/.reports/
75
- harness_data_*/
76
- blog/
77
- sales/
78
- videos/
79
- local-instance/
80
-
81
- # doc-claims verifier scratch output
82
- tools/verifier_out*/
83
-
84
- # Generated benchmark / runtime data (audit M-10: hundreds of untracked
85
- # data dirs at the workspace root slow every git status/search and risk
86
- # being packaged; results JSONs are artifacts, never source)
87
- benchmarks/data_*/
88
- benchmarks/**/fast_results/
89
- data_*/
90
- *_data/
91
- *_data_20*/
92
- cortexdb_data*/
93
- server_results/
94
- *.log
95
- docker_build.log
96
-
97
- # local gate/bench data stores
98
- gate*_data_*/
99
-
100
- # Code-plane benchmark local run outputs (official results are committed deliberately)
101
- benchmarks/codeplane/runs/
1
+ # Rust
2
+ /target
3
+ **/*.rs.bk
4
+
5
+ # Environment / secrets
6
+ .env
7
+ .env.local
8
+ .env*.local
9
+ *.pem
10
+ *.key
11
+ .npmrc
12
+
13
+ # SQLite database
14
+ *.sqlite
15
+ *.sqlite-wal
16
+ *.sqlite-shm
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+ desktop.ini
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+
29
+ # Data directories
30
+ cortexdb_data*/
31
+ /data/
32
+ # Per-bench tenant stores (RocksDB + Tantivy + HNSW state; regeneratable per run)
33
+ /data_*/
34
+ # Experimental per-branch stores (not tracked on this branch but left gitignored
35
+ # so checkout from other branches doesn't surface them in git status)
36
+ /event_memory_store/
37
+ /llm_cache/
38
+
39
+ # Benchmark inputs and per-run outputs (kept local, regenerated each run)
40
+ benchmarks/longmemeval/data/
41
+ benchmarks/longmemeval/server_results/
42
+ benchmarks/longmemeval/fast_results/
43
+ benchmarks/longmemeval/micro_results/
44
+ benchmarks/longmemeval/server_logs/
45
+ benchmarks/longmemeval/*.log
46
+ benchmarks/locomo/locomo_results*.json
47
+ benchmarks/locomo/server_results/
48
+ benchmarks/locomo/*.log
49
+ /answer_out.json
50
+
51
+ # Local Claude Code state
52
+ .claude/
53
+ .tmp/
54
+
55
+ # Python
56
+ __pycache__/
57
+ *.pyc
58
+ .venv/
59
+ venv/
60
+
61
+ # Node
62
+ node_modules/
63
+ dist/
64
+ .next/
65
+
66
+ # Egg info
67
+ *.egg-info/
68
+
69
+ # Scratch/debug text files at root
70
+ /*.txt
71
+ /*.log
72
+
73
+ # Local debug / marketing / private content (not for repo)
74
+ harness/.reports/
75
+ harness_data_*/
76
+ blog/
77
+ sales/
78
+ videos/
79
+ local-instance/
80
+
81
+ # doc-claims verifier scratch output
82
+ tools/verifier_out*/
83
+
84
+ # Generated benchmark / runtime data (audit M-10: hundreds of untracked
85
+ # data dirs at the workspace root slow every git status/search and risk
86
+ # being packaged; results JSONs are artifacts, never source)
87
+ benchmarks/data_*/
88
+ benchmarks/**/fast_results/
89
+ data_*/
90
+ *_data/
91
+ *_data_20*/
92
+ cortexdb_data*/
93
+ server_results/
94
+ *.log
95
+ docker_build.log
96
+
97
+ # local gate/bench data stores
98
+ gate*_data_*/
99
+
100
+ # Code-plane benchmark local run outputs (official results are committed deliberately)
101
+ benchmarks/codeplane/runs/
102
+
103
+ # Deliberately untracked, four times over: `git add docs/` keeps sweeping the
104
+ # investor deck sources and the 2.7 MB OpenClaw design PDF back in (7dda2a51,
105
+ # 3a778b5e, then again in the 2026-09-09 checkpoint). They live on disk; they do
106
+ # not belong in the repo. Stage explicit paths, never a directory.
107
+ docs/investor_deck/
108
+ docs/CORTEX_OPENCLAW_AGENT_OS_DESIGN.md
109
+ docs/CORTEX_OPENCLAW_AGENT_OS_DESIGN.pdf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cortexdb-mcp
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: MCP Server for CortexDB — expose memory operations to AI agents
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -9,6 +9,10 @@ Requires-Dist: httpx>=0.27
9
9
  Requires-Dist: mcp<2.0,>=1.0
10
10
  Requires-Dist: pydantic>=2.0
11
11
  Requires-Dist: uvicorn>=0.29
12
+ Provides-Extra: test
13
+ Requires-Dist: anyio<5,>=4.12; extra == 'test'
14
+ Requires-Dist: pytest-asyncio<2,>=1.3; extra == 'test'
15
+ Requires-Dist: pytest<10,>=9.0; extra == 'test'
12
16
  Description-Content-Type: text/markdown
13
17
 
14
18
  # CortexDB MCP Server
@@ -171,7 +175,7 @@ On Windows, MCP clients sometimes need the absolute path:
171
175
  | `memory_search` | `POST /v1/recall` | Search memories using natural language. |
172
176
  | `memory_forget` | `POST /v1/forget` | Delete memories. With `query`, narrows by subject. Accepts `from_preview_id` from `forget_preview`. |
173
177
  | `forget_preview` | `POST /v1/forget/preview` | Non-destructive dry run of a forget: per-layer estimated deletion counts + a `preview_id` for the safe two-phase flow. |
174
- | `get_context` | `POST /v1/recall` (holistic) | Deep context with facts + beliefs. |
178
+ | `get_context` | `POST /v1/recall` (configured view; holistic by default) | By default, deep context across the requested scope, authorized ancestors, and authorized descendants—never siblings. |
175
179
  | `advanced_search` | `POST /v1/recall` + temporal | Search with structured filters (time / source / type). |
176
180
 
177
181
  ### Event CRUD
@@ -222,12 +226,8 @@ Resources provide read-only data that AI tools can access:
222
226
  | Resource URI | Description |
223
227
  |---|---|
224
228
  | `cortexdb://health` | Server health status |
225
- | `cortexdb://metrics` | Request metrics (total, active, errors, rate-limited) |
226
- | `cortexdb://usage` | Usage statistics and tier limits |
227
- | `cortexdb://episodes` | Recent 50 episodes |
228
- | `cortexdb://entities` | Top 100 knowledge graph entities |
229
+ | `cortexdb://episodes` | Recent 50 events in the default scope |
229
230
  | `cortexdb://insights` | Proactive insights |
230
- | `cortexdb://ontology` | Entity and relationship type schema |
231
231
 
232
232
  ## Prompts
233
233
 
@@ -247,6 +247,10 @@ Pre-built prompt templates:
247
247
  |---|---|---|
248
248
  | `CORTEXDB_URL` | `https://api-v1.cortexdb.ai` | CortexDB server URL |
249
249
  | `CORTEXDB_API_KEY` | (none) | API key for authentication |
250
+ | `CORTEXDB_ACTOR` | (from signup state) | Actor for `X-Cortex-Actor`; must match the token subject |
251
+ | `CORTEXDB_SCOPE` | (from signup state) | Default scope for tool calls |
252
+ | `CORTEXDB_VIEW` | `holistic` | Recall reach: `holistic` = self + authorized ancestors + authorized descendants (no siblings); `descend` = self + authorized descendants; `granular` = exact scope |
253
+ | `CORTEXDB_TENANT_ID` | (none) | Legacy v0 tenant identifier |
250
254
  | `CORTEXDB_TIMEOUT` | `30.0` | HTTP request timeout (seconds) |
251
255
 
252
256
  ## Examples
@@ -158,7 +158,7 @@ On Windows, MCP clients sometimes need the absolute path:
158
158
  | `memory_search` | `POST /v1/recall` | Search memories using natural language. |
159
159
  | `memory_forget` | `POST /v1/forget` | Delete memories. With `query`, narrows by subject. Accepts `from_preview_id` from `forget_preview`. |
160
160
  | `forget_preview` | `POST /v1/forget/preview` | Non-destructive dry run of a forget: per-layer estimated deletion counts + a `preview_id` for the safe two-phase flow. |
161
- | `get_context` | `POST /v1/recall` (holistic) | Deep context with facts + beliefs. |
161
+ | `get_context` | `POST /v1/recall` (configured view; holistic by default) | By default, deep context across the requested scope, authorized ancestors, and authorized descendants—never siblings. |
162
162
  | `advanced_search` | `POST /v1/recall` + temporal | Search with structured filters (time / source / type). |
163
163
 
164
164
  ### Event CRUD
@@ -209,12 +209,8 @@ Resources provide read-only data that AI tools can access:
209
209
  | Resource URI | Description |
210
210
  |---|---|
211
211
  | `cortexdb://health` | Server health status |
212
- | `cortexdb://metrics` | Request metrics (total, active, errors, rate-limited) |
213
- | `cortexdb://usage` | Usage statistics and tier limits |
214
- | `cortexdb://episodes` | Recent 50 episodes |
215
- | `cortexdb://entities` | Top 100 knowledge graph entities |
212
+ | `cortexdb://episodes` | Recent 50 events in the default scope |
216
213
  | `cortexdb://insights` | Proactive insights |
217
- | `cortexdb://ontology` | Entity and relationship type schema |
218
214
 
219
215
  ## Prompts
220
216
 
@@ -234,6 +230,10 @@ Pre-built prompt templates:
234
230
  |---|---|---|
235
231
  | `CORTEXDB_URL` | `https://api-v1.cortexdb.ai` | CortexDB server URL |
236
232
  | `CORTEXDB_API_KEY` | (none) | API key for authentication |
233
+ | `CORTEXDB_ACTOR` | (from signup state) | Actor for `X-Cortex-Actor`; must match the token subject |
234
+ | `CORTEXDB_SCOPE` | (from signup state) | Default scope for tool calls |
235
+ | `CORTEXDB_VIEW` | `holistic` | Recall reach: `holistic` = self + authorized ancestors + authorized descendants (no siblings); `descend` = self + authorized descendants; `granular` = exact scope |
236
+ | `CORTEXDB_TENANT_ID` | (none) | Legacy v0 tenant identifier |
237
237
  | `CORTEXDB_TIMEOUT` | `30.0` | HTTP request timeout (seconds) |
238
238
 
239
239
  ## Examples
@@ -0,0 +1,17 @@
1
+ """CortexDB MCP Server -- expose CortexDB memory operations to AI agents via MCP."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version as _dist_version
4
+
5
+ # Derived, never restated (KAN-152). A hand-maintained copy of the version
6
+ # drifted the moment someone bumped pyproject.toml and not this file: the
7
+ # published 0.7.2 distribution reported 0.7.1 from `--version` and from this
8
+ # constant, so every version fact an agent could read disagreed with the
9
+ # artifact it had installed. Reading the installed distribution's metadata
10
+ # makes pyproject.toml the single source and removes the possibility of drift
11
+ # rather than fixing one instance of it.
12
+ try:
13
+ __version__ = _dist_version("cortexdb-mcp")
14
+ except PackageNotFoundError: # running from a source tree, not installed
15
+ __version__ = "0.0.0+unknown"
16
+
17
+ __all__ = ["__version__"]
@@ -59,9 +59,9 @@ class CortexMCPConfig:
59
59
  actor: str | None = None
60
60
  scope: str | None = None
61
61
  tenant_id: str | None = None
62
- # Default recall reach. "holistic" = the scope + its parents (the server
63
- # default); "descend" = the scope + all sub-scopes (e.g. query the org root
64
- # and reach every per-source sub-scope at once); "granular" = exact scope.
62
+ # Default public-recall reach. "holistic" = the scope + authorized ancestors
63
+ # + authorized descendants, never siblings (the server default); "descend"
64
+ # = the scope + authorized descendants; "granular" = exact scope.
65
65
  view: str = "holistic"
66
66
  timeout: float = 30.0
67
67
 
@@ -80,8 +80,10 @@ class CortexMCPConfig:
80
80
  CORTEXDB_API_KEY -- PASETO token (preferred) or legacy v0 key.
81
81
  CORTEXDB_ACTOR -- ActorId for X-Cortex-Actor (e.g. ``user:alice``).
82
82
  CORTEXDB_SCOPE -- Default scope path for tool calls.
83
- CORTEXDB_VIEW -- Default recall reach: ``holistic`` (default),
84
- ``descend`` (scope + all sub-scopes), or ``granular``.
83
+ CORTEXDB_VIEW -- Default public-recall reach: ``holistic``
84
+ (scope + authorized ancestors + authorized
85
+ descendants, never siblings), ``descend``
86
+ (scope + authorized descendants), or ``granular``.
85
87
  CORTEXDB_TENANT_ID -- Legacy tenant id (v0 callers only).
86
88
  CORTEXDB_TIMEOUT -- Request timeout in seconds (default ``30.0``).
87
89
 
@@ -1409,7 +1409,7 @@ async def code_explore(
1409
1409
  else:
1410
1410
  path = "/v1/code/workspace/context"
1411
1411
  payload = {"repos": selected, "task": query, "token_budget": budget}
1412
- result = await _request("POST", path, json=payload)
1412
+ result = await _request("POST", path, json_body=payload)
1413
1413
  envelope = result.get("envelope", {})
1414
1414
  items = envelope.get("items", [])
1415
1415
  if not items:
@@ -1428,7 +1428,7 @@ async def code_explore(
1428
1428
  f" files, {envelope.get('tokens_used', 0)}/{envelope.get('token_budget', budget)}"
1429
1429
  " tokens)"
1430
1430
  )
1431
- return "\\n".join(lines)
1431
+ return "\n".join(lines)
1432
1432
 
1433
1433
 
1434
1434
  @mcp.tool(structured_output=False)
@@ -1472,7 +1472,7 @@ async def code_impact(
1472
1472
  )
1473
1473
  if result.get("truncated"):
1474
1474
  lines.append("(truncated at the row budget — narrow the depth)")
1475
- return "\\n".join(lines)
1475
+ return "\n".join(lines)
1476
1476
 
1477
1477
 
1478
1478
  @mcp.tool(structured_output=False)
@@ -1534,7 +1534,7 @@ async def code_inventory(
1534
1534
  payload["cursor"] = max(0, int(cursor))
1535
1535
  if selected == "files" and path is not None:
1536
1536
  payload["path"] = path
1537
- result = await _request("POST", "/v1/code/inventory", json=payload)
1537
+ result = await _request("POST", "/v1/code/inventory", json_body=payload)
1538
1538
  if selected == "stats":
1539
1539
  stats = result.get("stats", {})
1540
1540
  counts = stats.get("predicate_counts", {})
@@ -1587,7 +1587,7 @@ async def code_inventory(
1587
1587
  if result.get("truncated"):
1588
1588
  lines.append(f"(more rows available; next cursor: {result.get('next_cursor')})")
1589
1589
  lines.append(f"({result.get('scanned', len(rows))} catalog members scanned)")
1590
- return "\\n".join(lines)
1590
+ return "\n".join(lines)
1591
1591
 
1592
1592
 
1593
1593
  @mcp.tool(structured_output=False)
@@ -1624,7 +1624,7 @@ async def code_graph(
1624
1624
  "max_nodes": max(1, min(int(max_nodes), 5000)),
1625
1625
  "max_edges": max(1, min(int(max_edges), 10000)),
1626
1626
  }
1627
- result = await _request("POST", "/v1/code/graph/query", json=payload)
1627
+ result = await _request("POST", "/v1/code/graph/query", json_body=payload)
1628
1628
  nodes = {
1629
1629
  row.get("id"): row.get("label") or row.get("symbol") or f"node {row.get('id')}"
1630
1630
  for row in result.get("nodes", [])
@@ -1655,7 +1655,8 @@ async def code_graph(
1655
1655
  )
1656
1656
  if result.get("truncated"):
1657
1657
  lines.append("(truncated at the explicit node/edge budget — narrow the query)")
1658
- return "\\n".join(lines)
1658
+ return "\n".join(lines)
1659
+
1659
1660
 
1660
1661
  def main() -> None:
1661
1662
  """Run the CortexDB MCP server over stdio transport."""
@@ -1,26 +1,37 @@
1
- [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "cortexdb-mcp"
7
- version = "0.7.2"
8
- description = "MCP Server for CortexDB — expose memory operations to AI agents"
9
- requires-python = ">=3.10"
10
- license = "MIT"
11
- readme = "README.md"
12
- dependencies = [
13
- # KAN-151: an unbounded `mcp>=1.0` resolved to mcp 2.0.0 on a fresh
14
- # install, which removed/relocated `mcp.server.fastmcp` — every new
15
- # install of cortexdb-mcp crashed with ModuleNotFoundError at import
16
- # time, before main() ran. Bound the major until this server is
17
- # migrated to the 2.x API.
18
- "mcp>=1.0,<2.0",
19
- "httpx>=0.27",
20
- "pydantic>=2.0",
21
- "fastapi>=0.110",
22
- "uvicorn>=0.29",
23
- ]
24
-
25
- [project.scripts]
26
- cortexdb-mcp = "cortexdb_mcp.server:main"
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cortexdb-mcp"
7
+ version = "0.7.4"
8
+ description = "MCP Server for CortexDB — expose memory operations to AI agents"
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ dependencies = [
13
+ # KAN-151: an unbounded `mcp>=1.0` resolved to mcp 2.0.0 on a fresh
14
+ # install, which removed/relocated `mcp.server.fastmcp` — every new
15
+ # install of cortexdb-mcp crashed with ModuleNotFoundError at import
16
+ # time, before main() ran. Bound the major until this server is
17
+ # migrated to the 2.x API.
18
+ "mcp>=1.0,<2.0",
19
+ "httpx>=0.27",
20
+ "pydantic>=2.0",
21
+ "fastapi>=0.110",
22
+ "uvicorn>=0.29",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ # `pip install -e ".[test]"` then `python -m pytest -q`. Pinned to the
27
+ # major lines the suite is verified against: pytest 9.x, pytest-asyncio 1.x
28
+ # (strict mode, `@pytest.mark.asyncio`), anyio 4.x (the async backend mcp
29
+ # and httpx run on; its pytest plugin must coexist with pytest-asyncio).
30
+ test = [
31
+ "pytest>=9.0,<10",
32
+ "pytest-asyncio>=1.3,<2",
33
+ "anyio>=4.12,<5",
34
+ ]
35
+
36
+ [project.scripts]
37
+ cortexdb-mcp = "cortexdb_mcp.server:main"
@@ -9,7 +9,7 @@ here cover only the surface the server actually exposes.
9
9
  from __future__ import annotations
10
10
 
11
11
  from datetime import datetime, timedelta, timezone
12
- from unittest.mock import AsyncMock, patch
12
+ from unittest.mock import create_autospec, patch
13
13
 
14
14
  import pytest
15
15
 
@@ -31,6 +31,7 @@ from cortexdb_mcp.server import (
31
31
  _is_synthetic_subject,
32
32
  _memories_from_context,
33
33
  _record_text,
34
+ _request,
34
35
  _typed_value_text,
35
36
  advanced_search,
36
37
  belief_declare,
@@ -62,8 +63,15 @@ from cortexdb_mcp.server import (
62
63
 
63
64
 
64
65
  def _mock_request(return_value: dict):
65
- """Patch ``_request`` with an async mock returning ``return_value``."""
66
- mock = AsyncMock(return_value=return_value)
66
+ """Patch ``_request`` with an awaitable mock returning ``return_value``.
67
+
68
+ The mock is autospecced from the real helper, so a tool that passes a
69
+ keyword ``_request`` does not accept (``json=`` where it takes
70
+ ``json_body=``) fails here with the same ``TypeError`` it raises in
71
+ production. A permissive ``AsyncMock`` hid exactly that for three of
72
+ the four code tools.
73
+ """
74
+ mock = create_autospec(_request, return_value=return_value)
67
75
  return patch("cortexdb_mcp.server._request", mock), mock
68
76
 
69
77
 
@@ -1254,6 +1262,8 @@ class TestGetInsightsTool:
1254
1262
 
1255
1263
 
1256
1264
  class TestCodeTools:
1265
+ """T-905: focused read-only code tools, budget-enforced."""
1266
+
1257
1267
  def test_code_tool_profile_is_exact_and_deterministic(self):
1258
1268
  available = ["memory_search", "code_impact", "code_explore", "entity_search"]
1259
1269
  assert _selected_tool_names("code", available) == ["code_explore", "code_impact"]
@@ -1267,8 +1277,6 @@ class TestCodeTools:
1267
1277
  tool = next(tool for tool in await srv.mcp.list_tools() if tool.name == name)
1268
1278
  assert tool.outputSchema is None
1269
1279
 
1270
- """T-905: focused read-only code tools, budget-enforced."""
1271
-
1272
1280
  @pytest.mark.anyio
1273
1281
  async def test_code_explore_renders_envelope_with_warnings(self):
1274
1282
  patcher, mock = _mock_request(
@@ -1296,6 +1304,16 @@ class TestCodeTools:
1296
1304
  assert "WARNING: prior attempt" in out
1297
1305
  assert "NOTE: truncated" in out
1298
1306
  assert "1/3 files" in out
1307
+ # One entry per line, joined with a real newline — the tool once
1308
+ # joined with the two-character string backslash-n.
1309
+ assert out.splitlines() == [
1310
+ "// shop:core/billing.ts@abc123 [changed]",
1311
+ "export function settle(i) {}",
1312
+ "WARNING: prior attempt on `settle`: broke rounding",
1313
+ "NOTE: truncated to fit budget: core/billing.ts",
1314
+ "(1/3 files, 90/100 tokens)",
1315
+ ]
1316
+ assert "\\n" not in out
1299
1317
  # Read-only: the request was a context POST, nothing else.
1300
1318
  method, path = mock.call_args[0][0], mock.call_args[0][1]
1301
1319
  assert (method, path) == ("POST", "/v1/code/context")
@@ -1305,10 +1323,10 @@ class TestCodeTools:
1305
1323
  patcher, mock = _mock_request({"envelope": {"items": []}})
1306
1324
  with patcher:
1307
1325
  await code_explore("q", repo="shop", token_budget=999999)
1308
- assert mock.call_args.kwargs["json"]["token_budget"] == 8000
1326
+ assert mock.call_args.kwargs["json_body"]["token_budget"] == 8000
1309
1327
  with patcher:
1310
1328
  await code_explore("q", repo="shop", token_budget=1)
1311
- assert mock.call_args.kwargs["json"]["token_budget"] == 100
1329
+ assert mock.call_args.kwargs["json_body"]["token_budget"] == 100
1312
1330
 
1313
1331
  @pytest.mark.anyio
1314
1332
  async def test_code_explore_supports_one_budgeted_multi_repo_call(self):
@@ -1346,7 +1364,7 @@ class TestCodeTools:
1346
1364
  )
1347
1365
  assert "api:src/client.ts" in out and "worker:src/job.py" in out
1348
1366
  assert mock.call_args[0][:2] == ("POST", "/v1/code/workspace/context")
1349
- assert mock.call_args.kwargs["json"]["repos"] == ["api", "worker"]
1367
+ assert mock.call_args.kwargs["json_body"]["repos"] == ["api", "worker"]
1350
1368
 
1351
1369
  @pytest.mark.anyio
1352
1370
  async def test_code_explore_without_repo_searches_visible_workspace(self):
@@ -1354,7 +1372,7 @@ class TestCodeTools:
1354
1372
  with patcher:
1355
1373
  await code_explore("find the owner")
1356
1374
  assert mock.call_args[0][:2] == ("POST", "/v1/code/workspace/context")
1357
- assert mock.call_args.kwargs["json"]["repos"] == []
1375
+ assert mock.call_args.kwargs["json_body"]["repos"] == []
1358
1376
 
1359
1377
  @pytest.mark.anyio
1360
1378
  async def test_code_impact_reports_tiers_and_truncation(self):
@@ -1372,6 +1390,13 @@ class TestCodeTools:
1372
1390
  assert "sym-main" in out and "Compiler" in out
1373
1391
  assert "node 9" in out and "Syntax" in out
1374
1392
  assert "truncated" in out
1393
+ assert out.splitlines() == [
1394
+ "Impact of changing 'settle' (depth 6):",
1395
+ "- sym-main (hops: 1, confidence: Compiler)",
1396
+ "- node 9 (hops: 2, confidence: Syntax)",
1397
+ "(truncated at the row budget — narrow the depth)",
1398
+ ]
1399
+ assert "\\n" not in out
1375
1400
  # Depth clamped to 6; read-only GET.
1376
1401
  assert mock.call_args.kwargs["params"]["depth"] == 6
1377
1402
  method, path = mock.call_args[0][0], mock.call_args[0][1]
@@ -1409,8 +1434,15 @@ class TestCodeTools:
1409
1434
  out = await code_inventory("routes", "shop", limit=99999)
1410
1435
  assert "configure -> /healthz [Syntax/Dynamic]" in out
1411
1436
  assert "next cursor: 17" in out
1437
+ assert out.splitlines() == [
1438
+ "Routes in 'shop':",
1439
+ "- configure -> /healthz [Syntax/Dynamic]",
1440
+ "(more rows available; next cursor: 17)",
1441
+ "(1 catalog members scanned)",
1442
+ ]
1443
+ assert "\\n" not in out
1412
1444
  assert mock.call_args[0][:2] == ("POST", "/v1/code/inventory")
1413
- assert mock.call_args.kwargs["json"]["limit"] == 1000
1445
+ assert mock.call_args.kwargs["json_body"]["limit"] == 1000
1414
1446
 
1415
1447
  @pytest.mark.anyio
1416
1448
  async def test_code_inventory_renders_stats_and_closed_predicate_schema(self):
@@ -1434,7 +1466,7 @@ class TestCodeTools:
1434
1466
  assert "calls=11, defines=8" in out
1435
1467
  assert "predicate schema: defines, calls, references" in out
1436
1468
  assert "freshness: ready, pending=0, strategy=unknown" in out
1437
- assert mock.call_args.kwargs["json"]["kind"] == "stats"
1469
+ assert mock.call_args.kwargs["json_body"]["kind"] == "stats"
1438
1470
 
1439
1471
  @pytest.mark.anyio
1440
1472
  async def test_code_inventory_preserves_dead_code_uncertainty(self):
@@ -1485,7 +1517,7 @@ class TestCodeTools:
1485
1517
  with patcher:
1486
1518
  out = await code_inventory("files", "subject", path="src")
1487
1519
  assert "src/cypher [directory]" in out
1488
- assert mock.call_args.kwargs["json"]["path"] == "src"
1520
+ assert mock.call_args.kwargs["json_body"]["path"] == "src"
1489
1521
 
1490
1522
  @pytest.mark.anyio
1491
1523
  async def test_code_graph_is_one_bounded_typed_query(self):
@@ -1526,7 +1558,132 @@ class TestCodeTools:
1526
1558
  assert "handler --handles_event[Syntax/Dynamic]--> invoice.created" in out
1527
1559
  assert "long-handler-identity" not in out
1528
1560
  assert "truncated" in out
1561
+ assert out.splitlines() == [
1562
+ "- handler --handles_event[Syntax/Dynamic]--> invoice.created (depth 1)",
1563
+ "(2 nodes, 1 edges, generation 7, depth 1)",
1564
+ "(truncated at the explicit node/edge budget — narrow the query)",
1565
+ ]
1566
+ assert "\\n" not in out
1529
1567
  assert mock.call_args[0][:2] == ("POST", "/v1/code/graph/query")
1530
- payload = mock.call_args.kwargs["json"]
1568
+ payload = mock.call_args.kwargs["json_body"]
1531
1569
  assert payload["depth"] == 6
1532
1570
  assert payload["max_edges"] == 10000
1571
+
1572
+
1573
+ # ---------------------------------------------------------------------------
1574
+ # Code tools — wire contract with ``_request``. The helper's body keyword is
1575
+ # ``json_body`` (keyword-only); ``code_explore``, ``code_inventory`` and
1576
+ # ``code_graph`` once passed ``json=`` and raised TypeError on every call,
1577
+ # which the old permissive AsyncMock accepted. Each test pins the exact
1578
+ # positional args and kwargs the tool hands to the helper.
1579
+ # ---------------------------------------------------------------------------
1580
+
1581
+
1582
+ class TestCodeToolsWireContract:
1583
+ def test_request_mock_rejects_kwargs_the_helper_does_not_accept(self):
1584
+ # Guard on the guard: the harness must reproduce the production
1585
+ # TypeError, or none of the assertions below could catch a regression.
1586
+ _, mock = _mock_request({})
1587
+ with pytest.raises(TypeError, match="json"):
1588
+ mock("POST", "/v1/code/context", json={"repo": "shop"})
1589
+ mock.assert_not_awaited()
1590
+
1591
+ @pytest.mark.anyio
1592
+ async def test_code_explore_single_repo_passes_json_body(self):
1593
+ patcher, mock = _mock_request({"envelope": {"items": []}})
1594
+ with patcher:
1595
+ await code_explore("how does settle work?", repo="shop", token_budget=250)
1596
+ assert mock.await_count == 1
1597
+ assert mock.call_args.args == ("POST", "/v1/code/context")
1598
+ assert mock.call_args.kwargs == {
1599
+ "json_body": {
1600
+ "repo": "shop",
1601
+ "task": "how does settle work?",
1602
+ "token_budget": 250,
1603
+ }
1604
+ }
1605
+
1606
+ @pytest.mark.anyio
1607
+ async def test_code_explore_workspace_passes_json_body(self):
1608
+ patcher, mock = _mock_request({"envelope": {"items": []}})
1609
+ with patcher:
1610
+ await code_explore(
1611
+ "trace the request", repo="api", repos=["worker", "api"], token_budget=250
1612
+ )
1613
+ assert mock.await_count == 1
1614
+ assert mock.call_args.args == ("POST", "/v1/code/workspace/context")
1615
+ assert mock.call_args.kwargs == {
1616
+ "json_body": {
1617
+ "repos": ["api", "worker"],
1618
+ "task": "trace the request",
1619
+ "token_budget": 250,
1620
+ }
1621
+ }
1622
+
1623
+ @pytest.mark.anyio
1624
+ async def test_code_impact_passes_params_only(self):
1625
+ patcher, mock = _mock_request({"reached": [], "identities": [{"id": 1}]})
1626
+ with patcher:
1627
+ await code_impact("settle", repo="shop", depth=2)
1628
+ assert mock.await_count == 1
1629
+ assert mock.call_args.args == ("GET", "/v1/code/impact")
1630
+ assert mock.call_args.kwargs == {
1631
+ "params": {"repo": "shop", "symbol": "settle", "depth": 2}
1632
+ }
1633
+
1634
+ @pytest.mark.anyio
1635
+ async def test_code_inventory_passes_json_body_with_cursor_and_path(self):
1636
+ patcher, mock = _mock_request({"rows": [], "scanned": 0})
1637
+ with patcher:
1638
+ await code_inventory("files", "shop", limit=50, cursor=17, path="src")
1639
+ assert mock.await_count == 1
1640
+ assert mock.call_args.args == ("POST", "/v1/code/inventory")
1641
+ assert mock.call_args.kwargs == {
1642
+ "json_body": {
1643
+ "repo": "shop",
1644
+ "kind": "files",
1645
+ "limit": 50,
1646
+ "cursor": 17,
1647
+ "path": "src",
1648
+ }
1649
+ }
1650
+
1651
+ @pytest.mark.anyio
1652
+ async def test_code_inventory_symbol_search_passes_params_only(self):
1653
+ patcher, mock = _mock_request({"matches": [], "truncated": False})
1654
+ with patcher:
1655
+ await code_inventory("symbols", "shop", prefix="cbm_", limit=50)
1656
+ assert mock.await_count == 1
1657
+ assert mock.call_args.args == ("GET", "/v1/code/symbol/search")
1658
+ assert mock.call_args.kwargs == {
1659
+ "params": {"repo": "shop", "prefix": "cbm_", "limit": 50}
1660
+ }
1661
+
1662
+ @pytest.mark.anyio
1663
+ async def test_code_graph_passes_json_body(self):
1664
+ patcher, mock = _mock_request({"nodes": [], "edges": [], "resolutions": []})
1665
+ with patcher:
1666
+ await code_graph(
1667
+ ["invoice.created"],
1668
+ repo="shop",
1669
+ predicates=["handles_event"],
1670
+ direction="incoming",
1671
+ depth=2,
1672
+ include_dynamic=True,
1673
+ max_nodes=10,
1674
+ max_edges=20,
1675
+ )
1676
+ assert mock.await_count == 1
1677
+ assert mock.call_args.args == ("POST", "/v1/code/graph/query")
1678
+ assert mock.call_args.kwargs == {
1679
+ "json_body": {
1680
+ "repo": "shop",
1681
+ "roots": ["invoice.created"],
1682
+ "predicates": ["handles_event"],
1683
+ "direction": "incoming",
1684
+ "depth": 2,
1685
+ "include_dynamic": True,
1686
+ "max_nodes": 10,
1687
+ "max_edges": 20,
1688
+ }
1689
+ }
@@ -1,3 +0,0 @@
1
- """CortexDB MCP Server -- expose CortexDB memory operations to AI agents via MCP."""
2
-
3
- __version__ = "0.7.1"
File without changes