cortexdb-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cortexdb_mcp/__init__.py +3 -0
- cortexdb_mcp/api.py +132 -0
- cortexdb_mcp/config.py +40 -0
- cortexdb_mcp/insights.py +640 -0
- cortexdb_mcp/server.py +1085 -0
- cortexdb_mcp-0.2.0.dist-info/METADATA +276 -0
- cortexdb_mcp-0.2.0.dist-info/RECORD +9 -0
- cortexdb_mcp-0.2.0.dist-info/WHEEL +4 -0
- cortexdb_mcp-0.2.0.dist-info/entry_points.txt +2 -0
cortexdb_mcp/server.py
ADDED
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
"""CortexDB MCP Server.
|
|
2
|
+
|
|
3
|
+
Exposes the full CortexDB API (memory, episodes, entities, knowledge graph,
|
|
4
|
+
search, usage, export/import) as MCP tools, resources, and prompts so that any
|
|
5
|
+
MCP-compatible AI tool can interact with CortexDB's long-term memory system.
|
|
6
|
+
|
|
7
|
+
Supported clients: Claude Desktop, Cursor, Windsurf, VS Code Copilot, and any
|
|
8
|
+
tool that speaks the Model Context Protocol.
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
cortexdb-mcp # stdio transport (default)
|
|
13
|
+
python -m cortexdb_mcp.server
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
from mcp.server.fastmcp import FastMCP
|
|
24
|
+
|
|
25
|
+
from cortexdb_mcp.config import CortexMCPConfig
|
|
26
|
+
from cortexdb_mcp.insights import InsightsEngine
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("cortexdb_mcp")
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Server & config
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
mcp = FastMCP(
|
|
35
|
+
"cortexdb",
|
|
36
|
+
instructions=(
|
|
37
|
+
"CortexDB is a long-term memory layer for AI systems. "
|
|
38
|
+
"Use these tools to store, search, and manage memories. "
|
|
39
|
+
"Memories are stored as immutable events and indexed into a knowledge graph. "
|
|
40
|
+
"Use memory_store to save information, memory_search or get_context to retrieve it, "
|
|
41
|
+
"and entity tools to explore the knowledge graph."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
_config = CortexMCPConfig.from_env()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _client() -> httpx.AsyncClient:
|
|
48
|
+
"""Return a configured ``httpx.AsyncClient`` for CortexDB."""
|
|
49
|
+
headers: dict[str, str] = {"Content-Type": "application/json"}
|
|
50
|
+
if _config.api_key:
|
|
51
|
+
headers["Authorization"] = f"Bearer {_config.api_key}"
|
|
52
|
+
return httpx.AsyncClient(
|
|
53
|
+
base_url=_config.url,
|
|
54
|
+
headers=headers,
|
|
55
|
+
timeout=_config.timeout,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _request(
|
|
60
|
+
method: str,
|
|
61
|
+
path: str,
|
|
62
|
+
*,
|
|
63
|
+
json_body: dict[str, Any] | None = None,
|
|
64
|
+
params: dict[str, Any] | None = None,
|
|
65
|
+
extra_headers: dict[str, str] | None = None,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
"""Execute an HTTP request against CortexDB and return parsed JSON."""
|
|
68
|
+
async with _client() as client:
|
|
69
|
+
try:
|
|
70
|
+
kwargs: dict[str, Any] = {}
|
|
71
|
+
if json_body is not None:
|
|
72
|
+
kwargs["json"] = json_body
|
|
73
|
+
if params is not None:
|
|
74
|
+
kwargs["params"] = params
|
|
75
|
+
if extra_headers is not None:
|
|
76
|
+
kwargs["headers"] = extra_headers
|
|
77
|
+
response = await client.request(method, path, **kwargs)
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
return response.json()
|
|
80
|
+
except httpx.HTTPStatusError as exc:
|
|
81
|
+
body = exc.response.text
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
f"CortexDB returned HTTP {exc.response.status_code}: {body}"
|
|
84
|
+
) from exc
|
|
85
|
+
except httpx.RequestError as exc:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"Failed to reach CortexDB at {_config.url}: {exc}"
|
|
88
|
+
) from exc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ===========================================================================
|
|
92
|
+
# TOOLS — Memory Operations
|
|
93
|
+
# ===========================================================================
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
async def memory_store(
|
|
98
|
+
content: str,
|
|
99
|
+
source: str | None = None,
|
|
100
|
+
episode_type: str = "message",
|
|
101
|
+
tenant_id: str | None = None,
|
|
102
|
+
namespace: str | None = None,
|
|
103
|
+
tags: list[str] | None = None,
|
|
104
|
+
ttl_seconds: int | None = None,
|
|
105
|
+
) -> str:
|
|
106
|
+
"""Store a new memory or episode in CortexDB.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
content:
|
|
111
|
+
The textual content to remember (max 1 MB).
|
|
112
|
+
source:
|
|
113
|
+
Origin of the memory (e.g. "slack", "github", "cursor").
|
|
114
|
+
episode_type:
|
|
115
|
+
Type of episode: message, code_change, incident, deployment, issue,
|
|
116
|
+
document, review, meeting, alert, decision, custom. Default "message".
|
|
117
|
+
tenant_id:
|
|
118
|
+
Tenant scope for multi-tenant deployments.
|
|
119
|
+
namespace:
|
|
120
|
+
Logical namespace within the tenant.
|
|
121
|
+
tags:
|
|
122
|
+
Optional list of tags for categorisation.
|
|
123
|
+
ttl_seconds:
|
|
124
|
+
Auto-expiry time in seconds. Omit for permanent storage.
|
|
125
|
+
"""
|
|
126
|
+
body: dict[str, Any] = {
|
|
127
|
+
"content": content,
|
|
128
|
+
"episode_type": episode_type,
|
|
129
|
+
}
|
|
130
|
+
if source is not None:
|
|
131
|
+
body["source"] = source
|
|
132
|
+
if tenant_id is not None:
|
|
133
|
+
body["tenant_id"] = tenant_id
|
|
134
|
+
if namespace is not None:
|
|
135
|
+
body["namespace"] = namespace
|
|
136
|
+
if tags is not None:
|
|
137
|
+
body["tags"] = tags
|
|
138
|
+
if ttl_seconds is not None:
|
|
139
|
+
body["ttl_seconds"] = ttl_seconds
|
|
140
|
+
|
|
141
|
+
path = "/v1/episodes" if source or tags else "/v1/remember"
|
|
142
|
+
result = await _request("POST", path, json_body=body)
|
|
143
|
+
|
|
144
|
+
event_id = result.get("event_id") or result.get("episode_id") or "unknown"
|
|
145
|
+
return f"Stored successfully. ID: {event_id}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@mcp.tool()
|
|
149
|
+
async def memory_search(
|
|
150
|
+
query: str,
|
|
151
|
+
tenant_id: str | None = None,
|
|
152
|
+
max_results: int = 10,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Search CortexDB for relevant memories using hybrid retrieval (BM25 + vector + graph).
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
query:
|
|
159
|
+
Natural-language search query.
|
|
160
|
+
tenant_id:
|
|
161
|
+
Tenant scope for multi-tenant deployments.
|
|
162
|
+
max_results:
|
|
163
|
+
Maximum number of results to return (default 10).
|
|
164
|
+
"""
|
|
165
|
+
body: dict[str, Any] = {
|
|
166
|
+
"query": query,
|
|
167
|
+
"max_results": max_results,
|
|
168
|
+
}
|
|
169
|
+
if tenant_id is not None:
|
|
170
|
+
body["tenant_id"] = tenant_id
|
|
171
|
+
|
|
172
|
+
result = await _request("POST", "/v1/recall", json_body=body)
|
|
173
|
+
|
|
174
|
+
context = result.get("context", "")
|
|
175
|
+
confidence = result.get("confidence")
|
|
176
|
+
items = result.get("results", [])
|
|
177
|
+
|
|
178
|
+
parts: list[str] = []
|
|
179
|
+
if context:
|
|
180
|
+
parts.append(context)
|
|
181
|
+
if items:
|
|
182
|
+
parts.append(f"\n--- {len(items)} result(s) ---")
|
|
183
|
+
for item in items:
|
|
184
|
+
parts.append(json.dumps(item, indent=2))
|
|
185
|
+
if confidence is not None:
|
|
186
|
+
parts.append(f"\nConfidence: {confidence}")
|
|
187
|
+
|
|
188
|
+
return "\n".join(parts) if parts else "No results found."
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool()
|
|
192
|
+
async def memory_forget(
|
|
193
|
+
reason: str,
|
|
194
|
+
query: str | None = None,
|
|
195
|
+
tenant_id: str | None = None,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Delete memories from CortexDB (supports GDPR erasure).
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
reason:
|
|
202
|
+
Reason for the deletion (required for audit trail).
|
|
203
|
+
query:
|
|
204
|
+
Optional query to select which memories to forget.
|
|
205
|
+
tenant_id:
|
|
206
|
+
Tenant scope for multi-tenant deployments.
|
|
207
|
+
"""
|
|
208
|
+
body: dict[str, Any] = {"reason": reason}
|
|
209
|
+
if query is not None:
|
|
210
|
+
body["query"] = query
|
|
211
|
+
if tenant_id is not None:
|
|
212
|
+
body["tenant_id"] = tenant_id
|
|
213
|
+
|
|
214
|
+
result = await _request("POST", "/v1/forget", json_body=body)
|
|
215
|
+
|
|
216
|
+
entities = result.get("entities_removed", result.get("forgotten_entities", 0))
|
|
217
|
+
edges = result.get("edges_removed", result.get("forgotten_edges", 0))
|
|
218
|
+
audit_id = result.get("audit_id", "")
|
|
219
|
+
msg = f"Forgotten. Entities removed: {entities}, edges removed: {edges}."
|
|
220
|
+
if audit_id:
|
|
221
|
+
msg += f" Audit ID: {audit_id}"
|
|
222
|
+
return msg
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
async def get_context(
|
|
227
|
+
topic: str,
|
|
228
|
+
tenant_id: str | None = None,
|
|
229
|
+
include_graph: bool = False,
|
|
230
|
+
) -> str:
|
|
231
|
+
"""Retrieve rich contextual information about a topic from CortexDB.
|
|
232
|
+
|
|
233
|
+
Performs deep retrieval combining keyword search, vector similarity, and
|
|
234
|
+
knowledge graph traversal to build comprehensive context.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
topic:
|
|
239
|
+
The entity or topic to retrieve context for.
|
|
240
|
+
tenant_id:
|
|
241
|
+
Tenant scope for multi-tenant deployments.
|
|
242
|
+
include_graph:
|
|
243
|
+
Whether to include graph relationships in the response.
|
|
244
|
+
"""
|
|
245
|
+
body: dict[str, Any] = {
|
|
246
|
+
"query": topic,
|
|
247
|
+
"max_tokens": 4096,
|
|
248
|
+
"max_results": 50,
|
|
249
|
+
}
|
|
250
|
+
if tenant_id is not None:
|
|
251
|
+
body["tenant_id"] = tenant_id
|
|
252
|
+
if include_graph:
|
|
253
|
+
body["include_graph"] = True
|
|
254
|
+
|
|
255
|
+
result = await _request("POST", "/v1/recall", json_body=body)
|
|
256
|
+
|
|
257
|
+
context = result.get("context", "")
|
|
258
|
+
graph = result.get("graph")
|
|
259
|
+
|
|
260
|
+
parts: list[str] = []
|
|
261
|
+
if context:
|
|
262
|
+
parts.append(context)
|
|
263
|
+
if graph:
|
|
264
|
+
parts.append("\n--- Graph Relationships ---")
|
|
265
|
+
parts.append(json.dumps(graph, indent=2))
|
|
266
|
+
|
|
267
|
+
return "\n".join(parts) if parts else f"No context found for '{topic}'."
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@mcp.tool()
|
|
271
|
+
async def advanced_search(
|
|
272
|
+
query: str,
|
|
273
|
+
tenant_id: str | None = None,
|
|
274
|
+
source: str | None = None,
|
|
275
|
+
episode_type: str | None = None,
|
|
276
|
+
time_range_start: str | None = None,
|
|
277
|
+
time_range_end: str | None = None,
|
|
278
|
+
limit: int = 20,
|
|
279
|
+
offset: int = 0,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""Search CortexDB with structured filters.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
query:
|
|
286
|
+
Natural-language search query.
|
|
287
|
+
tenant_id:
|
|
288
|
+
Tenant scope.
|
|
289
|
+
source:
|
|
290
|
+
Filter by source (e.g. "slack", "github").
|
|
291
|
+
episode_type:
|
|
292
|
+
Filter by type (e.g. "incident", "code_change", "deployment").
|
|
293
|
+
time_range_start:
|
|
294
|
+
ISO 8601 start time (e.g. "2026-03-01T00:00:00Z").
|
|
295
|
+
time_range_end:
|
|
296
|
+
ISO 8601 end time.
|
|
297
|
+
limit:
|
|
298
|
+
Max results (default 20).
|
|
299
|
+
offset:
|
|
300
|
+
Pagination offset.
|
|
301
|
+
"""
|
|
302
|
+
body: dict[str, Any] = {"query": query, "limit": limit, "offset": offset}
|
|
303
|
+
filters: dict[str, Any] = {}
|
|
304
|
+
if source:
|
|
305
|
+
filters["source"] = source
|
|
306
|
+
if episode_type:
|
|
307
|
+
filters["episode_type"] = episode_type
|
|
308
|
+
if time_range_start or time_range_end:
|
|
309
|
+
time_range: dict[str, str] = {}
|
|
310
|
+
if time_range_start:
|
|
311
|
+
time_range["start"] = time_range_start
|
|
312
|
+
if time_range_end:
|
|
313
|
+
time_range["end"] = time_range_end
|
|
314
|
+
filters["time_range"] = time_range
|
|
315
|
+
if filters:
|
|
316
|
+
body["filters"] = filters
|
|
317
|
+
if tenant_id:
|
|
318
|
+
body["tenant_id"] = tenant_id
|
|
319
|
+
|
|
320
|
+
result = await _request("POST", "/v1/search", json_body=body)
|
|
321
|
+
|
|
322
|
+
context = result.get("context", "")
|
|
323
|
+
episodes = result.get("episodes", [])
|
|
324
|
+
total = result.get("total_episodes", len(episodes))
|
|
325
|
+
confidence = result.get("confidence")
|
|
326
|
+
|
|
327
|
+
parts: list[str] = []
|
|
328
|
+
if context:
|
|
329
|
+
parts.append(context)
|
|
330
|
+
if episodes:
|
|
331
|
+
parts.append(f"\n--- {len(episodes)} of {total} episode(s) ---")
|
|
332
|
+
for ep in episodes:
|
|
333
|
+
ep_id = ep.get("episode_id", ep.get("id", ""))
|
|
334
|
+
ep_type = ep.get("episode_type", "")
|
|
335
|
+
created = ep.get("created_at", "")
|
|
336
|
+
snippet = (ep.get("content", "")[:150] + "...") if ep.get("content") else ""
|
|
337
|
+
parts.append(f" [{ep_type}] {ep_id} {created}")
|
|
338
|
+
if snippet:
|
|
339
|
+
parts.append(f" {snippet}")
|
|
340
|
+
if confidence is not None:
|
|
341
|
+
parts.append(f"\nConfidence: {confidence}")
|
|
342
|
+
|
|
343
|
+
return "\n".join(parts) if parts else "No results found."
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ===========================================================================
|
|
347
|
+
# TOOLS — Episode CRUD
|
|
348
|
+
# ===========================================================================
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@mcp.tool()
|
|
352
|
+
async def memory_list(
|
|
353
|
+
tenant_id: str | None = None,
|
|
354
|
+
limit: int = 20,
|
|
355
|
+
offset: int = 0,
|
|
356
|
+
episode_type: str | None = None,
|
|
357
|
+
) -> str:
|
|
358
|
+
"""List memories / episodes stored in CortexDB with pagination.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
tenant_id:
|
|
363
|
+
Tenant scope for multi-tenant deployments.
|
|
364
|
+
limit:
|
|
365
|
+
Maximum number of episodes to return (default 20).
|
|
366
|
+
offset:
|
|
367
|
+
Pagination offset (default 0).
|
|
368
|
+
episode_type:
|
|
369
|
+
Optional filter by episode type.
|
|
370
|
+
"""
|
|
371
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
372
|
+
if episode_type is not None:
|
|
373
|
+
params["episode_type"] = episode_type
|
|
374
|
+
|
|
375
|
+
extra_headers: dict[str, str] = {}
|
|
376
|
+
if tenant_id is not None:
|
|
377
|
+
extra_headers["x-tenant-id"] = tenant_id
|
|
378
|
+
|
|
379
|
+
result = await _request(
|
|
380
|
+
"GET", "/v1/episodes", params=params, extra_headers=extra_headers or None
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
episodes = result.get("episodes", result.get("items", []))
|
|
384
|
+
total = result.get("total", len(episodes))
|
|
385
|
+
|
|
386
|
+
if not episodes:
|
|
387
|
+
return "No episodes found."
|
|
388
|
+
|
|
389
|
+
parts: list[str] = [f"Showing {len(episodes)} of {total} episode(s):\n"]
|
|
390
|
+
for ep in episodes:
|
|
391
|
+
ep_id = ep.get("episode_id", ep.get("id", "unknown"))
|
|
392
|
+
ep_type = ep.get("episode_type", "unknown")
|
|
393
|
+
created = ep.get("created_at", "")
|
|
394
|
+
snippet = (ep.get("content", "")[:120] + "...") if ep.get("content") else ""
|
|
395
|
+
parts.append(f" [{ep_type}] {ep_id} {created}")
|
|
396
|
+
if snippet:
|
|
397
|
+
parts.append(f" {snippet}")
|
|
398
|
+
return "\n".join(parts)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@mcp.tool()
|
|
402
|
+
async def memory_get(
|
|
403
|
+
episode_id: str,
|
|
404
|
+
tenant_id: str | None = None,
|
|
405
|
+
) -> str:
|
|
406
|
+
"""Get a specific memory / episode by its ID.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
episode_id:
|
|
411
|
+
The unique identifier of the episode to retrieve.
|
|
412
|
+
tenant_id:
|
|
413
|
+
Tenant scope for multi-tenant deployments.
|
|
414
|
+
"""
|
|
415
|
+
extra_headers: dict[str, str] = {}
|
|
416
|
+
if tenant_id is not None:
|
|
417
|
+
extra_headers["x-tenant-id"] = tenant_id
|
|
418
|
+
|
|
419
|
+
result = await _request(
|
|
420
|
+
"GET", f"/v1/episodes/{episode_id}", extra_headers=extra_headers or None
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
parts: list[str] = [
|
|
424
|
+
f"Episode: {result.get('episode_id', result.get('id', episode_id))}",
|
|
425
|
+
f"Type: {result.get('episode_type', 'unknown')}",
|
|
426
|
+
]
|
|
427
|
+
if result.get("created_at"):
|
|
428
|
+
parts.append(f"Created: {result['created_at']}")
|
|
429
|
+
if result.get("source"):
|
|
430
|
+
parts.append(f"Source: {result['source']}")
|
|
431
|
+
if result.get("tags"):
|
|
432
|
+
parts.append(f"Tags: {', '.join(result['tags'])}")
|
|
433
|
+
if result.get("content"):
|
|
434
|
+
parts.append(f"\nContent:\n{result['content']}")
|
|
435
|
+
if result.get("metadata"):
|
|
436
|
+
parts.append(f"\nMetadata:\n{json.dumps(result['metadata'], indent=2)}")
|
|
437
|
+
return "\n".join(parts)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@mcp.tool()
|
|
441
|
+
async def memory_update(
|
|
442
|
+
episode_id: str,
|
|
443
|
+
content: str,
|
|
444
|
+
tenant_id: str | None = None,
|
|
445
|
+
metadata: dict[str, Any] | None = None,
|
|
446
|
+
) -> str:
|
|
447
|
+
"""Update an existing memory's content or metadata.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
episode_id:
|
|
452
|
+
The unique identifier of the episode to update.
|
|
453
|
+
content:
|
|
454
|
+
New textual content for the episode.
|
|
455
|
+
tenant_id:
|
|
456
|
+
Tenant scope for multi-tenant deployments.
|
|
457
|
+
metadata:
|
|
458
|
+
Optional metadata dictionary to merge into the episode.
|
|
459
|
+
"""
|
|
460
|
+
body: dict[str, Any] = {"content": content}
|
|
461
|
+
if metadata is not None:
|
|
462
|
+
body["metadata"] = metadata
|
|
463
|
+
|
|
464
|
+
extra_headers: dict[str, str] = {}
|
|
465
|
+
if tenant_id is not None:
|
|
466
|
+
extra_headers["x-tenant-id"] = tenant_id
|
|
467
|
+
|
|
468
|
+
result = await _request(
|
|
469
|
+
"PUT", f"/v1/episodes/{episode_id}",
|
|
470
|
+
json_body=body, extra_headers=extra_headers or None,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
updated_id = result.get("episode_id", result.get("id", episode_id))
|
|
474
|
+
return f"Updated successfully. Episode ID: {updated_id}"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@mcp.tool()
|
|
478
|
+
async def memory_delete(
|
|
479
|
+
episode_id: str,
|
|
480
|
+
tenant_id: str | None = None,
|
|
481
|
+
) -> str:
|
|
482
|
+
"""Delete a specific episode by ID.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
episode_id:
|
|
487
|
+
The unique identifier of the episode to delete.
|
|
488
|
+
tenant_id:
|
|
489
|
+
Tenant scope for multi-tenant deployments.
|
|
490
|
+
"""
|
|
491
|
+
extra_headers: dict[str, str] = {}
|
|
492
|
+
if tenant_id is not None:
|
|
493
|
+
extra_headers["x-tenant-id"] = tenant_id
|
|
494
|
+
|
|
495
|
+
await _request(
|
|
496
|
+
"DELETE", f"/v1/episodes/{episode_id}", extra_headers=extra_headers or None
|
|
497
|
+
)
|
|
498
|
+
return f"Deleted episode {episode_id}."
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@mcp.tool()
|
|
502
|
+
async def memory_bulk_delete(
|
|
503
|
+
query: str,
|
|
504
|
+
reason: str,
|
|
505
|
+
tenant_id: str | None = None,
|
|
506
|
+
dry_run: bool = False,
|
|
507
|
+
) -> str:
|
|
508
|
+
"""Delete multiple memories matching a query pattern.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
query:
|
|
513
|
+
Search query to select memories for deletion.
|
|
514
|
+
reason:
|
|
515
|
+
Reason for the deletion (audit trail / GDPR).
|
|
516
|
+
tenant_id:
|
|
517
|
+
Tenant scope for multi-tenant deployments.
|
|
518
|
+
dry_run:
|
|
519
|
+
If True, return the count of matching episodes without deleting.
|
|
520
|
+
"""
|
|
521
|
+
body: dict[str, Any] = {"query": query, "reason": reason, "dry_run": dry_run}
|
|
522
|
+
if tenant_id is not None:
|
|
523
|
+
body["tenant_id"] = tenant_id
|
|
524
|
+
|
|
525
|
+
result = await _request("POST", "/v1/forget", json_body=body)
|
|
526
|
+
|
|
527
|
+
deleted = result.get("deleted", result.get("entities_removed", 0))
|
|
528
|
+
if dry_run:
|
|
529
|
+
return f"Dry run: {deleted} episode(s) would be deleted."
|
|
530
|
+
return f"Bulk delete complete. {deleted} episode(s) removed."
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# ===========================================================================
|
|
534
|
+
# TOOLS — Knowledge Graph (Entities & Links)
|
|
535
|
+
# ===========================================================================
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@mcp.tool()
|
|
539
|
+
async def entity_list(
|
|
540
|
+
tenant_id: str | None = None,
|
|
541
|
+
limit: int = 50,
|
|
542
|
+
offset: int = 0,
|
|
543
|
+
) -> str:
|
|
544
|
+
"""List entities in the CortexDB knowledge graph.
|
|
545
|
+
|
|
546
|
+
Entities are automatically extracted from memories — people, services,
|
|
547
|
+
projects, concepts, and more.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
tenant_id:
|
|
552
|
+
Tenant scope for multi-tenant deployments.
|
|
553
|
+
limit:
|
|
554
|
+
Maximum number of entities to return (default 50).
|
|
555
|
+
offset:
|
|
556
|
+
Pagination offset.
|
|
557
|
+
"""
|
|
558
|
+
params: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
559
|
+
if tenant_id:
|
|
560
|
+
params["tenant_id"] = tenant_id
|
|
561
|
+
|
|
562
|
+
result = await _request("GET", "/v1/entities", params=params)
|
|
563
|
+
|
|
564
|
+
entities = result.get("entities", result.get("items", []))
|
|
565
|
+
if not entities:
|
|
566
|
+
return "No entities found."
|
|
567
|
+
|
|
568
|
+
parts: list[str] = [f"Found {len(entities)} entity/entities:\n"]
|
|
569
|
+
for ent in entities:
|
|
570
|
+
eid = ent.get("id", "")
|
|
571
|
+
name = ent.get("name", "unnamed")
|
|
572
|
+
etype = ent.get("entity_type", "unknown")
|
|
573
|
+
ep_count = ent.get("episode_count", 0)
|
|
574
|
+
parts.append(f" [{etype}] {name} (id: {eid}, {ep_count} episodes)")
|
|
575
|
+
return "\n".join(parts)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@mcp.tool()
|
|
579
|
+
async def entity_get(
|
|
580
|
+
entity_id: str,
|
|
581
|
+
tenant_id: str | None = None,
|
|
582
|
+
) -> str:
|
|
583
|
+
"""Get detailed information about a specific entity including relationships.
|
|
584
|
+
|
|
585
|
+
Parameters
|
|
586
|
+
----------
|
|
587
|
+
entity_id:
|
|
588
|
+
The unique identifier of the entity.
|
|
589
|
+
tenant_id:
|
|
590
|
+
Tenant scope for multi-tenant deployments.
|
|
591
|
+
"""
|
|
592
|
+
params: dict[str, Any] = {}
|
|
593
|
+
if tenant_id:
|
|
594
|
+
params["tenant_id"] = tenant_id
|
|
595
|
+
|
|
596
|
+
result = await _request("GET", f"/v1/entities/{entity_id}", params=params)
|
|
597
|
+
|
|
598
|
+
parts: list[str] = [
|
|
599
|
+
f"Entity: {result.get('name', 'unnamed')}",
|
|
600
|
+
f"Type: {result.get('entity_type', 'unknown')}",
|
|
601
|
+
f"ID: {result.get('id', entity_id)}",
|
|
602
|
+
]
|
|
603
|
+
if result.get("first_seen"):
|
|
604
|
+
parts.append(f"First seen: {result['first_seen']}")
|
|
605
|
+
if result.get("last_seen"):
|
|
606
|
+
parts.append(f"Last seen: {result['last_seen']}")
|
|
607
|
+
if result.get("episode_count"):
|
|
608
|
+
parts.append(f"Episodes: {result['episode_count']}")
|
|
609
|
+
|
|
610
|
+
rels = result.get("relationships", [])
|
|
611
|
+
if rels:
|
|
612
|
+
parts.append(f"\n--- {len(rels)} Relationship(s) ---")
|
|
613
|
+
for rel in rels:
|
|
614
|
+
direction = rel.get("direction", "")
|
|
615
|
+
rel_type = rel.get("relationship", rel.get("type", ""))
|
|
616
|
+
target = rel.get("target_name", rel.get("target_id", ""))
|
|
617
|
+
parts.append(f" {direction} {rel_type} -> {target}")
|
|
618
|
+
|
|
619
|
+
recent = result.get("recent_episodes", [])
|
|
620
|
+
if recent:
|
|
621
|
+
parts.append(f"\n--- Recent Episodes ({len(recent)}) ---")
|
|
622
|
+
for ep in recent:
|
|
623
|
+
ep_type = ep.get("episode_type", "")
|
|
624
|
+
created = ep.get("created_at", "")
|
|
625
|
+
snippet = (ep.get("content", "")[:100] + "...") if ep.get("content") else ""
|
|
626
|
+
parts.append(f" [{ep_type}] {created}")
|
|
627
|
+
if snippet:
|
|
628
|
+
parts.append(f" {snippet}")
|
|
629
|
+
|
|
630
|
+
return "\n".join(parts)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@mcp.tool()
|
|
634
|
+
async def entity_edges(
|
|
635
|
+
entity_id: str,
|
|
636
|
+
tenant_id: str | None = None,
|
|
637
|
+
) -> str:
|
|
638
|
+
"""Get all relationships (edges) for an entity in the knowledge graph.
|
|
639
|
+
|
|
640
|
+
Parameters
|
|
641
|
+
----------
|
|
642
|
+
entity_id:
|
|
643
|
+
The unique identifier of the entity.
|
|
644
|
+
tenant_id:
|
|
645
|
+
Tenant scope for multi-tenant deployments.
|
|
646
|
+
"""
|
|
647
|
+
params: dict[str, Any] = {}
|
|
648
|
+
if tenant_id:
|
|
649
|
+
params["tenant_id"] = tenant_id
|
|
650
|
+
|
|
651
|
+
result = await _request("GET", f"/v1/entities/{entity_id}/edges", params=params)
|
|
652
|
+
|
|
653
|
+
edges = result.get("edges", result.get("items", []))
|
|
654
|
+
if not edges:
|
|
655
|
+
return f"No relationships found for entity {entity_id}."
|
|
656
|
+
|
|
657
|
+
parts: list[str] = [f"Found {len(edges)} relationship(s):\n"]
|
|
658
|
+
for edge in edges:
|
|
659
|
+
rel = edge.get("relationship", edge.get("type", "unknown"))
|
|
660
|
+
source = edge.get("source_name", edge.get("source_id", ""))
|
|
661
|
+
target = edge.get("target_name", edge.get("target_id", ""))
|
|
662
|
+
confidence = edge.get("confidence", "")
|
|
663
|
+
fact = edge.get("fact", "")
|
|
664
|
+
line = f" {source} --[{rel}]--> {target}"
|
|
665
|
+
if confidence:
|
|
666
|
+
line += f" (confidence: {confidence})"
|
|
667
|
+
parts.append(line)
|
|
668
|
+
if fact:
|
|
669
|
+
parts.append(f" Fact: {fact}")
|
|
670
|
+
return "\n".join(parts)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@mcp.tool()
|
|
674
|
+
async def entity_link(
|
|
675
|
+
source_entity_id: str,
|
|
676
|
+
target_entity_id: str,
|
|
677
|
+
relationship: str,
|
|
678
|
+
fact: str = "",
|
|
679
|
+
confidence: float = 0.8,
|
|
680
|
+
tenant_id: str | None = None,
|
|
681
|
+
) -> str:
|
|
682
|
+
"""Create a relationship between two entities in the knowledge graph.
|
|
683
|
+
|
|
684
|
+
Parameters
|
|
685
|
+
----------
|
|
686
|
+
source_entity_id:
|
|
687
|
+
ID of the source entity.
|
|
688
|
+
target_entity_id:
|
|
689
|
+
ID of the target entity.
|
|
690
|
+
relationship:
|
|
691
|
+
Relationship type (e.g. "DEPENDS_ON", "OWNS", "RELATED_TO").
|
|
692
|
+
fact:
|
|
693
|
+
Human-readable description of the relationship.
|
|
694
|
+
confidence:
|
|
695
|
+
Confidence score from 0.0 to 1.0 (default 0.8).
|
|
696
|
+
tenant_id:
|
|
697
|
+
Tenant scope for multi-tenant deployments.
|
|
698
|
+
"""
|
|
699
|
+
body: dict[str, Any] = {
|
|
700
|
+
"source_entity_id": source_entity_id,
|
|
701
|
+
"target_entity_id": target_entity_id,
|
|
702
|
+
"relationship": relationship,
|
|
703
|
+
"fact": fact,
|
|
704
|
+
"confidence": confidence,
|
|
705
|
+
}
|
|
706
|
+
if tenant_id:
|
|
707
|
+
body["tenant_id"] = tenant_id
|
|
708
|
+
|
|
709
|
+
result = await _request("POST", "/v1/link", json_body=body)
|
|
710
|
+
|
|
711
|
+
link_id = result.get("id", "unknown")
|
|
712
|
+
return f"Link created. ID: {link_id}"
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# ===========================================================================
|
|
716
|
+
# TOOLS — Admin & Observability
|
|
717
|
+
# ===========================================================================
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@mcp.tool()
|
|
721
|
+
async def health_check() -> str:
|
|
722
|
+
"""Check CortexDB server health status."""
|
|
723
|
+
result = await _request("GET", "/v1/admin/health")
|
|
724
|
+
return json.dumps(result, indent=2)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
@mcp.tool()
|
|
728
|
+
async def get_usage(
|
|
729
|
+
tenant_id: str | None = None,
|
|
730
|
+
) -> str:
|
|
731
|
+
"""Get current usage statistics and tier limits.
|
|
732
|
+
|
|
733
|
+
Shows memory count, recall calls, and how close you are to tier limits.
|
|
734
|
+
|
|
735
|
+
Parameters
|
|
736
|
+
----------
|
|
737
|
+
tenant_id:
|
|
738
|
+
Tenant scope (uses API key's default tenant if omitted).
|
|
739
|
+
"""
|
|
740
|
+
result = await _request("GET", "/v1/admin/usage")
|
|
741
|
+
|
|
742
|
+
tier = result.get("tier", "unknown")
|
|
743
|
+
usage = result.get("usage", {})
|
|
744
|
+
limits = result.get("limits", {})
|
|
745
|
+
|
|
746
|
+
mem = usage.get("memories", {})
|
|
747
|
+
recalls = usage.get("recalls", {})
|
|
748
|
+
|
|
749
|
+
parts: list[str] = [
|
|
750
|
+
f"Tier: {tier}",
|
|
751
|
+
"",
|
|
752
|
+
"Memories:",
|
|
753
|
+
f" Total: {mem.get('total', 0):,}",
|
|
754
|
+
f" Limit: {limits.get('max_memories', 'unknown')}",
|
|
755
|
+
f" Utilization: {mem.get('utilization_pct', 0):.1f}%",
|
|
756
|
+
"",
|
|
757
|
+
"Recall Calls (this month):",
|
|
758
|
+
f" Used: {recalls.get('this_month', 0):,}",
|
|
759
|
+
f" Limit: {limits.get('max_recalls_per_month', 'unknown')}",
|
|
760
|
+
f" Utilization: {recalls.get('utilization_pct', 0):.1f}%",
|
|
761
|
+
"",
|
|
762
|
+
f"Entities: {usage.get('entities_count', 0):,}",
|
|
763
|
+
f"Lifetime recalls: {recalls.get('lifetime', 0):,}",
|
|
764
|
+
]
|
|
765
|
+
return "\n".join(parts)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
@mcp.tool()
|
|
769
|
+
async def get_insights(
|
|
770
|
+
tenant_id: str | None = None,
|
|
771
|
+
) -> str:
|
|
772
|
+
"""Generate proactive insights from CortexDB episodes.
|
|
773
|
+
|
|
774
|
+
Analyzes stored episodes to surface actionable intelligence such as
|
|
775
|
+
incident spikes, new dependencies, knowledge gaps, deployment risks,
|
|
776
|
+
stale documentation, and recurring issues.
|
|
777
|
+
|
|
778
|
+
Parameters
|
|
779
|
+
----------
|
|
780
|
+
tenant_id:
|
|
781
|
+
Tenant scope for multi-tenant deployments.
|
|
782
|
+
"""
|
|
783
|
+
engine = InsightsEngine(
|
|
784
|
+
cortex_url=_config.url,
|
|
785
|
+
api_key=_config.api_key,
|
|
786
|
+
tenant_id=tenant_id,
|
|
787
|
+
)
|
|
788
|
+
insights = await engine.generate_all()
|
|
789
|
+
|
|
790
|
+
if not insights:
|
|
791
|
+
return "No insights generated. Everything looks stable."
|
|
792
|
+
|
|
793
|
+
parts: list[str] = [f"Generated {len(insights)} insight(s):\n"]
|
|
794
|
+
for insight in insights:
|
|
795
|
+
severity_tag = f"[{insight.severity.value.upper()}]"
|
|
796
|
+
parts.append(f"{severity_tag} {insight.title}")
|
|
797
|
+
parts.append(f" {insight.description}")
|
|
798
|
+
if insight.entities:
|
|
799
|
+
parts.append(f" Entities: {', '.join(insight.entities)}")
|
|
800
|
+
parts.append("")
|
|
801
|
+
|
|
802
|
+
return "\n".join(parts)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
@mcp.tool()
|
|
806
|
+
async def get_ontology() -> str:
|
|
807
|
+
"""Get the entity types and relationship types known to CortexDB.
|
|
808
|
+
|
|
809
|
+
Useful for understanding what kinds of entities and relationships
|
|
810
|
+
exist in the knowledge graph.
|
|
811
|
+
"""
|
|
812
|
+
types_result = await _request("GET", "/v1/admin/ontology/types")
|
|
813
|
+
rels_result = await _request("GET", "/v1/admin/ontology/relationships")
|
|
814
|
+
|
|
815
|
+
entity_types = types_result.get("entity_types", [])
|
|
816
|
+
rel_types = rels_result.get("relationship_types", [])
|
|
817
|
+
|
|
818
|
+
parts: list[str] = [f"Entity Types ({len(entity_types)}):\n"]
|
|
819
|
+
for et in entity_types:
|
|
820
|
+
name = et.get("name", "")
|
|
821
|
+
desc = et.get("description", "")
|
|
822
|
+
parts.append(f" {name}: {desc}" if desc else f" {name}")
|
|
823
|
+
|
|
824
|
+
parts.append(f"\nRelationship Types ({len(rel_types)}):\n")
|
|
825
|
+
for rt in rel_types:
|
|
826
|
+
name = rt.get("name", "")
|
|
827
|
+
desc = rt.get("description", "")
|
|
828
|
+
parts.append(f" {name}: {desc}" if desc else f" {name}")
|
|
829
|
+
|
|
830
|
+
return "\n".join(parts)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
@mcp.tool()
|
|
834
|
+
async def export_data(
|
|
835
|
+
tenant_id: str | None = None,
|
|
836
|
+
format: str = "json",
|
|
837
|
+
) -> str:
|
|
838
|
+
"""Export memories from CortexDB.
|
|
839
|
+
|
|
840
|
+
Parameters
|
|
841
|
+
----------
|
|
842
|
+
tenant_id:
|
|
843
|
+
Tenant scope (exports all data for this tenant).
|
|
844
|
+
format:
|
|
845
|
+
Export format: "json" (default).
|
|
846
|
+
"""
|
|
847
|
+
body: dict[str, Any] = {"format": format}
|
|
848
|
+
if tenant_id:
|
|
849
|
+
body["tenant_id"] = tenant_id
|
|
850
|
+
|
|
851
|
+
result = await _request("POST", "/v1/export", json_body=body)
|
|
852
|
+
|
|
853
|
+
count = result.get("episode_count", result.get("count", 0))
|
|
854
|
+
export_id = result.get("export_id", "")
|
|
855
|
+
return f"Export complete. {count} episodes exported. Export ID: {export_id}"
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@mcp.tool()
|
|
859
|
+
async def import_data(
|
|
860
|
+
data: str,
|
|
861
|
+
tenant_id: str | None = None,
|
|
862
|
+
format: str = "json",
|
|
863
|
+
) -> str:
|
|
864
|
+
"""Import memories into CortexDB.
|
|
865
|
+
|
|
866
|
+
Parameters
|
|
867
|
+
----------
|
|
868
|
+
data:
|
|
869
|
+
JSON string of episodes to import.
|
|
870
|
+
tenant_id:
|
|
871
|
+
Tenant scope for the imported data.
|
|
872
|
+
format:
|
|
873
|
+
Import format: "json" (default).
|
|
874
|
+
"""
|
|
875
|
+
body: dict[str, Any] = {"data": data, "format": format}
|
|
876
|
+
if tenant_id:
|
|
877
|
+
body["tenant_id"] = tenant_id
|
|
878
|
+
|
|
879
|
+
result = await _request("POST", "/v1/import", json_body=body)
|
|
880
|
+
|
|
881
|
+
imported = result.get("imported", result.get("count", 0))
|
|
882
|
+
return f"Import complete. {imported} episodes imported."
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
# ===========================================================================
|
|
886
|
+
# RESOURCES
|
|
887
|
+
# ===========================================================================
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@mcp.resource("cortexdb://health")
|
|
891
|
+
async def health_resource() -> str:
|
|
892
|
+
"""CortexDB server health status."""
|
|
893
|
+
result = await _request("GET", "/v1/admin/health")
|
|
894
|
+
return json.dumps(result, indent=2)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@mcp.resource("cortexdb://metrics")
|
|
898
|
+
async def metrics_resource() -> str:
|
|
899
|
+
"""CortexDB server metrics (requests, errors, rate limits)."""
|
|
900
|
+
try:
|
|
901
|
+
result = await _request("GET", "/v1/admin/metrics")
|
|
902
|
+
return json.dumps(result, indent=2)
|
|
903
|
+
except RuntimeError:
|
|
904
|
+
return json.dumps({"error": "Metrics endpoint unavailable."})
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
@mcp.resource("cortexdb://usage")
|
|
908
|
+
async def usage_resource() -> str:
|
|
909
|
+
"""Current usage statistics and tier limits."""
|
|
910
|
+
try:
|
|
911
|
+
result = await _request("GET", "/v1/admin/usage")
|
|
912
|
+
return json.dumps(result, indent=2)
|
|
913
|
+
except RuntimeError:
|
|
914
|
+
return json.dumps({"error": "Usage endpoint unavailable."})
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
@mcp.resource("cortexdb://episodes")
|
|
918
|
+
async def episodes_resource() -> str:
|
|
919
|
+
"""Recent episodes stored in CortexDB (last 50)."""
|
|
920
|
+
try:
|
|
921
|
+
result = await _request(
|
|
922
|
+
"GET", "/v1/episodes", params={"limit": 50, "offset": 0}
|
|
923
|
+
)
|
|
924
|
+
episodes = result.get("episodes", result.get("items", []))
|
|
925
|
+
return json.dumps(episodes, indent=2)
|
|
926
|
+
except RuntimeError:
|
|
927
|
+
return json.dumps({"error": "Episodes endpoint unavailable."})
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
@mcp.resource("cortexdb://entities")
|
|
931
|
+
async def entities_resource() -> str:
|
|
932
|
+
"""Entities in the CortexDB knowledge graph (top 100)."""
|
|
933
|
+
try:
|
|
934
|
+
result = await _request("GET", "/v1/entities", params={"limit": 100})
|
|
935
|
+
entities = result.get("entities", result.get("items", []))
|
|
936
|
+
return json.dumps(entities, indent=2)
|
|
937
|
+
except RuntimeError:
|
|
938
|
+
return json.dumps({"error": "Entities endpoint unavailable."})
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
@mcp.resource("cortexdb://insights")
|
|
942
|
+
async def insights_resource() -> str:
|
|
943
|
+
"""Proactive insights generated from stored episodes."""
|
|
944
|
+
engine = InsightsEngine(
|
|
945
|
+
cortex_url=_config.url,
|
|
946
|
+
api_key=_config.api_key,
|
|
947
|
+
)
|
|
948
|
+
try:
|
|
949
|
+
insights = await engine.generate_all()
|
|
950
|
+
return json.dumps([i.to_dict() for i in insights], indent=2)
|
|
951
|
+
except Exception as exc:
|
|
952
|
+
return json.dumps({"error": str(exc)})
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
@mcp.resource("cortexdb://ontology")
|
|
956
|
+
async def ontology_resource() -> str:
|
|
957
|
+
"""Entity types and relationship types in the knowledge graph schema."""
|
|
958
|
+
try:
|
|
959
|
+
types_result = await _request("GET", "/v1/admin/ontology/types")
|
|
960
|
+
rels_result = await _request("GET", "/v1/admin/ontology/relationships")
|
|
961
|
+
return json.dumps({
|
|
962
|
+
"entity_types": types_result.get("entity_types", []),
|
|
963
|
+
"relationship_types": rels_result.get("relationship_types", []),
|
|
964
|
+
}, indent=2)
|
|
965
|
+
except RuntimeError:
|
|
966
|
+
return json.dumps({"error": "Ontology endpoints unavailable."})
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
# ===========================================================================
|
|
970
|
+
# PROMPTS
|
|
971
|
+
# ===========================================================================
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@mcp.prompt()
|
|
975
|
+
def investigate_incident(topic: str) -> str:
|
|
976
|
+
"""Investigate an incident using CortexDB memory.
|
|
977
|
+
|
|
978
|
+
Parameters
|
|
979
|
+
----------
|
|
980
|
+
topic:
|
|
981
|
+
The incident or topic to investigate.
|
|
982
|
+
"""
|
|
983
|
+
return (
|
|
984
|
+
f"Given the context from CortexDB, investigate this incident: {topic}\n\n"
|
|
985
|
+
"Steps:\n"
|
|
986
|
+
"1. Search CortexDB for all memories related to the incident.\n"
|
|
987
|
+
"2. Identify the timeline of events.\n"
|
|
988
|
+
"3. Determine root cause based on available evidence.\n"
|
|
989
|
+
"4. Suggest remediation steps.\n"
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
@mcp.prompt()
|
|
994
|
+
def summarize_knowledge(topic: str) -> str:
|
|
995
|
+
"""Summarize everything CortexDB knows about a topic.
|
|
996
|
+
|
|
997
|
+
Parameters
|
|
998
|
+
----------
|
|
999
|
+
topic:
|
|
1000
|
+
The topic to summarize knowledge for.
|
|
1001
|
+
"""
|
|
1002
|
+
return (
|
|
1003
|
+
f"Summarize everything CortexDB knows about: {topic}\n\n"
|
|
1004
|
+
"Include:\n"
|
|
1005
|
+
"- Key facts and relationships\n"
|
|
1006
|
+
"- Timeline of relevant events\n"
|
|
1007
|
+
"- Confidence levels for each piece of information\n"
|
|
1008
|
+
"- Any gaps in knowledge\n"
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@mcp.prompt()
|
|
1013
|
+
def deployment_review(service: str) -> str:
|
|
1014
|
+
"""Pre-deployment review using CortexDB memory.
|
|
1015
|
+
|
|
1016
|
+
Parameters
|
|
1017
|
+
----------
|
|
1018
|
+
service:
|
|
1019
|
+
The name of the service being deployed.
|
|
1020
|
+
"""
|
|
1021
|
+
return (
|
|
1022
|
+
f"Perform a deployment review for service: {service}\n\n"
|
|
1023
|
+
"Using CortexDB memory, evaluate:\n"
|
|
1024
|
+
"1. Recent incidents or outages related to this service.\n"
|
|
1025
|
+
"2. Dependencies and downstream consumers that may be affected.\n"
|
|
1026
|
+
"3. Configuration changes in the last 7 days.\n"
|
|
1027
|
+
"4. Open issues or known risks flagged in previous deployments.\n"
|
|
1028
|
+
"5. Rollback plan — is there a safe previous version?\n\n"
|
|
1029
|
+
"Provide a go/no-go recommendation with supporting evidence.\n"
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
@mcp.prompt()
|
|
1034
|
+
def onboard_to_codebase(repo: str) -> str:
|
|
1035
|
+
"""Onboard to a codebase using CortexDB's stored knowledge.
|
|
1036
|
+
|
|
1037
|
+
Parameters
|
|
1038
|
+
----------
|
|
1039
|
+
repo:
|
|
1040
|
+
The repository or project name.
|
|
1041
|
+
"""
|
|
1042
|
+
return (
|
|
1043
|
+
f"Help me understand the {repo} codebase using CortexDB.\n\n"
|
|
1044
|
+
"Please:\n"
|
|
1045
|
+
"1. Search for architecture decisions and design documents.\n"
|
|
1046
|
+
"2. List the key entities (services, modules, teams).\n"
|
|
1047
|
+
"3. Show dependency relationships from the knowledge graph.\n"
|
|
1048
|
+
"4. Summarize recent changes and ongoing work.\n"
|
|
1049
|
+
"5. Highlight any known issues or technical debt.\n"
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
@mcp.prompt()
|
|
1054
|
+
def weekly_digest(tenant_id: str | None = None) -> str:
|
|
1055
|
+
"""Generate a weekly activity digest from CortexDB.
|
|
1056
|
+
|
|
1057
|
+
Parameters
|
|
1058
|
+
----------
|
|
1059
|
+
tenant_id:
|
|
1060
|
+
Optional tenant scope.
|
|
1061
|
+
"""
|
|
1062
|
+
scope = f" for tenant {tenant_id}" if tenant_id else ""
|
|
1063
|
+
return (
|
|
1064
|
+
f"Generate a weekly digest{scope} from CortexDB.\n\n"
|
|
1065
|
+
"Include:\n"
|
|
1066
|
+
"1. Summary of all episodes ingested this week (by type and source).\n"
|
|
1067
|
+
"2. New entities discovered.\n"
|
|
1068
|
+
"3. Key decisions and their context.\n"
|
|
1069
|
+
"4. Incidents and their current status.\n"
|
|
1070
|
+
"5. Proactive insights and recommendations.\n"
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
# ---------------------------------------------------------------------------
|
|
1075
|
+
# Entry point
|
|
1076
|
+
# ---------------------------------------------------------------------------
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def main() -> None:
|
|
1080
|
+
"""Run the CortexDB MCP server over stdio transport."""
|
|
1081
|
+
mcp.run(transport="stdio")
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
if __name__ == "__main__":
|
|
1085
|
+
main()
|