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.
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/.gitignore +109 -101
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/PKG-INFO +11 -7
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/README.md +6 -6
- cortexdb_mcp-0.7.4/cortexdb_mcp/__init__.py +17 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/config.py +7 -5
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/server.py +8 -7
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/pyproject.toml +37 -26
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/tests/test_server.py +170 -13
- cortexdb_mcp-0.7.2/cortexdb_mcp/__init__.py +0 -3
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/Dockerfile +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/__main__.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/api.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/check_call.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/insights.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/cortexdb_mcp/render.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/tests/__init__.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/tests/test_check_call.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/tests/test_insights.py +0 -0
- {cortexdb_mcp-0.7.2 → cortexdb_mcp-0.7.4}/tests/test_integration.py +0 -0
|
@@ -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.
|
|
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) |
|
|
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://
|
|
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) |
|
|
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://
|
|
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 +
|
|
63
|
-
#
|
|
64
|
-
#
|
|
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``
|
|
84
|
-
|
|
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,
|
|
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 "
|
|
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 "
|
|
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",
|
|
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 "
|
|
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",
|
|
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 "
|
|
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.
|
|
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.
|
|
26
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
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["
|
|
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["
|
|
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["
|
|
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["
|
|
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["
|
|
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["
|
|
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["
|
|
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["
|
|
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
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|