Plugins for the Adolf gateway:
- hindsight-openclaw-plugin: expanded memory recall/retain surface for the
Cognee -> Hindsight migration
- todoist-capture-plugin: posts captured ideas to agap-mcp's /capture-idea,
sending the kb#180 bearer token when AGAP_MCP_TOKEN is present
- feedback-loop-openclaw-plugin, kimi-quota-footer-plugin, cognee-mcp,
cognee-openclaw-plugin
Plus migrate-adolf-memory-banks.mjs for the memory-bank split,
backup-hindsight-adolf.sh / backup-llm-dbs.sh (the Hindsight and adolf-state
backups that were previously missing), and gpu_preload_check.sh for the
GTX 1070 residency checks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2072 lines
77 KiB
Python
2072 lines
77 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import asyncio
|
|
import subprocess
|
|
from collections import deque
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Deque, List, Optional, Tuple
|
|
from cognee.modules.data.methods.get_datasets_by_name import get_datasets_by_name
|
|
from cognee.modules.data.methods.get_last_added_data import get_last_added_data
|
|
from cognee.modules.users.methods import get_default_user
|
|
from cognee.shared.logging_utils import get_logger, setup_logging, get_log_file_location
|
|
from cognee.shared.usage_logger import log_usage
|
|
import importlib.util
|
|
from contextlib import redirect_stdout
|
|
import mcp.types as types
|
|
from mcp.server import FastMCP
|
|
from mcp.server.transport_security import TransportSecuritySettings
|
|
from cognee.modules.storage.utils import JSONEncoder
|
|
from starlette.responses import JSONResponse
|
|
from starlette.middleware import Middleware
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
import uvicorn
|
|
|
|
try:
|
|
from .cognee_client import CogneeClient
|
|
except ImportError:
|
|
from cognee_client import CogneeClient
|
|
|
|
try:
|
|
from .strip_vectors import strip_vectors
|
|
except ImportError:
|
|
from strip_vectors import strip_vectors
|
|
|
|
try:
|
|
from .server_utils import (
|
|
format_recall_results,
|
|
format_search_results,
|
|
normalize_delete_mode,
|
|
normalize_search_type,
|
|
parse_cognify_data,
|
|
parse_csv_list,
|
|
validate_cognify_file_paths,
|
|
validate_top_k,
|
|
)
|
|
except ImportError:
|
|
from server_utils import (
|
|
format_recall_results,
|
|
format_search_results,
|
|
normalize_delete_mode,
|
|
normalize_search_type,
|
|
parse_cognify_data,
|
|
parse_csv_list,
|
|
validate_cognify_file_paths,
|
|
validate_top_k,
|
|
)
|
|
|
|
|
|
try:
|
|
from cognee.tasks.codingagents.coding_rule_associations import (
|
|
add_rule_associations,
|
|
get_existing_rules,
|
|
)
|
|
except ModuleNotFoundError:
|
|
from .codingagents.coding_rule_associations import (
|
|
add_rule_associations,
|
|
get_existing_rules,
|
|
)
|
|
|
|
|
|
mcp = FastMCP("Cognee")
|
|
|
|
logger = get_logger()
|
|
|
|
cognee_client: Optional[CogneeClient] = None
|
|
|
|
# Per-dataset error ring buffer (bounded so long-running servers don't accumulate
|
|
# unbounded memory). Each entry is (iso_timestamp, error_message).
|
|
_TASK_ERROR_HISTORY = 50
|
|
_task_errors: dict[str, Deque[Tuple[str, str]]] = {}
|
|
|
|
# Strong references to in-flight background tasks. asyncio's event loop only keeps
|
|
# weak references to tasks, so a fire-and-forget task can be GC'd mid-execution if
|
|
# the only reference is a local that went out of scope. Adding here pins them; the
|
|
# done_callback removes them on completion. See:
|
|
# https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
|
_background_tasks: set[asyncio.Task] = set()
|
|
|
|
|
|
def _track_background(coro) -> asyncio.Task:
|
|
"""Spawn a background task and pin it so the event loop won't GC it."""
|
|
task = asyncio.create_task(coro)
|
|
_background_tasks.add(task)
|
|
task.add_done_callback(_background_tasks.discard)
|
|
return task
|
|
|
|
|
|
def _record_task_error(dataset: str, error: str) -> None:
|
|
"""Append a background task error, bounded per-dataset."""
|
|
bucket = _task_errors.setdefault(dataset, deque(maxlen=_TASK_ERROR_HISTORY))
|
|
bucket.append((datetime.now(timezone.utc).isoformat(), error))
|
|
|
|
|
|
def _configure_transport_security(host: str) -> None:
|
|
"""Configure MCP transport security based on env vars and bind host.
|
|
|
|
Must be called before run_sse_with_cors() or run_http_with_cors(), since
|
|
the SDK reads mcp.settings.transport_security lazily when creating the app.
|
|
|
|
Env vars:
|
|
MCP_DISABLE_DNS_REBINDING_PROTECTION: Set to "true" to disable all
|
|
Host/Origin header validation. Useful for LAN or Docker deployments.
|
|
MCP_ALLOWED_HOSTS: Comma-separated additional Host header patterns
|
|
(e.g. "192.168.1.50:*,myserver.local:*"). Appended to the
|
|
localhost defaults. Requires the ":*" port glob suffix.
|
|
"""
|
|
disable = os.getenv("MCP_DISABLE_DNS_REBINDING_PROTECTION", "false").lower() == "true"
|
|
|
|
if disable:
|
|
mcp.settings.transport_security = TransportSecuritySettings(
|
|
enable_dns_rebinding_protection=False,
|
|
)
|
|
logger.info("MCP transport security: DNS rebinding protection disabled")
|
|
return
|
|
|
|
extra_hosts = [h.strip() for h in os.getenv("MCP_ALLOWED_HOSTS", "").split(",") if h.strip()]
|
|
|
|
# The SDK only auto-populates localhost defaults when transport_security is
|
|
# None AND host is a loopback address. When the user binds to 0.0.0.0 or a
|
|
# LAN IP, we must provide the full allowed list ourselves.
|
|
localhost_hosts = ["127.0.0.1:*", "localhost:*", "[::1]:*"]
|
|
localhost_origins = ["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"]
|
|
|
|
allowed_hosts = localhost_hosts + extra_hosts
|
|
# Derive origins from extra hosts so users don't need to set both.
|
|
allowed_origins = localhost_origins + [f"http://{h}" for h in extra_hosts]
|
|
|
|
if host not in ("127.0.0.1", "localhost", "::1") or extra_hosts:
|
|
mcp.settings.transport_security = TransportSecuritySettings(
|
|
enable_dns_rebinding_protection=True,
|
|
allowed_hosts=allowed_hosts,
|
|
allowed_origins=allowed_origins,
|
|
)
|
|
logger.info(
|
|
"MCP transport security: allowed_hosts=%s",
|
|
allowed_hosts,
|
|
)
|
|
else:
|
|
# Loopback-only with no extra hosts — let the SDK use its own defaults.
|
|
logger.info("MCP transport security: using SDK defaults (localhost only)")
|
|
|
|
|
|
def _is_running_in_docker() -> bool:
|
|
"""Check if the process is running inside a Docker container."""
|
|
return os.path.exists("/.dockerenv") or os.path.isdir("/app")
|
|
|
|
|
|
def _get_cors_origins() -> list[str]:
|
|
"""Parse CORS allowed origins from MCP_CORS_ALLOW_ORIGINS env var."""
|
|
raw = os.getenv("MCP_CORS_ALLOW_ORIGINS", "http://localhost:3000")
|
|
return [o.strip() for o in raw.split(",") if o.strip()]
|
|
|
|
|
|
async def run_sse_with_cors():
|
|
"""Custom SSE transport with CORS middleware."""
|
|
sse_app = mcp.sse_app()
|
|
sse_app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=_get_cors_origins(),
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
config = uvicorn.Config(
|
|
sse_app,
|
|
host=mcp.settings.host,
|
|
port=mcp.settings.port,
|
|
log_level=mcp.settings.log_level.lower(),
|
|
)
|
|
server = uvicorn.Server(config)
|
|
await server.serve()
|
|
|
|
|
|
async def run_http_with_cors():
|
|
"""Custom HTTP transport with CORS middleware."""
|
|
http_app = mcp.streamable_http_app()
|
|
http_app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=_get_cors_origins(),
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
config = uvicorn.Config(
|
|
http_app,
|
|
host=mcp.settings.host,
|
|
port=mcp.settings.port,
|
|
log_level=mcp.settings.log_level.lower(),
|
|
)
|
|
server = uvicorn.Server(config)
|
|
await server.serve()
|
|
|
|
|
|
@mcp.custom_route("/health", methods=["GET"])
|
|
async def health_check(request):
|
|
return JSONResponse({"status": "ok"})
|
|
|
|
|
|
@log_usage(function_name="MCP cognify", log_type="mcp_tool")
|
|
async def cognify(
|
|
data: str,
|
|
dataset_name: str = None,
|
|
graph_model_file: str = None,
|
|
graph_model_name: str = None,
|
|
custom_prompt: str = None,
|
|
) -> list:
|
|
"""
|
|
Transform ingested data into a structured knowledge graph.
|
|
|
|
This is the core processing step in Cognee that converts raw text and documents
|
|
into an intelligent knowledge graph. It analyzes content, extracts entities and
|
|
relationships, and creates semantic connections for enhanced search and reasoning.
|
|
|
|
Prerequisites:
|
|
- **LLM_API_KEY**: Must be configured (required for entity extraction and graph generation)
|
|
- **Vector Database**: Must be accessible for embeddings storage
|
|
- **Graph Database**: Must be accessible for relationship storage
|
|
|
|
Input Requirements:
|
|
- **Content Types**: Works with any text-extractable content including:
|
|
* Natural language documents
|
|
* Structured data (CSV, JSON)
|
|
* Code repositories
|
|
* Academic papers and technical documentation
|
|
* Mixed multimedia content (with text extraction)
|
|
|
|
Processing Pipeline:
|
|
1. **Document Classification**: Identifies document types and structures
|
|
2. **Permission Validation**: Ensures user has processing rights
|
|
3. **Text Chunking**: Breaks content into semantically meaningful segments
|
|
4. **Entity Extraction**: Identifies key concepts, people, places, organizations
|
|
5. **Relationship Detection**: Discovers connections between entities
|
|
6. **Graph Construction**: Builds semantic knowledge graph with embeddings
|
|
7. **Content Summarization**: Creates hierarchical summaries for navigation
|
|
|
|
Parameters
|
|
----------
|
|
data : str
|
|
The data to be processed and transformed into structured knowledge.
|
|
This can include natural language, file location, or any text-based information
|
|
that should become part of the agent's memory.
|
|
|
|
graph_model_file : str, optional
|
|
Path to a custom schema file that defines the structure of the generated knowledge graph.
|
|
If provided, this file will be loaded using importlib to create a custom graph model.
|
|
Default is None, which uses Cognee's built-in KnowledgeGraph model.
|
|
|
|
graph_model_name : str, optional
|
|
Name of the class within the graph_model_file to instantiate as the graph model.
|
|
Required if graph_model_file is specified.
|
|
Default is None, which uses the default KnowledgeGraph class.
|
|
|
|
custom_prompt : str, optional
|
|
Custom prompt string to use for entity extraction and graph generation.
|
|
If provided, this prompt will be used instead of the default prompts for
|
|
knowledge graph extraction. The prompt should guide the LLM on how to
|
|
extract entities and relationships from the text content.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with information about the
|
|
background task launch and how to check its status.
|
|
|
|
Next Steps:
|
|
After successful cognify processing, use search functions to query the knowledge:
|
|
|
|
```python
|
|
import cognee
|
|
from cognee import SearchType
|
|
|
|
# Process your data into knowledge graph
|
|
await cognee.cognify()
|
|
|
|
# Query for insights using different search types:
|
|
|
|
# 1. Natural language completion with graph context
|
|
insights = await cognee.search(
|
|
"What are the main themes?",
|
|
query_type=SearchType.GRAPH_COMPLETION
|
|
)
|
|
|
|
# 2. Get entity relationships and connections
|
|
relationships = await cognee.search(
|
|
"connections between concepts",
|
|
query_type=SearchType.GRAPH_COMPLETION
|
|
)
|
|
|
|
# 3. Find relevant document chunks
|
|
chunks = await cognee.search(
|
|
"specific topic",
|
|
query_type=SearchType.CHUNKS
|
|
)
|
|
```
|
|
|
|
Environment Variables:
|
|
Required:
|
|
- LLM_API_KEY: API key for your LLM provider
|
|
|
|
Optional:
|
|
- LLM_PROVIDER, LLM_MODEL, VECTOR_DB_PROVIDER, GRAPH_DATABASE_PROVIDER
|
|
- LLM_RATE_LIMIT_ENABLED: Enable rate limiting (default: False)
|
|
- LLM_RATE_LIMIT_REQUESTS: Max requests per interval (default: 60)
|
|
|
|
Notes
|
|
-----
|
|
- The function launches a background task and returns immediately
|
|
- The actual cognify process may take significant time depending on text length
|
|
- Check the log file for progress
|
|
|
|
"""
|
|
|
|
dataset_name = dataset_name or _agent_scoped_default_dataset()
|
|
|
|
try:
|
|
parsed_data = parse_cognify_data(data)
|
|
except ValueError as e:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"Error: {str(e)}",
|
|
)
|
|
]
|
|
|
|
file_error = validate_cognify_file_paths(
|
|
parsed_data.items,
|
|
is_running_in_docker=_is_running_in_docker,
|
|
)
|
|
if file_error:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"Error: {file_error}",
|
|
)
|
|
]
|
|
|
|
async def cognify_task(
|
|
data_items: list[str],
|
|
dataset_name: str = "main_dataset",
|
|
graph_model_file: str = None,
|
|
graph_model_name: str = None,
|
|
custom_prompt: str = None,
|
|
) -> str:
|
|
"""Build knowledge graph from the input text"""
|
|
# NOTE: MCP uses stdout to communicate, we must redirect all output
|
|
# going to stdout ( like the print function ) to stderr.
|
|
with redirect_stdout(sys.stderr):
|
|
logger.info("Cognify process starting.")
|
|
|
|
graph_model = None
|
|
if graph_model_file and graph_model_name:
|
|
if cognee_client.use_api:
|
|
logger.warning("Custom graph models are not supported in API mode, ignoring.")
|
|
else:
|
|
from cognee.shared.data_models import KnowledgeGraph
|
|
|
|
graph_model = load_class(graph_model_file, graph_model_name)
|
|
|
|
for data_item in data_items:
|
|
await cognee_client.add(data_item, dataset_name=dataset_name)
|
|
|
|
try:
|
|
await cognee_client.cognify(
|
|
datasets=[dataset_name], custom_prompt=custom_prompt, graph_model=graph_model
|
|
)
|
|
logger.info("Cognify process finished.")
|
|
except Exception as e:
|
|
logger.error("Cognify process failed.")
|
|
raise ValueError(f"Failed to cognify: {str(e)}") from e
|
|
|
|
async def cognify_task_wrapper(**kwargs):
|
|
"""Wrapper that captures errors from the background task."""
|
|
try:
|
|
await cognify_task(**kwargs)
|
|
except Exception as e:
|
|
dataset = kwargs.get("dataset_name", "main_dataset")
|
|
_record_task_error(dataset, str(e))
|
|
logger.error(f"Background cognify task failed for dataset '{dataset}': {e}")
|
|
|
|
_track_background(
|
|
cognify_task_wrapper(
|
|
data_items=parsed_data.items,
|
|
dataset_name=dataset_name,
|
|
graph_model_file=graph_model_file,
|
|
graph_model_name=graph_model_name,
|
|
custom_prompt=custom_prompt,
|
|
)
|
|
)
|
|
|
|
log_file = get_log_file_location()
|
|
text = (
|
|
f"Background process launched due to MCP timeout limitations.\n"
|
|
f"Queued {len(parsed_data.items)} item(s) for dataset '{dataset_name}'.\n"
|
|
f"Check the log file at: {log_file}"
|
|
)
|
|
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=text,
|
|
)
|
|
]
|
|
|
|
|
|
@log_usage(function_name="MCP save_interaction", log_type="mcp_tool")
|
|
async def save_interaction(data: str) -> list:
|
|
"""
|
|
Transform and save a user-agent interaction into structured knowledge.
|
|
|
|
Parameters
|
|
----------
|
|
data : str
|
|
The input string containing user queries and corresponding agent answers.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with information about the background task launch.
|
|
"""
|
|
|
|
async def save_user_agent_interaction(data: str) -> None:
|
|
"""Build knowledge graph from the interaction data"""
|
|
with redirect_stdout(sys.stderr):
|
|
logger.info("Save interaction process starting.")
|
|
|
|
await cognee_client.add(data, node_set=["user_agent_interaction"])
|
|
|
|
try:
|
|
await cognee_client.cognify()
|
|
|
|
user = await get_default_user()
|
|
datasets = await get_datasets_by_name("main_dataset", user_id=user.id)
|
|
dataset = datasets[0]
|
|
added_data = await get_last_added_data(dataset.id)
|
|
|
|
logger.info("Save interaction process finished.")
|
|
|
|
# Rule associations only work in direct mode
|
|
if not cognee_client.use_api:
|
|
logger.info("Generating associated rules from interaction data.")
|
|
await add_rule_associations(
|
|
data=data,
|
|
rules_nodeset_name="coding_agent_rules",
|
|
context={
|
|
"user": user,
|
|
"dataset": dataset,
|
|
"data": added_data,
|
|
},
|
|
)
|
|
logger.info("Associated rules generated from interaction data.")
|
|
else:
|
|
logger.warning("Rule associations are not available in API mode, skipping.")
|
|
|
|
except Exception as e:
|
|
logger.error("Save interaction process failed.")
|
|
raise ValueError(f"Failed to Save interaction: {str(e)}") from e
|
|
|
|
async def save_task_wrapper(**kwargs):
|
|
"""Wrapper that captures errors from the background task."""
|
|
try:
|
|
await save_user_agent_interaction(**kwargs)
|
|
except Exception as e:
|
|
_record_task_error("main_dataset", str(e))
|
|
logger.error(f"Background save_interaction task failed: {e}")
|
|
|
|
_track_background(save_task_wrapper(data=data))
|
|
|
|
log_file = get_log_file_location()
|
|
text = (
|
|
f"Background process launched to process the user-agent interaction.\n"
|
|
f"Check the log file at: {log_file}"
|
|
)
|
|
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=text,
|
|
)
|
|
]
|
|
|
|
|
|
@log_usage(function_name="MCP search", log_type="mcp_tool")
|
|
async def search(
|
|
search_query: str, search_type: str, top_k: int = 15, datasets: str = None
|
|
) -> list:
|
|
"""
|
|
Search and query the knowledge graph for insights, information, and connections.
|
|
|
|
This is the final step in the Cognee workflow that retrieves information from the
|
|
processed knowledge graph. It supports multiple search modes optimized for different
|
|
use cases - from simple fact retrieval to complex reasoning and code analysis.
|
|
|
|
Search Prerequisites:
|
|
- **LLM_API_KEY**: Required for GRAPH_COMPLETION and RAG_COMPLETION search types
|
|
- **Data Added**: Must have data previously added via `cognee.add()`
|
|
- **Knowledge Graph Built**: Must have processed data via `cognee.cognify()`
|
|
- **Vector Database**: Must be accessible for semantic search functionality
|
|
|
|
Search Types & Use Cases:
|
|
|
|
**GRAPH_COMPLETION** (Recommended):
|
|
Natural language Q&A using full graph context and LLM reasoning.
|
|
Best for: Complex questions, analysis, summaries, insights.
|
|
Returns: Conversational AI responses with graph-backed context.
|
|
|
|
**RAG_COMPLETION**:
|
|
Traditional RAG using document chunks without graph structure.
|
|
Best for: Direct document retrieval, specific fact-finding.
|
|
Returns: LLM responses based on relevant text chunks.
|
|
|
|
**CHUNKS**:
|
|
Raw text segments that match the query semantically.
|
|
Best for: Finding specific passages, citations, exact content.
|
|
Returns: Ranked list of relevant text chunks with metadata.
|
|
|
|
**SUMMARIES**:
|
|
Pre-generated hierarchical summaries of content.
|
|
Best for: Quick overviews, document abstracts, topic summaries.
|
|
Returns: Multi-level summaries from detailed to high-level.
|
|
|
|
**CODE**:
|
|
Code-specific search with syntax and semantic understanding.
|
|
Best for: Finding functions, classes, implementation patterns.
|
|
Returns: Structured code information with context and relationships.
|
|
|
|
**CYPHER**:
|
|
Direct graph database queries using Cypher syntax.
|
|
Best for: Advanced users, specific graph traversals, debugging.
|
|
Returns: Raw graph query results.
|
|
|
|
**FEELING_LUCKY**:
|
|
Intelligently selects and runs the most appropriate search type.
|
|
Best for: General-purpose queries or when you're unsure which search type is best.
|
|
Returns: The results from the automatically selected search type.
|
|
|
|
Parameters
|
|
----------
|
|
search_query : str
|
|
Your question or search query in natural language.
|
|
Examples:
|
|
- "What are the main themes in this research?"
|
|
- "How do these concepts relate to each other?"
|
|
- "Find information about machine learning algorithms"
|
|
- "What functions handle user authentication?"
|
|
|
|
search_type : str
|
|
The type of search to perform. Valid options include:
|
|
- "GRAPH_COMPLETION": Returns an LLM response based on the search query and Cognee's memory
|
|
- "RAG_COMPLETION": Returns an LLM response based on the search query and standard RAG data
|
|
- "CODE": Returns code-related knowledge in JSON format
|
|
- "CHUNKS": Returns raw text chunks from the knowledge graph
|
|
- "SUMMARIES": Returns pre-generated hierarchical summaries
|
|
- "CYPHER": Direct graph database queries
|
|
- "FEELING_LUCKY": Automatically selects best search type
|
|
|
|
The search_type is case-insensitive and will be converted to uppercase.
|
|
|
|
top_k : int, optional
|
|
Maximum number of results to return (default: 10).
|
|
Controls the amount of context retrieved from the knowledge graph.
|
|
- Lower values (3-5): Faster, more focused results
|
|
- Higher values (10-20): More comprehensive, but slower and more context-heavy
|
|
Helps manage response size and context window usage in MCP clients.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with the search results.
|
|
The format of the result depends on the search_type:
|
|
- **GRAPH_COMPLETION/RAG_COMPLETION**: Conversational AI response strings
|
|
- **CHUNKS**: Relevant text passages with source metadata
|
|
- **SUMMARIES**: Hierarchical summaries from general to specific
|
|
- **CODE**: Structured code information with context
|
|
- **FEELING_LUCKY**: Results in format of automatically selected search type
|
|
- **CYPHER**: Raw graph query results
|
|
|
|
Performance & Optimization:
|
|
- **GRAPH_COMPLETION**: Slower but most intelligent, uses LLM + graph context
|
|
- **RAG_COMPLETION**: Medium speed, uses LLM + document chunks (no graph traversal)
|
|
- **CHUNKS**: Fastest, pure vector similarity search without LLM
|
|
- **SUMMARIES**: Fast, returns pre-computed summaries
|
|
- **CODE**: Medium speed, specialized for code understanding
|
|
- **FEELING_LUCKY**: Variable speed, uses LLM + search type selection intelligently
|
|
|
|
Environment Variables:
|
|
Required for LLM-based search types (GRAPH_COMPLETION, RAG_COMPLETION):
|
|
- LLM_API_KEY: API key for your LLM provider
|
|
|
|
Optional:
|
|
- LLM_PROVIDER, LLM_MODEL: Configure LLM for search responses
|
|
- VECTOR_DB_PROVIDER: Must match what was used during cognify
|
|
- GRAPH_DATABASE_PROVIDER: Must match what was used during cognify
|
|
|
|
Notes
|
|
-----
|
|
- Different search types produce different output formats
|
|
- The function handles the conversion between Cognee's internal result format and MCP's output format
|
|
|
|
"""
|
|
|
|
try:
|
|
normalized_search_type = normalize_search_type(search_type)
|
|
normalized_top_k = validate_top_k(top_k)
|
|
except ValueError as e:
|
|
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
|
|
|
|
async def search_task(
|
|
search_query: str, search_type: str, top_k: int, datasets_list: list = None
|
|
) -> str:
|
|
"""
|
|
Internal task to execute knowledge graph search with result formatting.
|
|
|
|
Handles the actual search execution and formats results appropriately
|
|
for MCP clients based on the search type and execution mode (API vs direct).
|
|
|
|
Parameters
|
|
----------
|
|
search_query : str
|
|
The search query in natural language
|
|
search_type : str
|
|
Type of search to perform (GRAPH_COMPLETION, CHUNKS, etc.)
|
|
top_k : int
|
|
Maximum number of results to return
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Formatted search results as a string, with format depending on search_type
|
|
"""
|
|
# NOTE: MCP uses stdout to communicate, we must redirect all output
|
|
# going to stdout ( like the print function ) to stderr.
|
|
with redirect_stdout(sys.stderr):
|
|
search_results = await cognee_client.search(
|
|
query_text=search_query,
|
|
query_type=search_type,
|
|
top_k=top_k,
|
|
datasets=datasets_list,
|
|
)
|
|
|
|
# Strip embedding vectors from results to save LLM context
|
|
# text_vector contains raw floats (~92KB per result), useless for clients
|
|
search_results = strip_vectors(search_results)
|
|
|
|
if not cognee_client.use_api and search_type == "INSIGHTS":
|
|
return retrieved_edges_to_string(search_results)
|
|
|
|
return format_search_results(
|
|
search_results,
|
|
search_type,
|
|
json_encoder=JSONEncoder,
|
|
)
|
|
|
|
# Parse comma-separated datasets into list
|
|
datasets_list = parse_csv_list(datasets)
|
|
try:
|
|
search_results = await search_task(
|
|
search_query,
|
|
normalized_search_type,
|
|
normalized_top_k,
|
|
datasets_list,
|
|
)
|
|
except Exception as e:
|
|
error_msg = f"Search failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
return [types.TextContent(type="text", text=search_results)]
|
|
|
|
|
|
@log_usage(function_name="MCP get_document", log_type="mcp_tool")
|
|
async def get_document(
|
|
document_id: str,
|
|
include_metadata: bool = True,
|
|
max_chunks: int = 0,
|
|
) -> list:
|
|
"""
|
|
Retrieve a complete source document and its chunks from the knowledge graph.
|
|
|
|
Use this after a CHUNKS search or list_data lookup when you need the full source
|
|
context around a result. If a chunk ID is provided instead of a document ID, the
|
|
tool resolves the chunk's parent document and returns that document.
|
|
|
|
Parameters
|
|
----------
|
|
document_id : str
|
|
Document ID to retrieve. A DocumentChunk ID is also accepted and resolves to
|
|
its parent document.
|
|
include_metadata : bool
|
|
Include document metadata fields in the response (default: True).
|
|
max_chunks : int
|
|
Maximum chunks to return. Use 0 to return all chunks.
|
|
"""
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
result = await cognee_client.get_document(
|
|
document_id=document_id,
|
|
include_metadata=include_metadata,
|
|
max_chunks=max_chunks,
|
|
)
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=json.dumps(result, indent=2, cls=JSONEncoder),
|
|
)
|
|
]
|
|
except Exception as e:
|
|
error_msg = f"get_document failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
|
|
|
|
@log_usage(function_name="MCP get_chunk_neighbors", log_type="mcp_tool")
|
|
async def get_chunk_neighbors(
|
|
chunk_id: str,
|
|
neighbor_count: int = 2,
|
|
include_target: bool = True,
|
|
direction: str = "both",
|
|
) -> list:
|
|
"""
|
|
Retrieve neighboring chunks around a target chunk from the same document.
|
|
|
|
Use this after a CHUNKS search when the matching passage is too narrow and you
|
|
need local narrative context. Chunks are returned in reading order.
|
|
|
|
Parameters
|
|
----------
|
|
chunk_id : str
|
|
Target DocumentChunk ID.
|
|
neighbor_count : int
|
|
Number of neighboring chunks to retrieve on each side. Must be 1-10.
|
|
include_target : bool
|
|
Include the target chunk in the returned chunk list (default: True).
|
|
direction : str
|
|
One of "both", "forward", or "backward".
|
|
"""
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
result = await cognee_client.get_chunk_neighbors(
|
|
chunk_id=chunk_id,
|
|
neighbor_count=neighbor_count,
|
|
include_target=include_target,
|
|
direction=direction,
|
|
)
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=json.dumps(result, indent=2, cls=JSONEncoder),
|
|
)
|
|
]
|
|
except Exception as e:
|
|
error_msg = f"get_chunk_neighbors failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
|
|
|
|
@log_usage(function_name="MCP list_data", log_type="mcp_tool")
|
|
async def list_data(dataset_id: str = None) -> list:
|
|
"""
|
|
List all datasets and their data items with IDs for deletion operations.
|
|
|
|
This function helps users identify data IDs and dataset IDs that can be used
|
|
with the delete tool. It provides a comprehensive view of available data.
|
|
|
|
Parameters
|
|
----------
|
|
dataset_id : str, optional
|
|
If provided, only list data items from this specific dataset.
|
|
If None, lists all datasets and their data items.
|
|
Should be a valid UUID string.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with formatted information
|
|
about datasets and data items, including their IDs for deletion.
|
|
|
|
Notes
|
|
-----
|
|
- Use this tool to identify data_id and dataset_id values for the delete tool
|
|
- The output includes both dataset information and individual data items
|
|
- UUIDs are displayed in a format ready for use with other tools
|
|
"""
|
|
from uuid import UUID
|
|
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
output_lines = []
|
|
|
|
if dataset_id:
|
|
# Detailed data listing for specific dataset is only available in direct mode
|
|
if cognee_client.use_api:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text="❌ Detailed data listing for specific datasets is not available in API mode.\nPlease use the API directly or use direct mode.",
|
|
)
|
|
]
|
|
|
|
from cognee.modules.users.methods import get_default_user
|
|
from cognee.modules.data.methods import get_dataset, get_dataset_data
|
|
|
|
logger.info(f"Listing data for dataset: {dataset_id}")
|
|
dataset_uuid = UUID(dataset_id)
|
|
user = await get_default_user()
|
|
|
|
dataset = await get_dataset(user.id, dataset_uuid)
|
|
|
|
if not dataset:
|
|
return [
|
|
types.TextContent(type="text", text=f"❌ Dataset not found: {dataset_id}")
|
|
]
|
|
|
|
# Get data items in the dataset
|
|
data_items = await get_dataset_data(dataset.id)
|
|
|
|
output_lines.append(f"📁 Dataset: {dataset.name}")
|
|
output_lines.append(f" ID: {dataset.id}")
|
|
output_lines.append(f" Created: {dataset.created_at}")
|
|
output_lines.append(f" Data items: {len(data_items)}")
|
|
output_lines.append("")
|
|
|
|
if data_items:
|
|
for i, data_item in enumerate(data_items, 1):
|
|
output_lines.append(f" 📄 Data item #{i}:")
|
|
output_lines.append(f" Data ID: {data_item.id}")
|
|
output_lines.append(f" Name: {data_item.name or 'Unnamed'}")
|
|
output_lines.append(f" Created: {data_item.created_at}")
|
|
output_lines.append("")
|
|
else:
|
|
output_lines.append(" (No data items in this dataset)")
|
|
|
|
else:
|
|
# List all datasets - works in both modes
|
|
logger.info("Listing all datasets")
|
|
datasets = await cognee_client.list_datasets()
|
|
|
|
if not datasets:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text="📂 No datasets found.\nUse the cognify tool to create your first dataset!",
|
|
)
|
|
]
|
|
|
|
output_lines.append("📂 Available Datasets:")
|
|
output_lines.append("=" * 50)
|
|
output_lines.append("")
|
|
|
|
for i, dataset in enumerate(datasets, 1):
|
|
# In API mode, dataset is a dict; in direct mode, it's formatted as dict
|
|
if isinstance(dataset, dict):
|
|
output_lines.append(f"{i}. 📁 {dataset.get('name', 'Unnamed')}")
|
|
output_lines.append(f" Dataset ID: {dataset.get('id')}")
|
|
output_lines.append(f" Created: {dataset.get('created_at', 'N/A')}")
|
|
else:
|
|
output_lines.append(f"{i}. 📁 {dataset.name}")
|
|
output_lines.append(f" Dataset ID: {dataset.id}")
|
|
output_lines.append(f" Created: {dataset.created_at}")
|
|
output_lines.append("")
|
|
|
|
if not cognee_client.use_api:
|
|
output_lines.append("💡 To see data items in a specific dataset, use:")
|
|
output_lines.append(' list_data(dataset_id="your-dataset-id-here")')
|
|
output_lines.append("")
|
|
output_lines.append("🗑️ To delete specific data, use:")
|
|
output_lines.append(' delete(data_id="data-id", dataset_id="dataset-id")')
|
|
|
|
result_text = "\n".join(output_lines)
|
|
logger.info("List data operation completed successfully")
|
|
|
|
return [types.TextContent(type="text", text=result_text)]
|
|
|
|
except ValueError as e:
|
|
error_msg = f"❌ Invalid UUID format: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
except Exception as e:
|
|
error_msg = f"❌ Failed to list data: {str(e)}"
|
|
logger.error(f"List data error: {str(e)}")
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
|
|
@log_usage(function_name="MCP delete_dataset", log_type="mcp_tool")
|
|
async def delete_dataset(dataset_name: str) -> list:
|
|
"""
|
|
Delete an entire dataset and all its data from the knowledge graph.
|
|
|
|
This removes the dataset completely: graph data, vector indices,
|
|
and metadata in the relational database. This operation cannot be undone.
|
|
|
|
Parameters
|
|
----------
|
|
dataset_name : str
|
|
The name of the dataset to delete (e.g. 'main_dataset').
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a TextContent with deletion status.
|
|
"""
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
if cognee_client.use_api:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text="❌ delete_dataset is not available in API mode. Use the API directly.",
|
|
)
|
|
]
|
|
|
|
from cognee.modules.users.methods import get_default_user
|
|
from cognee.modules.data.methods import delete_dataset as _delete_dataset
|
|
from cognee.modules.data.methods import get_datasets
|
|
|
|
user = await get_default_user()
|
|
datasets = await get_datasets(user.id)
|
|
matching = [ds for ds in datasets if ds.name == dataset_name]
|
|
|
|
if not matching:
|
|
return [types.TextContent(type="text", text=f"Dataset '{dataset_name}' not found.")]
|
|
|
|
if len(matching) > 1:
|
|
ids = ", ".join(str(ds.id) for ds in matching)
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"Multiple datasets named '{dataset_name}' found (IDs: {ids}). Please delete by ID instead.",
|
|
)
|
|
]
|
|
|
|
await _delete_dataset(matching[0])
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"Dataset '{dataset_name}' deleted successfully. Graph, vectors, and metadata removed.",
|
|
)
|
|
]
|
|
except Exception as e:
|
|
return [types.TextContent(type="text", text=f"Error deleting dataset: {str(e)}")]
|
|
|
|
|
|
@log_usage(function_name="MCP delete", log_type="mcp_tool")
|
|
async def delete(data_id: str, dataset_id: str, mode: str = "soft") -> list:
|
|
"""
|
|
Delete specific data from a dataset in the Cognee knowledge graph.
|
|
|
|
This function removes a specific data item from a dataset while keeping the
|
|
dataset itself intact. It supports both soft and hard deletion modes.
|
|
|
|
Parameters
|
|
----------
|
|
data_id : str
|
|
The UUID of the data item to delete from the knowledge graph.
|
|
This should be a valid UUID string identifying the specific data item.
|
|
|
|
dataset_id : str
|
|
The UUID of the dataset containing the data to be deleted.
|
|
This should be a valid UUID string identifying the dataset.
|
|
|
|
mode : str, optional
|
|
The deletion mode to use. Options are:
|
|
- "soft" (default): Removes the data but keeps related entities that might be shared
|
|
- "hard": Also removes degree-one entity nodes that become orphaned after deletion
|
|
Default is "soft" for safer deletion that preserves shared knowledge.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with the deletion results,
|
|
including status, deleted node counts, and confirmation details.
|
|
|
|
Notes
|
|
-----
|
|
- This operation cannot be undone. The specified data will be permanently removed.
|
|
- Hard mode may remove additional entity nodes that become orphaned
|
|
- The function provides detailed feedback about what was deleted
|
|
- Use this for targeted deletion instead of the prune tool which removes everything
|
|
"""
|
|
from uuid import UUID
|
|
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
normalized_mode = normalize_delete_mode(mode)
|
|
logger.info(
|
|
f"Starting delete operation for data_id: {data_id}, dataset_id: {dataset_id}, mode: {normalized_mode}"
|
|
)
|
|
|
|
# Convert string UUIDs to UUID objects
|
|
data_uuid = UUID(data_id)
|
|
dataset_uuid = UUID(dataset_id)
|
|
|
|
# Call the cognee delete function via client
|
|
result = await cognee_client.delete(
|
|
data_id=data_uuid, dataset_id=dataset_uuid, mode=normalized_mode
|
|
)
|
|
|
|
logger.info(f"Delete operation completed successfully: {result}")
|
|
|
|
# Format the result for MCP response
|
|
formatted_result = json.dumps(result, indent=2, cls=JSONEncoder)
|
|
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"✅ Delete operation completed successfully!\n\n{formatted_result}",
|
|
)
|
|
]
|
|
|
|
except ValueError as e:
|
|
error_msg = f"❌ Invalid delete request: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
except Exception as e:
|
|
# Handle all other errors (DocumentNotFoundError, DatasetNotFoundError, etc.)
|
|
error_msg = f"❌ Delete operation failed: {str(e)}"
|
|
logger.error(f"Delete operation error: {str(e)}")
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
|
|
@log_usage(function_name="MCP prune", log_type="mcp_tool")
|
|
async def prune():
|
|
"""
|
|
Reset the Cognee knowledge graph by removing all stored information.
|
|
|
|
This function performs a complete reset of both the data layer and system layer
|
|
of the Cognee knowledge graph, removing all nodes, edges, and associated metadata.
|
|
It is typically used during development or when needing to start fresh with a new
|
|
knowledge base.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with confirmation of the prune operation.
|
|
|
|
Notes
|
|
-----
|
|
- This operation cannot be undone. All memory data will be permanently deleted.
|
|
- The function prunes both data content (using prune_data) and system metadata (using prune_system)
|
|
- This operation is not available in API mode
|
|
"""
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
await cognee_client.prune_data()
|
|
await cognee_client.prune_system(metadata=True)
|
|
return [types.TextContent(type="text", text="Pruned")]
|
|
except NotImplementedError:
|
|
error_msg = "❌ Prune operation is not available in API mode"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
except Exception as e:
|
|
error_msg = f"❌ Prune operation failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Session-aware memory operations (remember, recall, forget)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
@log_usage(function_name="MCP remember", log_type="mcp_tool")
|
|
async def remember(
|
|
data: str,
|
|
dataset_name: str = None,
|
|
session_id: str = None,
|
|
custom_prompt: str = None,
|
|
) -> list:
|
|
"""Store data in memory.
|
|
|
|
Two modes depending on whether session_id is provided:
|
|
|
|
Without session_id (permanent memory): Runs the full add + cognify
|
|
pipeline to ingest data and build the knowledge graph.
|
|
|
|
With session_id (session memory): Stores the data in the session
|
|
cache only. Fast, no entity extraction. Omit session_id when the
|
|
content should be stored as permanent graph memory.
|
|
|
|
Parameters
|
|
----------
|
|
data : str
|
|
The data to store (text content).
|
|
dataset_name : str, optional
|
|
Target dataset name. Defaults to the current MCP client's
|
|
agent-scoped dataset (e.g. "cursor_vscode_memory"), or
|
|
"main_dataset" if no client identity is detected.
|
|
session_id : str, optional
|
|
Session ID. When set, stores in session cache only.
|
|
custom_prompt : str, optional
|
|
Custom prompt for entity extraction (permanent mode only).
|
|
"""
|
|
dataset_name = dataset_name or _agent_scoped_default_dataset()
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
result = await cognee_client.remember(
|
|
data=data,
|
|
dataset_name=dataset_name,
|
|
session_id=session_id,
|
|
custom_prompt=custom_prompt,
|
|
)
|
|
status = result.get("status", "completed")
|
|
if session_id:
|
|
text = f"Stored in session cache (session_id={session_id}, status={status})."
|
|
else:
|
|
text = f"Stored permanently in knowledge graph (dataset={dataset_name}, status={status})."
|
|
return [types.TextContent(type="text", text=text)]
|
|
except Exception as e:
|
|
error_msg = f"Remember failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
|
|
|
|
@mcp.tool()
|
|
@log_usage(function_name="MCP recall", log_type="mcp_tool")
|
|
async def recall(
|
|
query: str,
|
|
search_type: str = None,
|
|
datasets: str = None,
|
|
session_id: str = None,
|
|
top_k: int = 15,
|
|
) -> list:
|
|
"""Search memory with auto-routing and session awareness.
|
|
|
|
When session_id is provided without datasets or search_type,
|
|
searches session cache first by keyword matching. Falls through
|
|
to the permanent knowledge graph if no session results match.
|
|
|
|
Auto-routing picks the best search strategy when search_type
|
|
is not specified.
|
|
|
|
Parameters
|
|
----------
|
|
query : str
|
|
Natural language query to search for.
|
|
search_type : str, optional
|
|
Override auto-routing. Options: GRAPH_COMPLETION,
|
|
GRAPH_COMPLETION_COT, RAG_COMPLETION, CHUNKS, SUMMARIES,
|
|
TEMPORAL, FEELING_LUCKY, etc.
|
|
datasets : str, optional
|
|
Comma-separated dataset names to search within.
|
|
session_id : str, optional
|
|
Session ID for session-first search.
|
|
top_k : int
|
|
Maximum results to return (default: 10).
|
|
"""
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
normalized_top_k = validate_top_k(top_k)
|
|
dataset_list = parse_csv_list(datasets)
|
|
results = await cognee_client.recall(
|
|
query_text=query,
|
|
search_type=search_type,
|
|
datasets=dataset_list,
|
|
session_id=session_id,
|
|
top_k=normalized_top_k,
|
|
)
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=format_recall_results(results, json_encoder=JSONEncoder),
|
|
)
|
|
]
|
|
except Exception as e:
|
|
error_msg = f"Recall failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
|
|
|
|
@mcp.tool()
|
|
@log_usage(function_name="MCP forget", log_type="mcp_tool")
|
|
async def forget(
|
|
dataset: str = None,
|
|
data_id: str = None,
|
|
everything: bool = False,
|
|
memory_only: bool = False,
|
|
) -> list:
|
|
"""Delete data from memory.
|
|
|
|
Can target a single data item, an entire dataset, or delete everything
|
|
the user owns. Removes data from the relational DB, graph DB, and
|
|
vector DB (unless memory_only is set).
|
|
|
|
Parameters
|
|
----------
|
|
dataset : str, optional
|
|
Dataset name. Alone, deletes the entire dataset. Combined with
|
|
data_id, scopes the single-item delete to this dataset.
|
|
data_id : str, optional
|
|
UUID of a single data item to remove (e.g. from
|
|
list_dataset_data_json). Requires 'dataset' to also be set.
|
|
everything : bool
|
|
If true, delete ALL data across all datasets. Ignores dataset/data_id.
|
|
memory_only : bool
|
|
If true, delete only the knowledge graph + vector embeddings for
|
|
the target (the dataset, or the single data item when data_id is
|
|
set), preserving the raw file/data record so it can be
|
|
re-cognified later.
|
|
|
|
Notes
|
|
-----
|
|
- Bug fix (kb#70): this tool previously had no `data_id` parameter at
|
|
all, so individual entries/facts could never be deleted -- only
|
|
whole datasets. The underlying cognee API always supported
|
|
dataset+data_id deletion; this tool just never exposed it.
|
|
"""
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
if not dataset and not everything:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text="Error: Specify 'dataset' name (optionally with 'data_id') or set 'everything' to true.",
|
|
)
|
|
]
|
|
if data_id and not dataset:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text="Error: 'data_id' requires 'dataset' to also be set.",
|
|
)
|
|
]
|
|
|
|
data_uuid = None
|
|
if data_id:
|
|
from uuid import UUID
|
|
|
|
try:
|
|
data_uuid = UUID(data_id)
|
|
except ValueError as e:
|
|
return [
|
|
types.TextContent(
|
|
type="text", text=f"Error: 'data_id' is not a valid UUID: {e}"
|
|
)
|
|
]
|
|
|
|
result = await cognee_client.forget(
|
|
dataset=dataset,
|
|
data_id=data_uuid,
|
|
everything=everything,
|
|
memory_only=memory_only,
|
|
)
|
|
status = result.get("status", "unknown") if isinstance(result, dict) else "completed"
|
|
ok = status == "success"
|
|
prefix = "✅" if ok else "⚠️"
|
|
if everything:
|
|
text = f"{prefix} All data deleted (status={status})."
|
|
elif data_id:
|
|
text = (
|
|
f"{prefix} Data item '{data_id}' deleted from dataset '{dataset}' "
|
|
f"(status={status})."
|
|
)
|
|
else:
|
|
text = f"{prefix} Dataset '{dataset}' deleted (status={status})."
|
|
return [types.TextContent(type="text", text=text)]
|
|
except Exception as e:
|
|
error_msg = f"Forget failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
|
|
|
|
@log_usage(function_name="MCP improve", log_type="mcp_tool")
|
|
async def improve(
|
|
dataset_name: str = None,
|
|
session_ids: str = None,
|
|
) -> list:
|
|
"""Enrich the knowledge graph and bridge session data to the permanent graph.
|
|
|
|
When session_ids is provided, runs a 4-stage pipeline:
|
|
1. Apply feedback weights from session scores to graph nodes/edges
|
|
2. Persist session Q&A text into the permanent knowledge graph
|
|
3. Enrich graph with triplet embeddings (memify)
|
|
4. Sync enriched graph knowledge back into session caches
|
|
|
|
Without session_ids, only stage 3 runs (triplet enrichment).
|
|
|
|
Parameters
|
|
----------
|
|
dataset_name : str, optional
|
|
Dataset to process. Defaults to the current MCP client's
|
|
agent-scoped dataset, or "main_dataset" if no client identity is
|
|
detected.
|
|
session_ids : str, optional
|
|
Comma-separated session IDs to bridge into the permanent graph.
|
|
"""
|
|
dataset_name = dataset_name or _agent_scoped_default_dataset()
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
session_list = parse_csv_list(session_ids)
|
|
result = await cognee_client.improve(
|
|
dataset_name=dataset_name,
|
|
session_ids=session_list,
|
|
)
|
|
status = result.get("status", "completed") if isinstance(result, dict) else "completed"
|
|
if session_list:
|
|
text = (
|
|
f"Improve completed (status={status}). "
|
|
f"Bridged {len(session_list)} session(s) into permanent graph."
|
|
)
|
|
else:
|
|
text = f"Graph enrichment completed (status={status})."
|
|
return [types.TextContent(type="text", text=text)]
|
|
except Exception as e:
|
|
error_msg = f"Improve failed: {str(e)}"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=f"Error: {error_msg}")]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# V1 pipeline status tool
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@log_usage(function_name="MCP cognify_status", log_type="mcp_tool")
|
|
async def cognify_status(
|
|
dataset_name: str = None,
|
|
pipelines: List[str] = None,
|
|
) -> list:
|
|
"""
|
|
Get the current status of selected pipelines.
|
|
|
|
This function retrieves information about current and recently completed
|
|
pipeline operations in the selected dataset. When `dataset_name` is omitted
|
|
it defaults to the current MCP client's agent-scoped dataset (e.g.
|
|
"cursor_vscode_memory") so each agent sees its own status.
|
|
|
|
Returns
|
|
-------
|
|
list
|
|
A list containing a single TextContent object with the status information as a string.
|
|
The status includes information about active and completed jobs for the
|
|
requested pipelines.
|
|
|
|
Notes
|
|
-----
|
|
- By default this checks "cognify_pipeline" (backward compatible)
|
|
- Use `pipelines` to restrict to specific pipeline names
|
|
- Status information includes job progress, execution time, and completion status
|
|
- The status is returned in string format for easy reading
|
|
- In API mode the dataset id is resolved over HTTP and status is read
|
|
from the server's `GET /api/v1/datasets/status` endpoint
|
|
"""
|
|
dataset_name = dataset_name or _agent_scoped_default_dataset()
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
if cognee_client.use_api:
|
|
# API mode: resolve the dataset id over HTTP (no local cognee
|
|
# instance exists in this process) before querying status.
|
|
datasets = await cognee_client.list_datasets()
|
|
dataset_id = next(
|
|
(d["id"] for d in datasets if d.get("name") == dataset_name), None
|
|
)
|
|
if dataset_id is None:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"❌ Dataset '{dataset_name}' not found via API",
|
|
)
|
|
]
|
|
else:
|
|
from cognee.modules.data.methods.get_unique_dataset_id import get_unique_dataset_id
|
|
from cognee.modules.users.methods import get_default_user
|
|
|
|
user = await get_default_user()
|
|
dataset_id = await get_unique_dataset_id(dataset_name, user)
|
|
|
|
requested_pipelines = list(dict.fromkeys(pipelines or ["cognify_pipeline"]))
|
|
|
|
if len(requested_pipelines) == 1:
|
|
status = await cognee_client.get_pipeline_status(
|
|
[dataset_id], requested_pipelines[0]
|
|
)
|
|
else:
|
|
status: dict[str, dict] = {str(dataset_id): {}}
|
|
for pipeline_name in requested_pipelines:
|
|
pipeline_status = await cognee_client.get_pipeline_status(
|
|
[dataset_id], pipeline_name
|
|
)
|
|
if str(dataset_id) in pipeline_status:
|
|
status[str(dataset_id)][pipeline_name] = pipeline_status[str(dataset_id)]
|
|
|
|
# Append any background task errors
|
|
status_text = str(status)
|
|
dataset_errors = _task_errors.get(dataset_name, [])
|
|
if dataset_errors:
|
|
error_lines = ["\n\nBackground task errors:"]
|
|
for ts, err in sorted(dataset_errors, reverse=True):
|
|
error_lines.append(f" [{ts}] {err}")
|
|
status_text += "\n".join(error_lines)
|
|
|
|
return [types.TextContent(type="text", text=status_text)]
|
|
except NotImplementedError:
|
|
error_msg = "❌ Pipeline status is not available in API mode"
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
except Exception as e:
|
|
error_msg = f"❌ Failed to get cognify status: {str(e)}"
|
|
# Still report background errors even if pipeline status fails
|
|
dataset_errors = _task_errors.get(dataset_name, [])
|
|
if dataset_errors:
|
|
error_lines = ["\n\nBackground task errors:"]
|
|
for ts, err in sorted(dataset_errors, reverse=True):
|
|
error_lines.append(f" [{ts}] {err}")
|
|
error_msg += "\n".join(error_lines)
|
|
logger.error(error_msg)
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
|
|
# MCP App: interactive graph visualization UI. Rendered by MCP Apps-capable
|
|
# hosts (Cursor, Claude Desktop) via the _meta.ui.resourceUri contract.
|
|
_VISUALIZE_APP_URI = "ui://cognee-visualize/graph.html"
|
|
|
|
|
|
@mcp.resource(
|
|
_VISUALIZE_APP_URI,
|
|
name="Cognee Graph Visualization UI",
|
|
description="Interactive MCP App UI that renders a Cognee knowledge graph.",
|
|
mime_type="text/html;profile=mcp-app",
|
|
)
|
|
def _visualize_graph_ui_resource() -> str:
|
|
# The bundle path is resolved as a sibling of this file. In a Docker /
|
|
# PyPI install, that's site-packages/src/app_bundles/. In from-source
|
|
# dev (running `python src/server.py` directly), it's cognee-mcp/src/
|
|
# app_bundles/. Both resolutions only work because we read via __file__
|
|
# rather than a hardcoded `/app/...` or repo-relative path, so the bundle
|
|
# lookup follows wherever this module was loaded from.
|
|
bundle = Path(__file__).parent / "app_bundles" / "visualize-graph.html"
|
|
if not bundle.is_file():
|
|
raise FileNotFoundError(
|
|
f"MCP App bundle not found at {bundle}. "
|
|
"Build it with: cd cognee-mcp/apps-src && npm install && npm run build"
|
|
)
|
|
return bundle.read_text(encoding="utf-8")
|
|
|
|
|
|
# CSS overrides appended to cognee's graph HTML so it fits the MCP App
|
|
# iframe better: the floating bottom control bar can wrap to multiple
|
|
# rows when the iframe is narrow, and the standalone "Light mode"
|
|
# toggle is hidden (the workspace owns theming).
|
|
#
|
|
# Note: d3 is loaded from a CDN by cognee's HTML, which the MCP App iframe
|
|
# blocks via CSP. The workspace bundles d3 from its npm dependency and
|
|
# substitutes the CDN <script> tag client-side before assigning srcDoc.
|
|
_GRAPH_VIZ_OVERRIDES = """
|
|
<style>
|
|
#theme-toggle { display: none !important; }
|
|
#controls {
|
|
flex-wrap: wrap;
|
|
max-width: calc(100vw - 16px);
|
|
justify-content: center;
|
|
bottom: 8px;
|
|
row-gap: 2px;
|
|
}
|
|
#controls .ctrl-btn { padding: 4px 8px; font-size: 10px; }
|
|
#controls .ctrl-sep { margin: 2px 2px; }
|
|
</style>
|
|
"""
|
|
|
|
|
|
def _inject_graph_viz_overrides(html: str) -> str:
|
|
if "</head>" in html:
|
|
return html.replace("</head>", _GRAPH_VIZ_OVERRIDES + "</head>", 1)
|
|
return html
|
|
|
|
|
|
@mcp.tool(
|
|
name="visualize_graph_ui",
|
|
description=(
|
|
"Open the Cognee workspace UI and render the current knowledge graph. "
|
|
"The UI also lets the user upload files to memory."
|
|
),
|
|
meta={"ui": {"resourceUri": _VISUALIZE_APP_URI}},
|
|
)
|
|
@log_usage(function_name="MCP visualize_graph_ui", log_type="mcp_tool")
|
|
async def visualize_graph_ui(dataset_name: str = None) -> types.CallToolResult:
|
|
"""Render the Cognee graph for a specific dataset.
|
|
|
|
With ENABLE_BACKEND_ACCESS_CONTROL=true, each (user, dataset) pair has its
|
|
own graph DB. Without dataset_name we'd hit the global default engine,
|
|
which is empty in that mode. Resolving dataset_name (explicit, or via
|
|
agent scoping) and routing through visualize_multi_user_graph picks up
|
|
the right per-dataset context.
|
|
"""
|
|
from cognee.api.v1.visualize import visualize_graph
|
|
|
|
explicit_dataset = dataset_name is not None
|
|
dataset_name = dataset_name or _agent_scoped_default_dataset()
|
|
|
|
# Per-dataset graph routing requires direct mode (we set the database
|
|
# context locally); in API mode the API server controls its own graph
|
|
# source. Reject explicit dataset selection there instead of silently
|
|
# falling back to a different graph.
|
|
if explicit_dataset and cognee_client.use_api:
|
|
return types.CallToolResult(
|
|
isError=True,
|
|
content=[
|
|
types.TextContent(
|
|
type="text",
|
|
text=(
|
|
"Error: per-dataset graph rendering is only supported in direct mode. "
|
|
"Drop the dataset_name argument or run cognee-mcp without --api-url."
|
|
),
|
|
)
|
|
],
|
|
)
|
|
|
|
with redirect_stdout(sys.stderr):
|
|
html: str | None = None
|
|
if dataset_name and not cognee_client.use_api:
|
|
from cognee.api.v1.visualize.visualize import visualize_multi_user_graph
|
|
|
|
user = await get_default_user()
|
|
datasets = await get_datasets_by_name(dataset_name, user.id)
|
|
if datasets:
|
|
html = await visualize_multi_user_graph([(user, datasets[0])])
|
|
if html is None:
|
|
html = await visualize_graph()
|
|
|
|
html = _inject_graph_viz_overrides(html)
|
|
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text="Cognee knowledge graph rendered.")],
|
|
structuredContent={"html": html},
|
|
)
|
|
|
|
|
|
@mcp.tool(
|
|
name="upload_file_ui",
|
|
description=(
|
|
"Open the Cognee workspace UI so the user can upload files to memory. "
|
|
"The UI also shows the current knowledge graph."
|
|
),
|
|
meta={"ui": {"resourceUri": _VISUALIZE_APP_URI}},
|
|
)
|
|
@log_usage(function_name="MCP upload_file_ui", log_type="mcp_tool")
|
|
async def upload_file_ui() -> types.CallToolResult:
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text="Cognee workspace opened.")],
|
|
)
|
|
|
|
|
|
@mcp.tool(
|
|
name="open_cognee_workspace",
|
|
description=(
|
|
"Open the Cognee workspace UI. Use for generic intents like "
|
|
"'run the cognee UI', 'show the cognee app', 'open cognee'. "
|
|
"The UI provides dataset management, file upload, text ingestion, "
|
|
"search, and graph visualization."
|
|
),
|
|
meta={"ui": {"resourceUri": _VISUALIZE_APP_URI}},
|
|
)
|
|
@log_usage(function_name="MCP open_cognee_workspace", log_type="mcp_tool")
|
|
async def open_cognee_workspace() -> types.CallToolResult:
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text="Cognee workspace opened.")],
|
|
)
|
|
|
|
|
|
_MAX_UPLOAD_BYTES = 10 * 1024 * 1024
|
|
|
|
|
|
@mcp.tool(
|
|
name="cognify_file",
|
|
description=(
|
|
"Ingest an uploaded file into Cognee memory. Accepts the file as base64. "
|
|
"Runs add synchronously, then launches cognify in the background."
|
|
),
|
|
)
|
|
@log_usage(function_name="MCP cognify_file", log_type="mcp_tool")
|
|
async def cognify_file(
|
|
filename: str,
|
|
content_base64: str,
|
|
dataset_name: str = None,
|
|
) -> list:
|
|
import base64
|
|
import tempfile
|
|
|
|
dataset_name = dataset_name or _agent_scoped_default_dataset()
|
|
|
|
try:
|
|
data = base64.b64decode(content_base64, validate=True)
|
|
except Exception as e:
|
|
return [types.TextContent(type="text", text=f"Error: invalid base64 content ({e}).")]
|
|
|
|
if len(data) > _MAX_UPLOAD_BYTES:
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"Error: file exceeds 10 MB limit ({len(data):,} bytes).",
|
|
)
|
|
]
|
|
|
|
# Sanitize to a basename with a fallback extension; preserves the original
|
|
# name so cognee's Data.name comes out as e.g. "alice_notes" rather than
|
|
# a tempfile slug like "tmp231sj_ac".
|
|
safe_name = Path(filename).name or "upload"
|
|
if not Path(safe_name).suffix:
|
|
safe_name += ".txt"
|
|
|
|
with redirect_stdout(sys.stderr):
|
|
tmp_dir = tempfile.mkdtemp(prefix="cognee_upload_")
|
|
tmp_path = os.path.join(tmp_dir, safe_name)
|
|
try:
|
|
with open(tmp_path, "wb") as f:
|
|
f.write(data)
|
|
await cognee_client.add(tmp_path, dataset_name=dataset_name)
|
|
finally:
|
|
try:
|
|
os.unlink(tmp_path)
|
|
except OSError:
|
|
pass
|
|
try:
|
|
os.rmdir(tmp_dir)
|
|
except OSError:
|
|
pass
|
|
|
|
async def _cognify_bg():
|
|
with redirect_stdout(sys.stderr):
|
|
try:
|
|
await cognee_client.cognify(datasets=[dataset_name])
|
|
logger.info(f"cognify_file: background cognify finished for '{dataset_name}'.")
|
|
except Exception as e:
|
|
ts = datetime.now(timezone.utc).isoformat()
|
|
_task_errors.setdefault(dataset_name, []).append((ts, str(e)))
|
|
logger.error(f"cognify_file: background cognify failed for '{dataset_name}': {e}")
|
|
|
|
asyncio.create_task(_cognify_bg())
|
|
|
|
return [
|
|
types.TextContent(
|
|
type="text",
|
|
text=(
|
|
f"Ingested '{filename}' ({len(data):,} bytes) into dataset '{dataset_name}'. "
|
|
f"Cognify is running in the background; refresh the workspace once it finishes."
|
|
),
|
|
)
|
|
]
|
|
|
|
|
|
@mcp.tool(
|
|
name="list_datasets_json",
|
|
description=(
|
|
"List datasets as structured JSON for the Cognee workspace UI. "
|
|
"Returns {datasets: [{id, name}, ...]} in structuredContent."
|
|
),
|
|
)
|
|
@log_usage(function_name="MCP list_datasets_json", log_type="mcp_tool")
|
|
async def list_datasets_json() -> types.CallToolResult:
|
|
with redirect_stdout(sys.stderr):
|
|
raw = await cognee_client.list_datasets()
|
|
|
|
datasets = []
|
|
for ds in raw or []:
|
|
if isinstance(ds, dict):
|
|
datasets.append({"id": str(ds.get("id", "")), "name": ds.get("name", "")})
|
|
else:
|
|
datasets.append({"id": str(ds.id), "name": ds.name})
|
|
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text=f"{len(datasets)} dataset(s).")],
|
|
structuredContent={"datasets": datasets},
|
|
)
|
|
|
|
|
|
@mcp.tool(
|
|
name="list_dataset_data_json",
|
|
description=(
|
|
"List data items in a dataset as structured JSON for the Cognee workspace UI. "
|
|
"Returns {data: [{id, name}, ...]} in structuredContent."
|
|
),
|
|
)
|
|
@log_usage(function_name="MCP list_dataset_data_json", log_type="mcp_tool")
|
|
async def list_dataset_data_json(dataset_id: str) -> types.CallToolResult:
|
|
from uuid import UUID
|
|
from cognee.modules.data.methods import get_dataset, get_dataset_data
|
|
|
|
if cognee_client.use_api:
|
|
return types.CallToolResult(
|
|
isError=True,
|
|
content=[
|
|
types.TextContent(
|
|
type="text",
|
|
text="Error: list_dataset_data_json is only available in direct mode.",
|
|
)
|
|
],
|
|
)
|
|
|
|
try:
|
|
dataset_uuid = UUID(dataset_id)
|
|
except ValueError as e:
|
|
return types.CallToolResult(
|
|
isError=True,
|
|
content=[types.TextContent(type="text", text=f"Error: invalid dataset_id ({e}).")],
|
|
)
|
|
|
|
with redirect_stdout(sys.stderr):
|
|
user = await get_default_user()
|
|
dataset = await get_dataset(user.id, dataset_uuid)
|
|
if not dataset:
|
|
return types.CallToolResult(
|
|
isError=True,
|
|
content=[
|
|
types.TextContent(type="text", text=f"Error: dataset not found: {dataset_id}.")
|
|
],
|
|
)
|
|
items = await get_dataset_data(dataset.id)
|
|
|
|
data = [{"id": str(item.id), "name": item.name or "(unnamed)"} for item in items]
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text=f"{len(data)} data item(s).")],
|
|
structuredContent={"data": data},
|
|
)
|
|
|
|
|
|
def _sanitize_client_name(name: str) -> str:
|
|
import re
|
|
|
|
# Strip parenthetical suffixes that bridges like mcp-remote append, e.g.
|
|
# "cursor-vscode (via mcp-remote 0.1.37)" -> "cursor-vscode".
|
|
cleaned = re.sub(r"\s*\(.*?\)\s*$", "", (name or "").strip())
|
|
s = re.sub(r"[^a-z0-9_]+", "_", cleaned.lower()).strip("_")
|
|
return s or "unknown"
|
|
|
|
|
|
def _is_agent_scoping_enabled() -> bool:
|
|
"""Whether per-client default datasets are active.
|
|
|
|
Controlled by COGNEE_MCP_AGENT_SCOPED env var (default: 'true').
|
|
When 'false', tools fall back to 'main_dataset' as the default,
|
|
matching the pre-agent-scoping behavior.
|
|
"""
|
|
return os.getenv("COGNEE_MCP_AGENT_SCOPED", "true").strip().lower() != "false"
|
|
|
|
|
|
def _agent_scoped_default_dataset() -> str:
|
|
"""Return the default dataset for the current MCP request.
|
|
|
|
With agent scoping enabled (default), reads clientInfo.name from the
|
|
active request context and returns '{sanitized}_memory' (e.g.
|
|
'cursor_vscode_memory'). Falls back to 'main_dataset' when:
|
|
- agent scoping is disabled via COGNEE_MCP_AGENT_SCOPED=false, or
|
|
- no client identity is available on the request.
|
|
|
|
Used as the runtime default for tool params like dataset_name so each
|
|
MCP client writes to its own scope automatically when the LLM omits
|
|
the argument.
|
|
"""
|
|
if not _is_agent_scoping_enabled():
|
|
return "main_dataset"
|
|
|
|
from mcp.server.lowlevel.server import request_ctx
|
|
|
|
try:
|
|
ctx = request_ctx.get()
|
|
params = getattr(ctx.session, "client_params", None)
|
|
if params and params.clientInfo and params.clientInfo.name:
|
|
return f"{_sanitize_client_name(params.clientInfo.name)}_memory"
|
|
except LookupError:
|
|
pass
|
|
return "main_dataset"
|
|
|
|
|
|
@mcp.tool(
|
|
name="get_client_info_json",
|
|
description=(
|
|
"Return the current MCP client identity and its agent-scoped default dataset. "
|
|
"The workspace UI uses this to automatically separate memory per agent "
|
|
"(e.g. Cursor writes to 'cursor_memory', Claude Code to 'claude_code_memory'). "
|
|
"The default dataset is created on demand. "
|
|
"Returns {client: {name, version}, default_dataset} in structuredContent."
|
|
),
|
|
)
|
|
@log_usage(function_name="MCP get_client_info_json", log_type="mcp_tool")
|
|
async def get_client_info_json() -> types.CallToolResult:
|
|
from mcp.server.lowlevel.server import request_ctx
|
|
|
|
client_name = "unknown"
|
|
client_version = ""
|
|
try:
|
|
ctx = request_ctx.get()
|
|
params = getattr(ctx.session, "client_params", None)
|
|
if params and params.clientInfo:
|
|
client_name = params.clientInfo.name or "unknown"
|
|
client_version = params.clientInfo.version or ""
|
|
except LookupError:
|
|
pass
|
|
|
|
agent_scoped = _is_agent_scoping_enabled()
|
|
|
|
if agent_scoped:
|
|
default_dataset = f"{_sanitize_client_name(client_name)}_memory"
|
|
if not cognee_client.use_api:
|
|
with redirect_stdout(sys.stderr):
|
|
from cognee.modules.data.methods.create_authorized_dataset import (
|
|
create_authorized_dataset,
|
|
)
|
|
|
|
user = await get_default_user()
|
|
await create_authorized_dataset(default_dataset, user)
|
|
else:
|
|
default_dataset = "main_dataset"
|
|
|
|
return types.CallToolResult(
|
|
content=[
|
|
types.TextContent(
|
|
type="text",
|
|
text=f"Agent: {client_name} → default dataset: {default_dataset}",
|
|
)
|
|
],
|
|
structuredContent={
|
|
"client": {"name": client_name, "version": client_version},
|
|
"default_dataset": default_dataset,
|
|
"agent_scoped": agent_scoped,
|
|
},
|
|
)
|
|
|
|
|
|
@mcp.tool(
|
|
name="create_dataset_json",
|
|
description=(
|
|
"Create an empty dataset with the given name (idempotent). "
|
|
"Returns {dataset: {id, name}} in structuredContent."
|
|
),
|
|
)
|
|
@log_usage(function_name="MCP create_dataset_json", log_type="mcp_tool")
|
|
async def create_dataset_json(name: str) -> types.CallToolResult:
|
|
name = (name or "").strip()
|
|
if not name:
|
|
return types.CallToolResult(
|
|
isError=True,
|
|
content=[types.TextContent(type="text", text="Error: dataset name is required.")],
|
|
)
|
|
if cognee_client.use_api:
|
|
return types.CallToolResult(
|
|
isError=True,
|
|
content=[
|
|
types.TextContent(
|
|
type="text",
|
|
text="Error: create_dataset_json is only available in direct mode.",
|
|
)
|
|
],
|
|
)
|
|
|
|
with redirect_stdout(sys.stderr):
|
|
from cognee.modules.data.methods.create_authorized_dataset import (
|
|
create_authorized_dataset,
|
|
)
|
|
|
|
user = await get_default_user()
|
|
dataset = await create_authorized_dataset(name, user)
|
|
|
|
return types.CallToolResult(
|
|
content=[types.TextContent(type="text", text=f"Dataset '{dataset.name}' ready.")],
|
|
structuredContent={"dataset": {"id": str(dataset.id), "name": dataset.name}},
|
|
)
|
|
|
|
|
|
def node_to_string(node):
|
|
node_data = ", ".join(
|
|
[f'{key}: "{value}"' for key, value in node.items() if key in ["id", "name"]]
|
|
)
|
|
|
|
return f"Node({node_data})"
|
|
|
|
|
|
def retrieved_edges_to_string(search_results):
|
|
edge_strings = []
|
|
for triplet in search_results:
|
|
node1, edge, node2 = triplet
|
|
relationship_type = edge["relationship_name"]
|
|
edge_str = f"{node_to_string(node1)} {relationship_type} {node_to_string(node2)}"
|
|
edge_strings.append(edge_str)
|
|
|
|
return "\n".join(edge_strings)
|
|
|
|
|
|
def load_class(model_file, model_name):
|
|
model_file = os.path.abspath(model_file)
|
|
|
|
# Reject obvious nonsense before we hand the path to the import machinery.
|
|
# Note: this does not sandbox imports — anyone who can call cognify() with
|
|
# a custom graph_model_file can already run arbitrary code by construction.
|
|
# Operators exposing this tool over HTTP/SSE must enforce auth at the
|
|
# transport layer.
|
|
if not model_file.endswith(".py"):
|
|
raise ValueError(f"graph_model_file must be a .py file, got: {model_file}")
|
|
if not os.path.isfile(model_file):
|
|
raise ValueError(f"graph_model_file not found: {model_file}")
|
|
|
|
spec = importlib.util.spec_from_file_location("graph_model", model_file)
|
|
if spec is None or spec.loader is None:
|
|
raise ValueError(f"Could not load module from: {model_file}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
model_class = getattr(module, model_name)
|
|
|
|
return model_class
|
|
|
|
|
|
async def main():
|
|
global cognee_client
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument(
|
|
"--transport",
|
|
choices=["sse", "stdio", "http"],
|
|
default="stdio",
|
|
help="Transport to use for communication with the client. (default: stdio)",
|
|
)
|
|
|
|
# HTTP transport options
|
|
parser.add_argument(
|
|
"--host",
|
|
default="127.0.0.1",
|
|
help="Host to bind the HTTP server to (default: 127.0.0.1)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=8000,
|
|
help="Port to bind the HTTP server to (default: 8000)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--path",
|
|
default="/mcp",
|
|
help="Path for the MCP HTTP endpoint (default: /mcp)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--log-level",
|
|
default="info",
|
|
choices=["debug", "info", "warning", "error"],
|
|
help="Log level for the HTTP server (default: info)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--no-migration",
|
|
default=False,
|
|
action="store_true",
|
|
help="Argument stops database migration from being attempted",
|
|
)
|
|
|
|
# Cognee API connection options
|
|
parser.add_argument(
|
|
"--api-url",
|
|
default=None,
|
|
help="Base URL of a running Cognee FastAPI server (e.g., http://localhost:8000). "
|
|
"If provided, the MCP server will connect to the API instead of using cognee directly.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--api-token",
|
|
default=None,
|
|
help="Authentication token for the API (optional, required if API has authentication enabled).",
|
|
)
|
|
|
|
# Cognee Cloud connection options
|
|
parser.add_argument(
|
|
"--serve-url",
|
|
default=None,
|
|
help="Cognee Cloud or remote instance URL (e.g., https://your-instance.cognee.ai). "
|
|
"Calls cognee.serve() at startup so all SDK operations route to the cloud. "
|
|
"Can also be set via COGNEE_SERVICE_URL env var.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--serve-api-key",
|
|
default=None,
|
|
help="API key for the Cognee Cloud instance. Can also be set via COGNEE_API_KEY env var.",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Initialize the global CogneeClient
|
|
cognee_client = CogneeClient(api_url=args.api_url, api_token=args.api_token)
|
|
|
|
mcp.settings.host = args.host
|
|
mcp.settings.port = int(args.port)
|
|
_configure_transport_security(args.host)
|
|
|
|
# Resolve cloud connection: CLI args take precedence over env vars
|
|
serve_url = args.serve_url or os.environ.get("COGNEE_SERVICE_URL", "")
|
|
serve_api_key = args.serve_api_key or os.environ.get("COGNEE_API_KEY", "")
|
|
|
|
# Connect to Cognee Cloud if configured (before migrations — cloud handles its own DB)
|
|
if serve_url and not args.api_url:
|
|
import cognee
|
|
|
|
serve_kwargs = {"url": serve_url}
|
|
if serve_api_key:
|
|
serve_kwargs["api_key"] = serve_api_key
|
|
await cognee.serve(**serve_kwargs)
|
|
logger.info(f"Connected to Cognee Cloud: {serve_url}")
|
|
|
|
# Skip migrations when in API or Cloud mode (remote handles its own database)
|
|
is_remote = bool(args.api_url) or bool(serve_url)
|
|
if not args.no_migration and not is_remote:
|
|
from cognee.modules.engine.operations.setup import setup
|
|
from cognee.run_migrations import run_migrations
|
|
|
|
logger.info("Running database migrations...")
|
|
|
|
await setup()
|
|
# Full startup migrations (relational schema + graph/vector revision
|
|
# chains) — MCP writes new-scheme data, so it must migrate like the API.
|
|
await run_migrations()
|
|
|
|
logger.info("Database migrations done.")
|
|
elif not is_remote:
|
|
logger.info("Skipping DB migrations")
|
|
|
|
try:
|
|
match args.transport.lower():
|
|
case "sse":
|
|
logger.info(f"Running MCP server with SSE transport on {args.host}:{args.port}")
|
|
await run_sse_with_cors()
|
|
case "http":
|
|
logger.info(
|
|
f"Running MCP server with Streamable HTTP transport on {args.host}:{args.port}{args.path}"
|
|
)
|
|
await run_http_with_cors()
|
|
case _:
|
|
logger.info("Running MCP server with stdio")
|
|
await mcp.run_stdio_async()
|
|
finally:
|
|
# Drain background tasks with a bounded timeout so a hung cognify can't
|
|
# block shutdown indefinitely. Then close the HTTP client pool.
|
|
if _background_tasks:
|
|
logger.info(f"Awaiting {len(_background_tasks)} background task(s) before shutdown")
|
|
try:
|
|
await asyncio.wait_for(
|
|
asyncio.gather(*_background_tasks, return_exceptions=True),
|
|
timeout=10.0,
|
|
)
|
|
except asyncio.TimeoutError:
|
|
logger.warning(
|
|
f"{len(_background_tasks)} background task(s) still running at shutdown; cancelling"
|
|
)
|
|
for t in _background_tasks:
|
|
t.cancel()
|
|
if cognee_client is not None:
|
|
await cognee_client.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logger = setup_logging()
|
|
|
|
try:
|
|
asyncio.run(main())
|
|
except Exception as e:
|
|
logger.error(f"Error initializing Cognee MCP server: {str(e)}")
|
|
raise
|