184 lines
5.9 KiB
Python
184 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
"""MNote LightRAG MCP facade.
|
|||
|
|
|
|||
|
|
This is intentionally thin: LightRAG owns retrieval, MNote owns citation/open
|
|||
|
|
mapping through its source registry.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
import httpx
|
|||
|
|
from mcp.server.fastmcp import FastMCP
|
|||
|
|
|
|||
|
|
|
|||
|
|
DEFAULT_ENV_FILE = "/mnt/Data1T/Mnote_data/lightrag/LightRAG/.env"
|
|||
|
|
DEFAULT_MNOTE_WEB_URL = "http://127.0.0.1:3000"
|
|||
|
|
DEFAULT_ROOT_URI = "file:///mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space"
|
|||
|
|
DEFAULT_WORKSPACE_ID = "local-ws:mnote-e2e:my-space"
|
|||
|
|
|
|||
|
|
mcp = FastMCP("MNote-LightRAG-Server")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _read_env_value(key: str, env_file: str) -> str:
|
|||
|
|
path = Path(env_file)
|
|||
|
|
if not path.exists():
|
|||
|
|
return ""
|
|||
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|||
|
|
if not line.startswith(f"{key}="):
|
|||
|
|
continue
|
|||
|
|
value = line.split("=", 1)[1].strip()
|
|||
|
|
if (value.startswith('"') and value.endswith('"')) or (
|
|||
|
|
value.startswith("'") and value.endswith("'")
|
|||
|
|
):
|
|||
|
|
value = value[1:-1]
|
|||
|
|
return value
|
|||
|
|
return ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _lightrag_base_url() -> str:
|
|||
|
|
env_file = os.environ.get("MNOTE_LIGHTRAG_ENV_FILE", DEFAULT_ENV_FILE)
|
|||
|
|
host = os.environ.get("LIGHTRAG_HOST") or _read_env_value("HOST", env_file) or "127.0.0.1"
|
|||
|
|
port = os.environ.get("LIGHTRAG_PORT") or _read_env_value("PORT", env_file) or "9621"
|
|||
|
|
return f"http://{host}:{port}".rstrip("/")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _lightrag_api_key() -> str:
|
|||
|
|
env_file = os.environ.get("MNOTE_LIGHTRAG_ENV_FILE", DEFAULT_ENV_FILE)
|
|||
|
|
return os.environ.get("LIGHTRAG_API_KEY") or _read_env_value("LIGHTRAG_API_KEY", env_file)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mnote_web_url() -> str:
|
|||
|
|
return os.environ.get("MNOTE_WEB_URL", DEFAULT_MNOTE_WEB_URL).rstrip("/")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mnote_headers() -> dict[str, str]:
|
|||
|
|
return {
|
|||
|
|
"content-type": "application/json",
|
|||
|
|
"x-mnote-actor-id": os.environ.get("MNOTE_ACTOR_ID", "mnote-e2e"),
|
|||
|
|
"x-mnote-actor-type": os.environ.get("MNOTE_ACTOR_TYPE", "user"),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _request_lightrag(path: str, *, method: str = "GET", json_body: Any = None) -> Any:
|
|||
|
|
headers = {"accept": "application/json"}
|
|||
|
|
api_key = _lightrag_api_key()
|
|||
|
|
if api_key:
|
|||
|
|
# 当前 LightRAG /query 接受 X-API-Key;Bearer 在本机版本会返回 Invalid token。
|
|||
|
|
headers["X-API-Key"] = api_key
|
|||
|
|
async with httpx.AsyncClient(timeout=180) as client:
|
|||
|
|
response = await client.request(
|
|||
|
|
method,
|
|||
|
|
f"{_lightrag_base_url()}{path}",
|
|||
|
|
headers=headers,
|
|||
|
|
json=json_body,
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
payload = response.json()
|
|||
|
|
except Exception:
|
|||
|
|
payload = {"text": response.text}
|
|||
|
|
if response.status_code >= 400:
|
|||
|
|
return {"status": "error", "response": None, "error": payload, "httpStatus": response.status_code}
|
|||
|
|
return {"status": "success", "response": payload, "error": None, "httpStatus": response.status_code}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@mcp.tool(
|
|||
|
|
name="verify_server_health",
|
|||
|
|
description="Check whether the configured local LightRAG server is healthy.",
|
|||
|
|
)
|
|||
|
|
async def verify_server_health() -> Any:
|
|||
|
|
return await _request_lightrag("/health")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@mcp.tool(
|
|||
|
|
name="check_indexing_status",
|
|||
|
|
description="Check the LightRAG document processing pipeline status.",
|
|||
|
|
)
|
|||
|
|
async def check_indexing_status() -> Any:
|
|||
|
|
return await _request_lightrag("/documents/pipeline_status")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@mcp.tool(
|
|||
|
|
name="list_all_docs",
|
|||
|
|
description="List documents currently known to LightRAG.",
|
|||
|
|
)
|
|||
|
|
async def list_all_docs() -> Any:
|
|||
|
|
return await _request_lightrag("/documents")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@mcp.tool(
|
|||
|
|
name="query_knowledge_graph",
|
|||
|
|
description="Search the local LightRAG knowledge base. Use mix by default; references include provider file_path/chunk_id for MNote citation mapping.",
|
|||
|
|
)
|
|||
|
|
async def query_knowledge_graph(
|
|||
|
|
prompt: str,
|
|||
|
|
search_mode: str = "mix",
|
|||
|
|
limit: int = 60,
|
|||
|
|
context_only: bool = False,
|
|||
|
|
prompt_only: bool = False,
|
|||
|
|
include_references: bool = True,
|
|||
|
|
include_chunk_content: bool = True,
|
|||
|
|
) -> Any:
|
|||
|
|
mode_aliases = {
|
|||
|
|
"keyword": "naive",
|
|||
|
|
"semantic": "hybrid",
|
|||
|
|
}
|
|||
|
|
mode = mode_aliases.get(search_mode, search_mode)
|
|||
|
|
body = {
|
|||
|
|
"query": prompt,
|
|||
|
|
"mode": mode,
|
|||
|
|
"top_k": limit,
|
|||
|
|
"chunk_top_k": limit,
|
|||
|
|
"only_need_context": context_only,
|
|||
|
|
"only_need_prompt": prompt_only,
|
|||
|
|
"include_references": include_references,
|
|||
|
|
"include_chunk_content": include_chunk_content,
|
|||
|
|
"response_type": "Multiple Paragraphs",
|
|||
|
|
}
|
|||
|
|
return await _request_lightrag("/query", method="POST", json_body=body)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@mcp.tool(
|
|||
|
|
name="open_mnote_reference",
|
|||
|
|
description="Map a LightRAG reference/file_path/chunk_id to an MNote clickable citationUrl using MNote source registry.",
|
|||
|
|
)
|
|||
|
|
async def open_mnote_reference(
|
|||
|
|
file_path: str,
|
|||
|
|
chunk_id: str = "",
|
|||
|
|
reference_id: str = "",
|
|||
|
|
workspace_id: str = DEFAULT_WORKSPACE_ID,
|
|||
|
|
root_uri: str = DEFAULT_ROOT_URI,
|
|||
|
|
include_registry: bool = False,
|
|||
|
|
) -> Any:
|
|||
|
|
body: dict[str, Any] = {
|
|||
|
|
"workspaceId": workspace_id,
|
|||
|
|
"rootUri": root_uri,
|
|||
|
|
"filePath": file_path,
|
|||
|
|
}
|
|||
|
|
if chunk_id:
|
|||
|
|
body["chunkId"] = chunk_id
|
|||
|
|
if reference_id:
|
|||
|
|
body["referenceId"] = reference_id
|
|||
|
|
async with httpx.AsyncClient(timeout=60) as client:
|
|||
|
|
response = await client.post(
|
|||
|
|
f"{_mnote_web_url()}/api/knowledge-rag/open-reference",
|
|||
|
|
headers=_mnote_headers(),
|
|||
|
|
json=body,
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
payload = response.json()
|
|||
|
|
except Exception:
|
|||
|
|
payload = {"text": response.text}
|
|||
|
|
if response.status_code >= 400:
|
|||
|
|
return {"status": "error", "response": None, "error": payload, "httpStatus": response.status_code}
|
|||
|
|
if not include_registry and isinstance(payload, dict):
|
|||
|
|
payload = {key: value for key, value in payload.items() if key != "registry"}
|
|||
|
|
return {"status": "success", "response": payload, "error": None, "httpStatus": response.status_code}
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
mcp.run()
|